Ga naar inhoud

Taak 2.1 — Nuxt-app, master-datamodel, Better Auth voor beheerders

Status: plan, opgesteld 17 augustus 2026, direct na de groene freeze-check. Scope: scaffolden, datamodel en inloggen. Geen klantenbeheer (2.4), geen platform-endpoints (2.2), geen provisioning (2.5). Wijkt de uitvoering af, werk dit document bij en noteer de afwijking bij 2.1 in Taken.

Nagekeken via npm view op 17 augustus 2026. Niet aannemen, niet verzinnen.

Pakket Versie Opmerking
nuxt 4.5.2 engines: ^22.19.0 || ^24.11.0 || >=26.0.0 → 22.x mag, maar ≥22.19
vue / vue-router 3.5.40 / 5.2.0 via Nuxt, niet los pinnen
nitropack 2.13.4 via @nuxt/nitro-server@4.5.2
h3 1.15.11 v1-API; toWebRequest(event) bestaat (types nagekeken)
vite ^8.2.0 Astro 7.2 gebruikt ^8.0.13 → één Vite in de monorepo (8.2.1)
tailwindcss + @tailwindcss/vite 4.3.3 peer vite: ^5.2 || ^6 || ^7 || ^8 — Vite 8 dus oké
better-auth + @better-auth/drizzle-adapter 1.6.26 latest is 1.6.29; pin gelijk houden aan @platform/db
drizzle-orm / drizzle-kit 0.45.2 / 0.31.10 al de laatste, geen bump nodig
vue-tsc 3.3.10 peer typescript >=5.0.0 → TypeScript 7 blijft staan
zod ^4.4.3 zoals de rest van de monorepo

Keuze 17 augustus 2026: op 22 blijven, want de Dockerfiles bouwen op node:22-bookworm-slim en dan zijn dev en image dezelfde major. 24.19.0 staat lokaal wel geïnstalleerd maar wordt niet gebruikt.

Let op: nvm alias default 22 resolvet naar de nieuwste lokaal geïnstalleerde 22.x, en dat is nu 22.16.0 — onder Nuxts ondergrens ^22.19.0. De alias is dus goed, de versie erachter niet.

  • nvm install 22 → 22.23.2 (nieuwste 22.x op 17 augustus 2026). De bestaande default 22-alias wijst daarna automatisch naar die versie. Geen pnpm install nodig voor de ABI: binnen dezelfde major verandert die niet.
  • node -v in een nieuwe shell controleren; 22.16.0 betekent dat de install niet is opgepikt.
  • Root package.json: engines.node van >=22.15.0 naar >=22.19.0.
  • Losse docs-wijzigingen committen (Taken, Deploy, Operations, untracked freeze-check.md), zodat 2.1 op een schone tree begint.

Tailwind 4 via @tailwindcss/vite, geen @nuxt/ui. Besloten op 17 augustus 2026. @nuxt/ui@4.10.0 zou 2.4 sneller maken (tabellen, forms, modals), maar pint typescript op ^5.6.3 || ^6.0.0 en zou de master-app dus net als site-template op TS 6 vastzetten. Tailwind 4 is al in huis, dezelfde Vite-plugin, nul nieuwe concepten. In Nuxt: plugin in vite.plugins van nuxt.config.ts plus één CSS-bestand via css: [...] met @import "tailwindcss";.

Harde regel 1 geldt niet voor het master dashboard. “Huisstijl komt uit data” beschermt klantsites tegen hardgecodeerde stijl. De master is ons eigen interne gereedschap zonder tenant-theme, dus daar zijn gewone Tailwind-utilities (bg-slate-900, …) juist het goede antwoord. Dit staat expliciet in de scoped rule, anders leest een volgende agent regel 1 verkeerd.

Master-schema als subpath @platform/db/master, geen nieuw package. getDb() en export * from "./schema" zijn niet herbruikbaar: de auth-tabellen heten in beide databases users/sessions/accounts/ verifications, dus één gedeelde index-export botst. Een eigen @platform/db-master zou drizzle, postgres en better-auth dupliceren voor vijf tabellen. In de app zetten kan niet: het eerste beheerdersaccount komt uit een ops-CLI (er is geen publieke signup), dus zowel de Nuxt-app als @platform/ops hebben het schema nodig.

Geen Better Auth admin-plugin, geen rollen. Iedereen in de master is beheerder. ac, statements en een role-kolom zouden plumbing zijn zonder tweede rol. Komt er later een read-only rol, dan is dat één migratie.

