Repository navigation
Releases
v1.0.0
🎉 ThruBox Server 1.0 is here. It is the first stable release of a small, self-hostable relay that works as a dumb encrypted mailbox. Apps put encrypted messages in a recipient's mailbox and the recipient picks them up later, even if the two were never online at the same time. The server stores and forwards opaque blobs and never sees plaintext; all encryption happens in the client.
ThruBox is one Go binary with an embedded SQLite database. It has no message broker, no external database and no peer-to-peer network to join. You can run your own relay or use someone else's. Chainvoice is the first app built on it, and any app can use it through the TypeScript SDK or plain HTTP.
"1.0" means the HTTP API and the configuration format are stable. Later 1.x releases add to them without breaking existing clients or config.yaml files.
Highlights
- A REST API you can use with
curl. Three endpoints: put a message in a mailbox, read a mailbox, and a health check. Payloads are opaque strings up to 500 KB by default. Messages leave the relay only by expiring. - Per-mailbox cursors. Every message gets a
seq, its position in the recipient's mailbox: 1, 2, 3, and so on.GET /api/messages/{address}?after=<seq>&limit=<n>returns only what a client has not handled yet, so polling no longer downloads the whole mailbox every time. Sequence numbers only go up and are never reused, even after messages expire, so a cursor never skips a message. Each device keeps its own cursor and the server stores no per-client state. (#69, #67) - Content-derived message IDs. A message's
idis the SHA-256 of its recipient, sender and payload. Sending the exact same message again is safe: nothing new is stored and the server returns the existing message with200instead of201. (#69, #67) - Rate limiting that clients cannot fake. By default the limiter keys on the connecting address and ignores
X-Forwarded-For/X-Real-IP. Behind a reverse proxy, settrusted_proxies: Nand the client IP is read from the NthX-Forwarded-Forentry counted from the right, the last one your own proxy wrote. Before this fix, rotating a fake header bypassed the limit entirely. (#68, #67) - Automatic expiry. Messages expire after
ttl_days(7 by default). A background job purges them every hour. Setttl_days: 0to keep messages forever; the server warns, because the database then only grows. - CORS for browser apps. Static front ends, such as one hosted on GitHub Pages, can call the relay directly. List the allowed origins in
allowed_origins; it is off by default. (#32, #36) - Optional API key. Set
api_keyto turn a relay into a private one: every request then needs theX-API-Keyheader./healthstays open for health checks. - Configuration that works on hosting platforms. Settings come from
config.yamland/orRELAY_*environment variables. The server also honoursPORT(Render, Railway, Heroku, Cloud Run), andRELAY_CONFIG_PATHpoints it at a config file anywhere. Invalid values stop the server at startup instead of being silently ignored. (#26, #27, #34, #35) - Docker images. Multi-arch images (
linux/amd64,linux/arm64) are published to GitHub Container Registry with provenance attestations and an SBOM. (#12, #18) - Self-hosting guides. Step-by-step guides cover putting the relay behind Nginx with Let's Encrypt or behind a Cloudflare Tunnel, plus a post-deployment security checklist. Thanks @tarunagnihotri534! (#11, #50)
How it works
Architecture
Getting started
docker run -d --name thrubox \
-p 127.0.0.1:3000:3000 \
-v thrubox-data:/data \
ghcr.io/aossie-org/thrubox-server:1.0.0
curl http://127.0.0.1:3000/health🐳 Image: ghcr.io/aossie-org/thrubox-server:1.0.0 for linux/amd64 and linux/arm64, also tagged 1.0 and 1.
Then send and read a message:
curl -X POST http://127.0.0.1:3000/api/messages \
-H "Content-Type: application/json" \
-d '{"to":"0xRecipient","from":"0xSender","payload":"<ciphertext>"}'
curl "http://127.0.0.1:3000/api/messages/0xRecipient?after=0"From TypeScript, use @aossie-org/thrubox-client 1.1.0 or later (npm install @aossie-org/thrubox-client@1.1.0) for cursor support. Earlier SDK versions keep working, but they read the whole mailbox on every poll.
Before you deploy
- Behind a proxy? Set
trusted_proxies. With the default0, every request appears to come from the proxy, so all your users share one rate-limit bucket and get429together. A single proxy (Nginx, Cloudflare Tunnel, Render and similar platforms) isRELAY_SECURITY_TRUSTED_PROXIES=1. Only the proxy should be able to reach the relay. See Behind a Reverse Proxy. - Upgrading from a pre-release build of
main? On first start the database is migrated in place: existing messages get sequence numbers in the order they arrived. Their old UUID IDs stay valid; only new messages get hash IDs.DELETE /api/messages/{id}is gone and returns405. The TypeScript SDK removesdelete()in 1.1.0. - Persist
/data. Messages live in/data/relay.dbinside the container. Without a volume they disappear when the container is replaced, which also happens on free hosting tiers that have no persistent disk.
Known limitations
ThruBox keeps the server deliberately simple, and in 1.0 that has a trade-off you should know about:
- Reading a mailbox is not authenticated. Anyone who knows an address can read the encrypted messages waiting for it. Confidentiality comes from client-side encryption, but a hostile caller can still see message metadata: who wrote to whom, and when. Wallet-signature authentication for reads is planned for a 1.x release. Until then, use the API key, the rate limit and short TTLs if this matters for your deployment.
- There is no delete endpoint. Pre-release builds had
DELETE /api/messages/{id}, but it did not check who was asking, so anyone could delete anyone's messages. It was removed before 1.0 (#70) and now returns405. Messages leave the relay by expiring afterttl_days. Authenticated deletion may return in a later release. - One server, one database file. There is no replication or clustering; scale by running separate relays.
- Docker is the supported way to run it. No prebuilt binaries are attached to this release. To run without Docker, build from source with Go 1.23+ and a C compiler (
CGO_ENABLED=1).
Resources
- README: API, configuration and self-hosting guides
- TypeScript SDK
- Changelog
- Questions and discussion: the Discord, #thrubox channel
- Bugs and feature requests: issue tracker
Thank you to the AOSSIE mentors for the design reviews behind this release, and to everyone who reported issues, reviewed pull requests and tested the relay with Chainvoice. 💚
What's Changed
🚀 Features
- feat: add configurable CORS support for direct browser clients by @Atharva0506 in #36
- docs: add self-hosted deployment guide with Nginx and Cloudflare Tunnel by @tarunagnihotri534 in #50
- Remove the DELETE /api/messages/{id} endpoint by @Atharva0506 in #70
- Fix rate-limit bypass, add mailbox cursor, and release 1.0.0 by @Atharva0506 in #67
⚙️ Backend
- feat: add Go module, config system, and default config file by @Atharva0506 in #5
- feat: add Message model and Store interface with SQLite backend by @Atharva0506 in #6
- feat: add HTTP handlers and middleware by @Atharva0506 in #7
- feat: add server entrypoint with graceful shutdown by @Atharva0506 in #8
- fix: support the standard PORT environment variable by @Atharva0506 in #34
- fix: load config.yaml from a configurable path by @Atharva0506 in #35
🐳 Docker
- feat: add Docker and Docker Compose support by @Atharva0506 in #9
- fix: update Dockerfile to Go 1.23 and add missing dependencies to go.mod by @Atharva0506 in #10
- chore: fix CI issues by @Atharva0506 in #24
- ci: configure docker publish to ghcr by @Atharva0506 in #18
⬆️ Dependencies
- chore(deps)(deps): bump github.com/mattn/go-sqlite3 from 1.14.48 to 1.14.52 by @dependabot[bot] in #48
🚀 CI/CD
- chore(deps)(deps): bump step-security/harden-runner from 2.19.4 to 2.20.0 by @dependabot[bot] in ht...