This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
# Build the CLI for production
npm run build
# Run in development mode (direct TypeScript execution)
npm run dev -- <module> <command>
# Example: npm run dev -- db status
# Link globally for testing
npm link
# After linking, use directly
postkit <module> <command>PostKit is a modular CLI toolkit built with TypeScript and Node.js. The main code lives in the cli/ directory.
cli/
├── src/
│ ├── index.ts # Main CLI entry point using commander
│ ├── commands/ # Top-level commands (e.g., init)
│ ├── common/ # Shared utilities used by all modules
│ │ ├── config.ts # Config loader (.env, postkit.config.json)
│ │ ├── logger.ts # Chalk-based console output
│ │ ├── shell.ts # Shell command execution wrapper
│ │ └── types.ts # Shared TypeScript types
│ └── modules/ # Pluggable command modules
│ ├── db/ # Database migration module
│ │ ├── index.ts # Module registration (registerDbModule)
│ │ ├── commands/ # Command handlers (start, plan, apply, commit, deploy, remote, etc.)
│ │ ├── services/ # Core business logic (database, pgschema, dbmate)
│ │ ├── utils/ # DB-specific utilities (session, db-config, remotes)
│ │ └── types/ # DB module types
│ ├── auth/ # Keycloak auth module
│ │ ├── index.ts # Module registration (registerAuthModule)
│ │ ├── commands/ # export, import, sync
│ │ ├── services/ # Keycloak API, Docker importer
│ │ └── utils/ # Auth-specific config
│ └── stack/ # Local backend stack module
│ ├── index.ts # Module registration (registerStackModule)
│ ├── commands/ # up, down, status, logs, restart, keys, realm
│ ├── services/ # compose, db-init, health, keycloak-keys, realm-init, scaffold, sync-providers
│ ├── utils/ # stack-config, stack-state
│ └── types/ # Stack config types
├── vendor/ # Bundled binaries (pgschema for all platforms) + providers/ (Keycloak JARs)
├── dist/ # Build output (generated)
├── package.json
└── tsup.config.ts # Build configuration
PostKit uses a modular plugin architecture. Each feature is implemented as a self-contained module:
- Module registration: Each module exports a
register<Name>Module(program: Command)function that registers its subcommands with the commander program. - Command handlers: Located in
modules/<name>/commands/, each file exports a command handler function. - Services: Core business logic lives in
modules/<name>/services/(e.g., database operations, external API calls). - Utils: Module-specific utilities in
modules/<name>/utils/.
Create a new directory under cli/src/modules/<name>/ with:
index.ts- Exportregister<Name>Module(program)functioncommands/- Command handler filesservices/- Business logicutils/- Module-specific utilitiestypes/- TypeScript types (if needed)
Then import and call the registration function in cli/src/index.ts.
The db module implements a session-based migration workflow:
- Session state: Tracked in
.postkit/db/session.json. IncludesremoteNameto track which remote was used, and optionalcontainerIDfor auto Docker containers. - Named remotes: Users can configure multiple named remote databases via
db.remotesin secrets:- At least one remote must be configured
- One remote can be marked as
default: true - Managed via
postkit db remotecommands - All remote data (url, default, addedAt) stored entirely in
postkit.secrets.json— nothing remote-related inpostkit.config.json
- Binary resolution: Both
pgschemaanddbmatebinaries are auto-resolved:pgschema: Bundled invendor/pgschema/for all platforms (darwin-{arm64,amd64}, linux-{arm64,amd64}, windows-{arm64,amd64})dbmate: npm-installed via thedbmatepackage
- Auto Docker container (
modules/db/services/container.ts): WhenlocalDbUrlis empty, PostKit usesresolveLocalDb(localDbUrl, remoteUrl, spinner)which:- Checks Docker availability (
checkDockerAvailable()) - Queries remote PG version via
getRemotePgMajorVersion()(usesSHOW server_version_num) — callers do not pass the version - Starts
postgres:{version}-alpineon a free port in range 15432–15532 - Runs
pg_dump/psqlinside the container viadocker exec(cloneDatabaseViaContainer()) - Stores
containerIDin session; cleaned up ondb abortordb deploycompletion - Used by
start,deploy, andimportcommands
- Checks Docker availability (
- Migration steps execution: The
deploycommand usesrunSteps()to execute multi-step operations with resume capability - if a step fails, re-running resumes from where it left off. - Schema directory structure (
db/schema/):infra/- Pre-migration (roles, schemas, extensions) - excluded from pgschemaextensions/,types/,enums/,tables/, etc. - pgschema-managedseeds/- Post-migration seed data - excluded from pgschema
PostKit files are split between committed (shared with team) and gitignored (user-specific/ephemeral):
.postkit/
├── db/
│ ├── session.json # GITIGNORED — active session state, local DB URL, container ID
│ ├── plan.sql # GITIGNORED — generated migration diff (ephemeral)
│ ├── schema.sql # GITIGNORED — generated schema artifact (ephemeral)
│ ├── session/ # GITIGNORED — temporary in-progress migrations
│ ├── committed.json # COMMITTED — migration tracking index (shared)
│ └── migrations/ # COMMITTED — committed SQL migrations for deploy (shared)
├── auth/
│ ├── raw/ # COMMITTED — auth raw config (shared)
│ ├── realm/ # COMMITTED — auth realm config (shared)
│ └── providers/ # GITIGNORED — Keycloak JAR providers (copied from vendor + project)
└── stack/
└── docker-compose.yml # GITIGNORED — generated compose file (ephemeral)
Key paths (from modules/db/utils/db-config.ts):
getPostkitDbDir()-.postkit/db/getSessionFilePath()-.postkit/db/session.jsongetCommittedFilePath()-.postkit/db/committed.jsongetPlanFilePath()-.postkit/db/plan.sqlgetGeneratedSchemaPath()-.postkit/db/schema.sqlgetSessionMigrationsPath()-.postkit/db/session/getCommittedMigrationsPath()-.postkit/db/migrations/
Key functions (from modules/db/services/):
generateSchemaSQLAndFingerprint()- Reads all schema files once and returns both the output path (.postkit/db/schema.sql) and a SHA-256 fingerprint of the source filesresolveLocalDb(localDbUrl, remoteUrl, spinner, spinnerText?)(container.ts) - WhenlocalDbUrlis empty, fetches PG version fromremoteUrland starts an auto Docker container. Used bystart,deploy, andimportcommands.withPgClient<T>(url, fn)(database.ts) - Scoped pg client wrapper; opens a connection, runsfn, closes on completion or errorcheckDbPrerequisites(verbose)(prerequisites.ts) - Shared pgschema + dbmate availability check used by all commands that need themrequireActiveSession()(utils/session.ts) - Returns active session or throws a descriptive errorassertLocalConnection(session, spinner)(utils/session.ts) - Tests local DB connection from session; throws if unreachableresolveApplyTarget(target?)(utils/apply-target.ts) - Resolves"local"or"remote"apply target; used by infra and seed commandsreadJsonFile<T>(path)/writeJsonFile(path, data)(utils/json-file.ts) - Typed JSON helpers used by remotes and committed migration tracking
The stack module manages a local backend service stack (Postgres, Keycloak, PostgREST, Traefik) using Docker Compose.
Services:
| Service | Image | Port | Purpose |
|---|---|---|---|
postgres |
postgres:16-alpine |
25432 | Database |
keycloak |
quay.io/keycloak/keycloak:26.6 |
via Traefik | Auth server |
postgrest |
postgrest/postgrest:latest |
via Traefik | REST API |
traefik |
traefik:v3.3 |
80 / 8080 | Reverse proxy + dashboard |
stack up startup sequence:
- Start
postgres+traefik(Phase 1 infrastructure) - Wait for health checks on infrastructure services
applyStackDeploy— createspostkitschema, appliesdb/infra/, committed migrations, seeds (hard failure)- Start
keycloak+postgrest(Phase 2 — only after DB is initialized) - Wait for health checks on all services
- If
is_initial=true: import realm template → fetch JWKs → update PostgREST → markis_initial=false
is_initial flag — stored in postkit.stack_config table in postkit schema:
true(default / missing row) → runs realm import + JWKs fetch on nextstack upfalse→ skips realm/JWKs on subsequent starts- Automatically resets when DB volumes are wiped (
stack down --volumes) - Manual reset:
postkit stack realmorpostkit stack keys
Keycloak providers (services/sync-providers.ts):
- Bundled JARs from
vendor/providers/are copied to.postkit/auth/providers/onpostkit init - Project-specific JARs from
auth/providers/<name>/target/*.jarare also synced - The providers directory is mounted into Keycloak at
/opt/keycloak/providers
Realm template + JWT Role Mapper (services/realm-init.ts):
- Default template scaffolded at
.postkit/auth/realm/postkit.json cleanRealmTemplate()strips builtin clients, strips IDs/secrets, injectsJWT_ROLE_MAPPER(script-primary-role.js) into every non-builtin client- Import uses
keycloak-config-cliviadocker run --network postkit-net
Key paths (from modules/stack/utils/stack-config.ts):
getStackDir()—.postkit/stack/getComposeFilePath()—.postkit/stack/docker-compose.ymlgetProvidersDir()—.postkit/auth/providers/(fromsync-providers.ts)
Key functions:
getStackConfig()(utils/stack-config.ts) — Loads + validates stack config, resolves defaults, reads JWKs/client secrets from secrets fileensureStackSecrets(config)(utils/stack-config.ts) — Auto-generates missing passwords/JWKs and writes topostkit.secrets.jsonwriteComposeFile(config, services)(services/compose.ts) — Generates.postkit/stack/docker-compose.ymlusing projectnameas Docker Compose project nameapplyStackDeploy(config, spinner)(services/db-init.ts) — Createspostkitschema, applies infra/migrations/seeds via connection retryreadStackIsInitial(config)/setStackInitialized(config)(utils/stack-state.ts) — Read/writeis_initialflag inpostkit.stack_configsyncKeycloakProviders(spinner?)(services/sync-providers.ts) — Copies JARs from vendor + project into.postkit/auth/providers/importRealmTemplate(config, spinner?)(services/realm-init.ts) — Cleans realm JSON and imports viakeycloak-config-clicontainercleanRealmTemplate(raw, realmName)(services/realm-init.ts) — Strips builtins, injects JWT Role Mapper
postkit init scaffold additions:
- Prompts for project name → generates
<name>_<8hexchars>, stored asnameinpostkit.config.json - Creates
db/infra/001_roles.sql(anon, authenticated, service_role, app_user, authenticator roles) - Creates
db/infra/002_schemas.sql(public, auth, storage schemas) - Copies vendor provider JARs to
.postkit/auth/providers/ - Scaffolds realm template at
.postkit/auth/realm/postkit.json
Config is loaded by loadPostkitConfig() from common/config.ts, which deep-merges two files:
| File | Committed | Purpose |
|---|---|---|
postkit.config.json |
Yes | Non-sensitive project settings (schema paths, flags, stack service config) |
postkit.secrets.json |
No (gitignored) | Credentials + all remote config (URLs, names, defaults) + stack secrets |
postkit.config.json (committed):
{
"name": "myapp_a3f2b1c0",
"db": {
"schemaPath": "db/schema",
"schemas": ["public"],
"infraPath": "db/infra"
},
"auth": { "configCliImage": "adorsys/keycloak-config-cli:latest-26" },
"stack": {
"keycloak": { "realmTemplate": ".postkit/auth/realm/postkit.json" }
}
}postkit.secrets.json (gitignored):
{
"db": {
"localDbUrl": "postgres://...",
"remotes": {
"dev": { "url": "postgres://...", "default": true, "addedAt": "2024-12-31T10:00:00.000Z" },
"staging": { "url": "postgres://..." }
}
},
"stack": {
"postgres": { "user": "postgres", "password": "<generated>" },
"keycloak": { "adminUser": "admin", "adminPassword": "<generated>" }
}
}localDbUrl: Leave empty to have PostKit automatically start a postgres:{version}-alpine Docker container. The version is queried from the remote DB via SHOW server_version_num. The container is started on db start and stopped on db abort.
Auto-migration: When loading config, if remotes is missing but remoteDbUrl exists, it's auto-migrated to create a default remote.
Key config paths:
POSTKIT_CONFIG_FILE= "postkit.config.json"POSTKIT_SECRETS_FILE= "postkit.secrets.json"POSTKIT_DIR= ".postkit" (session state, staged files)vendor/= Bundled binaries (resolved relative to CLI root, not project root)
Remotes are managed via utilities in modules/db/utils/remotes.ts:
getRemotes()- Get all configured remotes (throws if none)getRemote(name)- Get specific remotegetDefaultRemote()- Get default remote nameaddRemote(name, url, setAsDefault?)- Add new remoteremoveRemote(name, force?)- Remove remotesetDefaultRemote(name)- Set default remoteresolveRemote(name?)- Resolve {name, url} for a remote (uses default if no name)resolveRemoteUrl(name?)- Just get the URL
- tsup is used for bundling (ES modules only, Node 18+ target)
- tsx for development mode (direct TS execution without building)
- Output goes to
dist/with a shebang banner for CLI execution
| Command | Purpose |
|---|---|
postkit db start [--remote <name>] |
Clone remote DB to local, start session |
postkit db plan |
Generate schema diff with pgschema |
postkit db apply |
Apply migration to local DB (creates dbmate migration) |
postkit db commit |
Commit session migrations for deployment |
postkit db deploy [--remote <name>] |
Deploy committed migrations (with dry-run verification) |
postkit db status |
Show session state |
postkit db abort |
Cancel session, cleanup local resources |
postkit db migration [<name>] |
Create a manual SQL migration |
postkit db remote list |
List all configured remotes |
postkit db remote add <name> <url> |
Add a new remote |
postkit db remote remove <name> |
Remove a remote |
postkit db remote use <name> |
Set default remote |
postkit db infra [--apply] |
Manage infra SQL (roles, schemas, extensions) |
postkit db seed [--apply] |
Apply seed data |
postkit db schema add <name> |
Scaffold schema dirs + update infra + register in config |
| Command | Purpose |
|---|---|
postkit stack up [services...] |
Start full stack (two-phase: infra first, then keycloak+postgrest) |
postkit stack up --no-wait |
Start without waiting for health checks |
postkit stack up --no-keys |
Start without auto-fetching Keycloak JWKs |
postkit stack down |
Stop all services and remove containers |
postkit stack down --volumes |
Stop all services and remove containers + volumes |
postkit stack status |
Show running service health |
postkit stack logs [service] |
Tail logs for all or a specific service |
postkit stack logs [service] -f |
Follow log output (default behavior) |
postkit stack logs [service] -n <N> |
Show last N lines (default 100) |
postkit stack restart [services...] |
Restart one or more services (validates names) |
postkit stack keys |
Fetch Keycloak JWKs + client secrets, update PostgREST |
postkit stack keys --restart |
Fetch keys then restart PostgREST |
postkit stack keys --clients <names> |
Fetch keys for specific comma-separated client names |
postkit stack realm |
Re-import the Keycloak realm template |
Commands in modules/*/commands/ follow this pattern:
import type {CommandOptions} from "../../../common/types";
export async function someCommand(options: CommandOptions): Promise<void> {
const config = loadPostkitConfig();
// Business logic...
}CommandOptions includes the following global flags available to all commands:
verbose- Enable verbose/debug output (-v, --verbose)dryRun- Show what would be done without making changes (--dry-run)json- Output results as machine-readable JSON, useful for scripting/CI (--json)
Use the shell() utility from common/shell.ts for running external commands (pg_dump, psql, etc.).
Use the logger from common/logger.ts (chalk-based, handles verbose mode).
When working with remote databases:
import {resolveRemote, maskRemoteUrl} from "../utils/remotes";
// Resolve remote (uses default if no name specified)
const {name, url} = resolveRemote(options.remote);
logger.info(`Using remote: ${name}`);
// Mask URL for logging
logger.debug(`Remote URL: ${maskRemoteUrl(url)}`, options.verbose);PostKit ships with Claude Code agent skills that teach AI assistants how to work with PostKit workflows. They follow the Agent Skills open standard (SKILL.md files with YAML frontmatter).
| Skill | Invoke | Auto-triggers |
|---|---|---|
postkit-migrate |
/postkit-migrate |
Migration, deploy, schema diff, database change keywords |
postkit-setup |
/postkit-setup |
Init, config, remotes, postkit.config.json edits |
postkit-schema |
/postkit-schema |
Editing db/schema/** files, table/type/function changes |
postkit-auth |
/postkit-auth |
Keycloak, auth, SSO, identity provider keywords |
Each skill lives in its own directory under agent/skills/:
agent/skills/
├── postkit-migrate/
│ └── SKILL.md # Frontmatter (name, description, allowed-tools) + markdown instructions
├── postkit-setup/
│ └── SKILL.md
├── postkit-schema/
│ └── SKILL.md
└── postkit-auth/
└── SKILL.md
The YAML frontmatter controls how Claude discovers and uses the skill:
---
name: skill-name # Unique identifier and /invoke command
description: What the skill does... # Primary triggering mechanism — be specific
argument-hint: [step] # Optional: hint shown when invoking via /
paths: db/schema/** # Optional: auto-trigger when these files change
allowed-tools: Bash(postkit *) # Tool allowlist for the skill
---Key frontmatter fields:
- name — Skill identifier, also used as the
/skill-nameinvoke command - description — The primary auto-trigger mechanism. Include what the skill does and when to use it, covering synonyms and edge cases
- argument-hint — Shown to the user when invoking the skill via
/ - paths — Glob patterns that auto-trigger the skill when matching files are edited
- allowed-tools — Restricts which tools the skill can use
Use the skills CLI to install PostKit skills into your project:
# Install all PostKit skills (interactive)
npx skills add appritechnologies/Postkit
# List available skills first
npx skills add appritechnologies/Postkit --list
# Install specific skills only
npx skills add appritechnologies/Postkit --skill postkit-migrate --skill postkit-schema
# Install for a specific agent (e.g., Claude Code)
npx skills add appritechnologies/Postkit -a claude-code
# Non-interactive (CI/CD friendly)
npx skills add appritechnologies/Postkit --all -yThe CLI auto-detects which coding agents you have installed and places skills in the correct directory for each agent. By default, skills are symlinked (single source of truth, easy to update). Use --copy for independent copies.
| Scope | Flag | Use Case |
|---|---|---|
| Project (default) | Committed with your project, shared with team | |
| Global | -g |
Available across all your projects |
Update skills later:
npx skills update # Update all installed skills
npx skills update postkit-auth # Update a specific skillCreate agent/skills/<skill-name>/SKILL.md:
---
name: skill-name
description: When and what this skill does — be specific about trigger contexts
allowed-tools: Bash(postkit *)
---Skills can optionally include bundled resources for more complex workflows:
agent/skills/<skill-name>/
├── SKILL.md # Required — skill instructions
├── scripts/ # Optional — executable scripts for repetitive tasks
├── references/ # Optional — reference docs loaded into context as needed
└── assets/ # Optional — templates, icons, and other static files
When a skill grows beyond ~500 lines, split domain-specific content into references/ files and point to them from SKILL.md.
- All paths in
common/config.tsare resolved relative to eithercliRoot(the CLI installation) orprojectRoot(where the user runs commands). - Session files in
.postkit/db/track migration state and enable resume capability. - The
vendor/directory contains platform-specific binaries that are bundled with the CLI - no separate installation required. - The
.gitignoreincludes specific ephemeral paths (session.json, plan.sql, schema.sql, session/) — NOT the whole.postkit/directory. Committed migrations and auth state ARE tracked by git. - All migration-related files are in
.postkit/db/— the only user-maintained DB files should be indb/schema/.
Skills are invoked via /<skill-name> in Claude Code. Agents are sub-processes spawned by skills.
| Doc | Content |
|---|---|
cli/docs/architecture.md |
System architecture, module system, dependency direction |
cli/docs/db.md |
Database module workflow and commands |
cli/docs/auth.md |
Auth module workflow and commands |
cli/docs/stack.md |
Stack module — services, startup sequence, config, commands |
cli/docs/e2e-testing.md |
E2E testing guide and infrastructure |
| Skill | Invocation | Purpose | Sub-Agents |
|---|---|---|---|
| create-pr | /create-pr |
Generate PR description to temp/pr-description.md |
— |
| write-test-e2e | /write-test-e2e |
Write E2E tests using testcontainers | e2e-test-agent |
| write-test-unit | /write-test-unit |
Write unit tests with Vitest mocks | unit-test-agent |
| bugfix | /bugfix |
Diagnose and fix bugs (asks for info, 4 stages) | bugfixer, tester, reviewer, validator |
| create-feature | /create-feature |
Implement features (asks for info, 4 stages + tests) | feature-planner, senior-engineer, reviewer, validator, architect |
| architecture | /architecture |
Review architecture, generate ADRs | architect |
| update-docs | /update-docs |
Update documentation for code changes | docs-agent |
| Agent | File | Specialty | Used By |
|---|---|---|---|
| e2e-test-agent | .claude/agents/e2e-test-agent.md |
E2E test implementation (testcontainers) | write-test-e2e |
| unit-test-agent | .claude/agents/unit-test-agent.md |
Unit test implementation (Vitest mocks) | write-test-unit |
| bugfixer | .claude/agents/bugfixer.md |
Bug diagnosis and minimal fix | bugfix |
| tester | .claude/agents/tester.md |
Regression test creation | bugfix |
| reviewer | .claude/agents/reviewer.md |
Code review (reuse, lint, best practices) | bugfix, create-feature |
| validator | .claude/agents/validator.md |
Build/test/quality validation | bugfix, create-feature |
| feature-planner | .claude/agents/feature-planner.md |
Feature design and task breakdown | create-feature |
| senior-engineer | .claude/agents/senior-engineer.md |
Feature implementation | create-feature |
| architect | .claude/agents/architect.md |
Architecture analysis, ADR authoring | architecture |
| docs-agent | .claude/agents/docs-agent.md |
Documentation writing and maintenance | update-docs |