Oorspronkelijk plan van aanpak
- Astro 7.2 bestaat wél — het is net uit. Astro publiceerde “Astro 7.2” op 6 augustus 2026 (blogpost van Matthew Phillips), met o.a. experimentele incremental static builds. De aanname van de gebruiker klopt dus, al is de versie zó vers dat je op productie voorzichtig moet zijn met experimentele features. De vorige stabiele release was 7.1.6 (29 juli 2026); Astro 7.0 kwam op 22 juni 2026 met een Rust-compiler (
@astrojs/compiler-rsals default), Vite 8 en Node.js v22 als minimum. Strategisch cruciaal: Cloudflare heeft Astro in januari 2026 overgenomen, waardoor de Cloudflare-adapter de best onderhouden route wordt. - Aanbevolen aanpak: één monorepo (pnpm workspaces + Turborepo) met gedeelde packages, maar aparte deployment per klant via Coolify. Bouw het klantsite-template en het master dashboard als aparte apps binnen dezelfde repo. Gebruik Coolify (dat je al draait) als provisioning-laag via de v4 API, met database-per-tenant op self-hosted Postgres + Drizzle. Voor “klant koppelt eigen domein” is Cloudflare for SaaS de goedkoopste en meest geautomatiseerde optie ($0,10/hostname/maand na 100 gratis).
- Bouw het CMS zelf voor de MVP, maar ontwerp het datamodel nu al block-based zodat de pagebuilder later inpasbaar is. Gebruik Better Auth (Lucia is sinds maart 2025 deprecated). Overweeg serieus Payload CMS 3 als alternatief als de pagebuilder-ambitie zwaar weegt — maar dat betekent een Next.js-backend naast je Astro-sites, wat de architectuur complexer maakt.
Key Findings
Section titled “Key Findings”Versie-verificatie (kritisch)
Section titled “Versie-verificatie (kritisch)”- Astro 7.2 is gepubliceerd op 6 augustus 2026 (officiële Astro-blog, door Matthew Phillips), met als opvallende toevoeging experimentele incremental static builds — relevant voor de build-tijd/runtime-afweging van een CMS (zie hieronder). De direct voorafgaande stabiele release was 7.1.6 (29 juli 2026, bevestigd op de Astro Upgrade-docpagina: “The latest release of Astro is v7.1.6”). Astro 7.0 verscheen 22 juni 2026. Astro 7 vereist Node.js v22+ en draait op Vite 8 met de Rust-gebaseerde compiler als default. Advies: begin op 7.x; gebruik 7.2’s incremental static builds pas in productie als het experimentele label eraf is.
- Cloudflare heeft Astro overgenomen (januari 2026). Het hele Astro-team is bij Cloudflare; het framework blijft MIT/open-source. Dit maakt de Cloudflare-adapter en Cloudflare als hosting extra relevant, maar creëert ook een strategische afhankelijkheid.
- Lucia is deprecated (maart 2025). Better Auth is de de-facto standaard voor Astro-authenticatie in 2026. Auth.js/NextAuth is “security-only” onderhoud onder de Better Auth-paraplu sinds september 2025.
- Turso: let op. libSQL (de C-fork van SQLite) wordt nog onderhouden, maar “de toekomst is Turso” — een volledige Rust-herschrijving die nog in beta is. In de blogpost “Upcoming changes to the Turso Platform and Roadmap” (januari 2025) kondigde Turso aan resources te herrichten op de Rust-herschrijving en features voor nieuwe gebruikers te schrappen — waaronder het stopzetten van edge replicas en het verwijderen van Multi-DB schemas/database ATTACH, met als onderbouwing dat “70% van de Turso-gebruikers nooit geografische replicas aanmaakt”. De database-per-tenant use-case werkt nog (Free tier tot 100-500 databases), maar het platform is in transitie. Dit is een risico voor een langlopend platform.
Relevante Astro-features (7.x)
Section titled “Relevante Astro-features (7.x)”- Server Islands (
server:defer): combineer statische HTML met dynamisch, server-gerenderde componenten op één pagina — ideaal voor een CMS-site die deels statisch, deels live is. - Incremental static builds (7.2, experimenteel): alleen gewijzigde pagina’s herbouwen — direct relevant voor het “klant publiceert en ziet meteen resultaat”-scenario zonder volledige rebuild.
- Content Layer API: type-safe content uit elke bron (Markdown, CMS, database) met Zod-schema’s.
- Astro Actions: type-safe server-functies voor formulieren (contactformulier, CMS-CRUD).
- Sessions: ingebouwd session-management; op Cloudflare automatisch via Workers KV.
- astro:env: type-safe environment variables (belangrijk voor
TENANT_ID,DATABASE_URLper deployment). - Adapters: Node (voor Docker/Coolify), Vercel, Netlify, Cloudflare (workerd runtime, met bekende
nodejs_compat-valkuilen bij SSR + middleware — workaround viadisable_nodejs_process_v2compatibility flag).
Details
Section titled “Details”A. Hosting-vergelijking (open vraag → onderbouwde keuze)
Section titled “A. Hosting-vergelijking (open vraag → onderbouwde keuze)”| Optie | Kosten-indicatie (EUR) | Operationele last | Geschikt voor geautomatiseerd provisioning |
|---|---|---|---|
| Coolify self-hosted (Docker) op Hetzner | VPS vanaf €3,79/mnd (CX22: 2 vCPU/4GB/40GB); realistisch €6,80 (CX32: 4 vCPU/8GB) tot €16,40 (CX42: 8 vCPU/16GB) voor 10-50 sites; alle plannen incl. 20 TB traffic + 1 IPv4 | Middel-hoog: je beheert de VPS, updates, backups zelf | Ja — v4 API kan apps aanmaken, env vars zetten, domeinen toevoegen, deploys triggeren |
| Vercel | Gratis hobby; Pro ~$20/mnd/lid; per-project kosten schalen snel bij veel sites | Laag: fully managed | Zeer goed — “Vercel for Platforms”, @vercel/sdk voor projects/domains/deployments, wildcard + custom domains |
| Cloudflare Workers/Pages + for SaaS | Workers gratis tier ruim; custom hostnames: 100 gratis (Free/Pro/Business), daarna $0,10/hostname/mnd (PAYG-cap 50.000, verhoogd van 5.000 in mei 2025) | Laag-middel | Uitstekend voor custom domains via Cloudflare for SaaS API |
Aanbeveling: blijf op Coolify + Hetzner voor de deployments (je kent het, je draait er al maakm.studio en een Nuxt/Shopify-webshop, en de kosten zijn voorspelbaar bij groeiend aantal klanten). Combineer dit met Cloudflare for SaaS voor de custom-domain-koppeling van klanten — dat lost precies het lastigste stuk (per-klant TLS op eigen domein) elegant en goedkoop op. Vercel is de eenvoudigste route maar wordt bij aparte deployment-per-klant relatief duur en zet je vast in hun ecosysteem. Let op: wildcard custom hostnames bij Cloudflare for SaaS blijven Enterprise-only.
F. Provisioning & deployment-flow (kernvraag) — Coolify v4 API
Section titled “F. Provisioning & deployment-flow (kernvraag) — Coolify v4 API”De Coolify v4 API is bruikbaar voor automatisering maar in actieve ontwikkeling (nog beta-tags). Kernfeiten (geverifieerd tegen officiële docs en de openapi.yaml):
- Auth: Bearer-token (Laravel Sanctum), team-scoped, permissions
read/read:sensitive/write/deploy/root. Rate limit 200 req/min (configureerbaar viaAPI_RATE_LIMIT), daarna 429 metRetry-After. Base URL/api/v1. Optioneel IP-allowlisting. - App aanmaken:
POST /api/v1/applications/private-github-app(of/public,/private-deploy-key,/dockerfile,/dockerimage). Verplichte velden o.a.project_uuid,server_uuid,environment_name/environment_uuid,git_repository,git_branch,build_pack,ports_exposes; private-github-app vereistgithub_app_uuid. Domein-veld heetdomains(string, FQDN methttps://-prefix om TLS te triggeren; comma-separated voor meerdere), plusautogenerate_domain(default true → gebruikt server-wildcard of sslip.io-fallback) enforce_domain_override(default false → 409 bij domeinconflict).instant_deploy: truedeployt meteen. - Env vars: niet inline bij create; aparte endpoints —
POST /api/v1/applications/{uuid}/envsenPATCH /api/v1/applications/{uuid}/envs/bulk(atomair; hele request faalt als één var invalide is). Let op: geaccepteerde veldnamen (is_build_time,is_preview,is_literal) variëren per versie (gedocumenteerde 422-bug in issue #6847). - Domein wijzigen:
PATCH /api/v1/applications/{uuid}metdomains. Coolify vraagt automatisch een Let’s Encrypt-cert aan zodra je eenhttps://-domein zet en de DNS al naar de server wijst; bij falen een self-signed cert. App moet herstarten voor domeinwijziging effect heeft. (Docker-compose-apps zijn een uitzondering: die vereisen hetdocker_compose_domains-array-format, met bekende frictie/404’s via de API — issue #4326.) - Wildcard preview-URL’s: zet in de server-settings een Wildcard Domain (bv.
https://preview.mijndomein.nl) → Coolify genereert per apphttps://<uuid>.preview.mijndomein.nl. Belangrijke beperking: standaard doet Traefik per-hostname Let’s Encrypt via HTTP-01; voor échte wildcard-TLS (*.preview.mijndomein.nl) moet je Traefik op de DNS-01 challenge zetten (DNS-provider API-token) plus een wildcard A-record*.preview → server-IP. De officiële docs waarschuwen expliciet: “The Coolify Proxy won’t be able to issue SSL certificates for catch-all domains. For subdomains of a specific domain, you have the option to generate a Wildcard SSL certificate.” Voor een echte SaaS-catch-all route je met TraefikHostRegexp-labels. - Deploy triggeren:
GET /api/v1/deploy?uuid={uuid}&force={bool}(fire-and-forget; werk gebeurt async in Laravel Horizon, dus poll status). - Terraform-provider bestaat (99%+ API-coverage) als alternatief voor infrastructure-as-code.
- Bekende API-caveats: private-github-app-creatie via API heeft historie van breakage (
github_app_uuid-verwarring, issues #4864/#3209/#5467); env-veldvalidatie wisselt per versie; velden shiften tussen beta-builds. Test tegen je eigen instance en valideer tegen je instance-openapi.json.
Concrete flow “nieuwe klant → eigen domein”:
- In master dashboard: klant aanmaken (naam, slug
acme). - Master maakt tenant-database aan (zie G) en draait migraties.
- Master roept Coolify API aan:
POST /applications/private-github-appmet het gedeelde site-template-repo,domains: "https://acme.preview.mijndomein.nl",instant_deploy: false. - Master zet env vars:
TENANT_ID=acme,DATABASE_URL=...,SITE_DOMAIN=...via/envs/bulk. - Deploy triggeren; preview-URL live op wildcard-subdomein (met wildcard-TLS via DNS-01).
- Klant wil eigen domein
www.acme.nl: master registreert custom hostname via Cloudflare for SaaS API, genereert DNS-instructies (CNAME naar fallback origin), pollt verificatie-status, cert wordt automatisch uitgegeven. Toon status in dashboard. PATCH /applications/{uuid}met het nieuwe domein, of route via Cloudflare fallback origin naar de Coolify-app.
Containerized deploy-alternatief & resources
Section titled “Containerized deploy-alternatief & resources”Eén Docker image van het site-template, per klant een container met eigen env vars (TENANT_ID, DATABASE_URL, SITE_DOMAIN). Een Astro SSR Node-container in idle verbruikt weinig (~50-150 MB RAM). Bij 10-50 klanten is een CX42 (8 vCPU/16GB, €16,40/mnd) of CX52 (16 vCPU/32GB, €32,40/mnd) ruim voldoende; Postgres draait als aparte container met scheduled backups. Reken op enkele honderden MB per actieve SSR-site; statische sites kosten vrijwel niets in runtime.
Build-tijd vs runtime
Section titled “Build-tijd vs runtime”Voor een CMS waar de klant direct wil publiceren is volledige SSR het simpelst (wijziging is meteen live, geen rebuild). Alternatief: statisch bouwen + rebuild triggeren bij publish. Astro 7.2’s experimentele incremental static builds maken dit aantrekkelijker (alleen gewijzigde pagina’s herbouwen, ISR-achtig), maar het is nog experimenteel en geeft publicatie-latency. Aanbeveling: SSR met Server Islands — statische shell, dynamische content-islands, en aggressieve caching op Cloudflare ervoor. Evalueer incremental static builds later als het stabiel is.
G. Database-strategie (open vraag → keuze)
Section titled “G. Database-strategie (open vraag → keuze)”| Optie | Database-per-tenant | Kosten 5/20/50 klanten | Migraties over N db’s | Astro SSR |
|---|---|---|---|---|
| Postgres + Drizzle (self-hosted) | Ja (aparte DB of schema-per-tenant) | Vast: alleen VPS-kosten | Drizzle Kit + script over alle DB’s; drizzle-multitenant-toolkit met parallelle migraties |
Uitstekend (Node adapter) |
| Supabase | Project-per-klant (duur/omslachtig) of RLS gedeeld | Schaalt slecht bij project-per-klant | Handmatiger | Goed |
| Turso/libSQL | Expliciet ontworpen voor database-per-tenant | Free tot 100 DB’s; Developer $4,99/mnd unlimited | libSQL sync; embedded replicas | Goed, maar platform in transitie/beta |
Aanbeveling: self-hosted Postgres + Drizzle met database-per-tenant (of schema-per-tenant als je operationeel wilt vereenvoudigen). Redenen: volledige data-isolatie per klant, voorspelbare kosten (alleen VPS), rijke SQL-features, en Drizzle werkt uitstekend met Astro SSR via de Node-adapter. Migraties rol je uit met een script dat over alle tenant-DB’s itereert (patroon uit de Drizzle-community; de drizzle-multitenant-toolkit ondersteunt parallelle migraties, shared-schema migraties, en status-checks). Backups per klant via Coolify’s scheduled database backups of pg_dump per DB naar S3-compatible storage (Cloudflare R2/Backblaze B2).
Alternatief als self-hosting te zwaar wordt: Turso database-per-tenant (goedkoop, edge), maar accepteer de platform-onzekerheid; of Neon met database branching per klant (Neon documenteert expliciet database-per-tenant met Drizzle + GitHub Actions).
B. Architectuur & repo-strategie
Section titled “B. Architectuur & repo-strategie”Aanbeveling: één monorepo met pnpm workspaces + Turborepo. Dit is in 2026 de volwassen standaard voor gedeelde TypeScript-code zonder version-drift (Turborepo werd in 2024 herschreven naar Rust; remote cache gratis op Vercel). Nx pas als je later module-boundaries wilt afdwingen.
platform/├── apps/│ ├── site-template/ # Astro-klantsite (het template dat je per klant deployt)│ │ ├── src/│ │ ├── astro.config.mjs│ │ └── Dockerfile│ └── master-dashboard/ # Master admin (Nuxt of Astro — zie E)├── packages/│ ├── @platform/db/ # Drizzle schema's, migraties, tenant-connectie│ ├── @platform/blocks/ # Block-registry + componenten (basis voor pagebuilder)│ ├── @platform/core/ # Gedeelde types, utils, auth-helpers│ ├── @platform/contract/ # API-contract: gedeelde TS-types / Zod-schema's / OpenAPI│ └── @platform/ui/ # Gedeelde UI (Tailwind-config, componenten)├── docs/│ ├── PRD.md│ ├── ARCHITECTURE.md│ ├── CONTRACT.md│ ├── DATAMODEL.md│ └── TASKS.md├── .cursor/rules/├── pnpm-workspace.yaml├── turbo.json└── package.jsonContract tussen master en klantsite: definieer gedeelde TypeScript-types + Zod-schema’s in @platform/contract (single source of truth), eventueel met een OpenAPI-spec gegenereerd daaruit. Zo weet Cursor in beide apps exact hoe de koppeling eruitziet, ook al zijn het losse deployables. tRPC is een optie als beide kanten TypeScript zijn en je end-to-end type-safety wilt, maar gedeelde Zod-types via een package is simpeler en framework-agnostisch.
Site-template instantiëren per klant: het krachtigste mechanisme is niet forken per klant, maar één template-app die je meermaals deployt met verschillende env vars (TENANT_ID). Updates aan de template = één git-push → Coolify redeployt alle klant-apps (via auto-deploy of een master-script dat over alle app-UUID’s /deploy triggert). Dit is de kern van “aparte deployment per klant vanuit één gedeelde codebase”. Alternatief (git template repo / degit / scaffolding-CLI) alleen als klanten écht divergerende code nodig hebben — vermijd dat, want dan verlies je de “update-alle-klanten”-eigenschap.
C. Het kleine eigen CMS
Section titled “C. Het kleine eigen CMS”- Auth: Better Auth (sessies, rollen/permissies, database-adapter voor Drizzle). Lucia is dood, gebruik het niet.
- Media-uploads: S3-compatible object storage — Cloudflare R2 (geen egress-kosten, past bij Cloudflare-stack) of Backblaze B2. Combineer met Astro’s image-optimalisatie / Cloudflare Images binding.
- Content-model: zie datamodel hieronder — pagina’s, diensten (overzicht+detail), nieuws (overzicht+detail), referenties, globale settings/navigatie, contactform-inzendingen.
- Admin op subdomein:
admin.klant.nlals route/middleware in dezelfde Astro-app (SSR), beschermd met Better Auth.
Eerlijk oordeel zelfbouw vs bestaand CMS:
- Payload CMS 3 is het sterkste alternatief: self-hostable (38,5k GitHub-stars), admin-UI, officiële multi-tenant plugin (
@payloadcms/plugin-multi-tenant, voegt tenant-veld toe aan collections met optionele domein-isolatie), en een blocks-field dat je pagebuilder-ambitie direct dient. Nadeel: Payload draait op Next.js, dus je introduceert een tweede framework naast Astro, en Payload Cloud is sinds de Figma-overname (juni 2025) gesloten voor nieuwe signups → self-hosten (bij voorkeur VPS + Docker) is de enige weg. - Directus/Strapi: volwaardige headless CMS’en, maar zwaarder en minder “eigen”.
- Keystatic/TinaCMS: git-based, licht, goede Astro-integratie (TinaCMS is in 2026 “Astro by default”), maar minder geschikt voor database-per-tenant met dynamische pagebuilder.
Conclusie: voor een MVP met volledige controle en een heldere upgrade naar pagebuilder is zelfbouw in Astro verdedigbaar — mits je het datamodel block-based ontwerpt. Maar wees eerlijk: als de pagebuilder + multi-tenant + master-dashboard-ambitie zwaar weegt en je snelheid wilt, levert Payload CMS 3 met de multi-tenant plugin en Puck/blocks je maanden werk-besparing op. De trade-off is een Next.js-backend en minder “puur Astro”. Mijn advies: start zelfbouw voor de MVP van één site, en herevalueer Payload expliciet vóór je aan Fase 3 (pagebuilder) begint.
D. Pagebuilder op de roadmap
Section titled “D. Pagebuilder op de roadmap”Ontwerp de MVP nu al zo dat blocks first-class zijn:
- Block-registry: elk block = een key + Zod-schema (props) + Astro-renderer + (later) een editor-veld-definitie.
- Discriminated unions in TypeScript:
type Block = HeroBlock | TextBlock | GalleryBlock | ...met eentype-discriminator. - Opslag: een pagina = geordende JSON-array van blocks in de database (Postgres
jsonb), gevalideerd met Zod bij opslaan en renderen. - Rendering in Astro: een
<BlockRenderer blocks={...}/>die per block-type de juiste component kiest. - Later inpassen van een editor: Puck is de sterkste keuze — open-source (MIT), React, self-hosted, met 13.026 GitHub-stars en ~839k maandelijkse npm-downloads (per de officiële puckeditor.com en de
puckeditor/puckGitHub-org, stand eind juli 2026). Let op: de packages verhuisden naar de@puckeditor-scope met v0.21 (14 januari 2026), die AI-paginagenerering en inline rich-text-editing toevoegde. Puck mapt editor-velden op je bestaande React-componenten en je houdt eigenaarschap over data. Je embed het in het admin-gedeelte (React-island in Astro of in het master dashboard). GrapesJS is een alternatief maar lager-niveau. Builder.io/Storyblok zijn proprietary/hosted.
Zolang je pagina’s als block-JSON opslaat en via een registry rendert, kun je Puck later inpassen zonder herbouw — dat is de crux.
E. Master dashboard
Section titled “E. Master dashboard”- Tech-stack: Nuxt — jij bent er sterk in, het is uitstekend voor een data-rijk, interactief dashboard (SSR, server routes, auth), en het dashboard heeft geen baat bij Astro’s “zero-JS content site”-filosofie. Astro zou ook kunnen, maar voor een echte app-UI is Nuxt de betere fit. Deel types via
@platform/contract. - Statistieken verzamelen: push vanuit klantsites naar master via HMAC-gesigneerde webhooks (bij content-wijziging/activiteit) + een periodieke pull voor aggregatie/reconciliatie. Aantal pagina’s, laatste activiteit en audit-log lees je uit de tenant-DB’s of uit events die de sites pushen.
- Analytics: Umami self-hosted (MIT-licentie, licht ~512MB RAM, multi-site uit één installatie, REST API voor aggregatie in je dashboard) is de beste fit voor een portfolio van sites. Plausible is fraaier maar AGPL en zwaarder (Elixir + ClickHouse, ~1,8GB RAM idle). Cloudflare Web Analytics is gratis/privacy-vriendelijk als je toch op Cloudflare zit. Zet per klant-site één Umami website-ID; aggregeer via de Umami API in het master dashboard.
- Beveiliging master ↔ klantsites: API-key/service-token per site + HMAC-gesigneerde webhooks (shared secret per site). mTLS is overkill voor deze schaal.
I. Concreet plan van aanpak
Section titled “I. Concreet plan van aanpak”Aanbevolen architectuur (diagram-in-tekst)
Section titled “Aanbevolen architectuur (diagram-in-tekst)” ┌──────────────────────────┐ │ Master Dashboard (Nuxt) │ │ - klanten CRUD │ │ - provisioning-service │ │ - stats/audit aggregatie │ └───────┬───────────┬───────┘ │ │ Coolify v4 API │ │ Cloudflare for SaaS API (app aanmaken, │ │ (custom hostnames + TLS) env, deploy) │ │ ▼ ▼ ┌─────────────────────────────────────────────────────────┐ │ Coolify op Hetzner VPS (Docker) │ │ ┌────────────┐ ┌────────────┐ ┌────────────┐ │ │ │ site: acme │ │ site: bravo│ │ site: ... │ (SSR) │ │ │ TENANT_ID │ │ TENANT_ID │ │ │ │ │ └─────┬──────┘ └─────┬──────┘ └─────┬──────┘ │ │ │ │ │ │ │ ┌─────▼──────────────▼──────────────▼──────┐ │ │ │ Postgres (database-per-tenant) + backups │ │ │ └───────────────────────────────────────────┘ │ │ ┌───────────┐ ┌──────────────┐ │ │ │ Umami │ │ R2/B2 media │ │ │ └───────────┘ └──────────────┘ │ └─────────────────────────────────────────────────────────┘ Gedeelde codebase: monorepo (pnpm + Turborepo), @platform/* packagesOnderbouwde keuzes (met alternatief)
Section titled “Onderbouwde keuzes (met alternatief)”- Hosting: Coolify + Hetzner (je kent het, voorspelbare kosten, API-provisioning) + Cloudflare for SaaS voor custom domains. Alternatief als het te bewerkelijk wordt: Vercel for Platforms (duurder, maar nul ops).
- Database: self-hosted Postgres + Drizzle, database-per-tenant. Alternatief: Turso (edge, goedkoop, maar platform in transitie) of Neon (branching).
- Auth: Better Auth. Alternatief: Auth.js (security-only, maar volwassen).
- CMS: zelfbouw in Astro voor MVP. Alternatief: Payload CMS 3 + multi-tenant plugin (bespaart werk, maar introduceert Next.js).
Gefaseerde roadmap (één developer met AI-tooling)
Section titled “Gefaseerde roadmap (één developer met AI-tooling)”- Fase 0 — Voorbereiding (± 1 week): monorepo opzetten (pnpm + Turborepo),
@platform/*packages scaffolden, alle docs schrijven (PRD, ARCHITECTURE, DATAMODEL, CONTRACT, TASKS),.cursor/rulesopzetten. Deliverable: werkende workspace + documentatie waar Cursor op kan bouwen. - Fase 1 — MVP: één klantsite + eigen CMS (± 3-5 weken): Astro site-template met alle publieke pagina’s (home, over ons, diensten overzicht+detail, nieuws overzicht+detail, referenties, contact). Eigen CMS op
admin-subdomein met Better Auth, CRUD, media-upload naar R2, block-based content-model. Postgres + Drizzle, één tenant. Deploy op Coolify. Deliverable: één live, beheerbare site. - Fase 2 — Master dashboard + provisioning (± 3-4 weken): Nuxt-dashboard, klanten-CRUD, provisioning-service tegen Coolify API (app + DB + env + deploy), wildcard preview-URL’s, custom-domain-koppeling via Cloudflare for SaaS, migratie-runner over alle tenant-DB’s. Deliverable: nieuwe klant aanmaken → preview-URL → eigen domein, volledig geautomatiseerd.
- Fase 3 — Pagebuilder (± 3-5 weken): Puck integreren in admin, block-registry uitbreiden, drag-and-drop editing bovenop het bestaande block-model. Herevalueer hier Payload CMS. Deliverable: klant bouwt zelf pagina’s met blokken.
- Fase 4 — Analytics/uitbreidingen (doorlopend): Umami per site, aggregatie in dashboard, audit-log, activiteitenweergave, extra features.
Voorbeeld-datamodel
Section titled “Voorbeeld-datamodel”Klantsite (per tenant-database):
pages(id, slug, title, status, blocks jsonb, seo jsonb, published_at, updated_at)services(id, slug, title, excerpt, blocks jsonb, image_id, order, status)news(id, slug, title, excerpt, body/blocks, image_id, published_at, status)references(id, client_name, quote, logo_id, rating, order)media(id, key/url, mime, width, height, alt, size)settings(id, key, value jsonb) — globale settings/navigatienavigation(id, label, url, parent_id, order)form_submissions(id, form_type, payload jsonb, created_at, read)users(id, email, password_hash, role) + Better Auth-tabellen (sessions,accounts)audit_log(id, user_id, action, entity, entity_id, created_at)
Master dashboard (centrale database):
tenants(id, slug, name, status, coolify_app_uuid, database_url, created_at)domains(id, tenant_id, hostname, type[preview/custom], cf_hostname_id, ssl_status, verified_at)deployments(id, tenant_id, status, triggered_at, commit_sha)activity(id, tenant_id, type, payload jsonb, created_at) — via webhooksadmin_users(id, email, role) + Better Auth-tabellensite_stats(id, tenant_id, page_count, last_activity_at, umami_website_id)
Provisioning-flow (pseudo-code / concrete API-call)
Section titled “Provisioning-flow (pseudo-code / concrete API-call)”// master-dashboard: provisioning-serviceasync function provisionTenant(input: { slug: string; name: string }) { // 1. tenant-record + database aanmaken const dbUrl = await createTenantDatabase(input.slug); // CREATE DATABASE tenant_acme await runMigrations(dbUrl); // Drizzle Kit over de nieuwe DB
// 2. Coolify: applicatie aanmaken vanaf gedeeld template-repo const app = await coolify.post('/api/v1/applications/private-github-app', { project_uuid: PROJECT_UUID, server_uuid: SERVER_UUID, environment_name: 'production', github_app_uuid: GITHUB_APP_UUID, git_repository: 'maakm/site-template', git_branch: 'main', build_pack: 'dockerfile', ports_exposes: '4321', domains: `https://${input.slug}.preview.mijndomein.nl`, instant_deploy: false, });
// 3. env vars zetten (bulk) await coolify.patch(`/api/v1/applications/${app.uuid}/envs/bulk`, { data: [ { key: 'TENANT_ID', value: input.slug }, { key: 'DATABASE_URL', value: dbUrl }, { key: 'SITE_DOMAIN', value: `${input.slug}.preview.mijndomein.nl` }, ], });
// 4. deploy triggeren await coolify.get(`/api/v1/deploy?uuid=${app.uuid}&force=false`);
// 5. record wegschrijven await db.insert(tenants).values({ slug: input.slug, name: input.name, coolifyAppUuid: app.uuid, databaseUrl: dbUrl, status: 'provisioning', });}
// Later: custom domain koppelen via Cloudflare for SaaSasync function attachCustomDomain(tenantId: string, hostname: string) { const res = await cf.post(`/zones/${ZONE_ID}/custom_hostnames`, { hostname, ssl: { method: 'http', type: 'dv' }, }); // toon DNS-instructie (CNAME → fallback origin) + poll res.status tot 'active' await db.insert(domains).values({ tenantId, hostname, type: 'custom', cfHostnameId: res.id, sslStatus: res.status, });}Voorbeeld .cursor/rules-bestanden
Section titled “Voorbeeld .cursor/rules-bestanden”.cursor/rules/000-project.mdc (Always Apply, kort — hou onder ~200 woorden/2000 tokens):
---alwaysApply: true---# Platform contextMonorepo: pnpm workspaces + Turborepo. apps/site-template (Astro 7 SSR, Node adapter),apps/master-dashboard (Nuxt). Gedeelde code in packages/@platform/*.- Taal: code/config/bestandsnamen in het Engels; commentaar mag NL.- Types & Zod-schema's: single source of truth in @platform/contract. Nooit dupliceren.- Auth: Better Auth (NOOIT Lucia — deprecated).- DB: Postgres + Drizzle, database-per-tenant. Lees @docs/DATAMODEL.md.- Architectuur: zie @docs/ARCHITECTURE.md en @docs/CONTRACT.md..cursor/rules/astro-site.mdc (Auto Attached op apps/site-template/**):
---description: Astro klantsite conventiesglobs: apps/site-template/**/*.{astro,ts,tsx}---# Astro site-template- Astro 7.x, SSR met @astrojs/node. Node 22+.- Content rendert via <BlockRenderer/>; pagina's = block-JSON (jsonb) gevalideerd met Zod.- Blocks komen uit @platform/blocks. Nieuw block = schema + renderer + registry-entry.- Gebruik Server Islands (server:defer) voor dynamische delen; rest statisch.- Multi-tenant: lees TENANT_ID en DATABASE_URL uit astro:env. Nooit hardcoden.- Media via R2 (S3-compatible); gebruik Astro image-optimalisatie..cursor/rules/drizzle-db.mdc (Auto Attached op packages/@platform/db/**):
---description: Drizzle multi-tenant DB regelsglobs: packages/@platform/db/**/*.ts---# Database- Database-per-tenant. Schema in schema.ts; migraties via Drizzle Kit.- Migratie-runner itereert over alle tenant-DB's (zie scripts/migrate-all.ts).- Elke tenant-tabel: audit-velden created_at/updated_at.- Nooit tenant-data mengen; connectie altijd per DATABASE_URL.H. Cursor-specifieke werkwijze
Section titled “H. Cursor-specifieke werkwijze”- Gebruik
.cursor/rulesmet.mdc-bestanden, niet het verouderde.cursorrules(dat wordt in Agent-mode genegeerd). Vier rule-types: Always Apply (altijd, hou onder ~200 woorden/2000 tokens), Auto Attached (via globs, mag tot ~500 regels), Agent Requested (metdescription), Manual. Hou totale altijd-geladen rules onder ~4000-6000 tokens. - Context over de koppeling in losse apps: omdat het één monorepo is, kun je een multi-root workspace openen. Schrijf een Architectuur en Contract in
docs/en verwijs ernaar met@Docs/@file. Gebruik de gedeelde@platform/contract-package zodat types letterlijk gedeeld zijn. Cursor leest sinds 2026 ookAGENTS.md(portable, maar zonder globs/scoping) — inclusief geneste bestanden in submappen, waarbij specifiekere paden voorrang krijgen. - Documentatie vooraf (Fase 0): PRD, Architectuur, Datamodel, Contract, Taken. Deze geven Cursor de context om efficiënt te werken.
- Werkwijze: hak op in kleine taken; gebruik planning-mode/agent-mode; laat één taak per keer uitvoeren en review. Veelgemaakte fout: te grote context/te veel tegelijk → Cursor verliest overzicht. Hou rules klein en scoped; ruim rules op die weken niet “gevuurd” hebben.
- Claude Code met git worktrees + custom subagents: dit kun je prima parallel inzetten. Git worktrees zijn ideaal om meerdere features/apps tegelijk te bewerken (bv. dashboard en site-template in aparte worktrees) zonder branch-switch. Gebruik Cursor voor interactief werk in de editor en Claude Code-subagents voor langlopende, geïsoleerde taken. Beide lezen dezelfde
AGENTS.md/docs, dus je context blijft consistent.
Recommendations
Section titled “Recommendations”- Nu (Fase 0): Zet de monorepo op, schrijf de vijf docs en
.cursor/rules, en verifieer de exacte Astro 7.x-versie (7.2 is net uit — check of features die je gebruikt stabiel of experimenteel zijn) en Coolify-versie op je eigen instance. Bouw eerst de handmatige provisioning-flow (klant handmatig in Coolify aanmaken) vóór je automatiseert. - Fase 1: Bouw één klantsite + eigen CMS volledig af en deploy op Coolify. Dit valideert het block-based datamodel en de Astro-SSR + Drizzle-combinatie. Benchmark: publiceren-tot-live < 2s, en het datamodel moet Puck later aankunnen.
- Fase 2: Automatiseer provisioning tegen de Coolify API en Cloudflare for SaaS. Benchmark: een nieuwe klant van 0 naar preview-URL in < 5 minuten zonder handwerk. Los wildcard-TLS via DNS-01 op vóór je dit oplevert.
- Vóór Fase 3: Herevalueer expliciet Payload CMS 3. Drempel om te switchen: als je pagebuilder-eisen (blocks, drag-and-drop, revisies) meer dan ~4-6 weken zelfbouw kosten, weegt Payload’s kant-en-klare admin + blocks + multi-tenant plugin op tegen de Next.js-overhead.
- Schaal-drempels: blijf op Postgres database-per-tenant tot ~20-50 klanten; overweeg schema-per-tenant of managed (Neon/Turso) als backup/migratie-onderhoud te veel tijd kost. Upgrade de Hetzner-VPS van CX32 → CX42 → CX52 op basis van RAM-gebruik van de SSR-containers.
Caveats
Section titled “Caveats”- Snel veranderend: Astro (post-Cloudflare-overname; 7.2 is letterlijk twee dagen oud op moment van schrijven), de Coolify v4 API (beta, wijzigende velden), Turso (platform-transitie) en Cursor-features veranderen snel. Verifieer versies en API-velden op het moment van bouwen tegen de officiële docs.
- Astro 7.2’s incremental static builds zijn experimenteel — niet vertrouwen op productie tot het label eraf is.
- Coolify-API-details (veldnamen zoals
domains,instant_deploy, env-endpoints) zijn geverifieerd via officiële docs en deopenapi.yaml, maar variëren per versie en hebben bekende bugs (private-github-app-creatie, env-veldvalidatie) — test tegen je eigen instance en pin je Coolify-versie. - Wildcard-TLS op Coolify vereist DNS-01 (niet de standaard HTTP-01) plus een DNS-provider API-token; dit is de belangrijkste operationele hobbel en heb ik niet hands-on kunnen verifiëren — volg de officiële “Wildcard SSL Certificates”-doc.
- Prijzen (Hetzner CX22 €3,79 t/m CX52 €32,40, Cloudflare for SaaS $0,10/hostname, Turso) zijn indicaties uit 2026-bronnen; Hetzner heeft in juni 2026 enkele CCX/CPX-tarieven verhoogd.
- Cloudflare for SaaS wildcard/apex custom hostnames vereisen het Enterprise-plan; de per-hostname $0,10-prijs geldt voor gewone subdomein-custom-hostnames. Verifieer je scenario voor je erop bouwt.
- Grootste risico is scope: zelfbouw-CMS + pagebuilder + master dashboard + provisioning is een volwaardig SaaS-platform, geen “simpele website”. Voor een eenmanszaak is strikt faseren en een écht kleine MVP (één site, één tenant, handmatige deploy eerst) de belangrijkste succesfactor.