Gone

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

DecisionInstead ofWhyRevisit 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 browserWebAssembly 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).Argon2idArgon2id 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 apartAES-GCM can’t tell them apart; both just fail to authenticate.Never

Server

DecisionInstead ofWhyRevisit when
Nothing is kept once a secret is gone. Status answers only “pending” or 404.Tombstones, or an “opened” or “revoked” stateA 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 filesmattn/go-sqlite3 (CGO), or object storageA fully static binary with CGO_ENABLED=0, and one file plus a folder to back up.Instances outgrow it in practice
synchronous=FULLRelaxing it for speedA 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 raceWriters weren’t served in order, and a timeout surfaced as a 500.—
Rate limits are per process, in memory.A shared store, such as RedisGone 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 onA 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 whatGone 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 createIt 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 headerA 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 onlyConfig files or command-line flagsOne way to configure, and it suits containers.Never

Deployment

DecisionInstead ofWhyRevisit 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 versionOne 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 imageA gh-pages Helm repository index, or cosignIt 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 replicaCountSeveral replicas, or a rolling updateSQLite, 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 NetworkPolicyProxy 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 templateAn Ingress, or bothThe 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 controllerBundling them as chart dependenciesBoth 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 themBuilding 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 installedA gone system user made by the package, or starting on installsystemd 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 repositoryHosting 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 sudoInstalling the server too, or installing system-wideThe 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

DecisionInstead ofWhyRevisit 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 modulesNothing 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 CDNsNothing 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 NayukiA QR libraryNo 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.ParaglideParaglide 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 pageA reload would lose a link you just made or a secret you just opened.Never
A custom, keyboard-accessible language menuThe browser’s native <select>The native list didn’t fit the design.—

Command line

DecisionInstead ofWhyRevisit when
Saved requests in a gob file, written atomically under a lock, with permissions checked like sshSQLiteSQLite 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 subcommandsThe 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.

DecisionInstead ofWhyRevisit when
Prometheus text format written by handclient_golangIt would add about 10 modules for a small text format.—
Locale matching written by handA locale-matching libraryMatching 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 parserToken verification is security-critical, and go-jose is widely used and reviewed.—
Plain validation codego-playground/validatorIt was the only reason x/crypto was in the server’s build. Dropping it removed six modules.—