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.
Opbouw
Section titled “Opbouw”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.tsstuurt 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.tsen 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.tsencloudflare.ts(API-clients),custom-domains.ts,secrets.ts,webhooks.tsenreceive-site-webhook.ts,tenant-attention.ts,session.tsenauth.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.
Configuratie via runtimeConfig
Section titled “Configuratie via runtimeConfig”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.
Klantenbeheer
Section titled “Klantenbeheer”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.
Provisioning in zes stappen
Section titled “Provisioning in zes stappen”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:
- database: rol en database
tenant_<slug>aanmaken viaprovisionTenantDatabaseuit@platform/ops, metNUXT_POSTGRES_ADMIN_URL. - migrate: de tenant-migraties uitvoeren op de nieuwe database. De SQL-bestanden worden als Nitro server-assets meegebundeld (
provision-tenant-migrate.ts). - secrets:
BETTER_AUTH_SECRET,PLATFORM_API_KEYen het webhook-secret genereren en versleuteld opslaan in de*_encrypted-kolommen; de database-URL idem. - coolifyApp: een Coolify-applicatie aanmaken met de Dockerfile van
site-templateen de preview-hostnames zetten (<slug>.<base>enadmin.<slug>.<base>). Er komt ééndomains-rij mettype: preview. - coolifyEnv: de runtime-omgevingsvariabelen van de site zetten, inclusief
PLATFORM_WEBHOOK_URLnaar dit dashboard. - 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.
Domeinen
Section titled “Domeinen”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.tsvertaalt de rij naar een toestand voor de UI:unregistered(geen Cloudflare-token),pending,activeoffailed.POST …/domains/:id/verifyvraagt de status bij Cloudflare op en bewaart het laatste snapshot incf_verification;DELETEhaalt 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.
Webhooks ontvangen
Section titled “Webhooks ontvangen”POST /api/webhooks/site (receive-site-webhook.ts):
- Zoek de tenant op
X-Platform-Tenant. - Kies precies één HMAC-sleutel: het per-tenant secret als dat er is, anders
NUXT_PLATFORM_WEBHOOK_SECRET. - Weiger timestamps ouder dan vijf minuten of meer dan zestig seconden in de toekomst; vergelijk handtekeningen in constante tijd.
- Parse de envelope met
platformEventEnvelopeSchema;X-Platform-Tenantmoet gelijk zijn aanenvelope.tenantId. - Schrijf
webhook_receipts(opevent_id) enactivity. Een tweede levering van dezelfdeevent_idgeeft 200 zonder tweede activiteitsrij.
Onbekende slugs worden bewaard met tenant_id null en later gekoppeld als de klant wordt geregistreerd.
Secrets
Section titled “Secrets”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.
Authenticatie
Section titled “Authenticatie”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.
Wat er nog niet is
Section titled “Wat er nog niet is”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.