TILLSECRETS · CONFIG STORE

Versioned, encrypted config.

GA

The static config store holds the values you set and version: API keys, connection strings, feature flags. Everything is envelope-encrypted at rest, every change is an immutable version you can roll back to, and the whole thing is pulled at boot over one authenticated call.

01Model

Projects, environments, keys

Four levels, top to bottom:

  • Project — one per app or service. Owns environments and the service tokens.
  • Environmentdev, staging, prod, or whatever you name. This is the unit you pull and the unit a token is (optionally) pinned to.
  • Key — env-var-style: ^[A-Z_][A-Z0-9_]*$. Each key is either static (a stored value) or dynamic (a leased credential).
  • Version — every write to a static key creates a new immutable version and advances a current pointer.
02Write & read

Set, list, reveal

Writes require an admin or owner role. Listing shows keys and metadata but never values — the only way to see a plaintext is an audited get / reveal.

bash
# Prompts for the value (hidden) if you omit --value.
tilldev secrets set STRIPE_KEY --env env_…
tilldev secrets set DATABASE_HOST --env env_… --value db.internal.example.com

# List keys + metadata (kind, current version, updated_at) — never values.
tilldev secrets ls --env env_…

# Reveal one value. This is an AUDITED read; --version defaults to current.
tilldev secrets get scr_… --version 2
CommandWhat it does
secrets set KEY --envCreate the key or add a new version. Prompts for the value unless you pass --value.
secrets ls --envList keys, kind, and current version. No values.
secrets get scr_…Reveal a plaintext (current, or --version N). Admin-only and always written to the audit log.
secrets rm scr_…Soft-delete the key — it stops resolving in pulls, but its version history is retained.
03Versions

Immutable versions & rollback

A version is never edited or overwritten. Setting a key stores a new ciphertext under the next version number; the live value is whichever version the current pointer names. Rollback doesn’t restore or copy anything — it just moves that pointer to an older version, which is why it’s instant and lossless.

bash
tilldev secrets versions scr_…
#  v3   2026-07-10   "rotate live key"   (current)
#  v2   2026-06-02   "promote staging"
#  v1   2026-05-11   "initial"

# Roll back = move the "current" pointer to an older, unchanged version.
tilldev secrets rollback scr_… --version 2

A soft-delete (secrets rm) removes a key from pulls but keeps every version, so a delete is auditable and recoverable. Nothing about a value’s history is destroyed by day-to-day use.

Immutable by design
Because versions never mutate, the audit log can point at an exact version for every read and rollback. “What was in prod on the 3rd, and who read it?” is answerable.
04Tokens

Service tokens & scopes

A ts_… service token is what a non-human client authenticates with. Each token is scoped to one project, carries a scope, and can optionally be pinned to one environment. It can also be given an expiry.

ScopeGrants
readPull the environment (static values + the list of dynamic keys) and lease dynamic credentials. The right default for an app.
read_writeEverything read can do, plus revoking a dynamic lease early. It does not let a token set static values — writes are admin-gated in the dashboard/CLI.
bash
# read → pull only.  read_write → also revoke dynamic leases.
tilldev secrets tokens create --project prj_… --scope read --env env_…
tilldev secrets tokens create --project prj_… --scope read_write

tilldev secrets tokens ls --project prj_…
tilldev secrets tokens revoke tok_… --project prj_…   # instant, irreversible
Bearer only
Send the token as Authorization: Bearer ts_… — never in a query string or path, where it would land in logs. It’s stored sha256-hashed; if it leaks, revoke kills it immediately.
05API

The pull endpoint

Everything above sits on top of one HTTP call. All the SDKs make it for you, but it’s plain enough to hit directly:

bash
curl -X POST https://tilldev.dev/api/secrets/pull \
  -H "authorization: Bearer $TILLSECRETS_TOKEN" \
  -H "content-type: application/json" \
  -d '{}'   # env-pinned token. A project-scoped token sends {"environment_id":"env_…"}

# → {
#     "secrets": { "STRIPE_KEY": "sk_live_…", "DATABASE_HOST": "db.internal.example.com" },
#     "dynamic": [ "DATABASE_URL" ]
#   }

It returns { secrets, dynamic }: the decrypted key→value map, plus the names of any dynamic keys in the environment (their values are leased, never returned here). An env-pinned token needs no body; a project-scoped token must pass environment_id, and can only reach environments inside its own project.

06SDKs

The SDKs

Three packages, one client underneath:

PackageRuntimeEntry point
@tilldev/secrets-nodeNode 18+load() injects into process.env; createClient() reads TILLSECRETS_* from the environment.
@tilldev/secrets-edgeAny fetch runtimeCloudflare Workers, Vercel, Deno, Bun. createClient({ url, token }) — pass them explicitly.
@tilldev/secrets-coreRuntime-agnosticThe SecretsClient both wrap. Use it directly to inject a custom fetch.

The core client

ts
import { SecretsClient } from '@tilldev/secrets-core'

const client = new SecretsClient({
  url: 'https://tilldev.dev',     // optional; this is the default
  token: process.env.TILLSECRETS_TOKEN!,
  environmentId: process.env.TILLSECRETS_ENV,  // only for project-scoped tokens
})

const { secrets, dynamic } = await client.pull()
const key = await client.get('STRIPE_KEY')     // convenience over pull()

Environment variables the Node SDK reads

  • TILLSECRETS_TOKEN — the ts_… token. Required.
  • TILLSECRETS_URL — API base. Optional; defaults to https://tilldev.dev.
  • TILLSECRETS_ENV — environment id. Optional; only needed when the token is project-scoped rather than pinned to one environment.

load() follows dotenv’s rule — a variable already present in process.env is left alone unless you pass load({ override: true }).

07Sync

Sync targets

Not every workload can call the pull API — a CI build, a serverless platform that only reads its own env vars, a container that boots before your code runs. For those, a sync target pushes an environment’s current values into the platform you already use. TillSecrets stays the source of truth; the platform just sees plain env vars.

The targets are vendor-agnostic:

TargetWrites to
cloudflareWorker / Pages secrets.
vercelProject environment variables.
railwayService variables.
githubActions / repository secrets.
aws_ssmSSM Parameter Store (SecureString).
dotenvA generated .env for local use.

Each target’s credentials are encrypted at rest under the same per-org key as your secrets, and every push is written to the audit log as a sync.push. Configure targets from the dashboard under Secrets → Sync. The same environment can fan out to more than one target — no lock-in to any single platform.


Next: Dynamic secrets for credentials that expire on their own, or Security for the encryption and audit model. Back to the TillSecrets overview.