Appearance
Developer Setup
Local development guide for Exto Console. Assumes you've already completed the Bootstrap Guide.
Prerequisites
| Dependency | Purpose | Local default |
|---|---|---|
| Go 1.25+ | Backend API and worker | — |
| Node.js 20+ | Frontend build | — |
| PostgreSQL 15+ | Primary data store | localhost:5432 |
| Zitadel | Identity provider | http://localhost:8080 |
| Redis | SSE command delivery (optional) | redis://localhost:6379 |
1. Create the PostgreSQL database
Install PostgreSQL if you haven't already:
bash
# macOS (Homebrew)
brew install postgresql@17
brew services start postgresql@17
# Ubuntu / Debian
sudo apt install postgresql
sudo systemctl start postgresqlCreate the console database:
bash
createdb consoleSchema tables are created automatically on first startup via goose migrations embedded in the binary. You do not need to run any SQL manually.
Production note: Create a dedicated user instead of using the OS default:
sqlCREATE USER console_app WITH PASSWORD 'your-secure-password'; GRANT ALL PRIVILEGES ON DATABASE console TO console_app; -- After first startup creates tables: GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO console_app; ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO console_app;
2. Configure environment
Create .env in the project root. Copy the Zitadel values from .bootstrap.env and add the rest:
env
# Server
API_ADDR=:8000
DEVELOPMENT_MODE=true
LOG_LEVEL=info
# PostgreSQL
DATABASE_URL=postgres://localhost:5432/console?sslmode=disable
# Zitadel — copy these from .bootstrap.env
ZITADEL_ISSUER=http://localhost:8080
ZITADEL_API_URL=http://localhost:8080
ZITADEL_ADMIN_ORG_ID=<from .bootstrap.env>
EXTOID_PROJECT_ID=<from .bootstrap.env>
CONSOLE_SERVICE_CLIENT_ID=<from .bootstrap.env>
CONSOLE_SERVICE_CLIENT_SECRET=<from .bootstrap.env>
CONSOLE_PORTAL_CLIENT_ID=<from .bootstrap.env>
# Frontend public URL
CONSOLE_URL=http://localhost:5174
# Secret store — "noop" for local dev
SECRET_STORE_PROVIDER=noop
# Redis — optional, only needed for multi-replica SSE
# REDIS_URL=redis://localhost:6379
# Email — optional
# EMAIL_PROVIDER=smtp
# EMAIL_API_KEY=host:port:username:password
# EMAIL_FROM_ADDR=hello@example.comCreate web/.env:
env
VITE_CONSOLE_API_URL=http://localhost:80003. Install frontend dependencies
bash
cd web && npm install4. Run
With hot reload (recommended)
bash
# API + frontend (terminal 1)
make dev
# Worker (terminal 2)
make dev_workerWithout hot reload
bash
# Terminal 1 — API
make api
# Terminal 2 — Worker
make worker
# Terminal 3 — Frontend
cd web && npm run devOpen http://localhost:5174 in your browser.
Preflight checks
Both cmd/api and cmd/worker run preflight checks on startup. If any check fails, the process exits with a clear error.
| Check | API | Worker | What it validates |
|---|---|---|---|
| Config validation | Shared + API-specific env vars | Shared env vars | All mandatory env vars are set |
| Conditional config | Email provider, Key Vault URL | Same | Provider-dependent vars are consistent |
| PostgreSQL | Connect + ping | Connect + ping | Database is reachable |
| Migrations | goose Up (embedded SQL) | Same | Schema is up to date |
| Zitadel | client_credentials token fetch | Same | Service account works |
| Redis | Connect + ping (if REDIS_URL set) | — | Pub/sub is reachable |
| Secret store | Key Vault list (if azure-keyvault) | noop | Vault auth + connectivity |
Preflight logs always print at info level regardless of LOG_LEVEL.
Make targets
| Command | Description |
|---|---|
make dev | Frontend + API with hot reload |
make devbg | Frontend + worker with hot reload |
make dev_web | Frontend only |
make dev_api | API with hot reload (air) |
make dev_worker | Worker with hot reload (air) |
make api | Run API (no hot reload) |
make worker | Run worker (no hot reload) |
make build | Compile Go binaries to bin/ |
make bootstrap | Run Zitadel bootstrap script |
make lint | golangci-lint |
make test | Go tests |
make tidy | go mod tidy |
Frontend commands
bash
cd web && npm run dev # Vite dev server
cd web && npm run build # Type-check + production build
cd web && npm run lint # ESLint (must pass before completing work)
cd web && npm run test # VitestEnvironment variable reference
Shared (API + Worker)
| Variable | Required | Default | Description |
|---|---|---|---|
DATABASE_URL | Yes | postgres://localhost:5432/console?sslmode=disable | PostgreSQL connection string |
ZITADEL_ISSUER | Yes | http://localhost:8080 | OIDC issuer URL |
ZITADEL_API_URL | Yes | http://localhost:8080 | Zitadel Management API URL |
ZITADEL_ADMIN_ORG_ID | Yes | — | Admin org ID (from bootstrap) |
EXTOID_PROJECT_ID | Yes | — | Instances project ID (from bootstrap) |
CONSOLE_SERVICE_CLIENT_ID | Yes | — | Service account client ID (from bootstrap) |
CONSOLE_SERVICE_CLIENT_SECRET | Yes | — | Service account client secret (from bootstrap) |
LOG_LEVEL | No | info | debug, info, warn, error |
DEVELOPMENT_MODE | No | true | Allow non-HTTPS OIDC redirect URIs |
API only
| Variable | Required | Default | Description |
|---|---|---|---|
API_ADDR | No | :8000 | HTTP listen address |
CONSOLE_URL | Yes | http://localhost:5174 | Frontend public URL |
CONSOLE_PORTAL_CLIENT_ID | Yes | — | OIDC client ID for SPA (from bootstrap) |
REDIS_URL | No | — | Redis URL for SSE pub/sub |
RATE_LIMIT_PER_IP | No | 20 | Requests/min per IP |
RATE_LIMIT_PER_EMAIL | No | 10 | Requests/min per email |
Worker only
| Variable | Required | Default | Description |
|---|---|---|---|
HEALTH_POLL_INTERVAL_SECS | No | 300 | Instance health check interval |
USAGE_AGGREGATION_INTERVAL_SECS | No | 3600 | Usage rollup interval |
Conditional
| Variable | Required when | Default | Description |
|---|---|---|---|
SECRET_STORE_PROVIDER | No | noop | azure-keyvault or noop |
AZURE_KEY_VAULT_URL | SECRET_STORE_PROVIDER=azure-keyvault | — | Vault base URL |
EMAIL_PROVIDER | No | — | sendgrid, mailtrap, or smtp |
EMAIL_API_KEY | EMAIL_PROVIDER is set | — | Provider credentials |
EMAIL_FROM_ADDR | EMAIL_PROVIDER is set | noreply@exto360.com | Sender address |
ZITADEL_WEBHOOK_SECRET | No | — | Signing key returned by Zitadel when creating webhook target (from .bootstrap.env) |
Production differences
| Setting | Local dev | Production |
|---|---|---|
DEVELOPMENT_MODE | true | false |
LOG_LEVEL | info | warn |
SECRET_STORE_PROVIDER | noop | azure-keyvault |
REDIS_URL | optional | required (multi-replica) |
CONSOLE_URL | http://localhost:5174 | https://console.yourdomain.com |
| Frontend OIDC config | Fetched from API proxy | Fetched at runtime from GET /api/public/config |
File layout
.bootstrap-input.env # Bootstrap inputs — git-ignored
.bootstrap.env # Bootstrap outputs — git-ignored
.env # Runtime config for API + worker — git-ignored
web/.env # Vite dev proxy target — git-ignored
