Adapted from the repo's docs/architecture.md — Part of Internals: the platform's design set, published as content on the platform it describes.
Tier model
The repository is organized around one hard boundary — contract/ — and a strict
dependency direction everywhere else.
┌───────────────────┐
│ contract/ │ OpenAPI 3.1 · AsyncAPI 3.1 · Spectral
│ (team boundary) │ owned by contract-owner
└───────▲───▲───────┘
conforms-to │ │ consumes (generated types/clients)
┌───────────────────┘ └───────────────────┐
┌───────┴────────┐ ┌────────┴───────┐
│ backend/ │ │ frontend/ │
│ Laravel API │ no direct edge │ Next.js │
│ │ ◄──────── ✕ ──────────► │ │
└───────┬────────┘ └────────┬───────┘
│ │
└───────────────┐ ┌───────────────┘
┌──────┴───────────┴──────┐
│ infra/ │ DDEV topology: PHP, PostgreSQL,
│ (supports everything, │ Redis, Node — owned by
│ depends on nothing) │ platform-engineer
└─────────────────────────┘
Rules that follow from the diagram:
frontend/andbackend/are coupled only throughcontract/. Neither reads the other's source, types, or database.contract/references nothing — it is pure interface description.infra/supports all tiers and imports from none.
Backend layering (inside backend/)
app/Http transport: controllers, form requests, API resources, middleware
│ calls (owner: backend-api)
▼
app/Domain business logic: actions, services, events, policies, value objects,
│ defines ports and the PORT INTERFACES persistence must implement
▼ (owner: domain-engineer)
app/Models, persistence: Eloquent models, port implementations, migrations,
app/Infrastructure, seeders, Redis cache/queue strategy
database/ (owner: data-engineer)
Allowed direction: Http → Domain → Data ports. Forbidden edges:
DomainorDataimporting anything fromapp/Http.- Controllers touching Eloquent/query builder directly (must go through Domain).
Domaindepending on concrete persistence classes (only on its own port interfaces; bindings live in a service provider owned by data-engineer).
Data flow (request lifecycle)
- HTTP request enters via a route published in
contract/openapi.yaml. app/Httpvalidates (form request) and invokes a Domain action.- The Domain action enforces invariants, uses ports for persistence, and may raise domain
events (published shapes live in
contract/asyncapi.yaml). - Data layer executes against PostgreSQL; Redis serves cache/queues/sessions.
app/Httpmaps the result to an API resource matching the contract, including RFC 9457 problem details for errors.
Environments
Local development runs entirely under DDEV (see infra/README.md): PHP 8.5 web container
with Laravel, PostgreSQL 16 database service, Redis add-on, and a Node 22 frontend
service running the Next.js dev server. The topology is host-split (ADR-0010): the
reader app on publishing-platform.ddev.site, the API on
api.publishing-platform.ddev.site under /v1 (production naming reserved:
bflo.sh / api.bflo.sh). CI mirrors the same toolchain. No deployment target is
provisioned yet; adding one is a future ADR.
How this generic tier model is filled by the actual product — filesystem content source,
index/reindex loop, revalidation events — is the subject of
master-blueprint.md (design of record) and
how-it-works.md (technical walkthrough).
Stack pinning (this instantiation)
| Component | Version / choice | Where pinned |
|---|---|---|
| PHP | 8.5 | .ddev/config.yaml (source: infra/ddev/) |
| Laravel | 13.x (13.18.1) | backend/composer.json |
| PostgreSQL | 16 | .ddev/config.yaml (source: infra/ddev/) |
| Redis | DDEV add-on ddev/ddev-redis |
.ddev/ add-on config |
| Node (frontend service + contract tooling) | 22 | .ddev/config.yaml + frontend service (source: infra/ddev/) |
| Next.js | 16.x (16.2.10) | frontend/package.json |
| Spectral | CLI v6 + ruleset in repo | infra/ddev/commands/web/contract-lint, contract/.spectral.yaml |
This table and the pins are the only places versions live — everything else refers to components by role, not version.