twelve-factor
Provides guidance on implementing 12-Factor App architectural patterns for robust, deployable applications.
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.zipInstalls 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.Key capabilities
- →Store all application configuration in environment variables
- →Validate configuration at startup with a schema and fail fast on invalidity
- →Inject configuration via options objects to avoid `process.env` deep imports
- →Explicitly declare all dependencies in manifest files
- →Implement graceful shutdown handlers with drain timeouts
- →Write logs as structured JSON to stdout
How it works
The skill provides patterns for managing configuration, dependencies, backing services, disposability, and logging, ensuring applications are deployable and scalable.
Inputs & outputs
When to use twelve-factor
- →Structuring app startup/shutdown
- →Configuring backing services
- →Implementing env var validation
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:
- Config (Factor III) — add env var validation without restructuring
- Logs (Factor XI) — switch to structured stdout logging
- Disposability (Factor IX) — add graceful shutdown handlers
- Backing services (Factor IV) — abstract connections behind config URLs
- 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 ...')orchild_processcalls 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
Procfileor 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.
When not to use it
- →When hardcoding URLs, credentials, or per-environment values
- →When relying on implicit system-wide packages
- →When using in-memory session state or local filesystem state between requests
Limitations
- →Server-specific factors apply only to backend services that run as long-lived processes
- →It requires explicit declaration of all dependencies
- →It mandates storing all configuration in environment variables
How it compares
This skill provides a complete set of 12-Factor App patterns with code examples and a checklist, guiding the structuring of applications for cloud-native environments, unlike ad-hoc development practices.
Compared to similar skills
twelve-factor side by side with the closest alternatives in the catalog.
| Skill | Installs | Updated | Safety | Difficulty |
|---|---|---|---|---|
| twelve-factor (this skill) | 0 | 3mo | Review | Intermediate |
| cloudflare-manager | 25 | 9mo | Review | Intermediate |
| railway-cli-management | 9 | 8mo | Review | Intermediate |
| azure-functions | 10 | 5mo | Review | Intermediate |
Try saying
Example prompts that trigger this skill in your AI assistant.
More by chloebrett
View all by chloebrett →You might also like
cloudflare-manager
qdhenry
Comprehensive Cloudflare account management for deploying Workers, KV Storage, R2, Pages, DNS, and Routes. Use when deploying cloudflare services, managing worker containers, configuring KV/R2 storage, or setting up DNS/routing. Requires CLOUDFLARE_API_KEY in .env and Bun runtime with dependencies installed.
railway-cli-management
CaptainCrouton89
Deploy, manage services, view logs, and configure Railway infrastructure. Use when deploying to Railway, managing environment variables, viewing deployment logs, scaling services, or managing volumes.
azure-functions
aj-geddes
Create serverless functions on Azure with triggers, bindings, authentication, and monitoring. Use for event-driven computing without managing infrastructure.
nuxthub-migration
onmax
Use when migrating NuxtHub projects or when user mentions NuxtHub Admin sunset, GitHub Actions deployment removal, self-hosting NuxtHub, or upgrading to v1/nightly. Covers v0.9.X self-hosting (stable) and v1/nightly multi-cloud (experimental, database/blob not ready).
deployment-pipeline-design
wshobson
Design multi-stage CI/CD pipelines with approval gates, security checks, and deployment orchestration. Use when architecting deployment workflows, setting up continuous delivery, or implementing GitOps practices.
terraform-module-library
wshobson
Build reusable Terraform modules for AWS, Azure, and GCP infrastructure following infrastructure-as-code best practices. Use when creating infrastructure modules, standardizing cloud provisioning, or implementing reusable IaC components.