chore(services): rename services/provisioning -> services/platform-api

O.0 prep from OPERATOR-PLAN.md. Mechanical refactor before adding partner
management and operator-specific endpoints. The service now owns more than
just provisioning orchestration (it'll soon own partners, tenant lifecycle
actions, multi-audience JWT validation), so the name 'platform-api' reflects
its scope better.

What changed:
- Directory: services/provisioning/ -> services/platform-api/
- Package: @dezky/provisioning -> @dezky/platform-api
- Docker: container_name dezky-provisioning -> dezky-platform-api;
  compose service key 'provisioning' -> 'platform-api'; volume
  provisioning_node_modules -> platform_api_node_modules
- Portal: PROVISIONING_INTERNAL_URL env var -> PLATFORM_API_INTERNAL_URL,
  default URL http://provisioning:3001 -> http://platform-api:3001 in all
  three proxy routes (me.get.ts, tenants/index.post.ts, tenants/[slug]/
  reconcile.post.ts), plus NUXT_API_BASE updated
- Health endpoint service identifier and main.ts log lines updated to
  'dezky-platform-api'
- Docs swept: README, CLAUDE.md, SERVICES.md, AUTHENTIK-SETUP.md,
  NEXT-STEPS.md, TROUBLESHOOTING.md, OPERATOR-PLAN.md, traefik/dynamic.yml

