Summary
We need a deliberate, one-time decision on where business logic lives: in the Directus CMS (as coded, versioned, tested extensions) or in the Nuxt website's server layer (server/api/**). Right now it's drifting into both, apparently without a decision ever having been made — and we should settle it once, then refactor to match.
Where this came from
While building the newsletter double-opt-in flow ([#PR], branch newsletter-signup), the confirmation step raised the question: should the website flip a subscriber's status (pending → confirmed) via the authenticated Directus client, or should it hand a token to the CMS and let the CMS own that transition?
That turned out to be an instance of a broader, unspoken pattern rather than a one-off.
The observation
The Nuxt app was, as far as we can tell, originally intended as the presentation layer. But privileged business logic has already accumulated in its server layer. Concrete examples today:
nuxt-app/server/utils/authenticatedDirectus.ts holds the Directus admin token and exposes privileged mutations: createTicketOrder, updateTicketOrder, updateSpeaker, updateTicket, createNewsletterSubscriber, and now confirmNewsletterSubscriber.
- Token-validate-then-mutate flows live in the website server:
server/api/ticket-portal/*, server/api/speaker-portal/*, and (proposed) server/api/newsletter/confirm.get.ts.
- Domain rules (e.g. VAT, order state transitions, the DOI confirm state-machine) are partly encoded website-side.
Meanwhile the CMS bundle (directus-cms/extensions/directus-extension-programmierbar-bundle) is clearly the intended home for business logic: coded hooks/endpoints, version-controlled, and jest-tested (196 tests today).
So the same class of logic currently lives in two places depending on which flow you look at.
The core question
Do we accept business logic in the website, or do we treat the website strictly as presentation and keep all domain logic in the CMS (as coded extensions)?
Sub-questions that fall out of it:
- Should the website hold the Directus admin token at all, or only ever talk to purpose-built CMS endpoints authenticated with a narrowly-scoped credential (or a per-operation token, e.g. a confirm token as proof)?
- If logic belongs in the CMS, what's the boundary the website is still allowed to own? (Input validation? Spam/honeypot? Rate limiting? Purely presentational data shaping?)
- What's the authentication model between website → CMS for these privileged operations?
Options (high level)
- A — Status quo, made explicit. The website's Nitro server is an accepted backend-for-frontend and may hold privileged logic. Document it as a deliberate choice and standardise the pattern (it's coded/versioned/tested either way).
- B — CMS owns all domain logic. The website becomes presentation + thin proxy. Privileged operations move to coded Directus endpoint extensions; the website passes proof (tokens) and shapes responses. Requires refactoring the ticket/speaker/newsletter flows and rethinking the admin-token's presence in the website.
- C — Hybrid with an explicit rule. Define exactly which concerns are allowed website-side (e.g. validation, anti-spam, presentation) vs. CMS-side (all state transitions / domain rules), and refactor to the line.
Note: we've already agreed we do not want no-code Directus Flows for logic — any CMS-side logic should be coded extensions (versioned, ideally tested). This debate is CMS-extension vs website-server, not code vs no-code.
What we should NOT do
- Keep making this call per-feature by whoever is writing it. That's how we got a half-and-half boundary.
Ask
Have the debate, decide once, write the decision down (e.g. an ADR or a section in CLAUDE.md / the .claude/rules/*), and then refactor as necessary to match. The newsletter flow is currently built the pragmatic way (logic in the Nuxt server route, consistent with the existing ticket/speaker flows) — it should be refactored along with the others once we've decided, not treated as a special case.
Current pragmatic decision (for context, not a prejudgement)
For the newsletter PR we chose cohesion over architectural purity: the confirm flip stays in the Nuxt server route to match ticket-portal / speaker-portal. This issue is about whether that whole class of decisions should stand.
Summary
We need a deliberate, one-time decision on where business logic lives: in the Directus CMS (as coded, versioned, tested extensions) or in the Nuxt website's server layer (
server/api/**). Right now it's drifting into both, apparently without a decision ever having been made — and we should settle it once, then refactor to match.Where this came from
While building the newsletter double-opt-in flow ([#PR], branch
newsletter-signup), the confirmation step raised the question: should the website flip a subscriber's status (pending → confirmed) via the authenticated Directus client, or should it hand a token to the CMS and let the CMS own that transition?That turned out to be an instance of a broader, unspoken pattern rather than a one-off.
The observation
The Nuxt app was, as far as we can tell, originally intended as the presentation layer. But privileged business logic has already accumulated in its server layer. Concrete examples today:
nuxt-app/server/utils/authenticatedDirectus.tsholds the Directus admin token and exposes privileged mutations:createTicketOrder,updateTicketOrder,updateSpeaker,updateTicket,createNewsletterSubscriber, and nowconfirmNewsletterSubscriber.server/api/ticket-portal/*,server/api/speaker-portal/*, and (proposed)server/api/newsletter/confirm.get.ts.Meanwhile the CMS bundle (
directus-cms/extensions/directus-extension-programmierbar-bundle) is clearly the intended home for business logic: coded hooks/endpoints, version-controlled, and jest-tested (196 tests today).So the same class of logic currently lives in two places depending on which flow you look at.
The core question
Do we accept business logic in the website, or do we treat the website strictly as presentation and keep all domain logic in the CMS (as coded extensions)?
Sub-questions that fall out of it:
Options (high level)
Note: we've already agreed we do not want no-code Directus Flows for logic — any CMS-side logic should be coded extensions (versioned, ideally tested). This debate is CMS-extension vs website-server, not code vs no-code.
What we should NOT do
Ask
Have the debate, decide once, write the decision down (e.g. an ADR or a section in
CLAUDE.md/ the.claude/rules/*), and then refactor as necessary to match. The newsletter flow is currently built the pragmatic way (logic in the Nuxt server route, consistent with the existing ticket/speaker flows) — it should be refactored along with the others once we've decided, not treated as a special case.Current pragmatic decision (for context, not a prejudgement)
For the newsletter PR we chose cohesion over architectural purity: the confirm flip stays in the Nuxt server route to match
ticket-portal/speaker-portal. This issue is about whether that whole class of decisions should stand.