Files
ccn/Services/Website

Loud Website

The Loud public website service — a Hummingbird server that renders a static landing page and serves the site's static assets.

Overview

The service:

  • Serves the landing page at GET / (rendered once per supported language with Elementary and cached).
  • Negotiates each request's language from its Accept-Language header against the languages in the WebsiteLibrary String Catalog, falling back to the default (en); pages are served from the per-language cache with Content-Language and Vary: Accept-Language headers.
  • Answers a liveness check at GET /health with a static JSON payload, and a readiness check at GET /health/ready that reports whether the database is reachable (200 ready / 503 unavailable).
  • Serves static files (CSS, JS, icons, manifest, robots.txt) from Resources/Static via Hummingbird's FileMiddleware, tagged with media-type-specific Cache-Control; the production image ships minified copies (see Static assets).
  • Returns a custom HTML 404 page, localized like the landing page, for any request that matches neither a route nor a static file.
  • Compresses responses (gzip/deflate) above a configurable size when the client advertises support.
  • Stamps a hardened set of security headers on every response.
  • Persists data through Fluent, against either an ephemeral in-memory SQLite database (the default — no external infrastructure) or a MySQL/MariaDB server, selected by a single configuration key.

Requirements

  • Swift 6.3 toolchain (swift-tools-version:6.3).
  • Docker (optional) for the containerized run/deploy workflow.
  • The Hummingbird CLI (hb) — optional, only for make site-run (watch and rebuild on change).

Architecture

Two SwiftPM targets:

Target Kind Path Role
Website executable Sources/App Entry point: reads configuration, builds the persistence service, and either serves the website or runs the migrate-and-exit mode.
WebsiteLibrary library Sources/Library Controllers, middlewares, pages, cached responses, and configuration helpers.

The Website executable depends on three local packages:

  • Localization (Packages/Localization) — the Localize and Negotiate helpers and the LanguageList of catalog languages (used by WebsiteLibrary).
  • Infrastructure (Packages/Infrastructure) — the RouterController protocol the controllers conform to and the addController result-builder extension that registers their routes on the router declaratively.
  • Persistence (Packages/Persistence) — the Fluent-based data layer: the Driver selector, the Service factory that builds the Fluent service, the PrepareDB registrar that declares the migrations, and the Probe consulted by the readiness check; the models, migrations, and repositories stay internal to the package. It has no dependency on swift-configuration; the executable maps the database.* keys onto the driver.

The persistence backend runs as a Fluent service inside the application's ServiceLifecycle group, so it starts and stops alongside the HTTP server (which owns its connection-pool shutdown on graceful termination).

Requests pass through the middleware chain in this order (outermost first), then reach the routes:

