Files
crewli/dev-docs/ARCH-CONSOLIDATION-2026-04.md
bert.hausmans e6d1e2c88a docs: WS-3 session 1b-i complete — baseline 1451 → 231
ARCH-CONSOLIDATION-2026-04.md §6.8: session 1b-i recorded.
Risk-tiered eslint --fix pass (Tier 1 + Tier 2 + whitespace) reduced
the apps/app baseline from 1451 to 231 problems. Tier 3 config files
deferred to 1b-ii under hand-reviewed conditions.

.claude-sync/ regenerated locally (gitignored — not in this commit).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-29 11:16:10 +02:00

611 lines
26 KiB
Markdown

# Crewli — Architectuur Consolidatie Sprint (april 2026)
> **Status:** briefing document, vertrekpunt van de consolidatie-sprint.
> **Doelgroep:** Bert (product owner + solo dev), Claude (architect/PM rol in chat), Claude Code (executie).
> **Positie t.o.v. andere docs:** dit document is tijdelijk. Na afronding van de sprint wordt
> de inhoud gedistribueerd over `/CLAUDE.md`, `/dev-docs/SCHEMA.md`, `/dev-docs/ARCH-*.md` en
> `/dev-docs/dev-guide.md`. Tijdens de sprint is het de enkele bron van waarheid voor wát
> we aan het doen zijn en waaróm.
---
## 0. Context in één alinea
Crewli is een multi-tenant SaaS-platform voor festival- en event-personeelsbeheer
(vrijwilligers, crew, artiesten, leveranciers, pers, gasten). Pre-launch. Geen
gebruikers, geen live data, geen tijdsdruk. Ambitie: een enterprise-grade product
dat binnen Stichting Echt Feesten gebruikt wordt en daarna bredere markt op
kan. Het fundament is vandaag op ~80% — functioneel grotendeels aanwezig,
architecturaal op sommige plekken een compromis dat onder productie-druk had
moeten standhouden. Dit document legt vast hoe we die laatste 20% invullen
voordat we aan de 4 kern-workflows (EVENT_REGISTRATION, ARTIST_ADVANCE,
SUPPLIER_INTAKE en één generiek type) beginnen.
---
## 1. Leidende principes voor de sprint
Deze principes zijn toetssteen voor elke beslissing tijdens de consolidatie.
Bij twijfel tijdens executie: teruglezen, toetsen, beslissen.
**P1 — Enterprise betekent discipline in scope, niet afwezigheid van scope.**
We bouwen een fundament dat 23 denkbare purposes aan kan, maar we implementeren
er nu 4. De rest is vocabulaire, geen functionaliteit. Elke feature die niet
onmisbaar is voor het fundament of voor de 4 kern-workflows wordt op de backlog
gezet met duidelijke trigger-conditie ("als X, dan bouwen we Y").
**P2 — Queryable data krijgt eigen kolommen of tabellen. JSON is uitsluitend
voor echt opaque config.** Herinterpretatie van het bestaande principe: strenger
toepassen. Validatieregels, conditional logic, field options, bindings — allemaal
eigen tabellen. `settings` JSON krimpt fors.
**P3 — ULID overal. Zonder uitzonderingen.** Ook pivot-tabellen. Ook logging-tabellen.
Consistent = onderhoudbaar = veilig.
**P4 — Polymorfe relaties zijn een bewust architectuur-patroon, geen kludge.**
`form_schemas.owner_type/owner_id` en `form_submissions.subject_type/subject_id`
blijven polymorf. Nieuwe polymorfe relaties voegen we toe waar het domein dit
rechtvaardigt (niet speculatief).
**P5 — Eén SPA, twee hostnames.** Geen twee SPAs. Geen drie. Herconsolidatie
van `apps/portal/` in `apps/app/` met domein-gebaseerde routing. Zie §4.
**P6 — End-to-end-werkend hoofdpad voor fancy features.** FormBindingApplicator
+ Person-creation + entity-updates zijn eerste-klas fundament, niet een addendum.
Geen enkele fancy feature (conditional logic, webhooks, delegation) mag afgerond
heten zolang de basis-pipeline niet volledig gebouwd en getest is.
**P7 — Observability is fundament, geen nice-to-have.** Een enterprise-product
zonder gestructureerde logging, error tracking en performance monitoring is geen
enterprise-product. Deze laag wordt in de sprint neergezet.
**P8 — Documentatie loopt synchroon met code.** Elke sprint-PR wijzigt of
creëert dev-docs. Geen "docs volgen later". Dit is al praktijk bij Crewli en
blijft zo.
---
## 2. De drie valstrikken, en hoe we ze tegenhouden
De grote risico's van een open-ended consolidatie-sprint zijn bekend. Expliciet
benoemen zodat we ze kunnen herkennen als ze zich voordoen.
**Valstrik 1 — Scope-creep tijdens refactor.** "We zijn toch bezig in deze code,
laten we ook X meenemen." Weerstand: X komt op `/dev-docs/BACKLOG.md` met
trigger-conditie, wordt niet mee-genomen. Enige uitzondering: X is een directe
blocker voor het huidige consolidatie-werkpakket.
**Valstrik 2 — Gold-plating.** Iets verder perfectioneren dan nodig. Elke
refactor-actie moet een concrete trigger hebben: "dit veroorzaakt drift in Y"
of "dit blokkeert feature Z" of "dit is een security-risico". "Dit kan
eleganter" is geen geldige trigger.
**Valstrik 3 — Feature-sprawl verkapt als fundering.** "Als we toch bezig zijn
met bindings, laten we ook een binding-designer-UI bouwen." Nee. Fundering =
datamodel + services + tests. Features komen daarna, in de 4-workflow-build.
**Bij elke nieuwe ingeving tijdens de sprint stelt Claude de drie vragen:**
1. Is dit een scope-uitbreiding? → backlog
2. Is dit elegantie-verbetering zonder concrete trigger? → niet doen
3. Is dit een feature verkleed als fundering? → naar na de consolidatie
---
## 3. Vastgelegde besluiten (niet ter discussie in deze sprint)
Deze beslissingen zijn genomen en worden *niet* opnieuw geopend. Voor volgende
chats: behandel deze als gegeven.
1. **Binding als eerste-klas burger.** Aparte `form_field_bindings` tabel met
typed kolommen: `field_id`, `target_entity`, `target_attribute`, `merge_strategy`,
`trust_level`. Eén veld kan meerdere bindings hebben (multi-binding scenarios).
Details in §6.1.
2. **FormBindingApplicator als hoofdpad.** Nieuwe service die na
`FormSubmissionSubmitted` bindings doorloopt en entities bijwerkt. Inclusief
automatische Person-creation bij public EVENT_REGISTRATION submissions
zonder subject. Details in §6.2.
3. **Denormalized columns op `form_submissions`.** `event_id`, `organisation_id`
en `submitted_by_user_id` zijn al aanwezig of worden toegevoegd als directe
foreign keys, niet alleen via schema-joins. Details in §6.3.
4. **Purpose registry in config, geen `custom` escape.** Afgeslankte set van
7 purposes voor v1.0: EVENT_REGISTRATION, ARTIST_ADVANCE, SUPPLIER_INTAKE,
POST_EVENT_EVALUATION, INCIDENT_REPORT, SIGNATURE_CONTRACT, USER_PROFILE.
Registry leeft in `config/form_builder/purposes.php` + typed value-objects.
Nieuwe purposes vereisen een migratie + listener — dat is correct, niet
lastig. Details in §6.4.
5. **ULID consistent overal, ook pivots.** Bestaande integer-PK-pivots worden
gemigreerd. Opsporings-pas in week 1 van de sprint. Details in §6.5.
6. **Eigen tabellen voor `validation_rules`, `conditional_logic`,
`form_fields.options`.** Typed, queryable. `form_schemas.settings` krimpt
naar echt opaque config (rendering hints). Details in §6.6.
7. **Eén SPA op één domein.** `apps/portal/` wordt een route-tree binnen
`apps/app/`. Alles op `crewli.app`. Route-guards bepalen welke layout,
welke context, welke permissies. Multi-role users krijgen een context-
switcher in de nav-balk. Details in §4.
8. **Observability foundation.** Sentry voor error tracking, structured
logging via Laravel's context, performance monitoring via Laravel Telescope
(dev) + Sentry Performance (prod). Details in §6.7.
9. **Enterprise scope betekent 4 kern-workflows voor v1.0.** Niet 23. De
overige purposes in de registry zijn vocabulaire zonder implementatie;
zij krijgen implementatie wanneer een concrete klant-case dit rechtvaardigt.
---
## 4. Het één-SPA besluit: uitvoerbaar overzicht
Gekozen architectuur: **één codebase, één build, één domein.**
### 4.1 Hostnames
- `crewli.app` — alle gebruikers, alle rollen, één SPA
- `api.crewli.app` — Laravel backend (ongewijzigd)
- `docs.crewli.app` — VitePress docs (ongewijzigd)
Rationale: vrijwilligers en organizers zijn verschillende rollen binnen
hetzelfde product, niet verschillende producten op hetzelfde platform. Dit volgt
het patroon van Slack/Linear/Notion/GitHub, waar alle gebruikers onder één
domein werken en de UI zich aanpast aan rol + context. Subdomeinen per rol (zoals
Shopify/Stripe) worden pas zinvol wanneer er fundamenteel verschillende producten
zijn — niet ons geval.
### 4.2 Directory-structuur na consolidatie
```
apps/
app/ ← enige Vue SPA
src/
pages/
(auth)/ ← login, forgot-password, MFA
portal/ ← wat nu in apps/portal/src/pages/ zit
my-shifts/
availability/
profile/
...
register/ ← publieke formulier-flow (niet ingelogd)
[public_token].vue
events/ ← organizer: events + sub-pages
persons/ ← organizer: persons overview
organisations/ ← organizer: settings
platform/ ← super-admin (role:super_admin)
index.vue ← post-login landing of context-switcher
components/
organizer/ ← organizer-only componenten
portal/ ← portal-only componenten
shared/ ← gedeeld tussen beide
composables/
forms/ ← voormalige packages/form-schema/ komt hier
...
layouts/
OrganizerLayout.vue ← voor organizer routes, met organizer-nav
PortalLayout.vue ← voor portal routes, met portal-nav
PublicLayout.vue ← voor /register/* routes, minimaal
router/
index.ts ← route-guards bepalen layout + toegang
routes.ts ← één route-tree, layout per section
plugins/
theme.ts ← context-based Vuetify theme injectie
portal/ ← verdwijnt na sprint
api/ ← ongewijzigd
packages/
form-schema/ ← verdwijnt, inhoud verhuist naar apps/app/src/composables/forms/
```
### 4.3 Routing en context-switching
Er is geen hostname-detectie. Routes bepalen alles:
```ts
// Conceptueel — definitieve implementatie in sprint
const routes = [
// Publiek (geen auth)
{ path: '/login', component: LoginPage, meta: { layout: 'PublicLayout' } },
{ path: '/register/:publicToken', component: PublicFormPage, meta: { layout: 'PublicLayout' } },
{ path: '/forgot-password', component: ForgotPasswordPage, meta: { layout: 'PublicLayout' } },
// Portal routes (auth required, any role)
{
path: '/portal',
meta: { layout: 'PortalLayout', requiresAuth: true, context: 'portal' },
children: [
{ path: 'my-shifts', component: MyShiftsPage },
{ path: 'availability', component: AvailabilityPage },
{ path: 'profile', component: ProfilePage },
],
},
// Organizer routes (auth + organizer role)
{
path: '/',
meta: { layout: 'OrganizerLayout', requiresAuth: true, context: 'organizer',
requiresRole: 'organizer' },
children: [
{ path: '', component: OrganizerDashboard },
{ path: 'events', component: EventsList },
// ... etc
],
},
// Platform admin (auth + super_admin role)
{
path: '/platform',
meta: { layout: 'OrganizerLayout', requiresAuth: true, requiresRole: 'super_admin' },
children: [...]
},
]
```
**Post-login landing voor multi-role gebruikers:**
- Enkel portal-rol → redirect naar `/portal/my-shifts`
- Enkel organizer-rol → redirect naar `/` (dashboard)
- Beide rollen → landing-page op `/` met twee kaarten (Portal / Organizer) óf
direct naar laatst-gebruikte context (lokaal opgeslagen als preference)
**Context-switcher in nav-balk** voor multi-role users:
- Permanent zichtbaar in OrganizerLayout en PortalLayout
- Eén klik switch naar andere context
- Vergelijkbaar met Slack's workspace-switcher
### 4.4 Wat blijft werken ongewijzigd
- Sanctum session auth (één cookie, één domein) — simpeler dan eerst gedacht
- Portal-token-auth voor publieke routes (`/register/{public_token}`,
`/public/forms/...` API-endpoints) — blijft als aparte middleware
- Impersonation — blijft binnen organizer-tree
- API-calls naar `api.crewli.app` — ongewijzigd
**Wat wegvalt door één-domein-keuze:** cross-domain cookie isolation is niet meer
nodig. De `CookieBearerToken`-middleware per app (uit AUTH_ARCHITECTURE.md) mag
vereenvoudigd worden in de sprint — dit wordt meegenomen onder WS-3.
### 4.5 Wat dit kost
Reële inschatting: 2-4 dagen werk in de consolidatie-sprint.
- 1 dag: directory-structuur omzetten, imports repareren
- 1 dag: router + layouts + route-guards + context-switcher
- 1 dag: theming per context, Vuetify-config, auth-cleanup
- 1 dag: testen, deploy-config verifiëren
Iets simpeler dan bij het hostname-alternatief omdat we geen hostname-detector
hoeven te bouwen en omdat cross-domain cookie complexiteit vervalt.
### 4.6 Toekomst-deur openhouden
Als later één van deze drie scenario's optreedt, kunnen we alsnog splitsen
zonder dat we vandaag te vroeg gesplitst hadden:
- Portal wil SSR voor SEO → portal splitsen naar Nuxt op aparte subdomain
- Een third product (marketplace, kassa-app) verschijnt → apart project op
eigen subdomain
- Whitelabel per organisatie met eigen subdomain → routing-laag erboven
Geen van deze scenario's is vandaag aan de orde. Splitsen vanuit één SPA is
goed te doen wanneer nodig (één tot twee weken werk).
---
## 5. Prioriteitsvolgorde werkstromen
De consolidatie-sprint kent **acht werkstromen** (afgekort WS). Ze worden in
deze volgorde uitgevoerd, met waar mogelijk parallelle PRs binnen een werkstroom.
**WS-1 — Opsporings-pas (1 dag)**
Volledige scan van de codebase op: niet-ULID primary keys, JSON-kolommen met
queryable data die ik nog niet geïdentificeerd heb, polymorfe relaties die
inconsistent zijn. Output: een definitieve lijst van te migreren items.
Gaat voor alle andere werkstromen, omdat latere WS-en hun scope ervan
afhangen.
**WS-2 — Purpose registry + legacy purpose-cleanup (2-3 dagen)**
Van 23 naar 7 purposes. Registry in config. Typed value-objects. Migratie
die bestaande `form_schemas` opschoont (alle schemas met "weg te halen"
purposes → archief of delete, afhankelijk van of er seed-data hangt).
**WS-3 — Eén SPA consolidatie (3-5 dagen)**
Zoals in §4. Kan deels parallel aan WS-4 en WS-5 want raakt vooral
frontend. Apart maken van frontend-consolidatie en backend-consolidatie
vermijdt rebase-hel.
**WS-4 — ULID-consistentie + denormalized submission columns (2-3 dagen)**
Pivot-tabellen naar ULID. `event_id`, `organisation_id` op submissions
(en elders waar de opsporings-pas ze vindt).
**WS-5 — JSON-kolom-opsplitsing (4-6 dagen)**
Vier sub-werkstromen die in serie moeten (elk heeft data-migratie):
- WS-5a — `form_field_bindings` tabel
- WS-5b — `form_field_validation_rules` tabel
- WS-5c — `form_field_conditional_logic` tabel
- WS-5d — `form_field_options` tabel
**WS-6 — Binding-pipeline / FormBindingApplicator (4-5 dagen)**
Nieuwe service. Listener. Person-creation-flow. Guardrails in
FormSchemaService. Volledige testdekking voor alle 7 purposes (ook als
ze nog geen functionaliteit hebben — tests verifiëren dat de binding-
infrastructuur ze ondersteunt).
**WS-7 — Observability foundation (2-3 dagen)**
Sentry SDK (backend + frontend). Structured logging conventies. Telescope
voor dev. Performance monitoring. Runbook voor incident response.
**WS-8 — Documentatie-consolidatie (2-3 dagen)**
`/CLAUDE.md` bijwerken met nieuwe conventies. `/dev-docs/SCHEMA.md` volledige
rewrite naar v2.0 met nieuwe tabellen. `/dev-docs/ARCH-FORM-BUILDER.md`
bijwerken. Nieuwe `/dev-docs/ARCH-BINDINGS.md`. Nieuwe
`/dev-docs/ARCH-OBSERVABILITY.md`. `/dev-docs/BACKLOG.md` opschonen:
verwijderen wat door de sprint is opgelost, expliciet markeren wat na
de sprint nog wordt opgepakt.
**Totale inschatting sprint:** 22-32 dagen werk.
**Doorlooptijd bij actieve inzet:** 4-6 weken.
**Aantal PRs:** 25-35, veel zijn klein en focused.
---
## 6. Per werkstroom: scope, uitkomst, klaar-criteria
### 6.1 WS-5a — form_field_bindings
**Scope:** Nieuwe tabel `form_field_bindings`. Vrije `binding` JSON-kolom op
`form_fields` wordt vervangen door een queryable relatie. Eén veld kan meerdere
bindings hebben.
**Tabel-ontwerp (concept, finetunen in sprint):**
```
form_field_bindings:
id (ulid, primary)
form_field_id (ulid, foreign, cascade)
target_entity (string, 50) -- 'person', 'artist', 'company', 'user'
target_attribute (string, 100) -- 'email', 'first_name', 'stage_name', etc.
merge_strategy (enum) -- 'overwrite', 'append', 'replace', 'first_write_wins'
trust_level (int, default 50) -- 0-100, hoger = dominanter bij conflict
is_identity_key (bool, default false)-- wordt gebruikt voor person-matching?
created_at, updated_at
unique (form_field_id, target_entity, target_attribute)
```
**Uitkomst:**
- Bestaande `binding` JSON-data gemigreerd naar rijen in deze tabel
- Builder-UI (later) kan een dropdown tonen gevuld uit binding-registry
- Backend kan queryen "welke velden binden aan `person.email`"
- Conflict-strategie per binding expliciet
**Klaar-criteria:**
- Migratie loopt vooruit én terug zonder data-verlies
- Feature tests: schema create → binding toevoegen → submission submit → entity updated
- `form_fields.binding` JSON-kolom verdwenen
- ARCH-BINDINGS.md geschreven met complete uitleg
### 6.2 WS-6 — FormBindingApplicator
**Scope:** Nieuwe service `App\Services\FormBuilder\FormBindingApplicator`. Nieuwe
listener `App\Listeners\FormBuilder\ApplyBindingsOnFormSubmit`. Person-creation-
helper. Update van `FormSchemaService` met pre-publish-validation.
**Uitkomst:**
- Bij elke `FormSubmissionSubmitted` worden alle bindings doorlopen
- Voor elke binding: lookup value in submission, haal merge-strategie op,
update target entity
- Bij public EVENT_REGISTRATION zonder subject: creëer Person, zet als
subject op submission, voer dan bindings uit
- Identity-match draait na binding-applicatie op nieuwe Person
- Alle actions gelogged in activity log met bron-submission
**Klaar-criteria:**
- Test: public registratie → Person aangemaakt met alle binding-attributen
- Test: authenticated update-submission → bestaande Person bijgewerkt
- Test: conflict (twee submissions, zelfde email-binding, verschillende waarde)
→ last-write-wins met activity log entry
- Test: tag-picker additief (niet overschrijvend)
- Test: schema met AVAILABILITY_PICKER zonder event-koppeling kan niet publiceren
- Test: EVENT_REGISTRATION schema zonder email-binding kan niet publiceren
### 6.3 WS-4 — Denormalized submission columns
**Scope:** Kolommen `event_id` (nullable), `organisation_id` (niet-nullable),
`submitted_by_user_id` (bestaat al) op `form_submissions`. Observer vult ze bij
create. Backfill-script voor bestaande submissies (indien aanwezig in seed-data).
**Uitkomst:**
- Queries zoals "alle submissions voor event X" worden directe queries, geen joins
- Analytics / reporting kan efficiënt draaien
- Multi-event reuse scenario's (ARTIST_ADVANCE) werken: één schema, submissies
naar meerdere events
**Klaar-criteria:**
- Feature tests: submission krijgt correcte event_id en organisation_id
- Scope-middleware gebruikt event_id direct (waar relevant)
- SCHEMA.md bijgewerkt
### 6.4 WS-2 — Purpose registry
**Scope:** `app/FormBuilder/Purposes/PurposeDefinition.php` (value object).
`config/form_builder/purposes.php` (registry). `PurposeRegistry` service.
`form_schemas.purpose` blijft een string-kolom, maar `custom_purpose_slug`
kolom verdwijnt.
**Purposes v1.0:**
1. `event_registration` — vrijwilligers/crew aanmelden, subject=Person
2. `artist_advance` — artiesten advance invullen, subject=Artist
3. `supplier_intake` — leveranciers onboarding, subject=Company
4. `post_event_evaluation` — feedback na afloop, subject=Person
5. `incident_report` — incidenten melden, subject=Person
6. `signature_contract` — contracten ondertekenen, subject=User
7. `user_profile` — profiel-updates, subject=User
**Uitkomst:**
- Purposes zijn data-driven: je kunt in config een nieuwe toevoegen zonder
enum-migratie, maar je moet wel bewust zijn dat een nieuwe purpose meestal
een listener vereist
- Elke PurposeDefinition heeft: label, subject_type, default_submission_mode,
allows_public_access, required_bindings (lijst van bindings die het schema
moet hebben om publiceerbaar te zijn)
**Klaar-criteria:**
- Tests voor elke purpose: kan schema gecreëerd worden, kan gepubliceerd worden
(met juiste bindings), submit-flow werkt end-to-end
- ARCH-FORM-BUILDER.md bijgewerkt
### 6.5 WS-4 — ULID-consistentie
**Scope:** Alle pivot-tabellen met integer PK → ULID. Opsporings-pas (WS-1)
definieert de exacte lijst. Data-migratie per tabel.
**Uitkomst:** Consistent ID-gebruik. Geen ID-enumeration-risico meer.
Eenvormig test-patroon.
**Klaar-criteria:**
- Alle primary keys in business-schema zijn ULID
- Alle foreign keys consistent
- Tests lopen door
### 6.6 WS-5b/c/d — Remaining JSON-splits
Dezelfde aanpak als WS-5a maar voor:
- `form_field_validation_rules` (targeting `validation_rules` JSON)
- `form_field_conditional_logic` (targeting `conditional_logic` JSON, met
AND/OR tree structuur)
- `form_field_options` (targeting `options` JSON, met position + label +
metadata per optie)
**Conditional logic verdient speciale aandacht.** AND/OR trees in een relationele
tabel zijn niet triviaal. Overweging in de sprint: platte lijst met
`logical_group_id` vs recursieve tabel met `parent_id`. Beslissing tijdens
WS-5c, niet vooruit.
### 6.7 WS-7 — Observability
**Scope:**
- Sentry SDK installeren in `apps/api/` (Laravel) en `apps/app/` (Vue)
- Structured logging convention: alle logs via `Log::channel('structured')`
met context (organisation_id, user_id, request_id)
- Laravel Telescope in dev only
- Sentry Performance voor transaction tracking in prod
- Runbook in `dev-docs/RUNBOOK.md` met: hoe te reageren op alerts, waar
logs te vinden, hoe een error te tracen
**Klaar-criteria:**
- Test: geforceerde exception in een controller verschijnt in Sentry (staging)
- Test: elke request heeft een unique request_id in logs
- Telescope werkt in dev, niet in prod
### 6.8 WS-3 — Eén SPA consolidatie
Zie §4 voor scope en stappen.
**Voortgang:**
- **Sessie 1a (2026-04-29)** — _foundation gap closure, klaar._
Lefthook geïnstalleerd op repo-root; `.githooks/post-commit` en
`pre-push` 1:1 gemigreerd naar `lefthook.yml` (scripts blijven
source of truth, lefthook dispatcht). Drie layout-skeletons
toegevoegd in `apps/app/src/layouts/`: `OrganizerLayout.vue`,
`PortalLayout.vue`, `PublicLayout.vue` — nog niet aan router
gekoppeld. Vitest-smoketests dekken alle drie (8 nieuwe tests,
41 → 49 in apps/app). ESLint-baseline en router-wiring volgen
in 1b/1c.
- **Sessie 1b-i (2026-04-29)** — _lint baseline reductie + audit,
klaar._ Risk-tiered `eslint --fix` pass: Tier 1 (Vue templates) +
Tier 2 (TypeScript plumbing) + trailing-whitespace cleanup. De
baseline ging van **1451 → 231** problemen. Tier 3 (vite.config.ts,
themeConfig.ts, vitest.config.ts) is bewust uitgesteld naar 1b-ii
vanwege runtime-risico van quote/semi/sort-imports autofixes op
config bestanden. Audit rapport met 6-bucket categorisatie van de
resterende 231 items in `dev-docs/WS-3-LINT-BASELINE-AUDIT-2026-04-29.md`.
Vijf open vragen voor Bert + Claude Chat staan klaar voor 1b-ii.
**Klaar-criteria:**
- `apps/portal/` is verwijderd
- Beide hostnames serveren correcte route-tree
- Auth werkt op beide hostnames met isolatie
- Alle bestaande tests in apps/app/ draaien groen
- Nieuwe tests voor hostname-detectie
- Deploy-config (DirectAdmin) verifieert beide hostnames
---
## 7. Starten van de volgende chat
Deze chat wordt afgesloten. De volgende chat start met een dichte briefing die
**exact dit document** en de volgende meta-instructie als context geeft.
**Opening-prompt voor de volgende chat (kopieer letterlijk):**
```
Nieuwe chat in project Crewli. We beginnen de architectuur-consolidatie-sprint
zoals vastgelegd in /dev-docs/ARCH-CONSOLIDATION-2026-04.md (lees dat document
eerst uit Gitea, main branch).
Context:
- Pre-launch. Geen gebruikers, geen tijdsdruk. Doel: enterprise-grade fundament.
- 8 werkstromen (WS-1 t/m WS-8). We starten met WS-1: opsporings-pas.
- Ik wil dat je ook /CLAUDE.md en /dev-docs/SCHEMA.md leest voor basiscontext,
en kort aangeeft wat je hebt begrepen.
Verzoek voor deze eerste chat:
1. Bevestig dat je het consolidatie-document en CLAUDE.md hebt gelezen.
2. Lever de prompt voor WS-1 (opsporings-pas). Deze moet een Claude Code opdracht
zijn die het hele codebase doorloopt op:
- Primary keys die geen ULID zijn
- JSON-kolommen met queryable data die niet in §6 zijn benoemd
- Polymorfe relaties die inconsistent gemodelleerd zijn
- Overige architectuur-smells die we in de consolidatie moeten meenemen
3. Output van WS-1 is een rapport (Markdown), geen code. Op basis daarvan
beslissen we of de scope van andere werkstromen aanpassing behoeft.
Regels die tijdens de sprint gelden:
- Elke nieuwe ingeving: toets aan de drie valstrikken in §2 van het
consolidatie-document. Bij twijfel: backlog.
- Elke architectuur-keuze tijdens executie: toets aan de principes in §1.
- Geen scope-uitbreiding zonder expliciete go van mij.
Bevestig dat je klaar bent voor WS-1.
```
---
## 8. Verwijzingen naar bestaand materiaal
Voor de volgende chat is het nuttig dat deze documenten als achtergrond
beschikbaar zijn (niet allemaal lezen, maar weten dat ze bestaan):
- `/CLAUDE.md` — projectafspraken, zero-compromise regels
- `/dev-docs/SCHEMA.md` v2.1 — huidig database-schema
- `/dev-docs/ARCH-FORM-BUILDER.md` — bestaande form-builder architectuur
- `/dev-docs/AUTH_ARCHITECTURE.md` — dual-mode auth, cross-app isolation
- `/dev-docs/VUEXY_COMPONENTS.md` — frontend component-bibliotheek
- `/dev-docs/BACKLOG.md` — bestaande backlog, wordt opgeschoond in WS-8
---
## 9. Openstaand (niet in scope van consolidatie-sprint)
Om expliciet te zijn over wat *niet* in deze sprint gebeurt:
- Implementatie van de 4 kern-workflows (komt erna als aparte sprint S3c...S3f)
- Nieuwe UI-modules (komt erna)
- Marketplace / subscription / billing (toekomst)
- Mobile app (toekomst)
- Locale-uitbreiding voorbij NL/EN (komt wanneer marktbehoefte concreet is)
- Performance-optimalisatie voorbij observability foundation (wanneer metrics
dit rechtvaardigen)
---
## 10. Sign-off ruimte
Voor Bert om te bevestigen voordat we de volgende chat starten:
- [ ] Leidende principes §1 akkoord
- [ ] Vastgelegde besluiten §3 akkoord
- [ ] SPA-consolidatie §4 akkoord
- [ ] Werkstroom-volgorde §5 akkoord
- [ ] Purposes v1.0 lijst §6.4 akkoord
- [ ] Openings-prompt §7 klaar om te gebruiken
Na jouw sign-off commit ik dit document naar de repo en sluiten we deze chat.