Geen wachtwoordherstel per mail op de master. Bij een handvol beheerders doet de CLI dat, zoals create-user --reset-password op de tenant-kant. Scheelt de Resend-afhankelijkheid, die volgens de freeze-check stil faalt zonder config.

Kolom heet database_url_encrypted. Datamodel eist zelf versleutelde opslag; de naam moet dat afdwingen in plaats van het aan de lezer te laten.

1. Contract — packages/contract/src/platform.ts

Section titled “1. Contract — packages/contract/src/platform.ts”

Dit bestand staat al in Contract en .cursor/rules/contract.mdc ingetekend maar bestaat nog niet.

  • Zod-enums: tenantStatusSchema (provisioning|active|suspended|failed), domainTypeSchema (preview|custom), sslStatusSchema (pending|active|failed), deploymentStatusSchema (queued|running|success|failed).
  • Rij-schema’s voor tenant, domain, deployment, activity, siteStats; types via z.infer, nooit een handgeschreven interface ernaast.
  • Exporteren uit src/index.ts.

activity.type blijft text, geen enum: die waardes komen uit de PlatformEvent-union en horen bij 2.2/2.3, niet vooruit verzonnen hier. Om dezelfde reden nog géén events.ts.

2. Master-schema — packages/db/src/master/

Section titled “2. Master-schema — packages/db/src/master/”
  • schema.ts: tenants, domains, deployments, activity, site_stats. UUID-pk’s, created_at/updated_at, meervoud, snake_case, pgEnum-waardes letterlijk (zelfde patroon als contentStatusEnum).
  • auth-schema.ts: users, sessions, accounts, verifications — zonder de admin-plugin-kolommen (role, banned, ban_reason, ban_expires, impersonated_by).
  • client.ts: getMasterDb(url), zelfde vorm als getDb — URL altijd meegeven, nooit process.env in dit package.
  • index.ts + exports-entry "./master" in packages/db/package.json + pad @platform/db/master in tsconfig.base.json. Niet vanuit packages/db/src/index.ts re-exporteren: dat is precies de botsing.
  • secrets.ts: encryptSecret / decryptSecret (AES-256-GCM, key uit env). In 2.1 wordt er geen rij geschreven, maar de kolomvorm ligt nu vast in een migratie — 30 regels nu voorkomt improvisatie in 2.5. Bij uitvoering verplaatst naar apps/master-dashboard/server/utils/secrets.ts: database.mdc staat in packages/db geen niet-databasecode toe, en provisioning is een dashboard-taak.

Kolommen die afwijken van Datamodel, beide bij te werken in dat document:

Document Code Waarom
tenants.database_url database_url_encrypted naam dwingt versleuteling af
tabel admin_users Better Auth users die tabel is de beheerderstabel; geen tweede
  • packages/db/drizzle.master.config.tsschema: ./src/master/schema.ts, out: ./drizzle-master, url uit MASTER_DATABASE_URL.
  • scripts/migrate-master.mjs naar het model van scripts/migrate.mjs.
  • Scripts generate:master en migrate:master in packages/db/package.json.
  • Migratie genereren en committen.

De tenant-config leest alleen src/schema.ts, dus master-tabellen kunnen niet in een tenant-migratie lekken. Controleer dat na het genereren één keer.

4. Database aanmaken — scripts/provision-master-db.sh