LogRequestsMiddleware
  → SecurityHeadersMiddleware       (security headers on every response)
    → ResponseCompressionMiddleware  (gzip/deflate above the size threshold)
      → LocalizationMiddleware       (negotiates the request's language)
        → NotFoundMiddleware         (renders the localized 404 page on .notFound)
          → FileMiddleware           (serves Resources/Static)
RootController                       (GET / → landing page)
HealthController                     (GET /health → liveness, GET /health/ready → readiness)

Configuration

Configuration is read through swift-configuration from the following sources, highest precedence first:

  1. Command-line arguments (e.g. --http-host 0.0.0.0)
  2. Process environment variables
  3. A .env.local file in the working directory (optional)
  4. A .env file in the working directory (optional)
  5. Built-in defaults

The two files play different roles:

  • .env (git-ignored) holds your deployment values — it is the file the Makefile and Compose read for the ${VAR} placeholders, and typically selects the MySQL/MariaDB backend.
  • .env.local (tracked) holds the local development values — the in-memory database and debug logging, plus the image/deployment placeholders the Makefile falls back to when no .env exists. Because it sits above .env, a direct launch (swift run or a debugger) runs against the local values even when .env points at a deployment, the same way docker-compose.override.yml overrides the base Compose file. Compose itself never reads it, and the production image does not ship it — only the executable, its resources, and the static files are staged into the final stage.

Environment variable naming

A dotted config key maps to an environment variable by upper-casing, splitting camelCase, and replacing separators with _. For example http.serverNameHTTP_SERVER_NAME, security.strictTransportSecuritySECURITY_STRICT_TRANSPORT_SECURITY, cache.maxAge.textCACHE_MAX_AGE_TEXT.

To disable a header or override a value, leave the variable unset to fall back to the default. A variable that is set but blank is treated as an explicit empty value, not as "use the default".

Static file caching

Config key Environment variable Default Description
cache.maxAge.text CACHE_MAX_AGE_TEXT 3600 (1 hour) max-age for text assets (CSS, JS, plain text); also marked must-revalidate.
cache.maxAge.image CACHE_MAX_AGE_IMAGE 604800 (1 week) max-age for images (ICO, PNG, SVG).
cache.maxAge.default CACHE_MAX_AGE_DEFAULT 86400 (1 day) max-age for everything else (e.g. the web manifest).

Response compression

Config key Environment variable Default Description
compression.minimumResponseSize COMPRESSION_MINIMUM_RESPONSE_SIZE 1024 Minimum response body size, in bytes, before compression is applied.

HTTP server

Config key Environment variable Default Description
http.host HTTP_HOST none Host the server binds to. Supplied via the --http-host CLI flag (the Docker image passes 0.0.0.0).
http.port HTTP_PORT none Port the server listens on. Supplied via the --http-port CLI flag (the Docker image passes 8080).
http.serverName HTTP_SERVER_NAME LoudWebsite Server name and logger label.

Logging

Config key Environment variable Default Description
log.level LOG_LEVEL info Minimum log level.

Persistence

Config key Environment variable Default Description
database.driver DATABASE_DRIVER inMemory Backend: inMemory (ephemeral SQLite, no infrastructure) or mysql (MySQL/MariaDB).
database.migrate DATABASE_MIGRATE (flag --database-migrate) false When set, run the migrations and exit instead of serving.
database.host DATABASE_HOST localhost MySQL/MariaDB host. Ignored for inMemory.
database.port DATABASE_PORT 3306 MySQL/MariaDB port. Ignored for inMemory.
database.name DATABASE_NAME loud Database name. Ignored for inMemory.
database.username DATABASE_USERNAME loud Database username. Ignored for inMemory.
database.password DATABASE_PASSWORD (empty) Database password. Provide via the environment/a secret — never commit it.
database.tls DATABASE_TLS prefer TLS posture when connecting: off, prefer, or require. Ignored for inMemory.
database.pool.maxPerEventLoop DATABASE_POOL_MAX_PER_EVENT_LOOP 4 Maximum pooled connections per event loop. Ignored for inMemory.

See Persistence below for the workflow.

Paths

Config key Environment variable Default Description
path.staticFiles PATH_STATIC_FILES Resources/Static Directory, relative to the working directory, that static files are served from.

Security headers

Config key Environment variable Default
security.contentSecurityPolicy SECURITY_CONTENT_SECURITY_POLICY default-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'
security.contentTypeOptions SECURITY_CONTENT_TYPE_OPTIONS nosniff
security.frameOptions SECURITY_FRAME_OPTIONS DENY
security.referrerPolicy SECURITY_REFERRER_POLICY strict-origin-when-cross-origin
security.permissionsPolicy SECURITY_PERMISSIONS_POLICY accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()
security.strictTransportSecurity SECURITY_STRICT_TRANSPORT_SECURITY none (omitted)

Strict-Transport-Security has no default and is omitted unless explicitly configured: it only takes effect over HTTPS (browsers ignore it on plain HTTP) and is "sticky" in browsers, so it must stay off in local HTTP development. It is enabled for production in docker-compose.yml, where it only has an effect once traffic is served over HTTPS behind a TLS-terminating proxy.

Running locally

Directly with Swift:

swift run Website # binds to Hummingbird's default 127.0.0.1:8080
swift run Website --http-host 0.0.0.0 --http-port 9000 --log-level debug

A direct run picks up the local development overrides from .env.local (in-memory database, debug logging) over whatever .env configures. To run against another backend, override per launch — e.g. DATABASE_DRIVER=mysql swift run Website — since process environment variables outrank both files.

Or via the Makefile / Docker (uses docker-compose.override.yml, which builds from source and sets LOG_LEVEL=debug):

make pkg-build        # swift build
make site-run         # run locally with hot reload (hb watch)
make site-mount       # docker compose up --build --detach
make site-unmount     # docker compose down + remove the local image

make help lists every available target.

Persistence

The service persists data through Fluent and selects its backend at runtime with database.driver.

In-memory (default)

With no configuration, the service uses an ephemeral in-memory SQLite database. It is created and migrated on startup every launch, so swift run Website and docker compose up work with no external database — ideal for local development and tests.

MySQL / MariaDB

Set DATABASE_DRIVER=mysql and the connection values (DATABASE_HOST, DATABASE_NAME, DATABASE_USERNAME, DATABASE_PASSWORD, …). Unlike the in-memory backend, a MySQL/MariaDB database is not migrated on boot — a shared database is migrated out of band so multiple instances never race:

# Run the registered migrations against the configured database, then exit.
swift run Website --database-migrate
# In a container (production), against the managed database:
docker compose -f docker-compose.yml run --rm website --database-migrate

A local MariaDB for development lives behind the database Compose profile (so a plain docker compose up still runs in-memory). Its data directory is bind-mounted to Tests/DB (git-ignored), so the database survives db-unmount and container restarts:

make db-mount     # start MariaDB (docker compose --profile database up --wait mariadb)
make db-migrate   # run migrations against it
make db-shell     # open a SQL shell on it
make db-unmount   # stop and remove the container (data kept in Tests/DB)
make db-reset     # stop and remove the container
DATABASE_DRIVER=mysql make site-mount   # run the site against MariaDB

Note: because the data lives in the bind-mounted Tests/DB folder rather than a named volume, db-reset's --volumes flag does not clear it. To start from an empty database, delete Tests/DB by hand.

Health checks

GET /health is a liveness check (process is up, no dependency check). GET /health/ready is a readiness check that runs SELECT 1 against the database and returns 200 when reachable or 503 otherwise — so an orchestrator restarts on liveness failure but only withholds traffic on readiness failure.

docker-compose.yml configures the website container healthcheck against GET /health, so Compose reports process liveness without coupling container health to database reachability.

Testing

make pkg-test
# = swift test --disable-xctest --enable-code-coverage --enable-swift-testing --parallel

Tests use the Swift Testing framework. The Website.xctestplan covers the service's two targets — WebsiteTests (the executable/integration tests) and WebsiteLibraryTests (the library unit tests) — plus the local packages' suites: WebTests, PersistenceTests, and LocalizationTests.

The Persistence package has its own suite (run it from Packages/Persistence). Its tests run against the in-memory backend by default; the MySQL integration test is skipped unless a database is pointed at via MYSQL_TEST_HOST (with optional MYSQL_TEST_PORT/NAME/USERNAME/PASSWORD), so swift test stays runnable with no database:

cd ../../Packages/Persistence && swift test                      # in-memory only
# With the local MariaDB up (make db-mount):
cd ../../Packages/Persistence && MYSQL_TEST_HOST=127.0.0.1 swift test

Deployment

The production image is built for linux/amd64 in release mode with a statically linked Swift runtime and jemalloc, runs as a non-root hummingbird user, and exposes port 8080 (ENTRYPOINT ./Website --http-host 0.0.0.0 --http-port 8080).

Verify the image builds for its linux/amd64 target without tagging or publishing:

make img-check

Build, tag, and push a release to the registry (an explicit version is required):

make img-release version=1.2.3

Pull and run the prebuilt image in production — the -f docker-compose.yml flag is important, as it skips the local-development override:

docker compose -f docker-compose.yml pull
docker compose -f docker-compose.yml up -d

Static assets

The image build optimizes the files under Resources/Static in its assets stage, in place, with pinned optimizer versions so asset output is reproducible for a given Dockerfile commit:

  • CSS and JS are minified with esbuild.
  • PNG images are losslessly recompressed with oxipng — the output is pixel-identical, only encoded smaller.
  • The SVG icon is minified with svgo.

Files keep their names and paths, so the URLs derived from the StaticFile enumeration are unaffected. The sources in the repository stay readable and unminified: a direct swift run serves them as-is, while any image build — including the local make site-mount one, which builds the same Dockerfile — serves the optimized copies.

The Dockerfile copies only package manifests and Swift source inputs into the release build stage. Static assets are copied from the separate assets stage after the binary is built, so editing a CSS/JS/image file does not invalidate the release binary build cache.

Preview the optimized output locally — requires only Docker and writes to the git-ignored .build/minified:

make ast-minify

Icons

All icons are renditions of the star mark in icon.svg, which is the canonical source — there is no external design file to regenerate from.

File Size Used by Dark mode
icon.svg vector Tab icon in modern browsers (preferred over the ICO) Adapts: an embedded prefers-color-scheme style flips the accent paths from #000 to #f3ecf5.
favicon.ico 32×32 Tab icon in browsers without SVG favicon support (Safari) Theme-neutral by design: it is the orange star without the accent paths, so one raster reads on both themes. Keep it accent-free when regenerating.
icon-192.png, icon-512.png 192/512 site.webmanifest install icons and splash screens None (no platform mechanism); full artwork, light rendering, on a transparent background.
apple-touch-icon.png 180×180 iOS home-screen bookmarks None (fetched once, outside any page context); full artwork, deliberately opaque — iOS fills transparent regions with black.

The landing page pairs the icons with two theme-color metas: #0c0710 (media-qualified for dark, listed first — the first matching entry wins) and #fafafa as the fallback.

The raster icons are committed binaries, regenerated from icon.svg on demand via a throwaway container (no local toolchain needed) — e.g. for the 512px rendition:

docker run --rm -v "$PWD/Resources/Static:/work" alpine sh -c '
  apk add --no-cache imagemagick librsvg oxipng &&
  magick -background none -density 256 /work/icon.svg -depth 8 PNG32:/work/icon-512.png &&
  oxipng --opt max --strip safe /work/icon-512.png'

(-density scales the 192px viewBox: 96 × target ÷ 192. For favicon.ico, rasterize a star-only copy of the SVG at 32px and pack it with icotool -c --raw.)

Required variables

The Makefile and Compose files read these from a .env file (or the environment). Provide your own values — do not commit secrets.

Variable Used for
HOST_CONTAINER Container registry host (e.g. registry.example.com).
HOST_OWNER Registry namespace / owner.
HOST_USER, HOST_PASSWORD Registry credentials for make img-release.
IMAGE_NAME, IMAGE_TAG Image name and tag.
IMAGE_PLATFORM Build platform (e.g. linux/amd64).
HOST_PORT Host port mapped to the container's 8080 (default 8080).
LOG_LEVEL Runtime log level (default info).
HTTP_SERVER_NAME Runtime server name (default LoudWebsite).
SECURITY_STRICT_TRANSPORT_SECURITY HSTS header value (default max-age=31536000; includeSubDomains).
DATABASE_DRIVER inMemory (default) or mysql. Set to mysql in production to use a managed database.
DATABASE_HOST, DATABASE_PORT, DATABASE_NAME, DATABASE_USERNAME, DATABASE_PASSWORD MySQL/MariaDB connection (when DATABASE_DRIVER=mysql). Provide the password via a secret.
DATABASE_TLS TLS posture when connecting: off, prefer, or require (default require in production).

Run the migrations against the production database once before (or during) rollout: docker compose -f docker-compose.yml run --rm website --database-migrate.