Skip to content
bflo.sh

Architecture — one boundary, one direction

The tier model: a single hard boundary at the contract, and a strict dependency direction everywhere else.

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/ and backend/ are coupled only through contract/. 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:

  • Domain or Data importing anything from app/Http.
  • Controllers touching Eloquent/query builder directly (must go through Domain).
  • Domain depending on concrete persistence classes (only on its own port interfaces; bindings live in a service provider owned by data-engineer).

Data flow (request lifecycle)

  1. HTTP request enters via a route published in contract/openapi.yaml.
  2. app/Http validates (form request) and invokes a Domain action.
  3. The Domain action enforces invariants, uses ports for persistence, and may raise domain events (published shapes live in contract/asyncapi.yaml).
  4. Data layer executes against PostgreSQL; Redis serves cache/queues/sessions.
  5. app/Http maps 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.