TILLSECRETS · TILLDEV

Config, versioned.
Secrets, sealed.

One encrypted store for the values your app boots with. Versioned config you can pull into any environment and roll back in one command, and short-lived credentials leased on demand and revoked on expiry. Every value is envelope-encrypted — Postgres only ever holds ciphertext.

LIVE · sealing + leasing
aes-256-gcm · per-org data key · versioned · audited · dynamic-lease
WHAT’S INSIDE

One store, two kinds of secret.

Static configGA

Versioned config values scoped org → project → environment → secret → version. Pull them into any app at boot, or write them from the CLI.

› Read the docs
Dynamic secretsGA

Short-lived credentials leased on demand from an upstream backend and auto-revoked on expiry. Postgres roles today, more backends to come.

› Read the docs
Envelope encryptionGA

Every value is AES-256-GCM encrypted under a per-org data key that is itself wrapped by a master KEK. The database only ever sees ciphertext.

› Read the docs
Versioning & rollbackGA

Every write is a new version. See the history of a value, diff environments, and roll a bad deploy back to the last-known-good in one command.

› Read the docs
Sync targetsGA

Push an environment’s values to your own platform — Cloudflare, Vercel, Railway, GitHub, or AWS SSM. Vendor-agnostic, no lock-in.

› Read the docs
Audited readsGA

Reveal and pull are audited. Plaintext is assembled only in memory on an authorized read, and every one lands in the shared TillDev audit log.

› Read the docs
THE SHAPE OF IT

Config that lives. Secrets that don’t.

Most apps need both: long-lived config values you set once and read at boot, and credentials you’d rather never store at all. TillSecrets keeps both under one roof, one token, one audit log.

STATIC CONFIG · DOPPLER SHAPE

Values you set and read

Versioned, envelope-encrypted config scoped org → project → environment → secret → version.

Pulled into any app at boot with the SDK or the CLI — or synced to your own platform.

Every write is a new version, so rollback is a command, not an incident.

DYNAMIC SECRETS · VAULT SHAPE

Credentials you never keep

Short-lived credentials leased on demand from an upstream backend, auto-revoked on expiry.

Postgres roles are the first backend; the model is built to extend to more.

TillSecrets stores a lease reference, never the credential itself.

THE CONFIG LAYER

Pull it in at boot.

Config is scoped org → project → environmentdev, staging, prod — then down to each secret and each version. load() pulls the right environment into process.env, decrypted in memory, at startup.

Every write is a new version. When a deploy ships a bad value, roll the secret back to the last-known-good with tilldev secrets rollback — no redeploy, no digging through a chat log for the old value.

Apps authenticate with a service token (ts_…) — bearer-only, stored hashed, scoped to one project and environment.

Config & versioning guide →
ts · boot.ts
// Boot: pull this environment's config into process.env.
import { load } from '@tilldev/secrets-node'

// Reads the service token (ts_…) from the environment, fetches
// this project + environment, decrypts each value in memory, and
// injects it into process.env — nothing is written to disk.
await load()

console.log(process.env.STRIPE_KEY) // set, decrypted, in-memory only
OR STRAIGHT FROM THE CLI
bash · terminal
# Set a value in prod — versioned + envelope-encrypted server-side.
tilldev secrets set STRIPE_KEY --env prod

# Pull the whole environment (to .env, or straight into your shell).
tilldev secrets pull --env prod

# Ship a bad value? Roll it back to the previous version.
tilldev secrets rollback STRIPE_KEY --env prod
CIPHERTEXT, NOT SECRETS

The database never sees
a plaintext value.

Every value is AES-256-GCM encrypted under a per-org data key, and that data key is itself wrapped by a master KEK — envelope encryption. Postgres only ever holds ciphertext; a database dump is a wall of noise.

Plaintext is assembled only in memory, on an authorized read, and every reveal and pull is written to the audit log. You always know what was read, by which token, and when.

THE SECURITY MODEL
Envelope-encrypted

AES-256-GCM under a per-org data key, wrapped by a master KEK.

Ciphertext at rest

Postgres stores only encrypted bytes — never a readable value.

In-memory only

Plaintext is assembled on an authorized read and never persisted.

Bearer tokens, hashed

Service tokens are ts_… — bearer-only, stored as hashes, scoped and revocable.

Every read audited

Reveal and pull land in the shared TillDev audit log, with the acting token.

SSRF-guarded upstreams

Dynamic backends are reached through SSRF guards — no pivot to internal metadata.

NO LOCK-IN

Or push it to your own platform.

You don’t have to read config through TillSecrets at runtime. A sync target pushes an environment’s values into the platform you already deploy on, so the values live where your app expects them. TillSecrets stays the source of truth; the sync is one-way and vendor-agnostic.

Cloudflare
Workers + Pages secrets
Vercel
Project env vars
Railway
Service variables
GitHub
Actions secrets
AWS SSM
Parameter Store

Set a value once in TillSecrets, sync it out to any of these — no rewriting the same secret into five dashboards, and no proprietary runtime to depend on.

THE DYNAMIC LAYER

Credentials that expire.

The safest secret is the one that doesn’t exist yet. A dynamic secret is leased on demand from an upstream backend, valid for a short TTL, and auto-revoked upstream when it expires. A leaked lease is a dead lease in minutes.

Postgres roles are the first backend: ask for a role, get a username and password scoped to it, use them, let them expire. The model is built to extend to more backends over time.

TillSecrets stores only a lease reference — never the credential — and reaches the upstream through SSRF guards.

Dynamic secrets guide →
ts · report.ts
// Lease a short-lived Postgres role on demand.
import { lease } from '@tilldev/secrets-node'

const cred = await lease('postgres/reporting', { ttl: '15m' })
// cred.username / cred.password are valid for 15 minutes,
// then revoked upstream automatically.

const db = connect({ user: cred.username, password: cred.password })
// TillSecrets kept a lease reference — never the credential itself.
NOT A SILO

Same workspace. Same login.

TillSecrets isn’t a separate vault to run — it’s the config-and-credentials layer of the TillDev workspace you already have, with the same org, the same members and roles, and the same audit log.

01 · RECORDS

One audit log

Every reveal, pull, version, sync, and lease writes to the shared TillDev audit log, next to your Pulse, Auth, and Shield events — one history, not five.

02 · SHIPS WITH

The same shell

One login, one bill, one set of members and roles. Add TillSecrets to a workspace that already runs TillPulse and it inherits the org and billing you already have.

03 · FEEDS

The rest of the family

The config your apps boot with and the credentials they lease live where the rest of TillDev can see the audit trail — secrets aren’t a blind spot in your workspace.

TURN IT ON

Get your config out of a chat thread and your credentials out of a static string. Versioned, encrypted, and audited — one workspace away.

PART OF TILLDEV

One of seven.

You don't have to use the rest. But they fit together — same workspace, same audit log, same shortcut to switch between them. Add what you need when you need it.