agentskills.codes
TW

twelve-factor

12-Factor App patterns for deployable applications. Use when configuring environment variables, connecting to backing services, structuring application startup/shutdown, or handling graceful shutdown and process signals. Applies to any deployed application (services, APIs, frontends, workers). Serve

Install

mkdir -p .claude/skills/twelve-factor && curl -L -o skill.zip "https://agentskills.codes/api/skills/download/13216" && unzip -o skill.zip -d .claude/skills/twelve-factor && rm skill.zip

Installs to .claude/skills/twelve-factor

Activation

This is the description your AI agent reads to decide when to run this skill — the better it matches your request, the more reliably it fires.

12-Factor App patterns for deployable applications. Use when configuring environment variables, connecting to backing services, structuring application startup/shutdown, or handling graceful shutdown and process signals. Applies to any deployed application (services, APIs, frontends, workers). Server-specific factors (port binding, concurrency, disposability) apply only to backend services.
393 chars✓ has a “when” triggerlonger than Claude Code's old 250-char listing cap (fine on current versions)

About this skill

Twelve-Factor App Patterns

Core factors (config, dependencies, backing services, logs) apply to any deployed application — services, frontends, workers, and CLI tools. Server-specific factors (port binding, concurrency, disposability) apply only to backend services that run as long-lived processes.

Based on 12factor.net. All 12 factors are covered below. Factors that primarily affect code (config, dependencies, backing services, stateless processes, disposability, logging, concurrency) get full treatment with code examples. Factors that are primarily operational (codebase, build/release/run) get brief guidance on the code-level implications.

See the typescript-strict skill for schema-first patterns at trust boundaries. See the testing skill for how to TDD these patterns — config validation, shutdown behavior, and backing service integration are all testable through behavior-driven tests.

When to Apply

  • Greenfield projects: All 12-factor rules are mandatory. Structure the application to follow every applicable factor from the start.
  • Brownfield projects: Aim to follow as many factors as possible. Adopt incrementally in this priority order:
    1. Config (Factor III) — add env var validation without restructuring
    2. Logs (Factor XI) — switch to structured stdout logging
    3. Disposability (Factor IX) — add graceful shutdown handlers
    4. Backing services (Factor IV) — abstract connections behind config URLs
    5. Stateless processes (Factor VI) — migrate in-memory state to backing services

Codebase (Factor I)

One codebase tracked in revision control, many deploys. Each deployable service has its own codebase. Shared code between services is extracted into libraries managed via the package manager, not copy-pasted.

In a monorepo, each service should have its own entry point, its own deploy pipeline, and its own set of backing service connections. A single repo is fine as long as each service deploys independently.

Config (Factor III)

Store all configuration in environment variables. Never hardcode URLs, credentials, or per-environment values.

Validate config at startup with a schema. Fail fast if config is invalid:

import { z } from 'zod';

const ConfigSchema = z.object({
  PORT: z.coerce.number().default(3000),
  DATABASE_URL: z.string().url(),
  REDIS_URL: z.string().url(),
  API_URL: z.string().url(),
  LOG_LEVEL: z.enum(['debug', 'info', 'warn', 'error']).default('info'),
  API_KEY: z.string().min(1),
  SENTRY_DSN: z.string().url().optional(),
  ALLOWED_ORIGINS: z.string().default('').transform((s) => s === '' ? [] : s.split(',')),
});

type Config = z.infer<typeof ConfigSchema>;

export const createConfig = (env: Record<string, string | undefined> = process.env): Config => {
  const result = ConfigSchema.safeParse(env);
  if (!result.success) {
    console.error(JSON.stringify({ level: 'error', message: 'Invalid config', errors: result.error.flatten() }));
    process.exit(1);
  }
  return result.data;
};

Inject config via options objects — never import process.env deep in the call tree:

const UserSchema = z.object({ id: z.string(), name: z.string(), email: z.string().email() });
type User = z.infer<typeof UserSchema>;

export const createUserService = ({ config }: { config: Pick<Config, 'API_URL'> }) => ({
  async getUser(id: string): Promise<User> {
    const response = await fetch(`${config.API_URL}/users/${id}`);
    if (!response.ok) throw new Error(`Failed to fetch user: ${response.status}`);
    const data: unknown = await response.json();
    return UserSchema.parse(data);
  },
});

Provide .env.example as documentation (never .env with real values):

PORT=3000
DATABASE_URL=postgres://localhost:5432/myapp
REDIS_URL=redis://localhost:6379
API_URL=http://localhost:8080
LOG_LEVEL=info
API_KEY=your-api-key-here
SENTRY_DSN=
ALLOWED_ORIGINS=http://localhost:3000,http://localhost:5173

Config Anti-Patterns

const DB_HOST = 'prod-db.internal.example.com';

if (process.env.NODE_ENV === 'production') {
  connectTo('prod-db');
} else {
  connectTo('localhost');
}

const config = require(`./config.${process.env.NODE_ENV}.json`);

Why these are wrong: Config that varies by deploy belongs in env vars, not code. Environment-name branching creates combinatorial explosion and breaks dev/prod parity.

Dependencies (Factor II)

Explicitly declare all dependencies. Never rely on implicit system-wide packages.

