Absorbs the separate control-panel git repo as a subdirectory. Instances and backups directories excluded via .gitignore. Bunker Admin
27 KiB
Changemaker Control Panel (CCP) — Development Plan
A multi-tenant management system for provisioning, monitoring, and operating multiple Changemaker Lite instances from a single dashboard.
Vision
CCP replaces manual git clone / docker compose up / .env editing with a web UI that can:
- One-click provision a new Changemaker Lite instance (database, containers, config, tunnel)
- Monitor instance health across the fleet
- Start/stop/restart instances and individual services
- Back up and restore instance data
- Maintain a full audit trail of operator actions
- Manage Pangolin tunnels for production exposure
Architecture
┌─────────────────────────┐
│ CCP Admin GUI (5100) │ React + Vite + Ant Design
│ Dark theme, SPA │ Zustand auth store
└────────────┬─────────────┘
│ /api/* proxy
┌────────────▼─────────────┐
│ CCP API (5000) │ Express + TypeScript
│ JWT auth, RBAC │ Prisma ORM → PostgreSQL
│ Docker socket access │ Winston logger
└────────────┬─────────────┘
│
┌───────────┬───────────┼───────────┬──────────┐
▼ ▼ ▼ ▼ ▼
ccp-postgres ccp-redis Docker Socket /opt/ccp/ /var/backups/
(port 5480) (port 6399) (.sock) instances ccp-instances
Stack
| Layer | Technology | Notes |
|---|---|---|
| API | Express 4, TypeScript 5, Node 20 | express-async-errors for async route handling |
| ORM | Prisma 6 + PostgreSQL 16 | 10 models, mapped table names |
| Auth | JWT (jsonwebtoken) + bcryptjs | 15min access / 7d refresh, atomic rotation |
| Encryption | AES-256-GCM (Node crypto) | Secrets at rest in encrypted_secrets column |
| Frontend | React 19, Vite 6, Ant Design 5 | Dark theme, Zustand state, axios interceptors |
| Docker | Docker CLI + socket API | compose up/down/ps/exec/logs via shell + HTTP socket |
| Templates | Handlebars | .env, docker-compose.yml, nginx configs rendered per-instance |
| Logging | Winston | JSON in production, colorized in development |
| Config | Zod schema validation | Fails fast on startup with clear error messages |
Key Design Decisions
-
Docker CLI over Docker SDK — Shell out to
docker composecommands rather than using dockerode. Simpler, matches what operators would run manually, anddocker composehandles all the orchestration logic. -
Shared PostgreSQL — All CCP data in one database; each CML instance gets its own isolated PostgreSQL container with unique ports and passwords.
-
Port Range Allocation — Four non-overlapping port ranges prevent conflicts:
- API: 14000-14999
- Admin: 13000-13999
- PostgreSQL: 15400-15499
- Nginx: 10000-10999
-
Async Provisioning — Instance creation returns immediately; provisioning runs fire-and-forget with progress tracked via
status/statusMessagefields. Frontend polls for updates. -
Template-Based Config — Handlebars templates render per-instance docker-compose.yml, .env, nginx configs, and Pangolin resources. This avoids complex string manipulation and keeps configs readable.
Database Schema (Prisma)
CcpUser ──< CcpRefreshToken
│
├──< AuditLog
│
Instance ──< PortAllocation
│
├──< HealthCheck
├──< Backup
└──< AuditLog
CcpSetting (key-value)
Models
| Model | Purpose | Key Fields |
|---|---|---|
| CcpUser | Control panel operators | email, password (bcrypt), role (SUPER_ADMIN/OPERATOR/VIEWER) |
| CcpRefreshToken | JWT refresh token storage | token (SHA-256 hash), expiresAt |
| Instance | Managed CML instance | slug, domain, basePath, composeProject, status, portConfig (JSON), encryptedSecrets, feature flags |
| PortAllocation | Port registry | port (unique), service name, instanceId |
| HealthCheck | Periodic health snapshots | status (HEALTHY/DEGRADED/UNHEALTHY/UNKNOWN), serviceStatus (JSON), totalServices, healthyServices, responseTimeMs |
| Backup | Backup records | status (PENDING→IN_PROGRESS→COMPLETED/FAILED), archivePath, sizeBytes, manifest (JSON) |
| AuditLog | Action trail | action (18 types), userId, instanceId, details (JSON), ipAddress |
| CcpSetting | Global key-value config | key (PK), value (JSON) |
Audit Actions (18 types)
INSTANCE_CREATE, INSTANCE_UPDATE, INSTANCE_DELETE,
INSTANCE_START, INSTANCE_STOP, INSTANCE_RESTART, INSTANCE_UPGRADE,
BACKUP_CREATE, BACKUP_DELETE,
PANGOLIN_SETUP, PANGOLIN_SYNC,
USER_LOGIN, USER_CREATE, USER_UPDATE, USER_DELETE,
SETTINGS_UPDATE
API Endpoints
Authentication
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /api/auth/login |
Public | Login, returns JWT pair |
| POST | /api/auth/refresh |
Public | Rotate refresh token |
| POST | /api/auth/logout |
Public | Revoke refresh token |
| GET | /api/auth/me |
Authenticated | Current user profile |
Instances — CRUD
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/instances |
Authenticated | List all instances |
| GET | /api/instances/:id |
Authenticated | Instance detail (includes health + backups) |
| POST | /api/instances |
SUPER_ADMIN, OPERATOR | Create instance (triggers async provisioning) |
| PUT | /api/instances/:id |
SUPER_ADMIN, OPERATOR | Update instance config |
| DELETE | /api/instances/:id |
SUPER_ADMIN | Delete instance (stops containers, removes files) |
| GET | /api/instances/:id/secrets |
SUPER_ADMIN | Decrypt and return instance secrets |
Instances — Lifecycle
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /api/instances/:id/provision |
SUPER_ADMIN, OPERATOR | Retry provisioning |
| POST | /api/instances/:id/start |
SUPER_ADMIN, OPERATOR | Start all containers |
| POST | /api/instances/:id/stop |
SUPER_ADMIN, OPERATOR | Stop all containers |
| POST | /api/instances/:id/restart |
SUPER_ADMIN, OPERATOR | Restart (optionally ?service=api) |
Instances — Services & Logs
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/instances/:id/services |
Authenticated | Container status via docker compose ps |
| GET | /api/instances/:id/logs |
Authenticated | Logs (?service=api&tail=200&since=1h) |
Instances — Health
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /api/instances/:id/health-check |
SUPER_ADMIN, OPERATOR | Trigger manual health check |
| GET | /api/instances/:id/health-history |
Authenticated | Paginated health check history |
Instances — Backups
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /api/instances/:id/backup |
SUPER_ADMIN, OPERATOR | Create backup (async) |
| GET | /api/instances/:id/backups |
Authenticated | List instance backups |
Backups (cross-instance)
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/backups |
Authenticated | List all backups (?instanceId=...&page=1&limit=50) |
| DELETE | /api/backups/:id |
SUPER_ADMIN | Delete backup (file + record) |
| GET | /api/backups/:id/download |
SUPER_ADMIN | Stream backup archive |
Health (CCP-level)
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/health |
Public | CCP API health check |
| GET | /api/health/overview |
Authenticated | All instances with latest health |
Audit
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/audit |
Authenticated | Filtered, paginated audit log (?action=...&instanceId=...&userId=...&from=...&to=...) |
Settings
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/settings |
Authenticated | All settings as key-value map |
| PUT | /api/settings/:key |
SUPER_ADMIN | Upsert a setting value |
Frontend Pages
| Route | Page | Description |
|---|---|---|
/login |
LoginPage | Email/password form |
/app |
DashboardPage | Stats (total, running, healthy, degraded, stopped, errors) + instance cards |
/app/instances |
InstanceListPage | Table view with search/filter |
/app/instances/new |
CreateWizardPage | 5-step wizard (info, features, email, tunnel, review) |
/app/instances/:id |
InstanceDetailPage | Tabs: Overview, Services, Logs, Backups, Tunnel |
/app/backups |
BackupsPage | Cross-instance backup list with stats |
/app/audit |
AuditLogPage | Filterable audit log + detail drawer |
/app/settings |
SettingsPage | Port ranges, Pangolin config, defaults |
Sidebar Navigation
- Dashboard (home)
- Instances (list)
- Backups (cross-instance)
- Audit Log (activity trail)
- Settings (CCP config)
Provisioning Flow
When POST /api/instances is called, the system:
1. Validate uniqueness (slug + domain)
2. Allocate 4 ports from ranges
3. Generate 14 secrets (passwords, JWT keys, encryption keys)
4. Create Instance record (status: PROVISIONING)
5. [async] Create directory: /opt/ccp/instances/{slug}/changemaker.lite/
6. [async] rsync CML source (excluding node_modules, .git, .env, .claude)
7. [async] Decrypt secrets → build Handlebars context
8. [async] Render 7 templates: docker-compose.yml, .env, nginx configs, Pangolin, Prometheus
9. [async] Copy static files (nginx.conf)
10. [async] docker compose pull (non-fatal if images cached)
11. [async] docker compose build
12. [async] Start infrastructure: v2-postgres + redis-changemaker
13. [async] Wait for healthy (Docker healthcheck polling)
14. [async] Start API → run prisma migrate deploy → prisma db seed
15. [async] docker compose up (all services)
16. [async] Wait for HTTP health (localhost:{api_port}/api/health)
17. [async] Set status: RUNNING
Frontend polls GET /api/instances/:id every 3 seconds during provisioning to show progress.
Health Check System
How It Works
- Scheduler starts on API boot (default: every 5 minutes, configurable via
HEALTH_CHECK_INTERVAL_MS) - For each RUNNING instance, runs
docker compose ps --format json - Parses container states and health check results
- Determines overall status:
- HEALTHY — all containers running, all health checks passing
- DEGRADED — some containers running but not all, or some health checks failing
- UNHEALTHY — majority of containers down or failing health checks
- UNKNOWN — no containers found or compose project doesn't exist
- Stores
HealthCheckrecord with per-service status JSON, response time - Updates
instance.lastHealthChecktimestamp
Manual Trigger
POST /api/instances/:id/health-check runs an immediate check (SUPER_ADMIN/OPERATOR only).
Backup System
What Gets Backed Up
- PostgreSQL dump —
pg_dumpinside the instance's v2-postgres container - Uploads archive — tar.gz of the uploads directory (if it exists)
- Manifest — JSON file with file names, sizes, SHA-256 hashes
Backup Flow
1. Validate instance is RUNNING
2. Create Backup record (status: PENDING)
3. [async] Set status: IN_PROGRESS
4. [async] mkdir /var/backups/ccp-instances/{slug}/{timestamp}/
5. [async] docker compose exec v2-postgres pg_dump → v2-postgres.sql → gzip
6. [async] tar -czf uploads.tar.gz (if uploads/ exists)
7. [async] Write manifest.json with file inventory + SHA-256 hashes
8. [async] tar -czf final archive → /var/backups/ccp-instances/{slug}/backup-{slug}-{timestamp}.tar.gz
9. [async] Cleanup temp directory
10. [async] Update Backup record (COMPLETED, archivePath, sizeBytes, manifest)
11. [async] Write audit log
Retention
cleanupOldBackups(retentionDays) deletes archives + records older than the configured retention period (default: 30 days).
Phased Implementation
Phase 1: Foundation (COMPLETE)
Goal: Skeleton that boots — database, auth, project structure.
Delivered:
- Prisma schema with all 10 models (User, Instance, HealthCheck, Backup, AuditLog, etc.)
- JWT authentication (access + refresh tokens with atomic rotation)
- Role-based access control (SUPER_ADMIN, OPERATOR, VIEWER)
- Zod-validated environment configuration
- AES-256-GCM encryption for instance secrets
- React admin shell with dark theme, sidebar navigation, protected routes
- Zustand auth store with token persistence + refresh interceptor
- Login page, Dashboard placeholder, Settings page
- Docker Compose orchestration (PostgreSQL, Redis, API, Admin)
- Placeholder pages wired for Audit Log, Backups
Phase 2: Docker Lifecycle (COMPLETE)
Goal: Create and manage running CML instances.
Delivered:
- Instance CRUD with slug/domain uniqueness validation
- Port allocation across 4 ranges (API, Admin, PostgreSQL, Nginx)
- Secret generation (14 secrets: postgres, redis, JWT, encryption, admin passwords)
- Handlebars template engine rendering 7 config files per instance
- 13-step async provisioning (copy source → render config → pull → build → migrate → seed → start)
- Lifecycle operations: start, stop, restart (whole stack or individual service)
- Container status via
docker compose ps --format json - Log viewing via
docker compose logswith service/tail/since filters - 5-step Create Instance wizard (basic info → features → email → tunnel → review)
- Instance detail page with tabs (Overview, Services, Logs, Backups, Tunnel)
- Service health grid with per-container restart/log-view actions
- Provisioning progress indicator (polls every 3s)
- Audit logging on all lifecycle operations (7 action types)
Phase 3: Observability + Backups (COMPLETE)
Goal: Visibility into instance health, operator activity trail, and data protection.
Delivered:
Part A — Audit Log
- Audit service with filtered queries (action, instance, user, date range) + pagination
GET /api/auditendpoint with query parameter filtering- IP address capture on all audit log entries (existing + new)
USER_LOGINaudit event on successful authenticationSETTINGS_UPDATEaudit event on settings changes- Full AuditLogPage: filterable table, action-colored tags, detail drawer with JSON inspector, server-side pagination, 30s auto-refresh toggle
Part B — Health Checks
- Health service with
checkInstanceHealth()analyzing Docker container states - Overall status determination (HEALTHY/DEGRADED/UNHEALTHY/UNKNOWN) from per-container state
- Scheduled health checker (default 5-minute interval, configurable, 0 to disable)
POST /api/instances/:id/health-checkfor manual checksGET /api/instances/:id/health-historyfor paginated history- Health card in Instance Detail overview with "Check Now" button + history table
- Dashboard stat cards: Healthy and Degraded instance counts from
/api/health/overview
Part C — Backups
- Backup service: pg_dump via Docker exec, uploads tar.gz, SHA-256 manifest, final archive
- Async backup creation with PENDING → IN_PROGRESS → COMPLETED/FAILED progression
POST /api/instances/:id/backup— create backupGET /api/instances/:id/backups— instance-scoped backup listGET /api/backups— cross-instance backup list with paginationDELETE /api/backups/:id— delete backup (file + DB record)GET /api/backups/:id/download— stream backup archive for download- BackupsPage: cross-instance table, instance filter, stats (count/size/last), "Backup All Running"
- Enhanced Instance Detail backups tab: create/download/delete actions, status polling
- Backup storage volume mount in docker-compose.yml
- Old backup cleanup utility (configurable retention days)
Phase 4: Pangolin Integration (PLANNED)
Goal: Automated tunnel setup for exposing instances to the internet.
Scope:
- Pangolin API client (site creation, resource management, Newt credentials)
- Automated tunnel setup endpoint (
POST /api/instances/:id/setup-tunnel) - Per-instance Newt container management (start/stop with stored credentials)
- Resource sync for all instance subdomains (app, api, media, docs, etc.)
- Tunnel status monitoring in Instance Detail tunnel tab
- Bulk tunnel setup for fleet-wide deployment
Phase 5: Upgrades + Git Integration (PLANNED)
Goal: Rolling upgrades and version management.
Scope:
- Git pull + branch checkout for instance source code
- Database migration execution (prisma migrate deploy)
- Docker image rebuild + rolling restart
- Upgrade progress tracking (similar to provisioning)
- Rollback capability (pre-upgrade backup + restore)
- Version display on instance cards and detail pages
INSTANCE_UPGRADEaudit event
Phase 6: User Management + RBAC (PLANNED)
Goal: Multi-operator support with granular permissions.
Scope:
- User CRUD pages (create, edit, delete operators)
- Role assignment and management
- Per-instance access control (operator can only manage assigned instances)
- Invitation flow (email invite with temporary password)
- Password change and reset functionality
- Session management (view/revoke active sessions)
Phase 7: Monitoring Dashboard (PLANNED)
Goal: Fleet-wide observability with trends and alerting.
Scope:
- Health trend charts (uptime over time)
- Resource utilization (CPU, memory, disk via Docker stats)
- Alert rules (instance down > N minutes, disk usage > threshold)
- Notification channels (email, webhook)
- Backup compliance monitoring (last backup age alerts)
- Fleet summary dashboard with at-a-glance status
Phase 8: Instance Configuration UI (PLANNED)
Goal: Edit instance configuration without SSH.
Scope:
- Feature flag toggles (media, listmonk, gancio, monitoring)
- SMTP configuration management
- .env variable editor (safe subset)
- Docker Compose service scaling
- Configuration diff preview before applying
- Auto-restart on config change
Environment Variables
| Variable | Default | Description |
|---|---|---|
NODE_ENV |
development |
Environment mode |
PORT |
5000 |
API server port |
DATABASE_URL |
— | PostgreSQL connection string |
REDIS_URL |
redis://localhost:6399 |
Redis connection string |
JWT_ACCESS_SECRET |
— | JWT signing key (min 32 chars) |
JWT_REFRESH_SECRET |
— | Refresh token signing key (min 32 chars) |
JWT_ACCESS_EXPIRES_IN |
15m |
Access token lifetime |
JWT_REFRESH_EXPIRES_IN |
7d |
Refresh token lifetime |
ENCRYPTION_KEY |
— | AES-256 key for secrets at rest (min 32 hex chars) |
INITIAL_ADMIN_EMAIL |
admin@example.com |
Bootstrap admin email |
INITIAL_ADMIN_PASSWORD |
ChangeMe2025!! |
Bootstrap admin password (min 12 chars) |
CORS_ORIGINS |
http://localhost:5100 |
Allowed origins (comma-separated) |
INSTANCES_BASE_PATH |
/opt/ccp/instances |
Where instance directories live |
CML_SOURCE_PATH |
/home/bunker-admin/changemaker.lite |
CML source to clone from |
CML_GIT_REPO |
— | Git repo URL (if cloning remotely) |
CML_GIT_BRANCH |
v2 |
Default branch for new instances |
PORT_RANGE_API_START |
14000 |
API port range start |
PORT_RANGE_API_END |
14999 |
API port range end |
PORT_RANGE_ADMIN_START |
13000 |
Admin port range start |
PORT_RANGE_ADMIN_END |
13999 |
Admin port range end |
PORT_RANGE_POSTGRES_START |
15400 |
PostgreSQL port range start |
PORT_RANGE_POSTGRES_END |
15499 |
PostgreSQL port range end |
PORT_RANGE_NGINX_START |
10000 |
Nginx port range start |
PORT_RANGE_NGINX_END |
10999 |
Nginx port range end |
PANGOLIN_API_URL |
— | Pangolin API base URL |
PANGOLIN_API_KEY |
— | Pangolin API key |
PANGOLIN_ORG_ID |
— | Pangolin organization ID |
HEALTH_CHECK_INTERVAL_MS |
300000 |
Health check interval (0 to disable) |
BACKUP_STORAGE_PATH |
/var/backups/ccp-instances |
Backup archive storage directory |
BACKUP_RETENTION_DAYS |
30 |
Auto-cleanup threshold |
Docker Services
services:
ccp-postgres: # PostgreSQL 16 Alpine — CCP database (port 5480)
ccp-redis: # Redis 7 Alpine — rate limiting, caching (port 6399)
ccp-api: # Node 20 + Docker CLI — API server (port 5000)
ccp-admin: # Nginx + React SPA — admin GUI (port 5100)
Volume Mounts (ccp-api)
| Host | Container | Mode | Purpose |
|---|---|---|---|
./api |
/app |
rw | Source code (dev) |
./templates |
/app/templates |
ro | Handlebars templates |
/var/run/docker.sock |
/var/run/docker.sock |
— | Docker CLI access |
$INSTANCES_BASE_PATH |
Same | rw | Instance directories |
$CML_SOURCE_PATH |
Same | ro | CML source for provisioning |
$BACKUP_STORAGE_PATH |
Same | rw | Backup archives |
File Inventory
API (api/)
src/
├── server.ts # Express app + route mounting + health scheduler start
├── config/
│ ├── env.ts # Zod env validation (30+ vars)
│ └── redis.ts # Redis client config
├── middleware/
│ ├── auth.ts # authenticate + requireRole middleware
│ ├── error-handler.ts # AppError class + global error handler
│ └── validate.ts # Zod request body validation
├── modules/
│ ├── auth/
│ │ ├── auth.routes.ts # POST login/refresh/logout, GET /me
│ │ ├── auth.schemas.ts # Zod schemas for auth payloads
│ │ └── auth.service.ts # JWT generation, bcrypt verify, token rotation
│ ├── instances/
│ │ ├── instances.routes.ts # CRUD + lifecycle + services + logs + health + backups
│ │ ├── instances.schemas.ts # Zod create/update validation
│ │ ├── instances.service.ts # Business logic + audit logging with IP capture
│ │ └── provisioner.ts # 13-step async provisioning orchestration
│ ├── audit/
│ │ ├── audit.routes.ts # GET /api/audit with filters + pagination
│ │ └── audit.service.ts # Prisma query builder for audit logs
│ ├── backups/
│ │ └── backup.routes.ts # GET list, DELETE, GET download
│ ├── health/
│ │ └── health.routes.ts # Public health + authenticated overview
│ └── settings/
│ └── settings.routes.ts # GET all, PUT :key (+ audit logging)
├── services/
│ ├── docker.service.ts # Docker CLI wrapper (ps, exec, logs, up, down, etc.)
│ ├── health.service.ts # Health check logic + 5-minute scheduler
│ ├── backup.service.ts # pg_dump + tar + manifest + cleanup
│ ├── port-allocator.ts # Port range management
│ ├── secret-generator.ts # CML-compatible credential generation
│ └── template-engine.ts # Handlebars rendering for 7 config files
└── utils/
├── encryption.ts # AES-256-GCM encrypt/decrypt
└── logger.ts # Winston config
Admin (admin/)
src/
├── App.tsx # Route definitions + dark theme config
├── main.tsx # React entry
├── components/
│ ├── AppLayout.tsx # Sidebar nav + header + user dropdown
│ ├── InstanceCard.tsx # Card with status, health bar, features
│ ├── ServiceHealthGrid.tsx # Container state table with actions
│ ├── LogViewer.tsx # Log display with service filter
│ └── ProtectedRoute.tsx # Auth guard wrapper
├── pages/
│ ├── LoginPage.tsx # Auth form
│ ├── DashboardPage.tsx # Stats + health overview + instance cards
│ ├── InstanceListPage.tsx # Instance table
│ ├── CreateWizardPage.tsx # 5-step provisioning wizard
│ ├── InstanceDetailPage.tsx # Tabbed detail (overview/services/logs/backups/tunnel)
│ ├── BackupsPage.tsx # Cross-instance backup manager
│ ├── AuditLogPage.tsx # Filterable audit trail
│ └── SettingsPage.tsx # CCP configuration
├── stores/
│ └── auth.store.ts # Zustand auth + localStorage persistence
├── lib/
│ └── api.ts # Axios + auth interceptors + token refresh
└── types/
└── api.ts # TypeScript interfaces for all API models
Templates (templates/)
docker-compose.yml.hbs # Full CML docker-compose with ports/secrets/features
env.hbs # Instance .env file
nginx/
├── nginx.conf # Global nginx config (static copy)
└── conf.d/
├── default.conf.hbs # Subdomain routing
├── api.conf.hbs # API reverse proxy
└── services.conf.hbs # Service proxies
configs/
├── pangolin/resources.yml.hbs # Tunnel resource definitions
└── prometheus/prometheus.yml.hbs # Monitoring scrape targets
Quick Start
# 1. Clone and enter the CCP directory
cd changemaker.lite/changemaker-control-panel
# 2. Copy environment file
cp .env.example .env
# Edit .env: set strong passwords for JWT_ACCESS_SECRET, JWT_REFRESH_SECRET, ENCRYPTION_KEY
# 3. Start CCP
docker compose up -d
# 4. Run migrations + seed
docker compose exec ccp-api npx prisma migrate deploy
docker compose exec ccp-api npx prisma db seed
# 5. Access admin GUI
open http://localhost:5100
# Login with INITIAL_ADMIN_EMAIL / INITIAL_ADMIN_PASSWORD from .env
Development
# API development (hot reload)
cd api && npm install && npx tsx src/server.ts
# Admin development (Vite dev server)
cd admin && npm install && npm run dev
# Type checking
cd api && npx tsc --noEmit
cd admin && npx tsc --noEmit
# Database operations
cd api && npx prisma migrate dev # Create/apply migrations
cd api && npx prisma studio # Browse database GUI
cd api && npx prisma db seed # Re-seed admin user