Operations
Runbook voor oplevering, go-live-checks, restore en support. Fase-overgang naar het master dashboard: Freeze-check.
| Commando | Package |
|---|---|
bootstrap |
@platform/ops — site + pages + nav uit JSON |
create-user |
@platform/ops — CMS-gebruiker + eenmalig wachtwoord |
backup / restore / verify |
@platform/ops |
provision-db |
@platform/ops — rol + database tenant_<slug>, DATABASE_URL eenmalig op stderr |
provision-tenant-db.sh |
scripts/ — wrapper om provision-db met positioneel <tenant_id> [password] |
backup-media.sh / verify-backup.sh |
scripts/ — media-cron en dump-probe |
scratch-verify-db.sh |
scripts/ — scratch-database voor de klantvariant hieronder |
create-admin |
@platform/ops — beheerder voor het master dashboard |
provision-master-db.sh |
scripts/ — rol + database platform_master |
Elke ops-functie is puur (geen process.env/argv in de library); de CLI is een dunne wrapper. Taak 2.5 kan dezelfde functies aanroepen.
Master dashboard (fase 2)
Section titled “Master dashboard (fase 2)”Aparte database, aparte connectiestring: MASTER_DATABASE_URL, niet
DATABASE_URL. create-admin weigert een database met een site-tabel, dus een
vergissing schrijft geen gebruiker in een klantdatabase.
./scripts/provision-master-db.sh # rol + database platform_masterMASTER_DATABASE_URL='…' pnpm --filter @platform/db run migrate:masterMASTER_DATABASE_URL='…' pnpm --filter @platform/ops run create-admin -- \ --email … --yes # eenmalig wachtwoordLokaal is er geen psql op de host en is het compose-image alpine (geen bash),
dus draai de twee SQL-blokken uit provision-master-db.sh via de container:
docker compose exec -T -e PGPASSWORD=platform postgres psql -U platform -d postgres ….
Nieuwe beheerders en wachtwoordresets lopen ook via create-admin
(--list, --reset-password): het dashboard heeft geen gebruikersbeheer-UI en
verstuurt geen resetmail. Zet platform_master in de Coolify
per-database-backuplijst zodra hij op de VPS staat.
Provisioning vanaf het dashboard (2.5)
Section titled “Provisioning vanaf het dashboard (2.5)”Op de tenant-detailpagina: knop Provisioneren bij status provisioning of
failed. Die POSTt /api/tenants/:slug/provision en ververst het detail
(activiteit + deployments). Ciphertext blijft uit browser-JSON.
Lokaal zonder Coolify: zet NUXT_POSTGRES_ADMIN_URL (CREATEROLE+CREATEDB of
superuser) en laat NUXT_COOLIFY_TOKEN leeg. De run vult de database, migraties
en secrets, schrijft tenant.provision_partial, en laat status op
provisioning. Met Coolify-config gaat de run door tot start; een startfout
zet status failed en tenant.provision_failed.
Migraties vanaf het dashboard (2.8)
Section titled “Migraties vanaf het dashboard (2.8)”/migrations (link Migraties in de kop) toont per tenant de schemastand
tegen de migraties in het draaiende master-image, met Bijwerken per tenant
en Alles bijwerken. Eén run tegelijk; een mislukte tenant stopt de rest
niet. Het resultaat per database staat in de run-historie onderaan en als
tenant.migrated / tenant.migration_failed op de klantpagina.
Bestaande tenants (vóór 2.5 aangemaakt, zoals pilot en rb-media) hebben geen
DSN in de master. Koppel die eenmalig op de klantpagina, sectie Database:
plak de DATABASE_URL uit de wachtwoordmanager. De master controleert dat de
databasenaam tenant_<slug> is, dat hij verbinding krijgt en dat
site.tenant_id dezelfde slug draagt; daarna staat hij versleuteld in
database_url_encrypted. Overschrijven kan niet (409): rotatie is een los punt.
Volgorde bij een release met schemawijziging: master deployen → /migrations
groen → sites uitrollen. Geblokkeerd · database loopt vóór op dit image
betekent dat het master-image ouder is dan het site-image; eerst de master
herbouwen. Zie Deploy § Migraties.
Go-live checklist (per tenant)
Section titled “Go-live checklist (per tenant)”- Database + rol:
PGHOST=… PGUSER=postgres PGPASSWORD=… pnpm --filter @platform/ops run provision-db -- --tenant <id>(of./scripts/provision-tenant-db.sh <id>). De admin-rol is superuser, of heeftCREATEROLEenCREATEDBmetcreaterole_self_grant = 'set, inherit'. Zonder die instelling stopt Postgres 16 opmust be able to SET ROLE. DeDATABASE_URLverschijnt eenmalig op stderr, al percent-encoded. Bewaar hem in de wachtwoordmanager. Een tweede run laat het wachtwoord van een bestaande rol staan en meldt dat. Geef dan--password(ofTENANT_PASSWORD) mee om dezelfde URL terug te krijgen. - Coolify-app met build/runtime-env per Deploy.
- Deploy. Bij een nieuwe tenant slaat Coolify de pre-deployment migratie over (geen draaiende container om in te
docker exec) en meldt tóch “finished”. Draai hem één keer met de hand:docker exec <app-container> node /app/scripts/migrate.mjs. Zie Deploy. bootstrap -- --config ../../tenants/<id>.json --yes(eerst--dry-run). Pad is relatief aanpackages/ops; verbinding via de tunnel uit Deploy.create-user -- --email … --role admin --yes→ wachtwoord in de wachtwoordmanager.- DNS: apex én
admin.(beide in Coolifydomainsmethttps://-prefix). Preview-tenants: de provisioner vult die Coolify-string; de wildcard*.{previewBase}en DNS-01 staan in Deploy (sectie Preview-wildcard).admin.{slug}.{previewBase}valt buiten een één-label-wildcard. - Container
healthy;curl -Iop beide hosts met geldige certificaten. /robots.txttoontSitemap: https://<SITE_DOMAIN>/sitemap.xml.verify --http https://<domein> --json→ alles PASS. NaastDATABASE_URLzelf de vier build-vars meegeven (S3_BUCKET,S3_REGION,S3_PUBLIC_URL,S3_FORCE_PATH_STYLE): die zijn ingebakken en staan dus niet in de runtime-env van de container. Ontbreken ze, dan faaltmedia.s3.config. Heeft de tenant nog geen media-rijen, dan meldtverifyde media-checks alsSKIP(skipped: truein de JSON) en blijft de run groen — een SKIP is geen bewijs over R2, dus draai hem na stap 10 nog eens.- Handmatig: inloggen, primary-kleur wijzigen → publieke site verandert, logo uploaden, nieuws publiceren, contactformulier + mail, wachtwoordreset + mail.
/_image?href=…geeft 200 (bewijst ingebakkenS3_PUBLIC_URL/remotePatterns).grep -c build-placeholderoverdist/in de container → 0.
Restore-rehearsal
Section titled “Restore-rehearsal”Volledige vernietiging+restore hoort op een pilot-tenant, nooit op de klantdatabase. Bewijs vastleggen als nieuwe pagina onder Taken en beslissingen (besluiten/restore-test-YYYY-MM-DD.md), zoals de rehearsal van 12 augustus 2026.
Slagingscriteria: verify groen inclusief rijtellingen, admin logt in met hetzelfde wachtwoord, logo rendert met content-length == size_bytes, contactformulier mailt, enums bestaan, verse migrate is een no-op. Meet RTO (stopwatch) en RPO (backupinterval).
Na restore: app-container herstarten (connectiecache).
Veilige variant voor een echte klant
Section titled “Veilige variant voor een echte klant”Niets vernietigen: backup nemen, restoren in een scratch-database, verifiëren, droppen. Media wordt alleen gelézen.
# 1. Scratch-database naast de live tenant. Eigenaar is de bestaande tenant-rol,# dus de live DATABASE_URL werkt met alleen de databasenaam gewijzigd../scripts/scratch-verify-db.sh create <id>
# 2. Backup van de live tenant (read-only voor de klant).DATABASE_URL=<live> pnpm --filter @platform/ops run backup -- --tenant <id> --out ./backups
# 3. Restore in de scratch-DB. --skip-media is hier verplicht.pnpm --filter @platform/ops run restore -- --from ./backups/<id>/<ts> \ --database-url postgresql://tenant_<id>:…@<host>:5432/tenant_<id>_verify \ --skip-media --yes
# 4. Media apart, uitsluitend via HeadObject. Geen --http: de live site blijft erbuiten.DATABASE_URL=<scratch> pnpm --filter @platform/ops run verify -- \ --expect ./backups/<id>/<ts>/meta.json
# 5. Opruimen../scripts/scratch-verify-db.sh drop <id>Draai de scripts uit stap 1 en 5 in een postgres:18-alpine-container op het coolify-netwerk; de Coolify-Postgres is niet publiek exposed (zelfde truc als packages/ops/src/lib/docker-pg.ts).
restore weigert een *_verify-database zonder --skip-media. De manifest-keys zijn de live keys, dus een media-restore zou de klantobjecten in de bucket overschrijven — geen vlag maakt dat goed.
verify --expect is ook op een drukke site exact: backup leest de rijtellingen uit dezelfde snapshot als pg_dump. Een counts.match-melding is dus echt en geen timingartefact.
Accounts
Section titled “Accounts”- Eén admin voor de klant, één admin voor support, editors voor de rest.
- Wachtwoorden out-of-band via wachtwoordmanager-share, nooit per mail.
- De app dwingt geen wachtwoordwijziging bij eerste login af; er is nog geen gebruikersbeheer-UI — wijzigingen via
create-user/--reset-password.
EMAIL_FROM op een Resend-geverifieerd domein. mail.ts faalt stil bij ontbrekende config — bewijs vóór overdracht met één echte inzending en één echte resetmail.
Bewaartermijn / AVG
Section titled “Bewaartermijn / AVG”form_submissions bevat persoonsgegevens; backups dus ook.
| Dataset | Bewaartermijn | Toelichting |
|---|---|---|
Postgres-backups (Coolify → platform-backups) |
35 dagen | lifecycle-regel op de bucket |
Media-replica (B2, platform-media-backup) |
géén leeftijdsregel | levende spiegel van R2; een verwijderregel zou live media wissen |
Media-archive (B2, platform-media-archive) |
35 dagen, per incident beoordelen | --backup-dir per datum; regel tijdelijk uit als er een incident loopt |
Operator-backups (./backups/…) lokaal/offsite |
max 35 dagen, tenzij een incident loopt | geen PII langer bewaren dan nodig |
form_submissions in de live DB |
door klant te bepalen; default: handmatig opschonen tot er CMS-UI is |
Leg een klantspecifieke termijn vast in de overeenkomst vóór de eerste echte inzending.
Demo-seed
Section titled “Demo-seed”pnpm --filter @platform/db run seed is destructief en geblokkeerd zonder ALLOW_DESTRUCTIVE_SEED=1, en weigert niet-demo databases. Niet gebruiken op klantomgevingen.