secretdef
v0.1
zero-dependency
~2KB

Your app has secret dependencies. Declare them.

Every service you integrate needs API keys, tokens, and connection strings. But there's no standard way for code to say which secrets it needs, where to get them, or how they differ per environment.

$npm i secretdef

Deploys fail with useless errors

TypeError: Cannot read properties of undefined tells you nothing. You redeploy 3 times before you find the missing key.

Nobody knows what's provisioned

Is Stripe set up in staging? Which Supabase project is prod pointing at? Where do you get a Resend key?

AI agents hit a wall

Cursor and Claude Code scaffold integrations in seconds — then crash on a missing env var with a generic TypeError.

How it works

You list the secrets your app needs — the env var name, a human-readable description, and where to get the value. At startup, one call checks everything and tells you exactly what's missing.

1.List what your app needs

src/secrets.ts
// src/secrets.ts — your app's custom secrets
import { defineSecrets } from 'secretdef';

export const secrets = defineSecrets({
  DATABASE_URL: {
    envVar: 'DATABASE_URL',
    description: 'Postgres connection string — format: postgresql://user:pass@host/db',
    envOverrides: {
      development: { default: 'postgresql://localhost:5432/myapp_dev' },
    },
  },
});

2.Validate at startup

src/index.ts
// src/index.ts
import { enableAutoRegister, validateSecrets } from 'secretdef';
enableAutoRegister();

import '@secretdef/stripe';  // published — rich descriptions baked in
import './secrets';           // yours — custom app secrets

const env = validateSecrets();

3.Get actionable errors instead of cryptic crashes

Terminal
🔴 Missing 2 secret(s) [env=production]:
✗ STRIPE_SECRET_KEY
Stripe API secret key. Starts with sk_live_ (not pk_).
→ https://dashboard.stripe.com/apikeys
from: @secretdef/stripe

✗ DATABASE_URL
Postgres connection string — format: postgresql://user:pass@host/db
from: src/secrets.ts
✓ STRIPE_WEBHOOK_SECRET — set (from: @secretdef/stripe)
Want more control? Use explicit spreading instead
src/env.ts
// src/env.ts — the structured way
import { validateSecrets } from 'secretdef';
import { secrets as stripe } from '@secretdef/stripe';
import { secrets as app } from './secrets';

export const env = validateSecrets({
  ...stripe,  // published definitions
  ...app,     // your custom secrets
});

Same result, no global state. You spread each module's secrets into a single validation call.

Read secrets safely

useSecret is a drop-in for process.env that throws a structured, actionable error if the secret is missing — instead of silently returning undefined.

src/modules/stripe/client.ts
import { useSecret } from 'secretdef';

const key = useSecret('STRIPE_SECRET_KEY');
//
// If missing → throws with:
//   ✗ STRIPE_SECRET_KEY
//     Stripe API secret key. Starts with sk_live_ (not pk_).
//     → https://dashboard.stripe.com/apikeys
//     from: @secretdef/stripe
//
// Instead of: TypeError: Cannot read properties of undefined

Errors that fix themselves

With good context in your definitions, missing secrets become self-resolving. AI agents will write these descriptions for you — and read them back when something's missing.

STRIPE_SECRET_KEY

Stripe API secret key. Starts with sk_live_ (prod) or sk_test_ (dev). Not the publishable key (pk_). Provision at https://dashboard.stripe.com/apikeys — requires Admin role.

CLERK_SECRET_KEY

Clerk backend API key. Starts with sk_live_ or sk_test_. Dashboard: https://dashboard.clerk.com → API Keys. Not the publishable frontend key (pk_).

DATABASE_URL

Postgres connection string, format: postgresql://user:pass@host:5432/db. Neon: https://console.neon.tech → Connection Details. Supabase: Settings → Database → URI. Use the pooler URL for serverless.

These descriptions show up verbatim in error output. A dashboard link and a format hint can turn a 10-minute debugging session into a copy-paste fix.

Best practices

Define secrets where they're used

Put your Stripe secrets next to your Stripe code, database secrets next to your database module. Definitions that live far from usage drift and go stale.

Use useSecret() instead of process.env

process.env.X returns undefined silently. useSecret('X') tells you what's wrong and how to fix it.

Always include a description and URL

Future you (and your teammates, and AI agents) will thank you. A dashboard link turns a 10-minute scavenger hunt into a single click.

Use envOverrides for dev/staging differences

Stripe test keys, local database URLs, optional webhooks in dev — declare the differences per environment instead of documenting them in a README.

SDKs already exposing their secrets

Like @types for TypeScript, @secretdef/* packages declare the secrets each service needs — with descriptions, dashboard URLs, and environment-specific overrides baked in.

@secretdef/stripestripe
@secretdef/paypal@paypal/checkout-server-sdk
@secretdef/openaiopenai
@secretdef/anthropic@anthropic-ai/sdk
@secretdef/supabase@supabase/supabase-js
@secretdef/clerk@clerk/nextjs
@secretdef/auth0auth0
@secretdef/firebasefirebase-admin
@secretdef/sendgrid@sendgrid/mail
@secretdef/resendresend
@secretdef/postmarkpostmark
@secretdef/aws@aws-sdk/client-s3
@secretdef/gcp@google-cloud/storage
@secretdef/planetscale@planetscale/database
@secretdef/neon@neondatabase/serverless
@secretdef/turso@libsql/client
@secretdef/twiliotwilio
@secretdef/segmentanalytics-node
@secretdef/mixpanelmixpanel

Just import a package and its secrets are ready to validate. Using secretdef in your service? Add it to the registry so others can discover it.

Built for AI agents

When a secret is missing, the error includes the env var name, a description, a dashboard URL, and the file that registered it. That's enough for Cursor, Claude Code, or Copilot to resolve the issue in one step — no guessing loops.

Zero-dependency. ~2KB. Works with any secrets provider. Like @types for TypeScript, but for secrets.