secretdef

Documentation

Everything you need to know about secretdef. It's a small library — this is the whole doc.

Install

Terminal
npm i secretdef
# or
pnpm add secretdef

Zero dependencies. Works with Node.js, Bun, Deno. ~2KB minified.

Quick start with AI

secretdef includes an Agent Skill that works with Claude Code, Cursor, Codex, and other AI coding tools.

Terminal
npx skills add iplanwebsites/secretdef

Then open your project and tell the AI:

Create secret definitions. Ensure all secrets have definitions.

The skill scans your codebase for process.env usage, generates defineSecrets calls with rich descriptions, and wires up validation at startup.

Define your secrets

Create a secrets.ts file listing the environment variables your app (or module) needs. Include a description and a dashboard URL so anyone — human or AI — knows where to get the value.

src/secrets.ts
// src/secrets.ts
import { defineSecrets } from 'secretdef';

export const secrets = defineSecrets({
  DATABASE_URL: {
    description: 'Postgres connection string — check your hosting dashboard',
    validate: 'url',
    devDefault: 'postgresql://localhost:5432/myapp_dev',
  },
  STRIPE_SECRET_KEY: {
    description: 'Stripe API secret key — https://dashboard.stripe.com/apikeys',
    example: 'sk_live_...',
    validate: 'str',
    environments: {
      development: { default: 'sk_test_placeholder' },
    },
  },
  ANALYTICS_KEY: {
    description: 'Analytics write key',
    required: false,
  },
});

defineSecrets returns the same specs object as pure data. It does nothing at runtime unless auto-register is enabled.

Validate at startup

Call validateSecrets() once at app startup. It checks every declared secret against the environment and returns a typed map of resolved values.

Auto-register
easy

Call enableAutoRegister() once. Every defineSecrets call automatically pushes its specs to a global registry. Then validateSecrets() with no arguments checks them all.

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

// Import your secrets files — each defineSecrets call auto-registers
import './secrets';
import './modules/db/secrets';
import './modules/email/secrets';

// One call checks everything
const env = validateSecrets();

Explicit spreading
structured

For full control with no global state, spread each module's exports into a single call.

src/env.ts
// src/env.ts
import { validateSecrets } from 'secretdef';
import { secrets as app } from './secrets';
import { secrets as db } from './modules/db/secrets';
import { secrets as email } from './modules/email/secrets';

export const env = validateSecrets({
  ...app,
  ...db,
  ...email,
});

Validation behavior

Terminal
🔴 Missing 2 secret(s) [env=production]:
✗ STRIPE_SECRET_KEY
Stripe API secret key — https://dashboard.stripe.com/apikeys
registered by: src/secrets.ts

✗ DATABASE_URL
Postgres connection string
registered by: src/modules/db/secrets.ts
  • Production: missing required secrets print an error table and call process.exit(1).
  • Development: missing secrets print a warning. The server starts — they'll throw if accessed at runtime via useSecret().

Read secrets with useSecret()

A drop-in replacement for process.env.KEY. Returns the value or throws a structured error with the var name, description, dashboard URL, and which file registered it.

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

// Instead of: process.env.STRIPE_SECRET_KEY  (returns undefined silently)
const key = useSecret('STRIPE_SECRET_KEY');   // throws with actionable error if missing

With explicit spreading, pass the specs map directly:

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

// Pass an explicit map — no global registry needed
const key = useSecret('STRIPE_SECRET_KEY', secrets);

Validation

Add validate to check that a secret's value has the right format. Use a built-in validator name or pass a custom function. Add choices to restrict to an allowlist.

src/secrets.ts
import { defineSecrets } from 'secretdef';

