This commit contains the latest updates from the generic Website template, which move the toolchain to Swift 6.4, gate analytics and structured data behind traits, and rework page routing: - Updated the Swift toolchain for all the package dependencies and the Website service to version 6.4. (345b305) - Adapted all the existing middlewares in the Infrastructure package to Swift 6.4. (26008df) - Moved test resource teardown in the Persistence and the Website library into defer blocks. (27984a9) - Gated analytics behind a trait and reworked structured data in the Infrastructure package. (c7d4769) - Enabled the Analytics trait in the Website service. (f6a6bbd) - Reworked page routing in the Website service. (d1cb2c1) - Gated the structured data features in the Infrastructure package behind a trait. (f0a8791) - Enabled the StructuredData trait of the Infrastructure package in the Website service. (bbfdeb3) - Fixed the asset pin and a build warning in the Dockerfile of the Website service. (6ed0f9b) The root README change in 345b305 was left out, as this project no longer carries that file. From d1cb2c1, the RootController rework was left out, as the prototype serves a session-aware landing page and routes its other pages through their own controllers; the view-event reporter was appended to the prototype's shared script instead. The prototype code was adapted to the stricter Swift 6.4 settings: `String(format:)` was swapped for safe formatting, the NoIndex middleware takes a `@concurrent` next handler, and the tests flagged by warnings-as-errors were tightened. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Persistence
The Fluent-based data layer the platform's services build on: runtime selection between a PostgreSQL backend and an ephemeral in-memory SQLite one, single-place migration registration, and a database readiness probe.
Overview
| Role | Types |
|---|---|
| Backend selection | Driver (postgres or inMemory), Configuration (the PostgreSQL connection parameters), TLS (the connection's TLS posture) |
| Service | Service, which builds the Fluent service configured for the chosen driver |
| Migrations | PrepareDB, the single registrar declaring every migration, in order |
| Readiness | Probe, which reports whether the default database answers a SELECT 1 within a deadline |
| Accounts | AccountRepository, which finds an account by email or creates it, returning a User snapshot, and keeps each account's Passkeys — one is only ever registered together with a new account; CreateUser seeds the prototype's demo accounts |
| Collecting | CollectingRepository, which keeps an account's Wants with the listings meeting each, its collection of owned issues, the Series catalogue, and the upcoming Events with the shops trading at each |
| Trade | TradeRepository, which reads and writes Conversation, Offer, and Sale snapshots and enforces who may do what, throwing a TradeError otherwise; Sale carries the five-step handshake's rules as pure functions |
| Catalogue | CatalogueRepository, which opens a shop from a ShopDraft, publishes a seller's ListingDraft under a slug of its own, and returns Shop and Listing snapshots narrowed and ordered by a ListingFilter; SeedCatalogue fills it with the prototype's demo data |
Design rules
- The package reads no configuration. The executable maps its
database.*keys onto aDriverand hands it over; connection values arrive as plain data. The Website service'sConfigReader+Propertieshas the mapping. - One default database.
Serviceregisters the selected backend as the default database, so repositories resolve it with a plainfluent.db()and stay agnostic of which driver is in use. - Migrations are declared once, and append-only.
PrepareDBregisters every migration in the order it must run; alter the schema by adding a migration, never by editing one that has already run. Registering does not apply them: the in-memory backend is migrated at launch, while a shared PostgreSQL database is migrated out of band (the executable's migrate-and-exit mode), so booting instances never race. - Models never cross a concurrency boundary. FluentKit models are mutable reference types, so they stay internal to the package and repositories return
Sendablevalue-type snapshots (e.g.Listing) instead. - Registration order is not creation order.
AddTradeColumns,AddListingDeclarations, andAddShopRegistrationare registered ahead ofSeedCatalogue, whose models write their columns; Fluent tracks migrations by name, so a database already past the seed still gains them. - Prototype shortcuts.
CatalogueRepositoryfilters in memory, the catalogue fields (series, issue, publisher) are folded into the listing, and the demo data ships as a migration. All three go once real data arrives. - Readiness never throws, and never hangs.
Proberuns a schema-independentSELECT 1, maps every failure tofalse, and races the query against a deadline (2 seconds by default) — a hanging database yields a prompt "not ready" instead of a stalled endpoint. - A single connection for the in-memory store. The SQLite backend is capped at one connection per event loop, so every query reaches the same in-memory database instead of each pooled connection getting a private one.
- Method structs.
Service,PrepareDB, andProbehold their lifetime-fixed configuration ininitand take only per-call inputs incallAsFunction.
Note: the driver itself enforces both TLS postures —
preferupgrades only when the server advertises TLS and continues in plaintext otherwise,requirerefuses a server that offers none. Tests pin both against a fake plaintext-only server.
Layout
Sources are split by visibility, then by kind, one type per file:
Sources/
├── Public/
│ ├── Enumerations/ Driver, TLS, ShopError, TradeError
│ ├── Methods/ Service, PrepareDB, Probe
│ ├── Models/ Shop, ShopDraft, Listing, ListingDraft, ListingFilter, User, Passkey, Want, Series, Event, Conversation, Offer, Sale
│ ├── Repositories/ CatalogueRepository, AccountRepository, CollectingRepository, TradeRepository
│ └── Types/ Configuration
└── Internal/
├── Migrations/ CreateShop, CreateListing, AddTradeColumns, AddListingDeclarations, AddShopRegistration, SeedCatalogue, CreateUser, CreateTrade, CreatePasskey, CreateCollecting, CreateCommunity
├── Models/ ShopRecord, ListingRecord, UserRecord, PasskeyRecord, WantRecord, SeriesRecord, EventRecord, EventShopRecord, OwnedIssueRecord, ConversationRecord, MessageRecord, OfferRecord, SaleRecord
└── Seeds/ Seed
Tests/
├── Cases/ the test suites, mirroring the Sources/ layout
└── Utils/ the NotSQL* fakes backing the probe's non-SQL-database case, the
plaintext-only and silent fake PostgreSQL servers, and the suite Tag constants
Testing
The suite runs against the in-memory backend by default, so swift test needs no database. The PostgreSQL integration test is skipped unless POSTGRES_TEST_HOST points at one (with optional POSTGRES_TEST_PORT, POSTGRES_TEST_NAME, POSTGRES_TEST_USERNAME, and POSTGRES_TEST_PASSWORD); it reverts its migrations afterwards, leaving a shared database as it was found:
swift test # in-memory only
POSTGRES_TEST_HOST=127.0.0.1 swift test # against the local PostgreSQL (make db-mount)
Outside the application's service group, a built Fluent service must be shut down explicitly — even on failure — or its connection pool asserts on deinit; the suites' do/catch around fluent.shutdown() is the shape to follow.
Every suite carries a tag for the kind of API it exercises — .enumeration or .method, declared in Tests/Utils/Extensions/Tag+Constants.swift — so test plans and summaries can slice a run by kind. A new suite adopts the tag matching its subject, or adds one when none fits.
Requirements
- Swift 6.4 toolchain (
swift-tools-version:6.4). - macOS 15, matching the sibling packages (the services deploy to Linux containers; the packages carry no UI platforms).
- Package dependencies:
hummingbird-fluent,fluent-postgres-driver,fluent-sqlite-driver,sql-kit,postgres-nio, andswift-nio-ssl; the test target additionally depends onswift-niofor the TLS fallback tests' fake server.