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.
Vastgestelde versies
Section titled “Vastgestelde versies”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 |
Node — 22.x, gelijk aan de image
Section titled “Node — 22.x, gelijk aan de image”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 bestaandedefault 22-alias wijst daarna automatisch naar die versie. Geenpnpm installnodig voor de ABI: binnen dezelfde major verandert die niet. -
node -vin een nieuwe shell controleren; 22.16.0 betekent dat de install niet is opgepikt. - Root
package.json:engines.nodevan>=22.15.0naar>=22.19.0. - Losse docs-wijzigingen committen (Taken, Deploy,
Operations, untracked
freeze-check.md), zodat 2.1 op een schone tree begint.
Keuzes, met reden
Section titled “Keuzes, met reden”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.
Stappen
Section titled “Stappen”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 viaz.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 alscontentStatusEnum). -
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 alsgetDb— URL altijd meegeven, nooitprocess.envin dit package. -
index.ts+ exports-entry"./master"inpackages/db/package.json+ pad@platform/db/masterintsconfig.base.json. Niet vanuitpackages/db/src/index.tsre-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 naarapps/master-dashboard/server/utils/secrets.ts:database.mdcstaat inpackages/dbgeen 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 |
3. Migraties
Section titled “3. Migraties”-
packages/db/drizzle.master.config.ts—schema: ./src/master/schema.ts,out: ./drizzle-master, url uitMASTER_DATABASE_URL. -
scripts/migrate-master.mjsnaar het model vanscripts/migrate.mjs. - Scripts
generate:masterenmigrate:masterinpackages/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 vanprovision-tenant-db.sh. Die bestaande kan het niet: de regex verbiedt_en de databasenaam is hardcoded optenant_<id>— dezelfde val als bij de scratch-DB in 1.12. - Lokaal draaien tegen de compose-Postgres.
docker-compose.ymlkan dit niet doen:POSTGRES_DBmaakt er één, en het volume bestaat al.
5. Nuxt-app — apps/master-dashboard
Section titled “5. Nuxt-app — apps/master-dashboard”- Scaffolden op Nuxt 4.5.2,
compatibilityDatezetten, Nuxt 4-layout (app/als srcDir). -
runtimeConfigin plaats van losseprocess.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 voorsecrets.ts. -
.env.example, en scriptsdev/build/preview/typecheck(nuxt typecheck+vue-tscals devDependency, zodatpnpm turbo run typecheckde app meeneemt). - Tailwind 4:
@tailwindcss/viteinvite.plugins+app/assets/css/main.cssmet@import "tailwindcss";. -
turbo.json:build.outputsuitbreiden met.output/**(nu alleendist/**, dus een Nuxt-build zou nooit cachen) enNUXT_*inpassThroughEnv.
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.
6. Better Auth
Section titled “6. Better Auth”-
server/utils/auth.ts:betterAuthmetdrizzleAdapter(getMasterDb(...), { provider: "pg", usePlural: true }),emailAndPasswordaan,disableSignUp: true, secret enbaseURLuit 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. -
useSecureCookiesin productie.
7. Afscherming
Section titled “7. Afscherming”-
server/utils/session.tsmetrequireAdminSession(event)— dit is de grens, en elke server-route roept hem aan. -
app/middleware/auth.global.tsdoet 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.vueals 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,hashPassworduitbetter-auth/crypto. - Env
MASTER_DATABASE_URL, apart vanDATABASE_URL, zodat je in één shell niet per ongeluk een tenant raakt. - Guard in de stijl van
seed-guard: weiger een database met eensite-tabel of zondertenants-tabel — dan wijs je op een tenant. - Script
create-admininpackages/ops/package.json.
9. Documentatie en rules
Section titled “9. Documentatie en rules”- Datamodel: master-sectie met de echte kolomnamen en de twee afwijkingen uit stap 2.
- Architectuur: repo-structuur,
@platform/db/master, en dat de masterruntimeConfiggebruikt waar regel 4 nu alleenastro:envnoemt. - Contract:
platform.tsbestaat nu. - Nieuwe
.cursor/rules/master-dashboard.mdc, globapps/master-dashboard/**— inclusief de uitzondering op regel 1. -
AGENTS.md: commando’s voor master-migraties, dev encreate-admin. - Taken: 2.1 afvinken met vastgestelde versies en afwijkingen.
Klaar wanneer
Section titled “Klaar wanneer”-
pnpm turbo run typecheckenpnpm turbo run buildgroen, inclusief de nieuwe app. - Verse
platform_master→migrate:master→ negen tabellen aanwezig. -
create-admin→ inloggen oplocalhost: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.jsonen de root-engines gaan om:site-templatetypecheck (89 bestanden, 0 fouten) en build groen,verifyoptenant_demogroen op de verwachte media-SKIP, en de drietenants/*.jsonvalideren nog na het verplaatsen vantenantSlugSchema. -
verify --httpop pilot en rb-media. Niet gedraaid: vraagt de SSH-tunnel en productie-secrets. Nog te doen vóór de merge naarmain.
Buiten scope, wel vastgelegd
Section titled “Buiten scope, wel vastgelegd”- 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_mastermoet in de Coolify per-database-backuplijst zodra hij op de VPS staat. Dat is precies de val waar de freeze-check zelf in liep mettenant_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.tsvalt 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 metverify --httperna.