From zero to a pulled secret.
GACreate a project and an environment, set a value, mint a token, and pull it into a running app. A few minutes, no .env file to leak.
Create a project + environment
A project groups your app’s config; an environment (dev / staging / prod) is where the actual keys live. Do it in the dashboard under /<org>/secrets, or from the CLI:
# Create a project, then an environment inside it.
tilldev secrets projects create --name "Acme API" --slug acme-api
# → project prj_… (copy the id)
tilldev secrets env create --project prj_… --name Production --slug prod
# → environment env_… (copy the id)Set a secret
Keys are env-var-style — uppercase letters, digits, and underscores. Setting a key that already exists allocates a new version; the old one is kept.
# Set a value — prompts for it (hidden input) if you omit --value.
tilldev secrets set STRIPE_KEY --env env_…
tilldev secrets set DATABASE_HOST --env env_… --value db.internal.example.com
# List the keys in an environment (never the values).
tilldev secrets ls --env env_…Mint a service token
An app authenticates its pull with a ts_… service token. Scope it read for a consumer and pin it to a single environment so it can never read another:
# A read-only token, pinned to one environment — what an app boots with.
tilldev secrets tokens create --project prj_… --scope read --env env_…
# → ts_a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4 (shown once — copy it now)ts_… is printed a single time and stored only as a sha256 hash — copy it now. If you lose it, revoke it and mint another. It is a server secret: keep it out of client bundles and version control.Install an SDK
pnpm add @tilldev/secrets-nodeOn an edge runtime (Cloudflare Workers, Vercel, Deno, Bun) use @tilldev/secrets-edge instead. Both wrap the same @tilldev/secrets-core.
Provide the token through the environment:
TILLSECRETS_TOKEN=ts_a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4
# Optional — only needed if the token is project-scoped (not pinned to an env):
# TILLSECRETS_ENV=env_…
# Optional — override the API base (defaults to https://tilldev.dev):
# TILLSECRETS_URL=https://tilldev.devPull it into your app
On Node, load() injects everything into process.env. Call it before you read any config:
// server.ts — the first thing that runs.
import { load } from '@tilldev/secrets-node'
// Reads TILLSECRETS_TOKEN, pulls the environment, injects each KEY
// into process.env. Existing env vars win (like dotenv) unless you
// pass { override: true }.
await load()
console.log(process.env.STRIPE_KEY) // sk_live_… (pulled, not committed)Prefer an explicit client — or need the list of dynamic keys? Use createClient() and pull():
import { createClient } from '@tilldev/secrets-node'
const secrets = createClient() // from TILLSECRETS_* env
const { secrets: values, dynamic } = await secrets.pull()
values.DATABASE_HOST // 'db.internal.example.com'
dynamic // ['DATABASE_URL', …] — keys you lease separately
const stripe = await secrets.get('STRIPE_KEY') // convenience over pull()Or on the edge
Edge runtimes have no process.env, so pass url and token explicitly from the platform’s bindings:
import { createClient } from '@tilldev/secrets-edge'
export default {
async fetch(_req: Request, env: Env) {
// Edge runtimes have no process.env — pass url + token explicitly.
const secrets = createClient({ url: env.TILLSECRETS_URL, token: env.TILLSECRETS_TOKEN })
const { secrets: values } = await secrets.pull()
return new Response(values.GREETING ?? 'hello')
},
}What's next
- Config store — versions, rollback, soft-delete, token scopes, the SDKs in depth, and sync targets.
- Dynamic secrets — register a Postgres backend and lease credentials that expire on their own.
- Security — the envelope-encryption model and the audit trail.