Ga naar inhoud

Het master dashboard van binnen

apps/master-dashboard is een Nuxt 4-app (Vue, met Nitro als server) voor de platformbeheerder. Eén installatie, één eigen database (platform_master), en geen rechtstreekse toegang tot klantdatabases: alles wat het over sites weet, komt via webhooks binnen of gaat via de Coolify- en Cloudflare-API naar buiten. Het is de tegenhanger van de klantsite; het protocol tussen beide staat in Contract.

  • app/pages/: login.vue, index.vue (klantenoverzicht), tenants/new.vue (aanmaken), tenants/[slug].vue (detail met status, domeinen, deployments en activiteit). app/middleware/auth.global.ts stuurt bezoekers zonder sessie naar /login.
  • server/api/: de routes, één bestand per methode (tenants/index.get.ts, tenants/[slug]/provision.post.ts, webhooks/site.post.ts en zo verder).
  • server/utils/: de logica. Routes blijven dun en roepen deze functies aan: tenant-store.ts (lezen en schrijven van tenants), provision-tenant.ts (de orchestrator), coolify.ts en cloudflare.ts (API-clients), custom-domains.ts, secrets.ts, webhooks.ts en receive-site-webhook.ts, tenant-attention.ts, session.ts en auth.ts.
  • Tests staan ernaast als *.test.ts (pure logica) en *.integration.ts (met database).

Het master-schema komt uit @platform/db/master: de tabellen tenants, domains, deployments, activity, site_stats, webhook_receipts en de auth-tabellen. Zie Datamodel.

Het Nuxt-equivalent van harde regel 4: geen process.env in app- of servercode. nuxt.config.ts declareert private keys, gevuld door NUXT_*-omgevingsvariabelen (voorbeelden in .env.example):

Groep Variabelen Als ze ontbreken
Database en auth NUXT_DATABASE_URL, NUXT_BETTER_AUTH_SECRET, NUXT_BETTER_AUTH_URL De app start niet bruikbaar
Secrets NUXT_SECRETS_KEY (32 bytes, base64) Geen provisioning
Webhooks NUXT_PLATFORM_WEBHOOK_SECRET (gedeeld fallback-secret) Sites zonder eigen secret worden geweigerd
Database-provisioning NUXT_POSTGRES_ADMIN_URL (CREATEROLE en CREATEDB, of superuser) Databasestap uitgeschakeld
Preview-hostnames NUXT_PREVIEW_BASE_HOST (leeg betekent okhema.studio) Default
Coolify NUXT_COOLIFY_BASE_URL, NUXT_COOLIFY_TOKEN, project-, server-, environment- en private-key-UUID, git-repository en -branch Run stopt na secrets (tenant.provision_partial)
Cloudflare for SaaS NUXT_CF_API_TOKEN, NUXT_CF_ZONE_ID, NUXT_CF_CNAME_TARGET Domein wordt opgeslagen als unregistered
Gedeelde site-env Resend, EMAIL_FROM, S3-instellingen Niet doorgegeven aan nieuwe sites

Een lege token schakelt een integratie uit in plaats van een fout te geven. Dat is het lokale ontwikkelpad: database en secrets wel, Coolify en Cloudflare niet.

POST /api/tenants registreert een klant met slug, name en status provisioning; in dezelfde transactie worden eerdere webhooks van die slug (binnengekomen vóór registratie) alsnog gekoppeld. GET /api/tenants en GET /api/tenants/:slug geven het overzicht en het detail; PATCH wijzigt handmatig de status. Responses bevatten nooit *_encrypted-kolommen.

Het overzicht toont een afgeleid aandachtssignaal (tenant-attention.ts): quiet bij een actieve of mislukte klant zonder activiteit in de laatste 24 uur. Het is geen databasestatus, maar een berekening bij het lezen.

