Architecture¶
Traureise is a single Next.js application (App Router, TypeScript) with PostgreSQL. Its code is split into layers whose dependencies only point inward, so business rules can be tested without a browser, database or framework.
flowchart LR
UI["app/ + components/<br/>routes, server actions, UI"] -->|calls| APP["application/<br/>use cases"]
APP -->|uses| DOM["domain/<br/>rules and schemas"]
APP -->|depends on| PORTS(["ports<br/>(interfaces)"])
INFRA["infrastructure/<br/>Drizzle, SMTP, S3, sharp, themes"] -->|implements| PORTS
ROOT["composition/<br/>container"] -->|wires| INFRA
UI -->|gets use cases from| ROOT
Arrows point from the code that knows to the code it knows about: the domain knows nothing else, and only the composition root knows which adapter implements which port.
| Layer | Folder | Contains |
|---|---|---|
| Domain | src/domain |
Pure rules and zod schemas: slugs, sections and item fields, ordering, calendar export, themes, fonts, uploads |
| Application | src/application |
Use cases such as RequestMagicLink, CreateWebsite, AddItem, MoveItem, DeleteWebsite, and the ports they depend on |
| Infrastructure | src/infrastructure |
Adapters: Drizzle repositories, SMTP mailer, local and S3 storage, sharp image processing, theme loader |
| Composition | src/composition |
The container that wires adapters to ports |
| Web | src/app, src/components |
Routes, server actions and UI. They only call use cases. |
ESLint enforces the direction: src/domain and src/application may not import infrastructure,
framework or UI code.
Login flow¶
sequenceDiagram
autonumber
participant B as Browser
participant A as Traureise
participant DB as PostgreSQL
participant M as SMTP
B->>A: email address
A->>DB: rate-limit check, store hash of random token (15 min)
A->>M: email with the login link
B->>A: open /auth/verify?token=…
A->>DB: mark token used (only if unused), find or create user
A->>DB: store hash of new session token (30 days)
A-->>B: httpOnly session cookie, redirect to dashboard
Only hashes of tokens are stored. Using a token is a single conditional update, so a link works exactly once even when it is opened twice at the same moment.
The editor preview is the public site¶
The editor's preview and the public page render the same WeddingSite component. Saving a form runs a
server action, which calls a use case and revalidates the editor route, so the preview updates with
exactly what guests will see. Hidden sections, and visible sections without content, are not
rendered.
Data model¶
Every table holding user data cascades from users.id: deleting a user deletes their sessions,
websites, sections, items and upload records. Section items are one table with a jsonb column whose
shape is validated by the zod schemas in src/domain/sections.ts.
The reasoning behind these choices is recorded in the design decisions.