export const secrets = defineSecrets({
  PORT: {
    description: 'Server port',
    validate: 'port',          // built-in: port (0-65535)
    devDefault: '3000',
  },
  ADMIN_EMAIL: {
    description: 'Admin notification address',
    validate: 'email',         // built-in: email
  },
  LOG_LEVEL: {
    description: 'Logging verbosity',
    choices: ['debug', 'info', 'warn', 'error'],
    devDefault: 'debug',
  },
  SERVICE_CONFIG: {
    description: 'JSON config blob from dashboard',
    validate: 'json',          // built-in: parses JSON
  },
  CUSTOM_TOKEN: {
    description: 'Must start with tok_',
    validate: (v) => {         // custom function
      if (!v.startsWith('tok_')) throw new Error('Must start with tok_');
      return v;
    },
  },
});
Built-in validators
// Built-in validators:
// str    — non-empty string
// bool   — true/false/t/f/1/0/yes/no/on/off
// num    — any number
// email  — basic email format
// host   — domain name or IP address
// port   — integer 0-65535
// url    — valid URL (via new URL())
// json   — valid JSON (via JSON.parse())

Validation runs during validateSecrets() and useSecret(). Invalid values appear in the same error/warning table as missing secrets.

Environment overrides & dev defaults

Use devDefault for a quick default outside production, or environments for fine-grained control per environment (different required flag, default value, or env var name). The environment is read from NODE_ENV.

src/secrets.ts
export const secrets = defineSecrets({
  STRIPE_SECRET_KEY: {
    description: 'Stripe API secret key — https://dashboard.stripe.com/apikeys',
    example: 'sk_live_...',
    devDefault: 'sk_test_placeholder',   // shorthand for environments.development.default
  },
  STRIPE_WEBHOOK_SECRET: {
    description: 'Webhook signing secret',
    environments: {
      development: {
        required: false,        // optional in dev
        default: 'whsec_test',  // fallback value
      },
    },
  },
});

Framework examples

The repo includes complete working examples for Express, Hono, and Next.js in the examples/ directory. Each one shows the same pattern:

  • secrets.ts — declares secrets with defineSecrets
  • server.ts — calls validateSecrets at startup, useSecret at point of use
  • tests — covers clean start, missing warnings, production errors, dev defaults, and runtime throws
FrameworkRun
Expresspnpm --filter secretdef-example-express dev
Honopnpm --filter secretdef-example-hono dev
Next.jspnpm --filter secretdef-example-nextjs dev

Try dev:ok (all secrets set), dev:missing (missing in dev — warns), and dev:production (missing in prod — exits) to see the different behaviors.

API reference

defineSecrets(specs)

Declares secret requirements. Returns the same Record<string, SecretSpec> you passed in. If enableAutoRegister() was called first, also pushes specs to the global registry.

validateSecrets(specs?, env?, options?)

Validates an explicit specs map, or the auto-registry if no map is passed. Returns a Record<string, string> of resolved values. In production, missing required secrets exit the process. In development, they warn.

useSecret(key, specs?)

Reads a single secret. Returns the string value or throws a SecretNotAvailableError with structured context (env var, description, URL, registering file). Pass an explicit specs map or omit to use the auto-registry.

enableAutoRegister()

Opt-in. After calling this, every defineSecrets call also pushes its specs to a global registry. Call once at the top of your app, before imports.

builtinValidators

Record of all built-in validator functions (str, bool, num, email, host, port, url, json). Exported for advanced use — typically you just pass the name as a string to validate.

SecretSpec

types
// The object key IS the env var name
type SecretSpec = {
  description?: string;        // Human-readable — include a dashboard URL!
  required?: boolean;          // Default: true
  validate?: SecretValidation; // Built-in name or custom function
  choices?: string[];          // Allowlist of valid values
  example?: string;            // Example value shown in errors
  devDefault?: string;         // Default for non-production environments
  group?: string;              // Group label in validation output
  environments?: {             // Per-environment overrides
    [env: string]: {
      envVar?: string;         // Different env var name for this environment
      required?: boolean;      // Override required flag
      default?: string;        // Override default value
    };
  };
};

// String shorthand also works:
defineSecrets({ MY_KEY: 'description text' });
// → equivalent to { MY_KEY: { description: 'description text' } }

