Skip to content

Developer Setup

Local development guide for Exto Console. Assumes you've already completed the Bootstrap Guide.

Prerequisites

DependencyPurposeLocal default
Go 1.25+Backend API and worker
Node.js 20+Frontend build
PostgreSQL 15+Primary data storelocalhost:5432
ZitadelIdentity providerhttp://localhost:8080
RedisSSE 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 postgresql

Create the console database:

bash
createdb console

Schema 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:

sql
CREATE 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.com

Create web/.env:

env
VITE_CONSOLE_API_URL=http://localhost:8000

3. Install frontend dependencies

bash
cd web && npm install

4. Run

bash
# API + frontend (terminal 1)
make dev

# Worker (terminal 2)
make dev_worker

Without hot reload

bash
# Terminal 1 — API
make api

# Terminal 2 — Worker
make worker

# Terminal 3 — Frontend
cd web && npm run dev

Open 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.

CheckAPIWorkerWhat it validates
Config validationShared + API-specific env varsShared env varsAll mandatory env vars are set
Conditional configEmail provider, Key Vault URLSameProvider-dependent vars are consistent
PostgreSQLConnect + pingConnect + pingDatabase is reachable
Migrationsgoose Up (embedded SQL)SameSchema is up to date
Zitadelclient_credentials token fetchSameService account works
RedisConnect + ping (if REDIS_URL set)Pub/sub is reachable
Secret storeKey Vault list (if azure-keyvault)noopVault auth + connectivity

Preflight logs always print at info level regardless of LOG_LEVEL.

Make targets

CommandDescription
make devFrontend + API with hot reload
make devbgFrontend + worker with hot reload
make dev_webFrontend only
make dev_apiAPI with hot reload (air)
make dev_workerWorker with hot reload (air)
make apiRun API (no hot reload)
make workerRun worker (no hot reload)
make buildCompile Go binaries to bin/
make bootstrapRun Zitadel bootstrap script
make lintgolangci-lint
make testGo tests
make tidygo 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      # Vitest

Environment variable reference

Shared (API + Worker)

VariableRequiredDefaultDescription
DATABASE_URLYespostgres://localhost:5432/console?sslmode=disablePostgreSQL connection string
ZITADEL_ISSUERYeshttp://localhost:8080OIDC issuer URL
ZITADEL_API_URLYeshttp://localhost:8080Zitadel Management API URL
ZITADEL_ADMIN_ORG_IDYesAdmin org ID (from bootstrap)
EXTOID_PROJECT_IDYesInstances project ID (from bootstrap)
CONSOLE_SERVICE_CLIENT_IDYesService account client ID (from bootstrap)
CONSOLE_SERVICE_CLIENT_SECRETYesService account client secret (from bootstrap)
LOG_LEVELNoinfodebug, info, warn, error
DEVELOPMENT_MODENotrueAllow non-HTTPS OIDC redirect URIs

API only

VariableRequiredDefaultDescription
API_ADDRNo:8000HTTP listen address
CONSOLE_URLYeshttp://localhost:5174Frontend public URL
CONSOLE_PORTAL_CLIENT_IDYesOIDC client ID for SPA (from bootstrap)
REDIS_URLNoRedis URL for SSE pub/sub
RATE_LIMIT_PER_IPNo20Requests/min per IP
RATE_LIMIT_PER_EMAILNo10Requests/min per email

Worker only

VariableRequiredDefaultDescription
HEALTH_POLL_INTERVAL_SECSNo300Instance health check interval
USAGE_AGGREGATION_INTERVAL_SECSNo3600Usage rollup interval

Conditional

VariableRequired whenDefaultDescription
SECRET_STORE_PROVIDERNonoopazure-keyvault or noop
AZURE_KEY_VAULT_URLSECRET_STORE_PROVIDER=azure-keyvaultVault base URL
EMAIL_PROVIDERNosendgrid, mailtrap, or smtp
EMAIL_API_KEYEMAIL_PROVIDER is setProvider credentials
EMAIL_FROM_ADDREMAIL_PROVIDER is setnoreply@exto360.comSender address
ZITADEL_WEBHOOK_SECRETNoSigning key returned by Zitadel when creating webhook target (from .bootstrap.env)

Production differences

SettingLocal devProduction
DEVELOPMENT_MODEtruefalse
LOG_LEVELinfowarn
SECRET_STORE_PROVIDERnoopazure-keyvault
REDIS_URLoptionalrequired (multi-replica)
CONSOLE_URLhttp://localhost:5174https://console.yourdomain.com
Frontend OIDC configFetched from API proxyFetched 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