Skip to main content

Architecture

This page explains how the pieces fit together conceptually: a hosted Lighthouse CI (LHCI) server that collects scores, a CLI that registers new repos against it, a login-helpers package for auditing pages that sit behind authentication, and a shared config package used across the toolchain. It does not document specific hosting providers, service identifiers, or connection details — those are operational concerns, not part of the public API surface.

The pieces

The LHCI server is a long-running web service that receives Lighthouse report uploads from CI runs, stores them, and serves a small dashboard for comparing scores over time. For sites with no CI pipeline of their own, it can also collect scores on a schedule by running its own periodic audits against a configured URL, rather than waiting on an upload. It is the only piece of this system that runs continuously — the CLI and login-helpers are short-lived tools invoked from a developer's machine or from another repository's CI pipeline, and the docs site you're reading is a separate, statically-built deployment.

The CLI (@houses/lighthouse-cli) is what a new repo uses to onboard itself. Running its register command creates a project on the server and hands back a build token — but that token alone isn't enough for CI to upload successfully. The server also gates every route, including the upload endpoint, behind a separate, server-wide shared credential (not tied to any one project), so a repo's CI needs both the project's build token and that shared credential to authenticate uploads. The CLI never runs continuously; it's a one-time (or occasional) setup step, not a service.

The login-helpers package exists because Lighthouse, by default, can only audit pages that are reachable without a login. Many real apps gate their most interesting pages — dashboards, account pages — behind authentication. This package provides small helper functions that establish an authenticated browser session (via injected cookies, submitted credentials, or HTTP basic auth) before Lighthouse's headless browser visits the page, so those routes can be scored like any other.

The shared config package holds the TypeScript and lint configuration that the CLI and login-helpers build against, so those two published packages don't each duplicate their own compiler and lint setup. It isn't deployed anywhere itself, and the server doesn't use it — the server is plain JavaScript with no build step of its own.

Put together, the flow looks like: a repo's CI runs a Lighthouse audit (optionally using login-helpers to reach authenticated pages), then uploads the resulting report to the LHCI server, authenticating with both the build token from CLI registration and the server's shared credential — and the server persists the score history so it can be compared over time.

Why a generic, self-contained server

The LHCI server intentionally avoids baking in anything specific to one company, team, or private toolchain — no vendor-specific observability integration, no dependency on privately-hosted packages, and no build-time secrets required just to produce the container image. The reasoning is about ownership boundaries: a tool meant for tracking performance across a person's own projects shouldn't carry a dependency on infrastructure or credentials that belong to somewhere else. Keeping the image generic also means it's straightforward to reason about, rebuild, and redeploy without needing access to anything beyond this project itself.

A related decision was moving score storage out of the container and into an external Postgres database rather than a local file inside the container. Container-based hosting platforms typically treat containers as disposable — they get rebuilt and redeployed regularly, and anything written to the container's own filesystem doesn't reliably survive that. Using an external, persistent database means historical scores survive redeploys, restarts, and container replacement, which matters a lot for a tool whose entire value is trend data over time.

Why the database connection mode matters

Managed Postgres providers commonly offer more than one way to connect: a direct connection to the database, and one or more pooled connection modes that sit in front of it. These aren't interchangeable in practice — different hosting platforms have different network characteristics, and a connection mode that works fine from one environment can be unreachable from another. Some pooling modes are optimized for very short-lived connections (a web-request-per-connection pattern), while others support longer-lived sessions more like a traditional direct connection.

For a persistent server process — as opposed to a short-lived serverless function — the connection mode needs to tolerate a longer-lived session and needs to actually be reachable from wherever the server is hosted. Choosing the wrong mode doesn't fail loudly at build time; it fails when the deployed service tries to open its first database connection. This is the kind of fact that's easy to get wrong by picking whichever connection string a provider's dashboard shows first, rather than the one appropriate to how the consuming service actually behaves.

Why environment variables are scoped down

The server reads its configuration — database connection details, authentication credentials — directly from process environment variables at runtime, rather than from a bundled configuration file. Nothing resembling a .env file is ever packaged into the deployed container image; secrets are supplied by the hosting platform itself at deploy time, entered once and kept out of source control and out of the image entirely.

This is a deliberate, narrower convention than a full application stack might use. A larger web application with its own database schema, user accounts, and role-based access typically needs a much richer environment and configuration story — local development stacks, schema migrations, auth hooks, and so on. The LHCI server doesn't own an application-level schema or an authentication/authorization model of its own (it uses the database purely as a durable store, and gates access with a single shared credential), so it only needs the small set of environment variables that configuration actually requires. Applying a heavier convention wholesale would mean inventing fictional pieces — a user model, a migration pipeline — that have no real counterpart in what this tool does. Scoping environment handling down to exactly what's needed keeps the deployment surface small and easy to audit, at the cost of not looking identical to a larger, unrelated project's setup.

Summary

Each piece has one job: the server persists and serves score history (its own or a repo's CI uploads), the CLI is how a repo introduces itself to the server, login-helpers extends Lighthouse's reach to authenticated pages, and the shared config package keeps the two published packages' build tooling out of duplication. The architectural choices — a generic self-hosted image, external persistent storage, a deliberately chosen database connection mode, and a narrow environment-variable surface — all follow from treating this as a small, personally-owned tool rather than a scaled-down copy of a larger application's infrastructure.