Best practices

  • Define secrets where they're used.Put Stripe secrets next to your Stripe code, DB 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 exactly what's wrong and how to fix it.
  • Always include a description with a URL.Future you, your teammates, and AI agents will thank you. A dashboard link turns a 10-minute scavenger hunt into a single click.
  • Use environments for dev/staging differences.Stripe test keys, local database URLs, optional webhooks — declare the differences per environment instead of documenting them elsewhere.
  • One secrets.ts per module, not one giant file.Each module owns its secret definitions. Auto-register or explicit spreading collects them at the top level.

Example project structure

src/modules/db/secrets.ts
// src/modules/db/secrets.ts — lives next to your DB code
import { defineSecrets } from 'secretdef';

export const secrets = defineSecrets({
  DATABASE_URL: {
    description: 'Postgres connection string',
  },
  DATABASE_POOL_SIZE: {
    description: 'Connection pool size',
    required: false,
  },
});

CLAUDE.md integration

secretdef is designed to work with AI coding agents like Claude Code. Add a note about your secret setup to your project's CLAUDE.md so Claude understands how your app manages secrets.

CLAUDE.md (add to your project)
# CLAUDE.md
# Add secretdef info so Claude understands your secret setup

## Secrets
This project uses `secretdef` to declare environment variable requirements.
Secret definitions live in `src/secrets.ts` and per-module files.

Run `validateSecrets()` at startup (see src/index.ts).
Use `useSecret('KEY')` instead of `process.env.KEY`.

If you see SecretNotAvailableError, read the error — it tells you
the env var name, where to get it, and which file registered it.

How it works with AI agents

When an AI agent encounters a missing secret, secretdef's structured error output gives it everything needed to fix the problem — no guessing, no searching through docs:

Terminal
SecretNotAvailableError: STRIPE_SECRET_KEY is not available
env var: STRIPE_SECRET_KEY
description: Stripe API secret key. Starts with sk_live_
dashboard: https://dashboard.stripe.com/apikeys
registered: src/secrets.ts
environment: development

→ Set STRIPE_SECRET_KEY in .env or your hosting dashboard

The agent reads the env var name, the dashboard URL, and the fix instruction. Combined with the CLAUDE.md context and the bundled skill, Claude can resolve missing secrets without asking you.

Claude Code skill

secretdef ships with a Claude Code skill that teaches Claude how to define secrets, fix SecretNotAvailableError, and follow best practices — automatically.

Run npx skills add iplanwebsites/secretdef to install the skill. It auto-detects your AI tools (Claude Code, Cursor, Codex, etc.) and installs to the right location.

File layout
# Install the skill via npx:
npx skills add iplanwebsites/secretdef

# It installs to your project:
.claude/skills/secretdef/SKILL.md   # Claude Code
.cursor/skills/secretdef/SKILL.md   # Cursor
# ... auto-detected for each agent

When Claude encounters a missing secret error, it reads the structured error output and knows exactly which env var to set, where to find the value, and which file registered the requirement. The skill also helps Claude write good defineSecrets calls with descriptive, actionable descriptions.

Follows the Agent Skills open standard — works with any AI tool that supports it.

Community packages

Like @types for TypeScript, @secretdef/* packages provide ready-made definitions for popular services. These are community-maintained — contributions welcome.

Most SDKs don't hardcode which env vars they read. These packages represent common conventions, not guarantees. Verify against your SDK, or define your own.

CategoryPackages
Payment@secretdef/stripe, @secretdef/paypal
Auth@secretdef/auth0, @secretdef/clerk, @secretdef/supabase, @secretdef/firebase
Email@secretdef/sendgrid, @secretdef/resend, @secretdef/postmark
AI@secretdef/openai, @secretdef/anthropic
Cloud@secretdef/aws, @secretdef/gcp
Database@secretdef/planetscale, @secretdef/neon, @secretdef/turso
Messaging@secretdef/twilio
Analytics@secretdef/segment, @secretdef/mixpanel

GitHub · npm · MIT License