Section titled “4. Database aanmaken — scripts/provision-master-db.sh”
  • Rol + database platform_master, naar het model van provision-tenant-db.sh. Die bestaande kan het niet: de regex verbiedt _ en de databasenaam is hardcoded op tenant_<id> — dezelfde val als bij de scratch-DB in 1.12.
  • Lokaal draaien tegen de compose-Postgres. docker-compose.yml kan dit niet doen: POSTGRES_DB maakt er één, en het volume bestaat al.
  • Scaffolden op Nuxt 4.5.2, compatibilityDate zetten, Nuxt 4-layout (app/ als srcDir).
  • runtimeConfig in plaats van losse process.env — het equivalent van harde regel 4 aan de Nuxt-kant: NUXT_DATABASE_URL, NUXT_BETTER_AUTH_SECRET, NUXT_BETTER_AUTH_URL, plus de key voor secrets.ts.
  • .env.example, en scripts dev / build / preview / typecheck (nuxt typecheck + vue-tsc als devDependency, zodat pnpm turbo run typecheck de app meeneemt).
  • Tailwind 4: @tailwindcss/vite in vite.plugins + app/assets/css/main.css met @import "tailwindcss";.
  • turbo.json: build.outputs uitbreiden met .output/** (nu alleen dist/**, dus een Nuxt-build zou nooit cachen) en NUXT_* in passThroughEnv.

Grootste risico van deze taak. Onze workspace-packages exporteren TypeScript-source via hun exports-map. Astro loste dat op met vite.ssr.noExternal; in Nuxt is dat build: { transpile: [...] } en mogelijk óók iets in nitro.externals voor de server-bundle. Reserveer hier tijd voor en noteer wat er nodig bleek.

  • server/utils/auth.ts: betterAuth met drizzleAdapter(getMasterDb(...), { provider: "pg", usePlural: true }), emailAndPassword aan, disableSignUp: true, secret en baseURL uit runtimeConfig. Geen plugins.
  • server/api/auth/[...all].ts: defineEventHandler((event) => auth.handler(toWebRequest(event))). auth.handler(Request) is precies wat de Astro-route al doet.
  • useSecureCookies in productie.
  • server/utils/session.ts met requireAdminSession(event)dit is de grens, en elke server-route roept hem aan.
  • app/middleware/auth.global.ts doet alleen de redirect naar /login. Een pagina-middleware is navigatie, geen beveiliging.
  • app/lib/auth-client.ts (better-auth/vue), app/pages/login.vue, app/pages/index.vue als lege shell met e-mail + uitloggen.

Geen klantenoverzicht, geen kaarten, geen statussen — dat is 2.4.

8. Eerste beheerder — packages/ops/src/create-admin-user.ts

Section titled “8. Eerste beheerder — packages/ops/src/create-admin-user.ts”
  • Zelfde patroon als create-cms-user: --email, --name, --list, --reset-password, --yes, --json, hashPassword uit better-auth/crypto.
  • Env MASTER_DATABASE_URL, apart van DATABASE_URL, zodat je in één shell niet per ongeluk een tenant raakt.
  • Guard in de stijl van seed-guard: weiger een database met een site-tabel of zonder tenants-tabel — dan wijs je op een tenant.
  • Script create-admin in packages/ops/package.json.
  • Datamodel: master-sectie met de echte kolomnamen en de twee afwijkingen uit stap 2.
  • Architectuur: repo-structuur, @platform/db/master, en dat de master runtimeConfig gebruikt waar regel 4 nu alleen astro:env noemt.
  • Contract: platform.ts bestaat nu.
  • Nieuwe .cursor/rules/master-dashboard.mdc, glob apps/master-dashboard/** — inclusief de uitzondering op regel 1.
  • AGENTS.md: commando’s voor master-migraties, dev en create-admin.
  • Taken: 2.1 afvinken met vastgestelde versies en afwijkingen.
  • pnpm turbo run typecheck en pnpm turbo run build groen, inclusief de nieuwe app.
  • Verse platform_mastermigrate:master → negen tabellen aanwezig.
  • create-admin → inloggen op localhost:3000.
  • Dev-server herstarten en de sessie leeft nog. Dat bewijst dat de sessie in de database staat en niet in geheugen.
  • Uitgelogd naar een beschermde route → redirect naar /login.
  • Regressie op de tenant-kant, want turbo.json en de root-engines gaan om: site-template typecheck (89 bestanden, 0 fouten) en build groen, verify op tenant_demo groen op de verwachte media-SKIP, en de drie tenants/*.json valideren nog na het verplaatsen van tenantSlugSchema.
  • verify --http op pilot en rb-media. Niet gedraaid: vraagt de SSH-tunnel en productie-secrets. Nog te doen vóór de merge naar main.
  • Deployment van de master staat in geen enkele fase-2-taak. Niet in 2.1 dus. 2.5 gaat de Coolify-API aanroepen en kan een tijd lokaal draaien, daarna niet meer. Als los punt in Taken zetten.
  • platform_master moet in de Coolify per-database-backuplijst zodra hij op de VPS staat. Dat is precies de val waar de freeze-check zelf in liep met tenant_rb-media.
  • 2.3 heeft een idempotentie-tabel nodig voor eventId. Bewust niet in deze migratie: die hoort bij de taak die hem gebruikt.
  • Secret-rotatie voor de key van secrets.ts valt onder het bestaande losse punt “rotatie van per-site secrets zonder downtime”.
  • Node-major optrekken naar 24 is bewust niet gedaan: dev en de Dockerfiles (site-template, ops-runner) blijven allebei op 22, dus geen drift tussen lokaal en container. Wil je later naar 24, dan gaan de Dockerfiles mee en moeten pilot én rb-media opnieuw deployen met verify --http erna.