Technical decisions
How Gone is built, and the alternatives we turned down: the choice, what we didn’t do, why, and what would make us look again. Features we’ve said no to are in Rejected features; this page is about the how, not the what.
Each phase of the roadmap also records the decisions made while building it. This page collects the ones a contributor is most likely to question. A “Never” here means the alternative conflicts with one of Gone’s guarantees or with a non-goal.
Encryption
| Decision | Instead of | Why | Revisit when |
|---|---|---|---|
| Encryption runs on the browser’s built-in WebCrypto, with a separate Go implementation for the CLI. Shared test vectors keep the two in step. | WebAssembly, for example the Go code compiled for the browser | WebAssembly would add about 2 MB to every page, need a 'wasm-unsafe-eval' hole in the Content Security Policy, give up hardware-accelerated AES-GCM, and leave secrets in a garbage-collected heap that can’t be wiped. | Never, while the strict CSP stands |
| Passphrases are stretched with PBKDF2-SHA-256 at 600,000 iterations. The format reserves IDs for other functions (protocol.md §4.2). | Argon2id | Argon2id resists guessing better, but browsers only offer it through WebAssembly. Reconfirmed on 2026-10-16. | Browsers offer Argon2 natively |
| A wrong link key and a wrong passphrase give the same error. | Telling the two apart | AES-GCM can’t tell them apart; both just fail to authenticate. | Never |
Server
| Decision | Instead of | Why | Revisit when |
|---|---|---|---|
Nothing is kept once a secret is gone. Status answers only “pending” or 404. | Tombstones, or an “opened” or “revoked” state | A row kept after deletion leaks that the ID existed and when it was opened. This is why there are no read receipts. | Never |
SQLite through the pure-Go modernc.org/sqlite, with ciphertext over 8 KiB in files | mattn/go-sqlite3 (CGO), or object storage | A fully static binary with CGO_ENABLED=0, and one file plus a folder to back up. | Instances outgrow it in practice |
synchronous=FULL | Relaxing it for speed | A claim lost to a power cut would put the secret back to unclaimed, so it could be opened a second time. | Never |
An ordered write gate. Contention answers 503 busy. | Letting SQLite’s busy handler race | Writers weren’t served in order, and a timeout surfaced as a 500. | — |
| Rate limits are per process, in memory. | A shared store, such as Redis | Gone runs as one process. Large floods belong at the edge anyway. | Gone is commonly run with several replicas |
GONE_REQUIRE_PASSPHRASE covers only new secrets (POST /api/secret). Replies to requests are exempt. | Requiring a passphrase on replies too, or turning requests off while the setting is on | A passphrase protects against a leaked link. A secret link carries its own decryption key, so the passphrase is a second factor that travels another way. A reply link carries no decryption key: only the requester’s public key and a fill token that allows one reply. The reply is encrypted to a private key that never leaves the requester’s browser or CLI, and only the requester’s manage token can claim it. So whoever intercepts a reply link can send the one reply, but can never read one. Requiring a passphrase anyway would need a new protocol version and a second channel running backwards, from the person replying to the requester, and the server could still only check a version number. Turning requests off would remove the one mode that never puts a key in a link. | An operator needs replies covered for a reason other than a leaked link |
Sign-in for private instances (GONE_AUTH) checks that the proxy signed someone in, never who. Nothing about the user is stored, logged or shown. | Accounts in Gone, or recording who created what | Gone keeps no record of who sent what; the operator’s proxy already knows who its users are. | Never for accounts |
| User-level audit logging is out of scope. | Logging the identity behind each create | It would be exactly the record Gone promises not to keep. A possible future enterprise-only feature, possibly under a different licence. | An enterprise deployment asks for it |
Signed sign-in tokens are verified against the provider’s keys. Plain identity headers are believed only from GONE_TRUSTED_PROXIES, and such providers refuse to start without it. | Trusting any non-empty identity header | A plain header proves nothing if someone reaches Gone without the proxy; every vendor that sends one says to firewall the app. | — |
| Configuration from environment variables only | Config files or command-line flags | One way to configure, and it suits containers. | Never |
Deployment
| Decision | Instead of | Why | Revisit when |
|---|---|---|---|
The Helm chart lives in this repository and its version follows Gone’s (chart 3.13.0 runs v3.13.0) | A separate chart repository with its own version | One release makes the image and the chart together, so they can’t drift, and the chart’s install test runs against the code in the same pull request. | The chart needs releases of its own |
| The chart is published as an OCI artifact on GHCR, attested like the image | A gh-pages Helm repository index, or cosign | It sits beside the image, needs no extra hosting, and gh attestation verify checks it the same way. | — |
The chart runs one replica with the Recreate strategy, and refuses replicaCount | Several replicas, or a rolling update | SQLite, the blob directory and the rate limits each assume one process; two pods would lose or corrupt secrets. | Gone gains a shared store |
Plain-header sign-in (authelia, authentik, tailscale, custom) isn’t supported on Kubernetes. The chart refuses it; goned only warns when it finds itself in a pod. | Allowing it with a pod-network GONE_TRUSTED_PROXIES, or a NetworkPolicy | Proxy pods have no fixed address, and trusting the pod network lets any pod forge the header. A NetworkPolicy only helps where the network plugin enforces it, and fails open silently where it doesn’t. goned warns rather than refusing because a proxy sidecar in Gone’s own pod, trusted on 127.0.0.1, is safe. See Kubernetes. | Never |
| The chart routes with the Gateway API (an HTTPRoute) and has no Ingress template | An Ingress, or both | The Gateway API is where Kubernetes routing is heading. An Ingress is a few lines to write by hand. | Users ask for an Ingress template |
| The chart doesn’t install cert-manager or a Gateway controller | Bundling them as chart dependencies | Both install cluster-wide resources (CRDs and webhooks) that clash with copies a cluster already has, and make upgrades and uninstalls risky. | — |
| Linux packages are built with nfpm from the same binaries as the release archives, and attested with them | Building packages separately, or distribution build services (openSUSE Build Service, PPAs, COPR) | One build means the packages hold the exact attested binaries. Build services compile from source, so their binaries wouldn’t match the attestation. | — |
gone-server runs as a systemd DynamicUser and isn’t started when installed | A gone system user made by the package, or starting on install | systemd manages the user and the data directory, so the package needs no user scripts. Gone needs settings and a TLS proxy before it should listen. | — |
| Packages are release downloads, not an apt or dnf repository | Hosting a signed repository (on GitHub Pages or with Cloudsmith) | A repository means a signing key to keep and more release machinery, for a small audience so far. | People ask for apt upgrade |
install.sh installs only the CLI, into ~/.local/bin, and never uses sudo | Installing the server too, or installing system-wide | The server needs settings, a proxy and a service; the packages and Docker handle that. A script piped to sh shouldn’t ask for root. | — |
Web client
| Decision | Instead of | Why | Revisit when |
|---|---|---|---|
Vanilla JavaScript in small classic scripts, with no build step. Each script publishes one window.gone* namespace. | Front-end frameworks, a bundler, TypeScript or ES modules | Nothing to audit beyond what ships, and it works under a strict CSP. The repo’s only npm dependency is Playwright in test/e2e, which is test-only and never shipped. | Never, for shipped code |
| Everything is served from the operator’s own server. | Third-party scripts, fonts, analytics or CDNs | Nothing loads from anywhere else, so no outside party sees who uses an instance. | Never |
Our own QR encoder (web/js/qr*.js), checked against node-qrcode and Nayuki | A QR library | No new dependency, and nothing outside 'self' in the CSP. | — |
Translations in inlang’s message format, rendered by our own small runtime. Pages embed only the js.* and common.* messages. | Paraglide | Paraglide needs ES modules or a bundler. The catalog format keeps it possible later. | The web client moves to ES modules |
| The language switches in place, from catalogs fetched on demand. | Reloading the page | A reload would lose a link you just made or a secret you just opened. | Never |
| A custom, keyboard-accessible language menu | The browser’s native <select> | The native list didn’t fit the design. | — |
Command line
| Decision | Instead of | Why | Revisit when |
|---|---|---|---|
Saved requests in a gob file, written atomically under a lock, with permissions checked like ssh | SQLite | SQLite grew the binary from 10.2 to 16.7 MB. The format is deliberately not something to open, edit or copy-paste. | — |
gone (client) and goned (server) are separate binaries. | One binary with server subcommands | The client carries no server code or web assets, and the name users type is the short one. | — |
Dependencies
The server compiles in 17 external modules. Each new one needs a written reason in its PR.
| Decision | Instead of | Why | Revisit when |
|---|---|---|---|
| Prometheus text format written by hand | client_golang | It would add about 10 modules for a small text format. | — |
| Locale matching written by hand | A locale-matching library | Matching five locales takes a few dozen lines. | Gone supports many more locales |
github.com/go-jose/go-jose/v4 verifies sign-in tokens (v3.12). It adds one module and has no dependencies of its own. It parses key sets and requires the allowed algorithms to be listed when a token is parsed, so each provider pins its algorithm and a token’s own alg is never trusted. | A hand-written verifier; golang-jwt/jwt, which needs a separate key-set parser | Token verification is security-critical, and go-jose is widely used and reviewed. | — |
| Plain validation code | go-playground/validator | It was the only reason x/crypto was in the server’s build. Dropping it removed six modules. | — |