Skip to content

Architecture: where does business logic belong — CMS extensions vs. the Nuxt website server? #215

Description

@Jan0707

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:

  1. 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)?
  2. 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?)
  3. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    backendAll issues related to our CMS / api backendquestionFurther information is requestedtechwebsiteIssues in relation to the website/nuxt app

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions