Skip to content

Bootstrap Guide

One-time setup to configure Zitadel for Exto Console. Run this once per environment (local, staging, production). The script is idempotent — safe to re-run.

Prerequisites

  • Zitadel running and accessible (e.g. http://localhost:8080)
  • Go 1.25+ installed (to run the bootstrap script)

1. Create a bootstrap admin user in Zitadel

  1. Open Zitadel Console (http://localhost:8080/ui/console/)
  2. Users > New > Machine User
    • Username: console-admin
    • Access Token Type: JWT
  3. Memberships > Add IAM membership > IAM_OWNER role
  4. Personal Access Tokens > New — copy the token

2. Create the bootstrap input file

Create .bootstrap-input.env in the project root:

env
ZITADEL_URL=http://localhost:8080
ZITADEL_PAT=<paste-the-pat-here>

# Optional — defaults shown:
# CONSOLE_URL=http://localhost:5174
# DEVELOPMENT_MODE=true

For production: set ZITADEL_URL to your prod Zitadel, CONSOLE_URL to the real Console URL, and DEVELOPMENT_MODE=false.

3. Run bootstrap

bash
make bootstrap
# or: go run ./scripts/bootstrap/

Output (first run):

Connecting to Zitadel at http://localhost:8080 (developmentMode=true) ...

✓ Admin org: ZITADEL (362930185718267907)
✓ Created project 'Exto Instances': 363006148858609667
✓ Created machine user 'console-worker': 363006148900814851
✓ Granted console-worker IAM Owner
✓ Generated client credentials for console-worker
✓ Created Console Portal OIDC app: appId=... clientId=...
✓ Set Zitadel label policy to light theme
✓ Got signing key from Zitadel
✓ Created Zitadel webhook target → http://localhost:5174/internal/zitadel-events

Bootstrap complete. Values written to .bootstrap.env

Output (re-run — existing resources are reused):

✓ Admin org: ZITADEL (362930185718267907)
✓ Reusing existing project 'Exto Instances': 363006148858609667
✓ Reusing machine user 'console-worker': 363006148900814851
✓ Granted console-worker IAM Owner
✓ Skipping client secret generation — console-worker already exists (use existing credentials)
✓ Reusing existing Console Portal app: appId=... clientId=...
✓ Set Zitadel label policy to light theme
✓ Reusing existing webhook target 'console-profile-sync': ...

Bootstrap complete. Values written to .bootstrap.env

4. What bootstrap creates

ResourcePurpose
Exto Instances projectGroups all instance OIDC apps under one project
console-worker machine userService account with IAM_OWNER for Zitadel Management API calls. Access Token Type must be JWT — opaque tokens will fail JWKS validation on instances.
Console Portal OIDC appPKCE SPA app for the Console frontend (no client secret — public client)
Light theme label policyMatches Console's login page styling
console-profile-sync webhook targetREST webhook with JSON payload type for user profile sync events
Webhook executionsTriggers on user.human.profile.changed, user.human.email.changed, user.deactivated, user.reactivated

5. Bootstrap output

Results are written to .bootstrap.env:

env
ZITADEL_ISSUER=http://localhost:8080
ZITADEL_API_URL=http://localhost:8080
ZITADEL_ADMIN_ORG_ID=<org-id>
EXTOID_PROJECT_ID=<project-id>
CONSOLE_SERVICE_CLIENT_ID=<client-id>
CONSOLE_SERVICE_CLIENT_SECRET=<secret>
CONSOLE_PORTAL_CLIENT_ID=<client-id>
ZITADEL_WEBHOOK_SECRET=<signing-key-from-zitadel>

Copy these values into your deployment environment (.env, K8s secrets, etc.).

Note: On re-runs, CONSOLE_SERVICE_CLIENT_ID, CONSOLE_SERVICE_CLIENT_SECRET, and ZITADEL_WEBHOOK_SECRET are omitted if the resources already existed (the secret/key cannot be retrieved after initial creation).

6. Idempotency details

ResourceFirst runRe-run
ProjectCreatedReused (matched by name)
Machine userCreatedReused (matched by username)
Client credentialsGenerated (rotates secret)Skipped — avoids invalidating existing deployments
OIDC appCreatedReused (matched by name + project)
Webhook targetCreated (returns signing key)Skipped — signing key cannot be retrieved
Webhook executionsCreatedSkipped (target already exists)

7. Delete the bootstrap admin

After Console is running and verified, delete the console-admin machine user from the Zitadel Console. The PAT is a privileged credential and is not needed at runtime.

Production bootstrap

bash
# .bootstrap-input.env for production
ZITADEL_URL=https://auth.yourdomain.com
ZITADEL_PAT=<prod-pat>
CONSOLE_URL=https://console.yourdomain.com
DEVELOPMENT_MODE=false

Then run make bootstrap as normal. DEVELOPMENT_MODE=false ensures OIDC apps require HTTPS redirect URIs.

File layout

.bootstrap-input.env   # Bootstrap inputs (ZITADEL_URL, ZITADEL_PAT) — git-ignored
.bootstrap.env         # Bootstrap outputs (copy into deployment env) — git-ignored