POST /api/tenants/:slug/provision (provision-tenant.ts) accepteert status provisioning en failed, weigert active en suspended met 409, en antwoordt 202. De stappen, in vaste volgorde:

  1. database: rol en database tenant_<slug> aanmaken via provisionTenantDatabase uit @platform/ops, met NUXT_POSTGRES_ADMIN_URL.
  2. migrate: de tenant-migraties uitvoeren op de nieuwe database. De SQL-bestanden worden als Nitro server-assets meegebundeld (provision-tenant-migrate.ts).
  3. secrets: BETTER_AUTH_SECRET, PLATFORM_API_KEY en het webhook-secret genereren en versleuteld opslaan in de *_encrypted-kolommen; de database-URL idem.
  4. coolifyApp: een Coolify-applicatie aanmaken met de Dockerfile van site-template en de preview-hostnames zetten (<slug>.<base> en admin.<slug>.<base>). Er komt één domains-rij met type: preview.
  5. coolifyEnv: de runtime-omgevingsvariabelen van de site zetten, inclusief PLATFORM_WEBHOOK_URL naar dit dashboard.
  6. coolifyStart: de applicatie starten; er komt een deployments-rij.

Elke stap leest eerst de huidige kolommen en slaat over wat al bestaat, dus een crash halverwege is veilig: de volgende klik gaat verder bij de eerste onvoltooide stap. Zonder Coolify-token stopt de run na stap 3 met de activiteit tenant.provision_partial; de status blijft provisioning. Alleen een fout bij het starten zet de status op failed (tenant.provision_failed). Wat de provisioner bewust nog niet doet: eerste content (bootstrapTenant), de CMS-gebruiker, de Coolify-backuplijst en het markeren als active.

Twee soorten rijen in domains:

  • preview: automatisch bij provisioning, één per klant.
  • custom: door de beheerder geregistreerd via POST /api/tenants/:slug/domains (idempotent op hostname). custom-domains.ts vertaalt de rij naar een toestand voor de UI: unregistered (geen Cloudflare-token), pending, active of failed. POST …/domains/:id/verify vraagt de status bij Cloudflare op en bewaart het laatste snapshot in cf_verification; DELETE haalt de hostname bij Cloudflare weg en herberekent de Coolify-domeinenlijst.

Actief betekent hier TLS aan de rand bij Cloudflare, niet dat SITE_DOMAIN is omgezet: het CMS blijft op de preview-host tot een latere cutover. Het operator-recept staat bij Deploy.

POST /api/webhooks/site (receive-site-webhook.ts):

  1. Zoek de tenant op X-Platform-Tenant.
  2. Kies precies één HMAC-sleutel: het per-tenant secret als dat er is, anders NUXT_PLATFORM_WEBHOOK_SECRET.
  3. Weiger timestamps ouder dan vijf minuten of meer dan zestig seconden in de toekomst; vergelijk handtekeningen in constante tijd.
  4. Parse de envelope met platformEventEnvelopeSchema; X-Platform-Tenant moet gelijk zijn aan envelope.tenantId.
  5. Schrijf webhook_receipts (op event_id) en activity. Een tweede levering van dezelfde event_id geeft 200 zonder tweede activiteitsrij.

Onbekende slugs worden bewaard met tenant_id null en later gekoppeld als de klant wordt geregistreerd.

secrets.ts versleutelt met AES-256-GCM en een sleutel uit NUXT_SECRETS_KEY; formaat v1.<iv>.<tag>.<ciphertext> in base64url. De kolomnamen eindigen op _encrypted zodat niemand per ongeluk een leesbare waarde verwacht. Bewaar NUXT_SECRETS_KEY naast de databasebackups: zonder die sleutel zijn de opgeslagen tenant-secrets onbruikbaar.

Better Auth op de master-database, zonder admin-plugin en zonder rollen: iedereen met een account is beheerder. Publieke registratie staat uit en er is geen wachtwoordreset per mail; accounts en resets lopen via create-admin uit @platform/ops, zie Operations.

Een health-poll naar /api/platform/health, een periodieke /stats-pull als reconciliatie, verwijderen van klanten, gebruikersbeheer in de UI, en de fase-2-taken 2.8 (migratie-runner over alle tenants) en 2.9 (gecontroleerde uitrol). Het dashboard zelf draait nog niet als Coolify-app; zie Master dashboard op Coolify.