Encrypted config. Credentials that expire.
GATillSecrets is where your configuration and secrets live — encrypted at rest under a key only your org holds, versioned so every change is reversible, and pulled at boot over one authenticated call. It carries two kinds of secret under one model: static config (a value you set and version) and dynamic secrets (a short-lived credential leased on demand and auto-revoked on expiry). Same workspace, same login, same audit log as the rest of TillDev.
Two kinds of secret, one model
Every secret is a KEY — env-var-style, like STRIPE_KEY or DATABASE_URL — that lives in one environment. What differs is its kind:
| Kind | What it holds | How you read it |
|---|---|---|
| Static config | A value you set. Stored as an immutable, envelope-encrypted version; changing it allocates a new version and rollback re-points to an older one. | Returned in a pull, decrypted. |
| Dynamic secret | A reference to a backend (Postgres, API key, key pair or TillForge token). No value is stored — a fresh, short-lived credential is minted on demand and auto-revoked on expiry. | Leased from the backend service, once. |
Both sit in the same hierarchy: your org owns projects, a project has environments (dev / staging / prod), and an environment holds the keys. A service token is scoped to a project — and optionally pinned to one environment — so a prod token can never read dev.
org
└─ project "acme-api"
└─ environment dev · staging · prod
└─ KEY STRIPE_KEY, DATABASE_URL, …
├─ static → immutable versions (v1, v2, v3 …) + a "current" pointer
└─ dynamic → a backend reference; credentials are leased, never storedPull the environment at boot
Apps don’t ship secrets in their bundle or a .env file — they pull the environment at startup with a ts_… service token. On Node the one-liner injects everything into process.env:
// The very top of your server entry, before you read any config.
import { load } from '@tillstack/secrets-node'
// Reads TILLSECRETS_TOKEN (+ optional TILLSECRETS_URL / TILLSECRETS_ENV),
// pulls the environment, and injects each KEY into process.env.
await load()
// process.env.STRIPE_KEY is now populated — nothing was committed to disk.Under the hood every SDK does the same thing: a single POST /api/secrets/pull that returns the decrypted { secrets, dynamic, withheld } map for the token’s environment (withheld names restricted keys the token may not read). The three packages — @tillstack/secrets-node, @tillstack/secrets-edge, and the @tillstack/secrets-core they both wrap — are covered in the config guide. Start from the Quickstart for the full loop.
Credentials that expire on their own
A dynamic secret never has a stored value. You register a backend once, then declare a key that references it. For a Postgres backend, when an app needs a credential it leases one: TillSecrets opens a connection to your database, runs CREATE ROLE … VALID UNTIL with your grants, and hands back a username and password that stop working when the lease expires — dropped automatically. We store a reference to the lease, never the credential.
That means a leaked database password has a blast radius measured in minutes, and every app instance gets its own credential. TillSecrets can also mint API keys, SSH key pairs and TillForge tokens per lease. Full walkthrough in Dynamic secrets.
Sync to your own platform
If a workload can’t call the pull API — a build step, a platform that only reads its own env vars — TillSecrets can push an environment’s values into your own platform instead. The targets are vendor-agnostic:
Cloudflare · Vercel · Railway · GitHub · AWS SSM · dotenv
Pick a target, and TillSecrets writes the environment’s current values to it — so the source of truth is still TillSecrets, but your platform sees plain env vars. No lock-in: the same environment can fan out to more than one target. See Sync targets.
Encrypted end to end
This is the whole pitch, so it’s worth stating plainly:
- Envelope encryption. Every value is
AES-256-GCMencrypted under a data key (DEK) unique to your org, which is itself wrapped by a master key (KEK). Postgres holds only ciphertext; plaintext is assembled in memory only on an authorized read. - Everything is audited. Every read, write, rollback, token mint or revoke, and lease issue or revoke is appended to a tamper-evident log.
- Tokens are bearer-only.
ts_…tokens are stored sha256-hashed, never appear in a URL, and are revocable and optionally expiring.
ts_… token can decrypt an environment. Keep it in a server-side environment variable — never in a browser bundle, an app binary, or a committed file. Prefer a read-scoped token pinned to one environment for anything an app boots with.The full model — the DEK/KEK split, the audit actions, SSRF guards on dynamic backends — is in Security.
Start with the Quickstart or keep the How-to manual handy, then read the Config store, Dynamic secrets, the Secretless broker (use a secret without knowing it), Secret requests, Agent tokens, MCP servers, the SSH agent, Approvals, Sealed secrets, Spend anomalies, Canaries, Leaked values, Post-quantum keys, Encrypted delivery, Workload identity, Workload SVIDs, and Security. TillSecrets is part of TillDev — one workspace, one login, one audit log. Prefer the pitch? See the product page.