
Internal System
An internal control panel that watches the 24 projects I run from a single table. Domain and certificate expiry, deployment status and gaps in search and analytics wiring are collected in one place. The panel does not change code; it diagnoses, and I apply the fix in the project's own repository.
When you run twenty-four separate projects and domains, what breaks is not the code but the visibility. A certificate is expiring, a deployment failed and stayed failed, analytics were never installed on one site — none of these show up as an error; one day somebody just happens to notice. The real part of the problem is not detection but duration: without knowing how long a gap has been there, you cannot decide which one to fix first. The second issue is that the monitoring tool is itself a source of risk: a set of keys that can reach twenty-four projects puts the question of where those keys live at the centre of the design.
One user: me. That is a constraint which shaped the panel — no multi-user authorisation, invitation flow or role management was written. But there are two distinct moments of use: the morning look (what broke today) and the weekly look (which gap has been open the longest). The panel is divided along those two.
Single developer. The constraint analysis itself, data model, scanner script, panel interface, notification layer and deployment.
The system is split in two, and that split is the centre of the design. The bulk scan does not run in the panel's environment but outside it, as a scheduled job: it queries twenty-four projects in turn and writes the result directly into Neon Postgres. The panel only reads. No request goes to an external service while a page loads; everything on screen is a scan result written earlier. The data model rests on five concepts: the project in the inventory, a scan run and an individual scan, the finding a scan produces, the recommendation derived from a finding, and the task a decision turns into. Because changes are written to a separate log, when a finding was opened and how long it has been standing can be read from history. When the panel shows a finding it offers a copyable command alongside it; the command runs in my terminal, not in the panel.
Two hard constraints drove this. First, the scheduler: on the hosting plan in use, a scheduled job runs at most once a day and the trigger moment is uncertain. Second, duration: on the same plan the function timeout is ten seconds, and a scan across twenty-four projects does not finish in that time. Moving the scan outside removed both — no frequency limit, no timeout.
Trade-offThe system now lives in two environments and both configurations have to be correct together. A failure on the scan side does not appear as an error in the panel; the data simply is not refreshed, and noticing that requires looking at the scan runs themselves.
The split came out of the scheduler constraint, but its real value is security: the key set that reaches twenty-four projects lives where the scan runs, and the panel's environment holds only enough authority to query a single project. Compromising the panel does not mean access to the whole portfolio.
Trade-offThe same key is managed in two places, and when one is rotated the other can be forgotten. There is no automated check catching this; the forgotten side only surfaces when the related feature stops working.
The panel is a deployed application; it cannot reach a local filesystem and cannot read repositories. Rather than trying to work around that constraint, the scope was drawn to match it: the panel shows the problem and its context, puts a copyable command next to it, and I apply the fix in my own terminal. A tool that writes automatic fixes would have to work with the same assumptions across twenty-four different codebases.
Trade-offThe fix step is not automated; the panel can say something is broken but only sees it fixed on the next scan. In between, the same finding remains on screen.
The panel's inventory holds domain names, registrar and DNS details, traffic figures and contact records. Those belong to customers; an image shown in a portfolio cannot carry that information. In this case study the projects appear with numbered labels.
Trade-offMasking weakens the sense of real use: the screenshot explains what the system does but does not show what a user sees. That cost is paid deliberately.
Next.js and Drizzle on the panel side, a plain Node script on the scan side — both share the same schema, because the single source of truth is the database. Neon is on the same serverless model as the deployment and is reachable from the externally-running scan job as well; no second copy of the inventory is kept. Using a CI pipeline as the scheduler is not a preference but a constraint: it was the only option without frequency and duration limits. Because there is a single user, no elaborate authorisation model was built. Web Push exists to shorten the time it takes to notice a finding — an alert lost in an inbox is not an alert.
The panel is session-protected and route protection runs in the Node runtime. Sensitive inventory fields — registrar, DNS configuration, contact records — are visible only inside the panel and never enter any external output, screenshot or this case study. Broadly-scoped service account keys are not present in the panel's environment; it holds only enough authority to query a single project. Service account permissions are granted manually for each property — this cannot be automated, which also prevents permissions from quietly widening.
This panel is a closed system; because there is no date-stamped Lighthouse output, no page-speed score is written here. The structural measure is this: no request goes to an external service while a page loads, and every value on screen is read from the database. Because a scan across twenty-four projects did not fit in a ten-second function timeout, it was moved outside the panel; the only scan path left in the panel is the "scan now" endpoint, which runs for a single project and is measured in seconds.
I originally kept the inventory as YAML files in the repository and had written that I would not move it into a database; then I moved it and deleted the YAML side entirely. What I consider right is not the move but the deletion: when two copies of the same information exist, which one is correct eventually becomes an argument. The same lesson came out of Bergaz Operations — there, because four separate systems keep their own records, the same person exists as two entries in two places and matching is done by hand. The second point is the scan side: today I would have the scan write its own health into the database too. Right now the panel does not distinguish between "there is data" and "the data is fresh"; a failed scan run quietly leaves old data looking correct.
Tell me what you want to build; I'll tell you up front how long it takes and where to start.