import which from 'which';

export const checkSystemDependencies = (required: readonly string[]) => {
  const missing = required.filter((cmd) => !which.sync(cmd, { nothrow: true }));
  if (missing.length > 0) {
    throw new Error(`Missing required system dependencies: ${missing.join(', ')}`);
  }
};

Rules:

  • Every dependency in package.json (or equivalent manifest)
  • Lockfile (package-lock.json, pnpm-lock.yaml) committed to repo
  • Dependencies are isolated — the app does not leak from or depend on the system environment (use node_modules, not global installs)
  • No exec('imagemagick ...') or child_process calls to assumed system tools
  • If a system tool is required, document it explicitly and check for it at startup

Backing Services (Factor IV)

Treat every backing service (database, cache, queue, email, storage) as an attached resource identified by a URL in config.

export const createApp = ({ config }: { config: Pick<Config, 'DATABASE_URL' | 'REDIS_URL'> }) => {
  const db = createDbPool({ connectionString: config.DATABASE_URL });
  const cache = createRedisClient({ url: config.REDIS_URL });

  return {
    db,
    cache,
    async shutdown() {
      await Promise.all([db.end(), cache.quit()]);
    },
  } as const;
};

The code makes no distinction between local and third-party services. Swapping a local PostgreSQL for a managed cloud database requires only a config change, never a code change.

For projects using hexagonal architecture, backing services map naturally to ports (interfaces) and adapters (implementations). See the hexagonal-architecture skill.

Stateless Processes (Factor VI)

Execute the app as stateless, share-nothing processes. Any data that must persist lives in a backing service.

export const createSessionStore = <T>({
  redis,
  schema,
}: {
  redis: RedisClient;
  schema: z.ZodType<T>;
}) => ({
  async get(sessionId: string): Promise<T | undefined> {
    const data = await redis.get(`session:${sessionId}`);
    return data ? schema.parse(JSON.parse(data)) : undefined;
  },
  async set({ sessionId, data, ttlSeconds }: { sessionId: string; data: T; ttlSeconds: number }) {
    await redis.setex(`session:${sessionId}`, ttlSeconds, JSON.stringify(data));
  },
});

Stateless Anti-Patterns

const sessions = new Map<string, UserSession>();

app.post('/upload', (req, res) => {
  fs.writeFileSync(`/tmp/uploads/${req.file.name}`, req.file.data);
});

let requestCount = 0;
app.use(() => { requestCount++; });

setInterval(() => sendReport(), 60_000);

Why these are wrong: In-memory state is lost on restart and invisible to other process instances. Local filesystem state cannot be shared across processes. In-process schedulers run in only one instance. Use backing services (Redis, S3, database) and external schedulers instead.

See the functional skill for immutable data patterns that naturally support statelessness.

Concurrency (Factor VIII)

Scale out via the process model. Design the app so work can be divided across process types.

// web.ts — handles HTTP requests
const config = createConfig();
const app = createApp({ config });
await startServer({ app, config });

// worker.ts — processes background jobs from a queue backed by Redis
const config = createConfig();
const queue = createQueueConsumer({ url: config.REDIS_URL });
await queue.process('email', sendEmail);
await queue.process('report', generateReport);

Rules:

  • Separate entry points for each process type (web, worker, scheduler)
  • HTTP handlers dispatch background work to a queue, never process it inline
  • Each process type scales independently
  • Use a Procfile or equivalent to define process types
web: node dist/web.js
worker: node dist/worker.js

Disposability (Factor IX)

Maximize robustness with fast startup and graceful shutdown.

Health Check Endpoints

export const createHealthRoutes = ({ db }: { db: DbPool }) => ({
  '/health': async () => ({ status: 'ok' }),
  '/ready': async () => {
    await db.query('SELECT 1');
    return { status: 'ready' };
  },
});

Graceful Shutdown

const SHUTDOWN_TIMEOUT_MS = 30_000;

export const startServer = async ({ app, config }: { app: App; config: Pick<Config, 'PORT'> }) => {
  const server = app.listen(config.PORT);

  const shutdown = async (signal: 'SIGTERM' | 'SIGINT') => {
    const forceExit = setTimeout(() => process.exit(1), SHUTDOWN_TIMEOUT_MS);

    try {
      await new Promise<void>((resolve) => server.close(() => resolve()));
      await app.shutdown();
      clearTimeout(forceExit);
      process.exit(0);
    } catch (err: unknown) {
      const message = err instanceof Error ? err.message : String(err);
      const stack = err instanceof Error ? err.stack : undefined;
      console.error(JSON.stringify({ level: 'error', message: 'Shutdown error', signal, error: message, stack }));
      process.exit(1);
    }
  };

  process.on('SIGTERM', () => shutdown('SIGTERM'));
  process.on('SIGINT', () => shutdown('SIGINT'));

  return server;
};

Rules:

  • Handle SIGTERM and SIGINT for graceful shutdown
  • Set a drain timeout — force exit if shutdown hangs
  • Await server.close() to drain in-flight connections
  • Close database pools, Redis connections, queue consumers
  • Exit with non-zero code on shutdown failure
  • Keep star

Content truncated.

Search skills

Search the agent skills registry