Contract master ↔ klantsite
Status: v0, provisioneel. Opgesteld in fase 0 op basis van wat fase 2 nodig zal hebben. Details zullen wijzigen zodra het master dashboard daadwerkelijk gebouwd wordt. Behandel dit als richting, niet als vastgestelde API.
Uitgangspunt
Section titled “Uitgangspunt”Master dashboard en klantsites zijn losse deployments, maar delen één definitie van types en schema’s: @platform/contract. Elke wijziging aan de koppeling begint daar, niet in een van de apps.
packages/contract/src/├── theme.ts # themeSchema├── blocks.ts # block-union + per-blok-schema's├── content.ts # page, service, news, testimonial├── platform.ts # tenant, domain, deployment, activity (bestaat sinds 2.1)└── events.ts # webhook-events master ↔ site (bestaat sinds 2.2)platform.ts bevat ook tenantSlugSchema: dezelfde regel die tenant.ts
gebruikt voor tenantId en die provision-tenant-db.sh in bash herhaalt.
activity.type is er bewust géén enum. Die waardes komen uit events.ts.
Regel: als een type beide kanten raakt, staat het hier. Nooit lokaal herdefiniëren.
Richting van het verkeer
Section titled “Richting van het verkeer”| Richting | Mechanisme | Wanneer |
|---|---|---|
| Master → site | HTTP-verzoek met API-key | Op verzoek: statusopvraging, herindexering, cache-invalidatie |
| Site → master | HMAC-gesigneerde webhook | Bij gebeurtenissen: content gepubliceerd, gebruiker ingelogd, formulier ontvangen |
| Master → Coolify | Coolify v4 API met bearer-token | Provisioning en deploys |
| Master → Cloudflare | Cloudflare for SaaS API | Custom hostnames en TLS |
Statistieken worden gepusht door de site, niet gepolld door de master. Reden: de master hoeft dan geen databasetoegang tot elke tenant te hebben. Een periodieke pull blijft bestaan als reconciliatie voor het geval webhooks zijn gemist.
Authenticatie
Section titled “Authenticatie”Master → site. Header X-Platform-Key met een per-site gegenereerde sleutel. De sleutel staat als env-variabele in de site-container en versleuteld in de master-database.
Site → master. Elke webhook draagt:
X-Platform-Tenant— de tenant-slugX-Platform-Timestamp— unix-seconden als decimale stringX-Platform-Signature— HMAC-SHA256 overtimestamp + "." + body, lowercase hex, met het per-site secret (webhook_secret_encryptedin de master, anders het gedeeldeNUXT_PLATFORM_WEBHOOK_SECRET)
De master weigert verzoeken met een timestamp ouder dan vijf minuten of meer dan zestig seconden in de toekomst (replay-bescherming) en vergelijkt handtekeningen in constante tijd.
mTLS is voor deze schaal overkill.
Endpoints op de klantsite
Section titled “Endpoints op de klantsite”Alle onder /api/platform/, afgeschermd met X-Platform-Key.
| Methode | Pad | Doel |
|---|---|---|
| GET | /api/platform/health |
Draait de site, is de database bereikbaar, welke versie draait er |
| GET | /api/platform/stats |
Aantal pagina’s, diensten, nieuwsitems, laatste mutatie |
| POST | /api/platform/revalidate |
Cache leegmaken na een externe wijziging |
/health moet werken zonder databaseverbinding en die status apart rapporteren, zodat de master onderscheid kan maken tussen “site plat” en “database plat”.
Endpoints op de master
Section titled “Endpoints op de master”| Methode | Pad | Doel |
|---|---|---|
| POST | /api/webhooks/site |
Ontvangt gebeurtenissen van klantsites (bestaat sinds 2.3) |
| GET / POST | /api/tenants |
Klantenoverzicht en een ledger-rij aanmaken (bestaat sinds 2.4) |
| GET / PATCH | /api/tenants/:slug |
Detail met activiteit en handmatige statuswijziging (bestaat sinds 2.4) |
| POST | /api/tenants/:slug/provision |
Idempotente database-/secrets-/Coolify-provisioning (bestaat sinds 2.5) |
| POST | /api/tenants/:slug/domains |
Custom hostname registreren (201 nieuw, 200 hervat). Leeg CF-token: unregistered + CNAME (sinds 2.7) |
| POST | /api/tenants/:slug/domains/:id/verify |
Cloudflare-status pollen; geen Coolify-PATCH, geen SITE_DOMAIN-cutover |
| DELETE | /api/tenants/:slug/domains/:id |
Custom hostname weg (CF DELETE, rij weg, Coolify-lijst herberekend) |
| POST | /api/tenants/:slug/database |
Bestaande tenant-database koppelen; 409 als er al een staat, 422 bij verkeerde databasenaam, onbereikbaar of site.tenant_id van een andere slug (sinds 2.8) |
| GET | /api/migrations |
Schemastand per tenant (live) tegen de gebundelde migraties, plus de laatste 10 runs (sinds 2.8) |
| POST | /api/migrations/runs |
Migraties toepassen op alle tenants met een database, of { slugs } als subset; 200 met de run, 409 als er al een run loopt (sinds 2.8) |
De ontvanger zoekt de tenant op slug, kiest precies één HMAC-sleutel
(per-tenant ciphertext als die kolom niet null is, anders
NUXT_PLATFORM_WEBHOOK_SECRET), weigert timestamps ouder dan vijf minuten of
meer dan zestig seconden in de toekomst, en parst daarna
platformEventEnvelopeSchema. X-Platform-Tenant moet gelijk zijn aan
envelope.tenantId. Een al verwerkt eventId geeft 200 zonder tweede
activity-rij. Zodra webhook_secret_encrypted is gezet, faalt een
handtekening met het gedeelde secret.
De tenant-routes vereisen een master-dashboard-sessie en retourneren nooit
*_encrypted kolommen. Aanmaken registreert alleen de klant met status
provisioning. POST /api/tenants/:slug/provision accepteert provisioning
en failed, weigert active/suspended met 409, en antwoordt 202 met het
browser-veilige detail. Zonder Coolify-token stopt de run na secrets
(tenant.provision_partial); status blijft provisioning. Een fout ná start
van de orchestrator schrijft tenant.provision_failed. Alleen een Coolify
start-fout zet tenants.status=failed — niet CoolifyDisabledError.
Het detailscherm toont een Provisioneren-knop bij provisioning en failed.
Custom domains (2.7) schrijven één domains-rij type: custom. Het
dashboard leest een state-union (unregistered / pending / active /
failed); preview-rijen blijven state: preview. POST …/domains is
idempotent op hostname. 400 bij ongeldige hostname, 409 bij een hostname
van een andere tenant, een preview-rij, of een Cloudflare-account buiten
onze zone, 502 bij Cloudflare HTTP. active is edge-TLS, niet canonieke
SITE_DOMAIN. Het CMS blijft op admin.{previewHost} tot een latere
cutover. Leeg NUXT_CF_API_TOKEN is geen 503: de rij blijft staan en de
CNAME komt uit NUXT_CF_CNAME_TARGET.
Migraties (2.8): platformMigrationOverviewSchema en
platformMigrationRunSchema zijn de leesvormen, tenantSchemaStateSchema de
state-union per tenant (no_database / unreachable / blocked /
pending / up_to_date). De runner leest de schemastand live uit de
tenant-database en slaat alleen runs op. Een run stopt niet bij één mislukte
tenant; failedCount > 0 is het signaal. blocked (ahead, diverged,
identity_mismatch) wordt nooit gemigreerd. Het antwoord op
POST /api/tenants/:slug/database bevat, zoals elk tenant-antwoord, nooit de
DSN; het detail draagt alleen hasDatabase: boolean.
Gebeurtenissen (site → master)
Section titled “Gebeurtenissen (site → master)”type PlatformEvent = | { type: 'content.published'; entity: 'page' | 'service' | 'news'; entityId: string; slug: string } | { type: 'content.deleted'; entity: 'page' | 'service' | 'news'; entityId: string } | { type: 'form.submitted'; formType: string } | { type: 'user.login'; userId: string } | { type: 'site.error'; message: string; context?: Json }Elke event heeft daarnaast tenantId, occurredAt en een eventId (UUID) voor idempotentie: de master negeert een eventId die al verwerkt is.
Events bevatten geen contentinhoud en geen persoonsgegevens uit formulierinzendingen. Alleen metadata. De inzending zelf blijft in de tenant-database.
site.error staat op het schema sinds 2.2 maar wordt nog niet verzonden. Bekabeling van content.published, content.deleted, form.submitted en user.login zit in de site. Een generieke error-hook hoort bij een latere taak.
Foutafhandeling
Section titled “Foutafhandeling”- De site laat nooit een bezoeker falen omdat een webhook niet aankomt. Verzenden gebeurt fire-and-forget met een korte timeout.
- Mislukte webhooks gaan in
webhook_outboxop de tenant-database en worden opnieuw geprobeerd met exponentiële backoff, maximaal een etmaal. Retry herberekent timestamp en HMAC. 4xx stopt (ongeldige payload of HMAC); 5xx en netwerkfouten retrien. - Sinds 2.4 toont de master
quietals niet-opgeslagen aandachtssignaal bij een actieve of mislukte tenant zonder activiteit in de laatste 24 uur.degradedblijft gereserveerd voor de combinatie van stilte en een ontbrekende/mislukte healthcheck, zodra provisioning een hostname en platform-key beschikbaar maakt.
Versionering
Section titled “Versionering”@platform/contract krijgt een expliciet versienummer. Bij een breaking change:
- Nieuwe vorm toevoegen naast de oude.
- Alle klantsites uitrollen.
- Oude vorm verwijderen.
Sites en master worden nooit gelijktijdig omgezet — bij tientallen deployments is een big-bang-migratie geen realistische aanname.
Nog te bepalen
Section titled “Nog te bepalen”- Sinds 2.8 heeft de master de tenant-DSN’s en gebruikt die voor migraties en de schemastand (
drizzle.__drizzle_migrations,site.tenant_id). Voor content-rapportages blijft de voorkeur de site-API, zodat het contentmodel geïsoleerd blijft. - Hoe worden per-site secrets geroteerd zonder downtime?
- Hoe rapporteert een site zijn eigen template-versie, zodat het dashboard achterlopende deployments kan tonen?