Appearance
Get started
Managed SaaS
If you are evaluating or deploying Aletheia as managed SaaS, sign up at actanomos.com or access your workspace at console.actanomos.com. The guide below covers local developer setup and private self-deployment for engineering teams authoring apps, policies, or workflows.
This page takes the README quickstart one command at a time: what each command does, what to expect in each UI, and the setup steps. At the end you have the whole stack running on your machine with a seeded tenant; the tutorial then drives a decision through it.
Prerequisites
- Node 22.22.2 or later (
nvm usereads.nvmrc) and pnpm 10 (corepack enableornpm i -g pnpm). - Docker with the compose plugin. The local stack runs Postgres, Temporal and its UI, Zitadel and Garage (S3-compatible object storage); Zitadel alone takes about 512 MB of memory.
- macOS, or Linux with glibc 2.39 or newer (Ubuntu 24.04, Debian 13) to build WebAssembly apps, such as the test app the smoke uploads: the extism-js compiler they are built with needs it.
- Free ports:
5432(Postgres),7233(Temporal),8233(Temporal UI),8090(Zitadel),3900(Garage),4000and4001(API),4100(app runner),5176(admin console),5177(collection terminal),5178(landing page). curlandjqfor the tutorial; neither is needed to run the stack.
Step by step
1. Install and configure
bash
pnpm install
cp .env.example .envpnpm install resolves the workspace (the services under apps/, the packages and the test apps under tests/apps/). .env.example holds the development defaults: the two database URLs (the owner DATABASE_URL for migrations and seeds, the non-owner DATABASE_APP_URL the API and worker run with so row-level security applies), Temporal, the API port, COLLECTION_FLOW_URL and a placeholder SUBMISSION_TOKEN_SECRET. COLLECTION_FLOW_URL is the base of the links applicants open, so it must be the collection terminal's address, http://localhost:5177. The Zitadel lines are empty until step 5. Root scripts such as pnpm dev and pnpm smoke load .env for you; the file is gitignored.
Documents live in object storage, and so do the modules of the apps the app runner runs (the smoke uploads test apps). Make sure .env has these lines, adding the ones it lacks: the Garage values (Documents), the runner's address and token, the API's internal listener the runner reaches, the secret that signs document reads, the catalogue directory, the secret store key, and the switch that lets apps reach private networks, which the smoke's http-api installs need to call their vendor stub on 127.0.0.1 (a development stack's setting only: production leaves it out, Environment).
STORAGE_ENDPOINT=http://localhost:3900
STORAGE_PUBLIC_ENDPOINT=http://localhost:3900
STORAGE_REGION=garage
STORAGE_BUCKET=aletheia-documents
STORAGE_ACCESS_KEY=GKaletheiadev0000000000000
STORAGE_SECRET_KEY=aletheiadevsecret0000000000000000000000000000000000000000000000ab
STORAGE_CORS_ORIGINS=http://localhost:5176,http://localhost:5177
APP_RUNNER_URL=http://localhost:4100
APP_RUNNER_API_URL=http://localhost:4001
APP_RUNNER_TOKEN=<32 characters or more: openssl rand -hex 32>
APP_CALL_TOKEN_SECRET=<32 characters or more: openssl rand -hex 32>
APP_CATALOG_DIR=<the absolute path of your checkout>/catalog/dist
APP_RUNNER_ALLOW_PRIVATE_NETWORKS=true
SECRET_STORE_KEY=<openssl rand -base64 32>2. Build the test app and the catalogue
bash
pnpm tools:extism # extism-js and binaryen, pinned, into .tools/
# the test app, the catalogue's apps and the SDK they are built with:
pnpm turbo run build --filter=@aletheia-dev/test-vendor-app --filter='./catalog/*'
pnpm catalog:index # catalog/dist: the catalogue's index.json and modulesApps are WebAssembly modules built with the pinned compiler. The smoke (step 6) uploads tests/apps/test-vendor, a vendor double, as the tenant's own app; run the build again after changing it. pnpm catalog:index writes catalog/dist, the platform catalogue the API publishes when it starts (App catalogue): http-api, the generic connector a tenant installs once per vendor, and the model apps claude, chatgpt, gemini, jev and openai-compatible. The index needs every catalogue package built, so the build comes first.
3. Start the infrastructure
bash
pnpm infra:up # docker compose up -dStarts Postgres (:5432), Temporal (:7233, UI on :8233), Zitadel (:8090) and Garage (:3900). Zitadel initialises its first instance on an empty database: the aletheia-dev organization, a human admin and a bootstrap machine user whose personal access token it writes to docker/zitadel/bootstrap/bootstrap.pat (the folder is mounted into the container and gitignored). Wait for docker compose ps to show every service healthy; Zitadel takes the longest, up to a few minutes on first start.
Linux
The Zitadel container runs as an unprivileged user and must write into the mounted folder. On Linux hosts make it writable before the first start, as CI does (.github/workflows/ci.yml): chmod 0777 docker/zitadel/bootstrap. Docker Desktop on macOS and Windows hides this.
4. Migrate and seed the database
bash
pnpm db:migrate && pnpm db:seeddb:migrate applies the migrations under packages/db/drizzle, including the aletheia_app role and the row-level security policies. db:seed fills the development tenant (00000000-0000-0000-0000-000000000001) with the definitions the tutorial uses: the blocked_countries list, six rules (country_allowed, submitted_country_allowed, country_blocklisted, high_ownership_foreign_ubo, too_many_signups_per_ip, txn_amount_24h), the kyb-onboarding rule set, the kyb-basic collection flow, the workflows hello-world, kyb-onboarding and transaction-monitoring, and four case queues. It installs no app, and no seeded workflow calls one. The seed is idempotent: a definition that already matches is left alone, one that differs gets a new published version.
5. Seed Zitadel and paste its values
bash
pnpm auth:seedUsing the bootstrap PAT, the seed converges Zitadel to what the API and the admin console expect: the project aletheia with the roles admin, analyst and integration; the OIDC application admin-console (sign-in for http://localhost:5176) and the API application the API uses for token introspection; the human user analyst; and four machine users with PATs under docker/zitadel/bootstrap/: aletheia-smoke (smoke.pat, role admin), aletheia-smoke-integration (smoke-integration.pat, role integration), aletheia-directory (directory.pat, reads the user directory for the reviewer picker) and aletheia-management (management.pat, manages service users for Connect › API keys). It also brands Zitadel's login page like the admin console: its colours, the Archivo font, the Aletheia wordmark, and no Zitadel watermark. Every organization inherits this branding. With DATABASE_URL set (it is, through .env) it also links the organization to the development tenant.
Progress goes to stderr. Stdout is a ready-to-paste block, and this is the step to get right: copy these lines into .env, replacing the empty ones from the example (add any the example lacks):
ZITADEL_ISSUER=http://localhost:8090
ZITADEL_PROJECT_ID=...
ZITADEL_API_CLIENT_ID=...
ZITADEL_API_CLIENT_SECRET=...
ZITADEL_DIRECTORY_PAT=...
ZITADEL_MANAGEMENT_PAT=...
VITE_ZITADEL_ISSUER=http://localhost:8090
VITE_ZITADEL_PROJECT_ID=...
VITE_ZITADEL_CLIENT_ID_ADMIN_CONSOLE=...
VITE_ZITADEL_ORG_DOMAIN=aletheia-dev.localhostThe ZITADEL_* lines are for the API (it refuses to start without them); the VITE_* lines are for the admin console's sign-in. The collection terminal needs none: applicants never sign in. Two trailing comment lines name the PAT files. The seed is idempotent and prints the same block on every run, so pnpm auth:seed > zitadel.env is a safe way to keep it; the secrets exist only in Zitadel and these files, so do not delete seed.json or the .pat files unless you also reset Zitadel (below).
6. Run everything
bash
pnpm devTurbo builds every library once, then watches: the API on :4000 (and its internal listener on :4001), the Temporal worker, the app runner on :4100, the admin console on :5176, the collection terminal on :5177 and the landing page on :5178. The UIs forward /api/* on their own origin to the API on :4000 (ADMIN_CONSOLE_API_TARGET, COLLECTION_TERMINAL_API_TARGET and LANDING_API_TARGET point them elsewhere), so the browser never calls the API directly. At start-up the API logs catalogue reconciled once it has read the catalogue's index.
bash
pnpm smokeOptional, in a second terminal: the end-to-end smoke drives every seeded workflow through the running stack with the PATs from step 5, uploads the test app from step 2 as the tenant's own, calls a local vendor stub through installs of the catalogue's http-api and a stub of every model vendor through its model apps, and prints SMOKE OK. It is the same script CI runs, and the tutorial's commands are taken from it.
What to expect in each UI
| URL | What you see |
|---|---|
| http://localhost:5176 | Admin console. A sign-in page asks for the organization domain, then Home with the case figures. Operate › Cases is the workbench: the queues on the left (the system queues from All open to All cases, then the four seeded team queues), the filters and the table, empty until a run opens a case. Define lists the seeded definitions with their version and status. Connect › Apps lists http-api and the model apps from the catalogue; you, or the smoke, add installs of them. |
| http://localhost:5177 | Collection terminal. A "Secure forms" page that tells visitors to open the link they were sent; links look like /flow/<submissionId>#token=…, render one submission at a time and need no sign-in. You reach it through a link a workflow run or POST /collection-submissions produces. |
| http://localhost:5178 | Landing page. The SaaS product showcase with interactive examples, industry solution pages, and signup form (actanomos.com in production). Signup answers signup_unavailable on the development stack unless signup mailer and identity service settings are provided (Environment). |
| http://localhost:4000/docs | API reference (swagger-ui) for the running instance; /health/ready reports database and workflow engine readiness. |
| http://localhost:8233 | Workflow engine UI. Inspect running and completed workflows, named wf:<tenantId>:<runId>; useful to see where a run is waiting. |
| http://localhost:8090/ui/console | Identity provider console. Reviewers, machine accounts, projects, and roles. |
First login
On the sign-in page enter the organization domain aletheia-dev.localhost. Zitadel then asks for a login name and password: [email protected] / Password1! for the admin role, or [email protected] with the same password for a role that can read and decide cases but not edit definitions. On the first login Zitadel offers to set up a second factor; "Skip" is fine in development. You land back in the console signed in.
Common failures
- A port is already in use.
docker compose upfails with "port is already allocated", or a UI exits because its port is taken (the Vite configs setstrictPort: the console's OIDC redirect URI names5176and collection links name5177). Stop whatever holds the port; do not move the UIs. pnpm auth:seedsaysbootstrap PAT not found at .../bootstrap.pat. Zitadel has not finished starting (docker compose ps), or on Linux it could not write into the folder: run thechmodfrom step 3 and restart the container.pnpm tools:extismor the app build fails. extism-js needs macOS or glibc 2.39; on an older Linux, build in a container fromnode:22-trixie-slim, as the image build does.pnpm catalog:indexsayscatalog/<name> has no build. The index takes every catalogue package or none: run step 2's build, which includes--filter='./catalog/*', then the index again. Without the index, or with the API started before it, Connect › Apps does not list the catalogue's apps until the API starts again.- Apps answer
apps_unavailable. The API or the worker runs without apps:APP_RUNNER_URLor theSTORAGE_*values are missing from.env, or the app runner is not running (curl localhost:4100/health). Acall_appstep that fails withunknown_installnames an install the tenant does not have: steps and rules name an install, which can differ from the app's name (Apps: authoring). - The API exits at start-up complaining about environment variables. The
ZITADEL_*block was not pasted, or.envstill has the empty lines from the example. Paste it and runpnpm devagain. - A run stays in
waiting_collectionwith nosubmissionUrl. The worker signs collection links withSUBMISSION_TOKEN_SECRET(32 characters or more) and builds them fromCOLLECTION_FLOW_URL; both must be set in.env. The smoke reports this asno context.submissionUrl (is SUBMISSION_TOKEN_SECRET set for the worker?). - A collection link does not load. Links are built from
COLLECTION_FLOW_URL, which on the development stack must behttp://localhost:5177, wherepnpm devserves the collection terminal. Fix it in.envand restartpnpm dev; links already issued keep the old address, so start a new run. - Sign-in works but every API call answers 403
forbidden. The organization was not linked to a tenant, so the API rejects tokens issued for it. Runpnpm tenant:add --org <org id> --name dev --id 00000000-0000-0000-0000-000000000001; the seed prints this exact command when it cannot reach the database. - Starting over.
pnpm infra:down, thendocker compose down -vto drop the volumes, and deletedocker/zitadel/bootstrap/seed.jsonand the.patfiles together with them: Zitadel's state and the bootstrap files must match.
See the environment reference for every variable the processes read.
Next
You have a running stack and a seeded tenant. Go to Tutorial: a first decision to create a subject, run kyb-onboarding, decide the case it opens and change a rule to see the outcome change.