What deliberately stays:
- Internal module names ProvisioningService / ProvisioningModule (those
  describe an orchestration sub-concern, not the service's purpose)
- Tenant.provisioningStatus / provisioningErrors field names (state
  per integration, not service name)
- File services/platform-api/src/tenants/provisioning.service.ts
- 'Hetzner provisioning' references in production-prep docs (infrastructure
  provisioning, unrelated)

Verified end-to-end after rename: /api/me returns 200 with profile + 2
tenants + subscription, /api/tenants/dezky/reconcile returns 200 with
Authentik integration still ok.

OPERATOR-PLAN.md O.0 checkboxes ticked.
This commit is contained in:
Ronni Baslund
2026-05-24 00:35:01 +02:00
parent fb3d7aa716
commit 22b2583f0b
49 changed files with 66 additions and 60 deletions
+4 -4
View File
@@ -27,7 +27,7 @@ All components are Apache 2.0 / MIT licensed for clean commercial multi-tenant h
| OCIS | `owncloud/ocis:7.0` | File storage (S3-compatible backend) | `https://files.dezky.local` | | OCIS | `owncloud/ocis:7.0` | File storage (S3-compatible backend) | `https://files.dezky.local` |
| Collabora | `collabora/code:latest` | Office document editor inside OCIS | `https://office.dezky.local` | | Collabora | `collabora/code:latest` | Office document editor inside OCIS | `https://office.dezky.local` |
| Portal stub | (built from `./apps/portal`) | Nuxt 3 customer portal | `https://app.dezky.local` | | Portal stub | (built from `./apps/portal`) | Nuxt 3 customer portal | `https://app.dezky.local` |
| Provisioning | (built from `./services/provisioning`) | NestJS provisioning worker | (internal, port 3001) | | Platform API | (built from `./services/platform-api`) | NestJS service · tenants/partners/users/provisioning orchestration | `api.dezky.local` (+ internal port 3001) |
**NOT included in this dev setup** (added in later phases): **NOT included in this dev setup** (added in later phases):
- Jitsi Meet (4-5 sub-containers — see `docker-compose.optional.yml` when ready) - Jitsi Meet (4-5 sub-containers — see `docker-compose.optional.yml` when ready)
@@ -65,7 +65,7 @@ dezky/
├── apps/ ├── apps/
│ └── portal/ # Nuxt 3 portal (stub for now) │ └── portal/ # Nuxt 3 portal (stub for now)
├── services/ ├── services/
│ └── provisioning/ # NestJS worker (stub for now) │ └── platform-api/ # NestJS service · platform control plane
├── packages/ # Shared TypeScript packages (empty for now) ├── packages/ # Shared TypeScript packages (empty for now)
├── infrastructure/ ├── infrastructure/
│ └── docker-compose/ │ └── docker-compose/
@@ -226,8 +226,8 @@ See `docs/TROUBLESHOOTING.md` for detailed solutions.
## After local dev works ## After local dev works
1. Build out the Nuxt portal (`apps/portal`) — start with auth flow via Authentik OIDC 1. Build out the Nuxt portal (`apps/portal`) — start with auth flow via Authentik OIDC
2. Build the provisioning service (`services/provisioning`) — first endpoint: create tenant 2. Build the platform API (`services/platform-api`) — first endpoint: create tenant
3. Wire portal → provisioning → Authentik/OCIS/Stalwart admin APIs 3. Wire portal → platform-api → Authentik/OCIS/Stalwart admin APIs
4. Add Zulip + Jitsi when ready (`docker-compose.optional.yml`) 4. Add Zulip + Jitsi when ready (`docker-compose.optional.yml`)
5. When portal MVP is solid → migrate to Hetzner AX41 production 5. When portal MVP is solid → migrate to Hetzner AX41 production
+1 -1
View File
@@ -42,7 +42,7 @@ The bootstrap script:
``` ```
dezky/ dezky/
├── apps/portal/ Nuxt 3 customer portal ├── apps/portal/ Nuxt 3 customer portal
├── services/provisioning/ NestJS provisioning worker ├── services/platform-api/ NestJS service · tenants, partners, users, provisioning orchestration
├── packages/ Shared TypeScript libraries ├── packages/ Shared TypeScript libraries
├── infrastructure/ ├── infrastructure/
│ └── docker-compose/ Local development stack │ └── docker-compose/ Local development stack
+1 -1
View File
@@ -63,7 +63,7 @@ export default defineNuxtConfig({
// Authentik's access tokens aren't always parseable as JWT — skip strict parsing // Authentik's access tokens aren't always parseable as JWT — skip strict parsing
skipAccessTokenParsing: true, skipAccessTokenParsing: true,
// Expose access token in the server-side session so Nitro route handlers can // Expose access token in the server-side session so Nitro route handlers can
// forward it to provisioning. Token never reaches the browser. // forward it to platform-api. Token never reaches the browser.
exposeAccessToken: true, exposeAccessToken: true,
}, },
}, },
+4 -4
View File
@@ -1,8 +1,8 @@
// Scaffolding route: pulls the signed-in user's profile + tenants + subscriptions // Scaffolding route: pulls the signed-in user's profile + tenants + subscriptions
// from the provisioning service, using the user's Authentik access token forwarded // from platform-api, using the user's Authentik access token forwarded from the
// from the encrypted server-side session. // encrypted server-side session.
// //
// Verifies the full chain: portal session → access token → provisioning JWT guard → Mongo. // Verifies the full chain: portal session → access token → platform-api JWT guard → Mongo.
import { getUserSession } from 'nuxt-oidc-auth/runtime/server/utils/session.js' import { getUserSession } from 'nuxt-oidc-auth/runtime/server/utils/session.js'
@@ -14,7 +14,7 @@ export default defineEventHandler(async (event) => {
throw createError({ statusCode: 401, statusMessage: 'Not signed in or no access token' }) throw createError({ statusCode: 401, statusMessage: 'Not signed in or no access token' })
} }
const base = process.env.PROVISIONING_INTERNAL_URL ?? 'http://provisioning:3001' const base = process.env.PLATFORM_API_INTERNAL_URL ?? 'http://platform-api:3001'
const headers = { Authorization: `Bearer ${accessToken}` } const headers = { Authorization: `Bearer ${accessToken}` }
const [profile, tenants, subscriptions] = await Promise.all([ const [profile, tenants, subscriptions] = await Promise.all([
@@ -7,7 +7,7 @@ export default defineEventHandler(async (event) => {
throw createError({ statusCode: 401, statusMessage: 'Not signed in' }) throw createError({ statusCode: 401, statusMessage: 'Not signed in' })
} }
const slug = getRouterParam(event, 'slug') const slug = getRouterParam(event, 'slug')
const base = process.env.PROVISIONING_INTERNAL_URL ?? 'http://provisioning:3001' const base = process.env.PLATFORM_API_INTERNAL_URL ?? 'http://platform-api:3001'
return $fetch(`${base}/tenants/${slug}/reconcile`, { return $fetch(`${base}/tenants/${slug}/reconcile`, {
method: 'POST', method: 'POST',
headers: { Authorization: `Bearer ${accessToken}` }, headers: { Authorization: `Bearer ${accessToken}` },
+5 -5
View File
@@ -1,7 +1,7 @@
// Dev/scaffolding: proxies POST /tenants to the provisioning service with the // Dev/scaffolding: proxies POST /tenants to platform-api with the logged-in
// logged-in user's access token. Lets you create a tenant from the browser // user's access token. Lets you create a tenant from the browser without
// without minting tokens by hand. Will be replaced by a real "create workspace" // minting tokens by hand. Will be replaced by a real "create workspace" flow
// flow with proper UI later. // with proper UI later.
import { getUserSession } from 'nuxt-oidc-auth/runtime/server/utils/session.js' import { getUserSession } from 'nuxt-oidc-auth/runtime/server/utils/session.js'
@@ -13,7 +13,7 @@ export default defineEventHandler(async (event) => {
} }
const body = await readBody(event) const body = await readBody(event)
const base = process.env.PROVISIONING_INTERNAL_URL ?? 'http://provisioning:3001' const base = process.env.PLATFORM_API_INTERNAL_URL ?? 'http://platform-api:3001'
return $fetch(`${base}/tenants`, { return $fetch(`${base}/tenants`, {
method: 'POST', method: 'POST',
+4 -4
View File
@@ -130,9 +130,9 @@ EOF
Note: Stalwart's OIDC integration is configured in `infrastructure/docker-compose/configs/stalwart/config.toml`. For local dev with internal users, OIDC is optional. Note: Stalwart's OIDC integration is configured in `infrastructure/docker-compose/configs/stalwart/config.toml`. For local dev with internal users, OIDC is optional.
## 4. Get the API token for provisioning service ## 4. Get the API token for platform-api
The provisioning service needs to call Authentik's API to create tenants, users, and applications. `.env` holds a pre-generated value in `AUTHENTIK_BOOTSTRAP_TOKEN`, but Authentik 2025.10 does **not** materialize that env var into a usable API token on first boot. You need to create the token once and bind it to `akadmin`. platform-api needs to call Authentik's API to create tenants, users, and applications. `.env` holds a pre-generated value in `AUTHENTIK_BOOTSTRAP_TOKEN`, but Authentik 2025.10 does **not** materialize that env var into a usable API token on first boot. You need to create the token once and bind it to `akadmin`.
### One-time setup ### One-time setup
@@ -158,7 +158,7 @@ print('Token bound to akadmin')
" "
``` ```
Alternative: create the token through the UI — **Directory → Tokens & App passwords → Create**, set `Intent: API`, `User: akadmin`, then copy the key into `.env` and restart the provisioning service. Alternative: create the token through the UI — **Directory → Tokens & App passwords → Create**, set `Intent: API`, `User: akadmin`, then copy the key into `.env` and restart platform-api.
### Verify it works ### Verify it works
@@ -187,7 +187,7 @@ For local dev, you can either:
- Tenant subdomain pattern: `{tenant}.auth.dezky.local` - Tenant subdomain pattern: `{tenant}.auth.dezky.local`
- More realistic but more setup overhead - More realistic but more setup overhead
For dev, start with Option A. The provisioning service should be built to support Option B from day one (data model includes `tenantId`). For dev, start with Option A. platform-api should be built to support Option B from day one (data model includes `tenantId`).
## 6. Test SSO flow end-to-end ## 6. Test SSO flow end-to-end
+6 -6
View File
@@ -41,15 +41,15 @@ Goal: Users can log in to the portal via Authentik.
## Phase 3: Tenant data model (week 1-2) — done ## Phase 3: Tenant data model (week 1-2) — done
- [x] Mongoose schemas in `services/provisioning/src/schemas/` (Tenant, User, Subscription) - [x] Mongoose schemas in `services/platform-api/src/schemas/` (Tenant, User, Subscription)
- [x] Tenant: slug, name, status, plan, domains, authentikGroupId, ocisSpaceId, stalwartDomain, billingInfo - [x] Tenant: slug, name, status, plan, domains, authentikGroupId, ocisSpaceId, stalwartDomain, billingInfo
- [x] User: authentikSubjectId, tenantIds[], email, name, role, active, lastLoginAt - [x] User: authentikSubjectId, tenantIds[], email, name, role, active, lastLoginAt
- [x] Subscription: tenantId, plan, status, stripeCustomerId, stripeSubscriptionId, period dates - [x] Subscription: tenantId, plan, status, stripeCustomerId, stripeSubscriptionId, period dates
- [x] CRUD endpoints behind `JwtAuthGuard` (validates Authentik JWT via JWKS) - [x] CRUD endpoints behind `JwtAuthGuard` (validates Authentik JWT via JWKS)
- [x] Group-based authorization: users see only tenants whose slug matches one of their Authentik `groups`; `dezky-platform-admins` group has global access - [x] Group-based authorization: users see only tenants whose slug matches one of their Authentik `groups`; `dezky-platform-admins` group has global access
- [x] Idempotent seed (`SeedService`) creates the `dezky` tenant + matching subscription on bootstrap - [x] Idempotent seed (`SeedService`) creates the `dezky` tenant + matching subscription on bootstrap
- [x] Provisioning exposed at `https://api.dezky.local` (Traefik label, dev only) and via internal `http://provisioning:3001` - [x] platform-api exposed at `https://api.dezky.local` (Traefik label, dev only) and via internal `http://platform-api:3001`
- [x] Portal Nitro route at `/api/me` forwards the user's encrypted access token to provisioning — verified end-to-end - [x] Portal Nitro route at `/api/me` forwards the user's encrypted access token to platform-api — verified end-to-end
### Endpoints ### Endpoints
@@ -91,9 +91,9 @@ upstream-specific work.
| Concern | File | | Concern | File |
|---|---| |---|---|
| Integration clients | `services/provisioning/src/integrations/{authentik,stalwart,ocis}.client.ts` | | Integration clients | `services/platform-api/src/integrations/{authentik,stalwart,ocis}.client.ts` |
| Orchestration | `services/provisioning/src/tenants/provisioning.service.ts` | | Orchestration | `services/platform-api/src/tenants/provisioning.service.ts` |
| `/tenants/:slug/reconcile` | `services/provisioning/src/tenants/tenants.controller.ts` | | `/tenants/:slug/reconcile` | `services/platform-api/src/tenants/tenants.controller.ts` |
| Portal proxy routes | `apps/portal/server/api/tenants/index.post.ts` + `[slug]/reconcile.post.ts` | | Portal proxy routes | `apps/portal/server/api/tenants/index.post.ts` + `[slug]/reconcile.post.ts` |
### Quick smoke test ### Quick smoke test
+13 -10
View File
@@ -5,7 +5,7 @@ for Dezky staff: managing tenants, partners, operating the platform.
Distinct from the customer portal at `app.dezky.local`. Different OAuth client, Distinct from the customer portal at `app.dezky.local`. Different OAuth client,
different cookie domain, different surface — though they share Authentik as the different cookie domain, different surface — though they share Authentik as the
IdP and (eventually) the provisioning service as the backend. IdP and (eventually) platform-api as the backend.
This file is the running record of decisions made during the design grilling This file is the running record of decisions made during the design grilling
session. Updated inline as questions resolve. session. Updated inline as questions resolve.
@@ -42,7 +42,7 @@ renders against mock-data fixtures until its backend is built.
Two genuinely new things on the backend: Two genuinely new things on the backend:
1. **Partner schema and CRUD** in `services/provisioning` — id, name, domain, 1. **Partner schema and CRUD** in `services/platform-api` — id, name, domain,
status, customers count (computed), MRR (computed), margin, sinceDate. Tenants status, customers count (computed), MRR (computed), margin, sinceDate. Tenants
gain an optional `partnerId` field. The existing `dezky` seed gets no partner. gain an optional `partnerId` field. The existing `dezky` seed gets no partner.
2. **Tenant lifecycle actions** beyond create — suspend, resume, change plan, 2. **Tenant lifecycle actions** beyond create — suspend, resume, change plan,
@@ -247,15 +247,18 @@ In rough priority order:
Tick boxes as work lands. Each phase is roughly one commit. Phases must be Tick boxes as work lands. Each phase is roughly one commit. Phases must be
done in order — earlier ones unblock later ones. done in order — earlier ones unblock later ones.
### O.0 · Prep — service rename ### O.0 · Prep — service rename
- [ ] Rename `services/provisioning/``services/platform-api/` - [x] Rename `services/provisioning/``services/platform-api/`
- [ ] Update `package.json` name → `@dezky/platform-api` - [x] Update `package.json` name → `@dezky/platform-api`
- [ ] Update `docker-compose.yml`: container name, service key, network - [x] Update `docker-compose.yml`: container name, service key, volume name,
alias, volume names, env var `PROVISIONING_INTERNAL_URL` env var `PROVISIONING_INTERNAL_URL` `PLATFORM_API_INTERNAL_URL`,
`PLATFORM_API_INTERNAL_URL` NUXT_API_BASE points at new hostname
- [ ] Update portal proxy routes to point at `http://platform-api:3001` - [x] Update portal proxy routes to read `PLATFORM_API_INTERNAL_URL` and
- [ ] Verify customer portal `/api/me` still works end-to-end after rename default to `http://platform-api:3001`
- [x] Sweep docs (README, CLAUDE.md, SERVICES.md, AUTHENTIK-SETUP.md,
NEXT-STEPS.md, TROUBLESHOOTING.md) for stale references
- [x] Verify customer portal `/api/me` still works end-to-end after rename
### O.1 · Authentik — operator OAuth client ### O.1 · Authentik — operator OAuth client
+8 -8
View File
@@ -219,7 +219,7 @@ curl -k https://office.dezky.local/hosting/discovery
**Environment:** **Environment:**
- `NUXT_PUBLIC_AUTH_URL`: Authentik URL (client-side) - `NUXT_PUBLIC_AUTH_URL`: Authentik URL (client-side)
- `NUXT_API_BASE`: provisioning service URL (server-side) - `NUXT_API_BASE`: platform-api URL (server-side)
- `MONGODB_URI`: MongoDB connection string - `MONGODB_URI`: MongoDB connection string
**Debug:** **Debug:**
@@ -233,12 +233,12 @@ docker compose exec portal sh
--- ---
## Provisioning Service (NestJS) ## Platform API (NestJS)
**Container:** `dezky-provisioning` **Container:** `dezky-platform-api`
**Port:** 3001 (internal only) **Port:** 3001 (also exposed via Traefik at `api.dezky.local`)
**Source:** `services/provisioning/` **Source:** `services/platform-api/`
**Purpose:** Tenant lifecycle, billing webhooks, service orchestration **Purpose:** Platform control plane — tenants, partners, users, subscriptions, provisioning orchestration, billing webhooks
**Endpoints to implement:** **Endpoints to implement:**
- `POST /tenants` — Create tenant - `POST /tenants` — Create tenant
@@ -255,8 +255,8 @@ docker compose exec portal sh
**Debug:** **Debug:**
```bash ```bash
docker compose logs -f provisioning docker compose logs -f platform-api
# Test health endpoint # Test health endpoint
docker compose exec provisioning wget -qO- http://localhost:3001/health docker compose exec platform-api wget -qO- http://localhost:3001/health
``` ```
+1 -1
View File
@@ -203,7 +203,7 @@ export default defineNuxtConfig({
}) })
``` ```
### NestJS provisioning doesn't restart ### NestJS platform-api doesn't restart
Same issue. The `start:dev` command uses nodemon under the hood. Make sure your `package.json` has: Same issue. The `start:dev` command uses nodemon under the hood. Make sure your `package.json` has:
```json ```json
@@ -31,7 +31,7 @@ http:
stsSeconds: 15552000 stsSeconds: 15552000
customFrameOptionsValue: "SAMEORIGIN" customFrameOptionsValue: "SAMEORIGIN"
# CORS for API calls between portal and provisioning service # CORS for API calls between portal and platform-api
cors: cors:
headers: headers:
accessControlAllowMethods: accessControlAllowMethods:
@@ -29,7 +29,7 @@ volumes:
ocis_config: ocis_config:
ocis_data: ocis_data:
portal_node_modules: portal_node_modules:
provisioning_node_modules: platform_api_node_modules:
services: services:
# ───────────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────────
@@ -361,7 +361,8 @@ services:
NUXT_PORT: 3000 NUXT_PORT: 3000
NUXT_PUBLIC_AUTH_URL: https://auth.dezky.local NUXT_PUBLIC_AUTH_URL: https://auth.dezky.local
NUXT_PUBLIC_PORTAL_URL: https://app.dezky.local NUXT_PUBLIC_PORTAL_URL: https://app.dezky.local
NUXT_API_BASE: http://provisioning:3001 NUXT_API_BASE: http://platform-api:3001
PLATFORM_API_INTERNAL_URL: http://platform-api:3001
MONGODB_URI: mongodb://root:${MONGO_ROOT_PASSWORD}@mongo:27017/dezky?authSource=admin MONGODB_URI: mongodb://root:${MONGO_ROOT_PASSWORD}@mongo:27017/dezky?authSource=admin
# OIDC (confidential client) — used by Nuxt server middleware # OIDC (confidential client) — used by Nuxt server middleware
NUXT_OIDC_CLIENT_ID: ${PORTAL_OIDC_CLIENT_ID} NUXT_OIDC_CLIENT_ID: ${PORTAL_OIDC_CLIENT_ID}
@@ -389,11 +390,12 @@ services:
- traefik.http.services.portal.loadbalancer.server.port=3000 - traefik.http.services.portal.loadbalancer.server.port=3000
# ───────────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────────
# Provisioning service — NestJS worker for tenant lifecycle # platform-api — NestJS service. Owns tenants, partners, users,
# subscriptions, and provisioning orchestration.
# ───────────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────────
provisioning: platform-api:
image: node:20-alpine image: node:20-alpine
container_name: dezky-provisioning container_name: dezky-platform-api
restart: unless-stopped restart: unless-stopped
working_dir: /app working_dir: /app
command: sh -c "corepack enable && corepack prepare pnpm@latest --activate && pnpm install && pnpm start:dev" command: sh -c "corepack enable && corepack prepare pnpm@latest --activate && pnpm install && pnpm start:dev"
@@ -414,8 +416,8 @@ services:
# Trust mkcert root CA for Node fetch (dev only) # Trust mkcert root CA for Node fetch (dev only)
NODE_EXTRA_CA_CERTS: /etc/ssl/mkcert-root.pem NODE_EXTRA_CA_CERTS: /etc/ssl/mkcert-root.pem
volumes: volumes:
- ../../services/provisioning:/app - ../../services/platform-api:/app
- provisioning_node_modules:/app/node_modules - platform_api_node_modules:/app/node_modules
- ./certs/mkcert-root.pem:/etc/ssl/mkcert-root.pem:ro - ./certs/mkcert-root.pem:/etc/ssl/mkcert-root.pem:ro
networks: [dezky] networks: [dezky]
depends_on: depends_on:
@@ -1,8 +1,8 @@
{ {
"name": "@dezky/provisioning", "name": "@dezky/platform-api",
"version": "0.0.1", "version": "0.0.1",
"private": true, "private": true,
"description": "Dezky tenant provisioning worker — NestJS", "description": "Dezky platform API — tenants, partners, users, provisioning orchestration (NestJS)",
"scripts": { "scripts": {
"build": "nest build", "build": "nest build",
"start": "nest start", "start": "nest start",
@@ -6,7 +6,7 @@ export class HealthController {
check() { check() {
return { return {
status: 'ok', status: 'ok',
service: 'dezky-provisioning', service: 'dezky-platform-api',
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
} }
} }
@@ -1,5 +1,6 @@
// Dezky Provisioning Service — Entry point // Dezky platform API — Entry point.
// Handles tenant lifecycle: create, suspend, delete, billing webhooks. // Owns the platform control plane: tenants, partners, users, subscriptions,
// plus the provisioning orchestration (Authentik / Stalwart / OCIS).
import { ValidationPipe } from '@nestjs/common' import { ValidationPipe } from '@nestjs/common'
import { NestFactory } from '@nestjs/core' import { NestFactory } from '@nestjs/core'
@@ -29,10 +30,10 @@ async function bootstrap() {
const port = Number(process.env.PORT ?? 3001) const port = Number(process.env.PORT ?? 3001)
await app.listen(port, '0.0.0.0') await app.listen(port, '0.0.0.0')
console.log(`Provisioning service listening on http://0.0.0.0:${port}`) console.log(`platform-api listening on http://0.0.0.0:${port}`)
} }
bootstrap().catch((err) => { bootstrap().catch((err) => {
console.error('Failed to start provisioning service', err) console.error('Failed to start platform-api', err)
process.exit(1) process.exit(1)
}) })