Ga naar inhoud

Taken

Historisch werklog. Open werk staat in Linear, team Okhema. Begin bij die issue, niet bij het volgende open vinkje hier. Eén Linear-issue per sessie. Na afloop: Linear bijwerken (status + bewijs) en hier afvinken als het een genummerde fasetaak is. Regel: begin geen taak uit een volgende fase voordat de huidige fase af is.

  • 0.1 Monorepo-skelet: pnpm workspaces, Turborepo, contract / db / blocks als lege packages, tsconfig-base, docker-compose voor Postgres. Astro- en Node-versie verifiëren via npm.
  • 0.2 Documentatie: PRD, Architectuur, Datamodel, Contract, Taken.
  • 0.3 Agent-configuratie: AGENTS.md in de root, .cursor/rules/000-project.mdc (always apply), plus scoped rules voor apps/site-template/** en packages/db/**.

Fase 1 — MVP: één klantsite met eigen CMS

Section titled “Fase 1 — MVP: één klantsite met eigen CMS”
  • 1.1 Astro-app scaffolden, astro:env inrichten, Drizzle opzetten, tenant-datamodel bouwen, theming-fundament (design tokens → CSS-variabelen), seed-script. Bewijs: kleur in database wijzigen verandert de site zonder code-aanpassing.
  • 1.2 Blokkenbibliotheek v1: hero, tekst, afbeelding, CTA, kaartenrij, quote. Elk blok krijgt een Zod-schema, een renderer en een registry-entry. BlockRenderer die op type de juiste component kiest.
  • 1.3 Publieke pagina’s: home, over ons, dienstenoverzicht, dienstdetail, nieuwsoverzicht, nieuwsdetail, referenties. Layout, header en footer op basis van navigation.
  • 1.4 Contactpagina met formulier: Astro Actions, validatie via Zod, opslag in form_submissions, e-mailnotificatie, spam-bescherming.
  • 1.5 SEO-basis: meta-tags uit seo en seo_defaults, sitemap, robots, Open Graph, canonieke URL’s.
  • 1.6 Better Auth opzetten: inloggen, sessies, rollen beheerder/redacteur, wachtwoordherstel.
  • 1.7 CMS-shell op het admin-subdomein: routing, layout, navigatie, afscherming via middleware.
  • 1.8 CMS-CRUD: pagina’s, diensten, nieuws, referenties, navigatie, instellingen. Inclusief audit_log-registratie bij elke mutatie.
  • 1.9 Media-upload naar R2 of B2, met verplichte alt-tekst en koppeling aan Astro’s beeldoptimalisatie.
  • 1.10 Blok-editor in het CMS: blokken toevoegen, herordenen en bewerken via formulieren. Nog geen drag-and-drop.
  • 1.11 Dockerfile voor site-template, handmatig deployen op Coolify, domein koppelen, backups inrichten.
    • Keuze gemaakt: TENANT_ID en SITE_DOMAIN zijn runtime (access: "secret"), zodat één image meerdere tenants kan dienen. Astro valideert secrets bij import van astro:env/server tijdens de build — Dockerfile heeft dus build-time placeholders nodig; echte waarden komen bij container-start. Raakt ook taak 2.9 (template-update naar alle klanten).
    • Code klaar: .dockerignore, apps/site-template/Dockerfile (lokaal healthy smoketest), migrate.mjs als Coolify pre-deployment command, Deploy + Backups.
    • Live sinds 12 augustus 2026: pilot.okhema.studio en admin.pilot.okhema.studio, TLS bij Traefik (Cloudflare-proxy grijs). Postgres: twee Coolify-planningen op postgres:180 3 * * * UTC dump-all als vangnet, 30 3 * * * UTC per-database — naar R2 platform-backups, 7 lokale kopieën en 35 dagen op S3 plus bucket-lifecycle. Media: host-cron 15 3 * * * UTC, rclone sync R2 → B2 met --backup-dir per datum. Maandelijkse dump-probe 0 4 1 * * UTC in een wegwerp-Postgres. Nog open: geen alarmering bij een falende cron. Hoort bij 4.4 (noemt mislukte backups letterlijk); 2.2 en 2.3 leveren de plumbing. De stamp-bestanden onder /var/lib/media-backup en /var/lib/dump-verify zijn wat een statuspagina straks kan uitlezen.
  • 1.12 Eerste echte klant opleveren en de restore-procedure één keer testen.
    • Code klaar: @platform/ops (bootstrap, create-user, backup, restore, verify), tenants/*.json, seed-guard, Operations. Lokale backup→drop→restore→verify tegen tenant_demo geslaagd.
    • Repetitie gedaan (12 aug 2026): vernietig-en-restore op de pilot uit beide bronnen — operator-backup en Coolify per-database .dmp. RTO 24 s per stap, gemeten RPO 85 min (schema-worst-case 24 u). Bewijs met verify-output en de drie verrassingen in Restore-rehearsal 12 augustus 2026.
    • Klantvariant uitvoerbaar gemaakt (13 aug 2026): de veilige variant stond alleen als proza in OPERATIONS.md en liep op drie punten vast. Nu: scripts/scratch-verify-db.sh create|drop <id> maakt en dropt tenant_<id>_verify (provision-tenant-db.sh kon dat niet: de tenant-regex verbiedt _ en de databasenaam is hardcoded, en droppen ontbrak); restore weigert een *_verify-doel zonder --skip-media, want de manifest-keys zijn de live keys en een media-restore overschreef dus de klantbucket; backup leest de rijtellingen uit dezelfde pg_dump-snapshot, zodat counts.match niet meer valselijk faalt op een site met verkeer. Aangetoond onder gelijktijdige inserts: oude code 291!=298, nieuwe code exact gelijk.
    • Eerste echte klant live (13 aug 2026): rb-media op rbmedia.okhema.studio + admin.rbmedia.okhema.studio, geldige certificaten, container healthy. verify --http volledig groen zónder SKIPs: media bewezen via HeadObject (content-length == size_bytes) en /_image levert webp, dus de ingebakken S3_PUBLIC_URL en remotePatterns kloppen. grep -rl build-placeholder /app/dist = 0.
    • Klantvariant uitgevoerd op echte data (13 aug 2026): backup (mét media), restore in tenant_rb-media_verify met --skip-media, verify --expect meta.json volledig groen inclusief counts.match en site.tenant_id, daarna scratch gedropt. De media-guard is onderweg één keer echt getriggerd: restore zónder --skip-media werd geweigerd. De live database is nooit aangeraakt.
    • Val gevonden bij de eerste deploy: Coolify slaat het pre-deployment commando over als er nog geen container draait (No running containers found. Skipping.) en meldt tóch “finished”. Schema bleef leeg, container werd healthy omdat /robots.txt de database niet raakt, en bootstrap viel om. Migratie één keer met de hand gedraaid; vastgelegd in Deploy en in stap 3 van de go-live-checklist.

Korte go/no-go vóór 2.1. Geen nieuwe fase. Checklist: Freeze-check.

  • Freeze-check uitgevoerd 17 augustus 2026. A + D groen; 2.1 mag. Restpunten in Freeze-check § Uitgevoerd: zelf inloggen op rb-media CMS, vannacht eerste tenant_rb-media.dmp, optioneel www-DNS.

Uit Review-fixes; geen taak afgevinkt, alleen bestaand werk hersteld.

  • A1 CMS_SECTION_PREFIXES wordt nu afgeleid uit cmsNavItems, zodat sidebar en routing niet meer uit elkaar kunnen lopen. (/media stond er al in sinds 1.10.)
  • A2 safeRedirectPath() in lib/safe-redirect.ts weert protocol-relatieve paden; server- en clientkant van login.astro gebruiken dezelfde helper. Losse module en niet hosts.ts, omdat die astro:env/server importeert.
  • A3 deleteMedia blokkeert nu met een CONFLICT als een blok in pages/services/news nog naar de media verwijst, en noemt waar.
  • A4 Statische assets slaan de sessielookup over; /_actions expliciet niet.
  • A5 Postgres 23503 (foreign key) geeft CONFLICT in plaats van 500. throwIfUniqueViolation heet nu throwIfConstraintViolation.
  • A6 /diensten, /nieuws en /referenties staan alleen in de sitemap als er minstens één item is.
  • B1 theme.logo.mediaId rendert in Header.astro, met de sitenaam als fallback en als alt. Het dark-logoveld is uit het formulier gehaald; de opgeslagen waarde blijft behouden.
  • B2 --font-scale stuurt de root-fontsize in global.css, --spacing-base is gemapt op Tailwinds --spacing.
  • B3 requireCmsAdmin beschermt updateSiteSettings en updateFormRecipient; de sectie Instellingen is voor redacteuren verborgen en de pagina redirect. Content blijft voor beide rollen open. Nagekomen: het dashboard bouwde zijn kaarten uit een eigen lijst en toonde redacteuren daardoor alsnog een kaart naar Instellingen. De kaarten komen nu uit cmsNavItemsFor(), dezelfde bron als de sidebar.
  • C Niet gewijzigd, alleen genoteerd bij taak 1.11.

Fase 2 — Master dashboard en provisioning

Section titled “Fase 2 — Master dashboard en provisioning”

Niet beginnen vóór de freeze-check hierboven groen is.

  • 2.1 Nuxt-app scaffolden, master-datamodel, Better Auth voor beheerders.
    • Plan: Taakplan 2.1 (vastgestelde versies, keuzes, stappen, bewijs).
    • Versies vastgesteld (17 augustus 2026): Nuxt 4.5.2 (Nitro 2.13.4, Vite 8.2.1, Vue 3.5.41, vue-router 5.2.0), h3 1.15.11 dus toWebRequest voor de auth-handler. better-auth 1.6.26, gelijk aan @platform/db — 1.6.29 bestaat, pin bewust niet gebumpt. Tailwind 4.3.3 via @tailwindcss/vite (peer accepteert Vite 8). nuxt typecheck groen met vue-tsc 3.3.10 + TypeScript 6.0.3, dezelfde TS-pin als site-template. Node lokaal 22.23.2, root engines nu >=22.19.0 (blijft geldig voor de node:22-images).
    • Master-datamodel: @platform/db/master als subpath-export, niet als nieuw package en niet in de app — het eerste account komt uit een ops-CLI, dus zowel de Nuxt-app als @platform/ops heeft het schema nodig. Negen tabellen in drizzle-master/0000_messy_lifeguard.sql, eigen config en eigen MASTER_DATABASE_URL. Aangetoond: tenant-migraties bevatten géén master-tabellen en drizzle-kit check op de tenant-config blijft groen.
    • Afwijkingen van Datamodel (document bijgewerkt): database_url heet database_url_encrypted, en er is géén admin_users-tabel of role-kolom — de Better Auth users-tabel is de beheerderstabel.
    • Afwijking van het plan: de AES-256-GCM-helper staat in apps/master-dashboard/server/utils/secrets.ts en niet in packages/db, want database.mdc staat daar geen niet-databasecode toe. Roundtrip, willekeurige iv, manipulatiedetectie en key-lengtecontrole handmatig geverifieerd. Nog niet in gebruik: 2.1 schrijft geen tenant-rijen.
    • Auth: Better Auth zonder admin-plugin en zonder rollen; publieke signup uit (bewezen: 400 EMAIL_PASSWORD_SIGN_UP_DISABLED); geen wachtwoordherstel per mail, de CLI doet dat. create-admin weigert een tenant-database (site-tabel aanwezig) en een ongemigreerde database, beide messages geverifieerd.
    • Bewijs (17 augustus 2026): dev-server én productiebuild getest. Sign-in 200 met cookie, /api/session geeft de gebruiker, / 302 → /login zonder sessie, /login 302 → / met sessie, fout wachtwoord 401. Sessie overleeft een herstart van de server, dus hij staat in de database. Geen secret uit .env terug te vinden in .output. Regressie: turbo typecheck 6/6, build 2/2, verify op tenant_demo groen op de verwachte media-SKIP, en de drie tenants/*.json valideren nog na het verplaatsen van tenantSlugSchema naar platform.ts.
    • Nog open: verify --http op pilot en rb-media is niet gedraaid — dat vraagt de SSH-tunnel en productie-secrets. Master-deployment en de backuplijst staan bij de losse punten.
  • 2.2 Platform-endpoints op de klantsite (/health, /stats, /revalidate) plus webhook-verzending met HMAC.
    • Plan: Taakplan 2.2.
    • Gebouwd: packages/contract/src/events.ts (discriminated union op type; tenantId via tenantSlugSchema). Drie routes onder /api/platform/ met X-Platform-Key en Cache-Control: no-store. /health gebruikt een eenmalige postgres()-client (max: 1, connect_timeout: 2), niet getDb. /stats telt alle rijen op pages/services/news en neemt max(greatest(created_at, updated_at)) over die drie tabellen. /revalidate roept purgeWholeSite aan. Uitgaande webhooks via sendPlatformEvent + signEnvelope (HMAC-SHA256 over timestamp + "." + body). PLATFORM_WEBHOOK_URL leeg is een no-op. Een mislukte fetch faalt de CMS-actie niet. site.error staat op het schema, heeft geen call site.
    • Login-emit: hook in createAuth() (hooks.after + ctx.context.newSession). better-auth 1.6.26 types in @better-auth/core exposen beide. [...all].ts blijft stuffing-limiter plus handler.
    • Bewust uitgesteld: webhook-ontvangst en idempotentie (2.3), retry-wachtrij, per-tenant sleutels in de master (2.5), site.error-verzending.
    • Afwijking van SKETCH.md: userIdFromSignInResponse en de wrap van [...all].ts vervallen door de hook. astro:env en de contract-barrel in send-event.ts zijn dynamische imports zodat de geïsoleerde signEnvelope-test in Node kan draaien. HMAC blijft in send-event.ts, niet in @platform/contract (geen crypto in dat package). PlatformEventInput is een distributieve Omit, anders houdt TypeScript alleen de gemeenschappelijke keys van de union over. requirePlatformKey returned een 401-Response; een throw werd in Astro 7.2 een HTTP 500.
  • 2.3 Webhook-ontvangst op de master, met idempotentie en retry-afhandeling.
    • Plan: Taakplan 2.3.
    • Gebouwd: POST /api/webhooks/site (HMAC over de ruwe body, timestamp ±5 min / toekomst +60 s, platformEventEnvelopeSchema). webhook_receipts in de master (event_id = envelope-UUID). Duplicate is 200 zonder tweede activity. Onbekende slug: tenant_id null. NUXT_PLATFORM_WEBHOOK_SECRET tot 2.5. Site: webhook_outbox in de tenant-database, dezelfde eventId, drain met exponentiële backoff, max 24 uur. 4xx stopt, 5xx/netwerk retriet. Fire-and-forget blijft.
    • Bewust uitgesteld: per-tenant secrets (2.5), activiteit-UI (2.4), site.error-emit, Coolify-deploy van de master, /stats-pull.
    • Afwijking van het plan: content.published na een geslaagde drain is bewezen via enqueuePlatformEvent + drainWebhookOutbox (het pad van sendPlatformEvent), niet via een CMS-klik in de browser.
  • 2.4 Klantenbeheer: overzicht, aanmaken, detailweergave met status en activiteit.
    • Plan: Taakplan 2.4.
    • Gebouwd: sessiebeveiligde tenant-API’s (lijst, ledger-insert, detail, statuswijziging), Nuxt-overzicht, aanmaakformulier en detail met activiteit, lege domein/deployment-staten en gedeelde contract-schema’s.
    • Backfill: webhook-activiteit en receipts die vóór de tenant-rij binnenkwamen worden bij aanmaken transactioneel gekoppeld; de nieuwste eventtijd vult site_stats.last_activity_at.
    • Statuskeuze: quiet is een afgeleid aandachtssignaal na 24 uur stilte, geen vijfde database-status. Echte degraded vraagt ook healthcheck-data en blijft uitgesteld tot provisioning een hostname en platform-key levert.
    • Bewijs (3 september 2026): typecheck 6/6, build 2/2, 10 helper- en HMAC-tests groen. API: 401 zonder sessie, 400 validatie, 409 duplicate, orphan-backfill inclusief receipt, webhook-idempotentie en geen encrypted URL in responses. Browser: aanmaken → detail → actief → quiet-badge → overzicht en logout-redirect volledig groen.
    • Bewust uitgesteld: database/Coolify/env en per-tenant secrets (2.5), preview/custom domains (2.6/2.7), health-poll, delete en /stats-pull.
  • 2.5 Provisioning-service: tenant-database aanmaken, migraties draaien, Coolify-app aanmaken, env-variabelen zetten, deploy triggeren.
    • Opgeleverd: POST /api/tenants/:slug/provision (sessie) met idempotente stappen database → migrate → secrets → coolifyApp → coolifyEnv → coolifyStart. Per-tenant secrets in *_encrypted. Eerste migratie via tenant-DSN vóór Coolify. Zonder Coolify-token: stop na secrets, tenant.provision_partial, status blijft provisioning. Coolify-startfout zet tenant.provision_failed en status failed. Detail-UI: knop Provisioneren bij provisioning/failed, deployments-lijst.
    • Lokaal pad: NUXT_POSTGRES_ADMIN_URL met CREATEROLE/CREATEDB (of superuser); NUXT_COOLIFY_TOKEN leeg houdt de run bij secrets.
    • Bewust uitgesteld: bootstrapTenant / bootstrap-JSON, CMS-user (createCmsUser), Coolify per-database backup-lijst, master-dashboard Coolify-deploy, markeren als active. Preview-hostnames zijn 2.6.
  • 2.6 Wildcard preview-URL’s inclusief DNS-01 voor wildcard-TLS.
    • Gebouwd: previewBinding + NUXT_PREVIEW_BASE_HOST (default okhema.studio). Eén domains-rij type: preview bij provision (ook zonder Coolify). Coolify domains op create en PATCH-retry. Detail-UI: publieke URL + afgeleide CMS-host, SSL “In behandeling”.
    • Operator: wildcard A *.{previewBase} en Traefik DNS-01 in Deploy. admin.{slug}.{base} valt buiten een één-label-wildcard.
    • Infrastructuur uitgevoerd (7 september 2026): Cloudflare DNS + DNSSEC, wildcard A-record en wildcardcertificaat via een aparte Traefik DNS-01-resolver. Pilot-routing, publieke preview, CMS-login, publiceren en e-mail zijn gecontroleerd; zie Preview-domeinen.
  • 2.7 Custom domains via Cloudflare for SaaS: hostname registreren, DNS-instructies tonen, verificatie pollen, status weergeven.
    • Gebouwd: POST/DELETE /api/tenants/:slug/domains + POST …/verify. View-union op state (unregistered/pending/active/failed; preview blijft 2.6). cf_verification jsonb voor ownership-TXT. Leeg NUXT_CF_API_TOKEN slaat unregistered op en toont de CNAME. Coolify domains deelt desiredCoolifyDomains met provision (custom zonder admin.). Geen SITE_DOMAIN-cutover, geen admin.{custom} bij Cloudflare.
    • Operator: CNAME naar NUXT_CF_CNAME_TARGET; apex vraagt flattening. Actief = edge-TLS, CMS blijft op de preview-host. Recept in Deploy.
  • 2.8 Migratie-runner over alle tenant-databases, met statusrapportage per database.
    • Plan: Taakplan 2.8 (8 september 2026).
    • Gebouwd: runner in het master dashboard. GET /api/migrations (schemastand per tenant, live uit drizzle.__drizzle_migrations tegen de gebundelde migraties, plus laatste 10 runs), POST /api/migrations/runs ({ slugs? }, sequentieel op created_at, advisory lock, 409 bij een lopende run, doorgaan na een fout), POST /api/tenants/:slug/database (bestaande database koppelen met naam-, bereikbaarheids- en site.tenant_id-check; 409 bij overschrijven). Pagina /migrations met badges, “Bijwerken” per tenant, “Alles bijwerken” en run-historie; sectie Database op de klantpagina. Tabel migration_runs (drizzle-master/0004_unusual_mephisto); activity tenant.migrated, tenant.migration_failed, tenant.database_attached. Contract: tenantSchemaStateSchema (no_database / unreachable / blocked / pending / up_to_date), platformMigrationRunSchema, platformTenantDetailSchema.hasDatabase.
    • blocked wordt nooit gemigreerd: ahead (master-image ouder dan site-image), diverged (andere hash of gat in de boekhouding; Drizzle 0.45.2 leest hash zelf nooit terug), identity_mismatch (verkeerde DSN). applyTenantMigrations heeft nu lock_timeout 10 s en statement_timeout 5 min als default, ook voor de Coolify-hook.
    • Bewijs (8 september 2026): typecheck en build 2/2 groen; 81 master-tests groen (8 state, 7 runner, 4 attach, rest regressie). Lokaal: 401 zonder sessie op alle drie routes. tenant_demo gekoppeld → up_to_date 3/3, tweede keer 409. Lege tenant_migr8 gekoppeld → pending 3 → canary-run applied met drie tags (103 ms) → alles-run up_to_date ×2 plus skipped no_database; migrate.mjs daarna no-op. Neprij met nieuwe created_atblocked/ahead en skipped in de run; gewijzigde hash → blocked/diverged; hersteld → up_to_date. Verkeerde databasenaam 422, ongeldige URL 400, onbekende slug 404, ongeldige body 400. Geen connectiestring in enige response. SSR van /migrations en de klantpagina met sessie toont badges, koppelformulier en “Gekoppeld”.
    • Afwijkingen van het plan: de status per tenant nest de state-union onder schema in plaats van een vijfvoudige extend; het runresultaat kreeg een detail-veld. Master-tests draaien met node --import tsx --test --test-force-exit vanuit packages/db (tsx is vanuit de app niet resolvbaar; zonder force-exit houdt de open pool het proces vast). De lokale platform_master bleek weg en is opnieuw aangemaakt. Live is identity_mismatch alleen via de unit-test bewezen; de naamcheck vuurt eerder.
    • Nog open: eerste productie-run wacht op het losse punt “Master dashboard deployen”: pilot en rb-media koppelen, up_to_date 3/3, één no-op-run. Klik-door in een echte browser is niet gedaan; het SSR-bewijs via curl wel.
  • 2.9 Uitrolmechanisme: template-update naar alle klanten met controleerbare volgorde en rollback.

Doel van deze fase: nieuwe klant van niets naar preview-URL in minder dan vijf minuten, zonder handwerk.

  • 3.1 Herevaluatie: zelfbouw voortzetten of overstappen op Payload CMS 3. Beslissen vóór 3.2.
  • 3.2 Puck integreren in het CMS, gemapt op de bestaande blokken.
  • 3.3 Blokkenbibliotheek uitbreiden en varianten per blok mogelijk maken.
  • 3.4 Preview-modus: concept bekijken zonder te publiceren.
  • 4.1 Umami self-hosted, één website-ID per klantsite.
  • 4.2 Aggregatie in het master dashboard via de Umami API.
  • 4.3 Activiteitenweergave op basis van audit_log en events.
  • 4.4 Statuspagina: achterlopende deployments, mislukte backups, verlopen certificaten.
  • Restore-procedure documenteren en aantoonbaar testen
  • Master dashboard deployen. Staat in geen enkele fase-2-taak, draait nu alleen lokaal. 2.5 (provisioning) kan een tijd lokaal, daarna niet meer.
  • platform_master in de Coolify per-database-backuplijst zodra hij op de VPS staat. Dat handwerk is precies waar de freeze-check zelf in liep met tenant_rb-media; dump-all is het vangnet, de .dmp niet.
  • demo-seed herschrijven als bootstrap-config
  • Rotatie van per-site secrets zonder downtime
  • Font-strategie uitwerken (nu systeemstack met optionele webfont-URL)
  • Beslissen of klanten zelf de huisstijl mogen aanpassen
  • Dark-mode-logo: theme.logo.mediaIdDark blijft in het schema staan maar heeft geen formulierveld en geen renderer zolang er geen dark mode is. Zodra het veld terugkomt, moet het ook in findMediaUsage — nu bewust niet, zie het punt hieronder.
  • Media die als sitelogo of OG-afbeelding is ingesteld, wordt niet meegenomen in de gebruikscheck van deleteMedia. Opgelost: findBlockUsage is findMediaUsage geworden in lib/cms/media-usage.ts en dekt nu ook site.theme.logo.mediaId, site.seoDefaults.ogImageId en pages.seo.ogImageId (die laatste zat niet in de melding maar had dezelfde bug). De FK-velden (services.imageId, news.imageId, testimonials.logoId) zitten er ook in, zodat de melding het item bij naam noemt; de 23503-afhandeling blijft als vangnet voor de race met de delete. /media/[id] toont de lijst vooraf en zet de verwijderknop uit. Keuze: theme.logo.mediaIdDark wordt bewust niet gecheckt — er is geen formulierveld om die verwijzing weg te halen, dus blokkeren zou de media onverwijderbaar maken.
  • pnpm turbo run build faalt, ook met alle env-variabelen gezet. Opgelost: TENANT_ID/SITE_DOMAIN naar runtime (access: "secret"); turbo.json build heeft passThroughEnv voor secrets/tenant-vars en env voor overgebleven public geïnline’de vars. Zie taak 1.11.