This guide starts the marketplace API, PostgreSQL, Redis, and self-hosted ZITADEL from a fresh clone. Authentication is not embedded in NestJS: the browser signs in through ZITADEL Hosted Login and sends its access token to the API.
For the IAM design and production runbook, also read ZITADEL Authentication and Self-Hosting.
Install:
- Node.js 20.19+ (Node 22 LTS is the recommended development baseline);
- Corepack and pnpm 10.14;
- Docker Engine 24+ with Docker Compose v2;
- Terraform 1.8+;
- Git.
Docker should have at least 2 GB available for ZITADEL in addition to the marketplace services.
Verify the tools:
node --version
corepack --version
docker --version
docker compose version
terraform version
git --versionDefault local ports are:
| Service | URL/port |
|---|---|
| Marketplace API | http://localhost:3000 |
| Swagger | http://localhost:3000/docs |
| ZITADEL | http://auth.localhost:8080 |
| Marketplace PostgreSQL | localhost:5432 |
| Redis | localhost:6379 |
From the repository root:
corepack enable
corepack pnpm install --frozen-lockfile
Copy-Item .env.example .env
Copy-Item infra/zitadel/.env.example infra/zitadel/.env
New-Item -ItemType Directory -Force infra/zitadel/bootstrap | Out-Null
New-Item -ItemType Directory -Force secrets/zitadel | Out-NullThe root .env, ZITADEL .env, bootstrap files, runtime keys, Terraform state, and migration exports are ignored. Do not force-add them.
Before starting ZITADEL for the first time, replace these values in infra/zitadel/.env:
ZITADEL_MASTERKEY: a randomly generated value of exactly 32 characters; it cannot be changed after initialization;ZITADEL_FIRSTINSTANCE_ORG_HUMAN_PASSWORD: a strong temporary administrator password;POSTGRES_ADMIN_PASSWORDand the matching password inZITADEL_DATABASE_POSTGRES_DSN.
The local stack uses plain HTTP. Never expose it beyond an isolated development machine.
Start the pinned ZITADEL, Login, Traefik, and independent PostgreSQL stack:
Set-Location infra/zitadel
docker compose --env-file .env -f compose.yml up -d --wait
docker compose --env-file .env -f compose.yml ps
Set-Location ../..Open http://auth.localhost:8080/ui/console/. The initial login normally follows zitadel-admin@zitadel.auth.localhost. Use the temporary password from the ZITADEL .env and change it.
Provision the marketplace organizations, project, capability roles, browser/API applications, default customer-registration actions, grants, and runtime machine keys:
Set-Location infra/zitadel/terraform
Copy-Item terraform.tfvars.example terraform.tfvars
terraform init
terraform plan
terraform apply
terraform output
terraform output -raw web_client_id
terraform output -raw api_client_id
Set-Location ../../..Copy the output values to the corresponding root .env variables:
ZITADEL_ISSUER=http://auth.localhost:8080
ZITADEL_PROJECT_ID=<project_id>
ZITADEL_WEB_CLIENT_ID=<web_client_id>
ZITADEL_API_CLIENT_ID=<api_client_id>
ZITADEL_PLATFORM_ORG_ID=<platform_organization_id>
ZITADEL_CUSTOMER_ORG_ID=<customer_organization_id>
ZITADEL_API_PRIVATE_KEY_PATH=./secrets/zitadel/marketplace-api-key.json
ZITADEL_PROVISIONER_KEY_PATH=./secrets/zitadel/provisioner-key.json
ZITADEL_PROVISIONING_ENABLED=trueLeave ZITADEL_ROLE_CLAIM and ZITADEL_WEB_SCOPES empty to derive the project-specific defaults. The generated key paths must exist and match their configured client/user IDs.
Start the marketplace PostgreSQL and Redis services:
docker compose up -d postgres redis
docker compose psGenerate Prisma Client, apply every checked-in migration, and load deterministic commerce fixtures:
corepack pnpm prisma:generate
corepack pnpm prisma:validate
corepack pnpm migrate:deploy
corepack pnpm seedmigrate:deploy is for checked-in migrations. Use migrate:dev -- --name <name> only while authoring a new local migration. Never use prisma db push as a production deployment strategy, and never reset a database containing valuable data.
pnpm seed also runs migrate:deploy before loading fixtures. If an older database is missing User.zitadelUserId even though Prisma reports the original ZITADEL migration as applied, the checked-in reconciliation migration adds the missing ZITADEL columns and provisioning records without deleting marketplace or rollback data.
The seed creates no credentials or ZITADEL assignments. Its seed-* subjects are deterministic references for automated tests, not accounts you can log into.
corepack pnpm start:devIn another terminal:
Invoke-RestMethod http://localhost:3000/api/v1/health/live
Invoke-RestMethod http://localhost:3000/api/v1/health/ready
Invoke-RestMethod http://localhost:3000/api/v1/auth/configReadiness verifies marketplace PostgreSQL, Redis when queues are enabled, ZITADEL discovery, and the introspection application key. Liveness intentionally verifies only the API process.
Open Swagger at http://localhost:3000/docs. Protected calls need a ZITADEL access token in Swagger's Bearer authorization field. There is no /auth/login, /auth/register, or backend login UI.
For customer registration, a frontend OIDC client should:
- read
/api/v1/auth/config; - run Authorization Code with PKCE against the returned issuer/client ID;
- add the returned
customerOrganizationScopefor customer login/registration; - use
prompt=createfor the sign-up entry point; - send the access token—not the ID token—to the API.
Terraform configures the customer organization as the default and attaches fail-closed internal/external post-creation actions that assign the customer-safe capability bundle.
To test platform operations without a frontend, use ZITADEL Console to create a human user under marketplace-platform, then create a marketplace project role assignment for the required capabilities. Persona bundles are arrays in config/zitadel-capabilities.json; the role group labels make batch selection easier in Console. ZITADEL instance/Console administrator roles do not automatically grant marketplace permissions.
The first valid API request JIT-creates a minimal local User keyed by ZITADEL sub. Email/profile changes remain in ZITADEL; addresses and marketplace preferences remain local.
With suitable customer, vendor, moderator, and platform-admin tokens:
- A customer submits a vendor application and verification metadata.
- A platform administrator approves it. The vendor remains
APPROVEDwhile an IAM job creates the ZITADEL vendor organization/project grant/owner assignment. - Successful IAM provisioning moves the vendor to
ACTIVE. - The owner creates a product, variants, media references, and inventory and submits it.
- A platform moderator approves/publishes the product.
- A customer adds products to a cart and checks out with an
Idempotency-Key. - The local mock capture endpoint creates a signed webhook; authoritative handling commits stock and creates parent/vendor orders.
- Each vendor processes only its scoped order, creates a shipment, and advances tracking in development.
- Delivery records commission/vendor earnings and allows settlement eligibility and verified review.
Staff is assigned with POST /api/v1/vendors/{vendorId}/staff using a ZITADEL user ID and revoked with DELETE /api/v1/vendors/{vendorId}/staff/{zitadelUserId}. Durable operation status/retry is under /api/v1/admin/iam/provisioning.
Keep the ZITADEL stack running, then from the repository root:
docker compose --profile application up --build -d
docker compose --profile application run --rm migrate pnpm seed
docker compose psThe API container maps auth.localhost to the Docker host gateway, so both browser and container use the exact issuer http://auth.localhost:8080. Generated key files are mounted read-only from ZITADEL_KEYS_DIR (default ./secrets/zitadel).
Inspect logs:
docker compose logs -f api
docker compose -f infra/zitadel/compose.yml --env-file infra/zitadel/.env logs -f zitadel-api zitadel-loginBack up and restore-test both databases. After applying the additive ZITADEL migration, run the read-only legacy IAM report:
corepack pnpm zitadel:migration:exportResolve unknown capability keys before writing sensitive artifacts:
corepack pnpm zitadel:migration:export -- --output ./secrets/zitadel/migrationFollow the reconciliation/cutover procedure in docs/zitadel.md. The old physical credential/token/session/RBAC/vendor-membership data is deliberately retained for rollback. Dropping it is a separate, explicitly approved post-soak migration.
corepack pnpm format:check
corepack pnpm lint
corepack pnpm typecheck
corepack pnpm prisma:validate
corepack pnpm test
corepack pnpm buildThe database-backed suite is destructive to its disposable fixtures (it creates orders and consumes inventory), so use only a development/test database:
$env:RUN_DATABASE_E2E='true'
corepack pnpm test:e2eUnit/e2e tests replace network introspection with deterministic fakes. A real-ZITADEL Hosted Login/provisioning integration test remains a pre-launch environment check.
Show state and logs:
docker compose ps --all
docker compose logs -f postgres redis
docker compose -f infra/zitadel/compose.yml --env-file infra/zitadel/.env psStop containers while retaining volumes:
docker compose --profile application down
docker compose -f infra/zitadel/compose.yml --env-file infra/zitadel/.env downDo not add -v unless permanent deletion of the explicitly inspected local database volumes is intended.
- Use a real FQDN, trusted TLS, correct external host/port/secure settings, and HTTP/2-capable proxying.
- Use non-default credentials, encrypted Terraform remote state, a secret manager, restricted runtime machine roles, and key-expiry/rotation procedures.
- Configure/test SMTP, MFA/recovery policy, abuse controls, privacy/retention, and emergency account/session revocation.
- Back up marketplace and ZITADEL PostgreSQL independently; custody and restore-test the immutable ZITADEL master key.
- Rehearse ZITADEL upgrades in staging with the next pinned version and a database snapshot.
- Monitor certificate/key expiry, discovery/introspection latency and errors, failed IAM operations, ZITADEL/PostgreSQL capacity, queue failures, and security audit events.
- Treat Compose as single-host/semi-production. Select an orchestrated highly available ZITADEL topology when availability requirements demand it.
Use corepack pnpm ... or run corepack enable; the repository pins its package manager version.
It should resolve to loopback on modern systems. Add a local hosts entry for development only if your resolver does not implement .localhost. Do not change the API to an internal issuer: browser and API must observe the exact same public issuer.
Verify the issuer's /.well-known/openid-configuration, exact scheme/host/port, ZITADEL container health, API key path/mount, and that the key's clientId matches ZITADEL_API_CLIENT_ID.
Use an access token, request the project audience/roles scopes from /auth/config, and verify issuer, project ID, expiry, and role-claim name. An inactive or malformed token is always rejected.
Inspect the role assignment organization. Platform capabilities are accepted only under the platform organization; vendor capabilities must be under the resource vendor's exact organization. A role with the correct name under the wrong organization is intentionally denied.
Inspect /api/v1/admin/iam/provisioning and logs. Correct ZITADEL credentials/permissions/network/configuration, then invoke the explicit retry endpoint. Do not manually set the vendor active without the organization/grant/owner assignment.
Confirm infra/zitadel/bootstrap/admin-machine-key.json was created on first ZITADEL initialization and bootstrap_key_file points to it. Do not regenerate first-instance state by deleting a populated ZITADEL database.
ZITADEL_DOMAIN, ZITADEL_EXTERNALPORT, and ZITADEL_EXTERNALSECURE must describe the public URL exactly. Preserve the original host through the reverse proxy.