Check whether your feed is valid. For more information about JSON Feed, see the specification. Find the validator source code on GitHub.
GET validation response in JSON format.
{ "version": "https://jsonfeed.org/version/1.1", "title": "Engineer — Portfolio and notes", "home_page_url": "https://engineer.company/", "feed_url": "https://engineer.company/feed.json", "description": "Engineer ApS — software development & IT consulting in Copenhagen, Denmark. A portfolio of proven engineering achievements across software, data, cloud and IT.", "language": "en", "icon": "https://engineer.company/assets/images/brand/card.webp", "favicon": "https://engineer.company/assets/icons/apple/apple-touch-icon.png", "authors": [ { "name": "Engineer ApS", "url": "https://engineer.company/" } ], "items": [ { "id": "https://engineer.company/portfolio/a-site-that-meets-the-accessibility-standard/", "url": "https://engineer.company/portfolio/a-site-that-meets-the-accessibility-standard/", "title": "A site that meets the accessibility standard", "summary": "Conformance is claimed at Level AA in three languages and two themes, with one AAA criterion beyond it and one AA criterion named as an exception, and every…", "content_html": "<p><strong>Situation.</strong> A consultancy that sells engineering judgement and ships an inaccessible website has a credibility problem before it has an accessibility one. The site also has an unusually wide surface for it: three languages, two colour themes, a full print stylesheet, a dark‑first palette, hand‑drawn annotations and an illustrated character — every one of which is a way to fail a criterion in one configuration while passing it in another.</p>\n<p><strong>Task.</strong> The site had to conform to WCAG 2.2 at Level AA across every page, every language and both themes, and the conformance had to be defended by checks rather than asserted in a document.</p>\n<p><strong>Action.</strong> A conformance record documents every criterion in scope, each row naming what satisfies it and what checks it. One Level AAA criterion is taken beyond the target: visual presentation, in full. Focus appearance is offered rather than claimed, because its contrast over the photograph cannot be proven. The automation runs an accessibility rule engine with its best‑practice rules enabled rather than only the conformance ones, which is a thirty‑rule difference and mattered immediately: one of the extra rules failed the moment it was switched on, because the brand mark sat outside every landmark on every inner page. A second, slower check runs thirty‑two representative pages across two colour schemes and two widths, and what it asserts is unusual: focus is proven in pixels rather than in the document, by pressing the tab key for real and comparing screenshots, because identical pixels mean the user cannot tell where focus is regardless of what the markup says. It also walks the whole page looking for a keyboard trap, applies the specified text‑spacing overrides, doubles the root font size, and measures line length, justification, centring and paragraph spacing.</p>\n<p><strong>Result.</strong> Conformance is claimed at Level AA in three languages and two themes, with one AAA criterion beyond it and one AA criterion named as an exception, and every claim has a check behind it. The hardest of them was contrast over the photograph, which an automated rule engine reports as unevaluable and which the site had been defending with an argument rather than a number: the real figures were 49 of 64 page and theme pairs below AA. Two surfaces the design had never owned turned out to be the cause, and the check that found them is a floor now — every pair must carry AA over the wall the build draws, and any pair that does not fails the build. What the site says on its own credits page is the honest part: no automated tool finds more than about a third of WCAG failures, the labels that appear on hover cannot be dismissed with a key because that needs a script and this site runs one, and there has been no testing with real assistive technology — those limits are written where a reader will see them rather than left out of the claim.</p>\n", "date_published": "2026-10-25T09:11:16+01:00", "date_modified": "2026-10-25T09:11:16+01:00", "language": "en", "tags": [ "Design Systems & UI", "Documentation", "Frontend Engineering", "Internationalization", "Testing & QA", "UX / UI Design", "Web Development", "AI Agent Readiness", "Frontend Development", "Internationalization & Localization", "UI/UX Design & Design Systems", "Website Development & CMS" ] }, { "id": "https://engineer.company/portfolio/made-a-geographical-data-pipeline-50x-faster/", "url": "https://engineer.company/portfolio/made-a-geographical-data-pipeline-50x-faster/", "title": "Made a geographical data pipeline 50x faster", "summary": "Accelerated a geographical data pipeline 50x through SQL and data-model tuning across PostgreSQL, MS SQL and Google BigQuery.", "content_html": "<p><strong>Situation.</strong> The organization faced significant delays in processing large sets of geographical data, which affected the efficiency of data‑driven decision‑making processes and analytical reporting.</p>\n<p><strong>Task.</strong> The objective was to optimize the geographical data pipeline to enhance performance and reduce processing time, ensuring that data analytics could be performed more effectively and in real‑time.</p>\n<p><strong>Action.</strong> The existing SQL programming and data modeling structures across PostgreSQL, MS SQL, and Google Cloud BigQuery were meticulously analyzed, which surfaced bottlenecks related to inefficient indexing strategies, suboptimal query designs, and lack of appropriate constraints. To address these issues:</p>\n<ul>\n<li>The data models were redesigned to normalize critical datasets, with partitioning strategies implemented to improve data retrieval times.</li>\n<li>Applied advanced indexing techniques, including B‑tree and GiST indexes for PostgreSQL, filtered indexes in MS SQL, and clustering in BigQuery.</li>\n<li>Optimized complex SQL queries by refactoring subqueries, reducing join operations, and incorporating materialized views where relevant.</li>\n<li>Established data integrity constraints such as foreign keys and check constraints to ensure data consistency without compromising performance.</li>\n</ul>\n<p><strong>Result.</strong> These comprehensive optimizations accelerated the geographical data pipeline performance by 50x, significantly reducing data processing time. This improvement facilitated real‑time analytics, enhanced reporting capabilities, and empowered stakeholders with timely, data‑driven insights, ultimately contributing to more informed strategic decisions.</p>\n", "date_published": "2026-10-21T18:38:05+02:00", "date_modified": "2026-10-21T18:38:05+02:00", "language": "en", "tags": [ "Data Engineering", "Data Governance", "Data Pipelines (ETL/ELT)", "Data Warehousing", "Databases", "GIS / Geospatial", "Performance Tuning", "PostgreSQL", "SQL", "AI Data Readiness", "Data Pipeline Development (ETL/ELT)", "Data Warehouse Design", "Database Performance Tuning", "GIS & Geospatial Solutions" ] }, { "id": "https://engineer.company/portfolio/designed-and-ran-6-etl-elt-pipelines/", "url": "https://engineer.company/portfolio/designed-and-ran-6-etl-elt-pipelines/", "title": "Designed and ran 6 ETL/ELT pipelines", "summary": "Designed and ran 6 ETL/ELT pipelines (BigQuery, MSSQL, PostgreSQL), cutting a 3-hour manual job to under 20 minutes with daily syncs.", "content_html": "<p><strong>Situation.</strong> The organization needed to consolidate and process large volumes of tabular data from a remote data warehouse located behind an IPsec VPN. This data was crucial for powering internal analytics dashboards and external Python APIs used by clients and partners.</p>\n<p><strong>Task.</strong> The objective was to design, implement, and maintain a set of robust and automated ETL/ELT pipelines to securely retrieve, transform, and load data into Google BigQuery, ensuring data accuracy, performance, and scalability across systems.</p>\n<p><strong>Action.</strong> 6 end‑to‑end ETL/ELT pipelines were designed, implemented, and administered, spanning multiple technologies including Google BigQuery, MSSQL, PostgreSQL, Shell scripting, PL/pgSQL, and Transact‑SQL.</p>\n<ul>\n<li>Established secure connections to a remote server protected by an IPsec VPN to automate data retrieval.</li>\n<li>Scheduled and executed downloads of compressed data archives containing Parquet, CSV, and .bak files.</li>\n<li>Developed Bash and Python scripts to extract and classify files based on type and schema.</li>\n<li>For .bak files, backups were restored into a local MSSQL Server instance using RESTORE DATABASE workflows and schema integrity validated.</li>\n<li>Loaded tabular data into staging schemas in MSSQL and PostgreSQL, using bcp, psql, and SSIS tools, depending on the source format.</li>\n<li>Wrote modular and reusable PL/pgSQL and T‑SQL procedures to clean, normalize, and enrich the data based on business logic.</li>\n<li>Exported curated datasets and tables into intermediate formats, compressed them using gzip, and securely transferred them to a Google Cloud Storage bucket.</li>\n<li>Automated the upload and schema mapping processes to Google BigQuery, using bq CLI and Python‑based data ingestion scripts.</li>\n</ul>\n<p><strong>Result.</strong> These automated pipelines significantly reduced the manual overhead and processing time — from 3+ hours of manual work to under 20 minutes end‑to‑end, while improving data freshness from weekly to daily syncs. The Python APIs consuming this data saw a 30% performance improvement, and the enhanced visibility helped business analysts deliver faster insights to stakeholders. The solution remains scalable and extensible for onboarding new data sources as the business grows.</p>\n", "date_published": "2026-10-21T18:38:05+02:00", "date_modified": "2026-10-21T18:38:05+02:00", "language": "en", "tags": [ "Automation & CI/CD", "Cloud", "Data Engineering", "Data Pipelines (ETL/ELT)", "Data Warehousing", "Databases", "Networking & VPN", "PostgreSQL", "Python", "SQL", "AI Data Readiness", "Backend & API Development", "Data Pipeline Development (ETL/ELT)", "Data Warehouse Design", "Database Design & Modeling" ] }, { "id": "https://engineer.company/portfolio/wrote-1000-rbac-rules-to-secure-linux-servers/", "url": "https://engineer.company/portfolio/wrote-1000-rbac-rules-to-secure-linux-servers/", "title": "Wrote 1,000 RBAC rules to secure Linux servers", "summary": "Hardened security with 1,000 RBAC rules across developers, apps and Linux/DB servers — unauthorised-access risk down 85%, Ansible-automated.", "content_html": "<p><strong>Situation.</strong> The organization needed to strengthen access controls across multiple systems, including developer environments, application instances, PostgreSQL, MS SQL, and Linux servers. While the underlying RBAC model was straightforward, the challenge lay in managing over 1,000 individual rules to cover diverse user roles and system requirements. The goal was to ensure strict access restrictions without introducing complexity.</p>\n<p><strong>Task.</strong> Implement a scalable RBAC solution by defining and enforcing 1,000+ rules for access control. This involved mapping permissions to specific roles (e.g., developers, application instances, database admins) and ensuring rules were applied consistently across all systems. The task also required documenting the rules and automating their deployment to avoid manual errors.</p>\n<p><strong>Action.</strong> The approach focused on creating a simple, modular RBAC structure, breaking down permissions into clear, reusable categories (e.g., “read‑only access to production databases”). Using Ansible, the configuration of each rule was automated, ensuring consistency across environments. For example, developers were granted access only to their designated servers, while application instances had limited permissions to prevent lateral movement. The process prioritized clarity over complexity, with each rule explicitly tied to a specific role and system.</p>\n<p><strong>Result.</strong> The implementation secured over 1,000 rules without introducing unnecessary complexity, reducing unauthorized access risks by 85%. Automation streamlined deployment, cutting setup time by 60% compared to manual methods. The documented framework allowed teams to quickly audit or modify rules, ensuring scalability as the infrastructure grew. By focusing on simplicity and volume, the solution achieved robust security while maintaining operational efficiency.</p>\n", "date_published": "2026-10-21T18:38:05+02:00", "date_modified": "2026-10-21T18:38:05+02:00", "language": "en", "tags": [ "Automation & CI/CD", "Databases", "DevOps", "Documentation", "Infrastructure", "Linux & Servers", "Security", "System Administration", "AI Agent Security", "Database Administration (DBA)", "Infrastructure as Code", "Security & Access Management" ] }, { "id": "https://engineer.company/portfolio/led-access-management-across-m365-and-sso/", "url": "https://engineer.company/portfolio/led-access-management-across-m365-and-sso/", "title": "Led access management across M365 and SSO", "summary": "Led access management with M365, 1Password, Red Hat SSO and OKTA, sharply cutting unauthorised-access risk and making access auditable.", "content_html": "<p><strong>Situation.</strong> Access to systems and services across the company was managed inconsistently — permissions granted ad hoc, over time, by different people. That’s a double problem: it opens the door to access nobody intended, and it makes auditing nearly impossible, because nobody can actually say who can reach what, or why they can.</p>\n<p><strong>Task.</strong> The goal was to close off the security exposure by centralising and tightening access management across the whole organisation.</p>\n<p><strong>Action.</strong> The overhaul consolidated the identities and access across M365, 1Password, Red Hat SSO and OKTA SSO, so there was a coherent picture instead of scattered per‑system permissions. Least privilege was enforced — people and systems having exactly what they needed and nothing spare — and the onboarding and offboarding standardised, so access got granted and, just as importantly, revoked promptly and consistently rather than lingering after someone had moved on.</p>\n<p><strong>Result.</strong> The unauthorised‑access risk dropped sharply and access became auditable and consistent — you could finally answer “who can reach this, and why.” The odd part is it also made daily life simpler for the team: the right doors opened easily and the wrong ones stayed shut, which is what good access management actually feels like.</p>\n", "date_published": "2026-10-21T18:38:05+02:00", "date_modified": "2026-10-21T18:38:05+02:00", "language": "en", "tags": [ "Automation & CI/CD", "Cloud", "Infrastructure", "Security", "System Administration", "AI Agent Security", "Security & Access Management" ] }, { "id": "https://engineer.company/portfolio/designed-a-json-passthrough-from-postgres-to-api/", "url": "https://engineer.company/portfolio/designed-a-json-passthrough-from-postgres-to-api/", "title": "Designed a JSON passthrough from Postgres to API", "summary": "Designed a JSON passthrough where PostgreSQL functions return complete JSON forwarded verbatim by the Go API, decoupling the frontend from schema.", "content_html": "<p><strong>Situation.</strong> The usual way data gets from a database to a browser is a relay race of transformations. The database hands the API rows, the API unmarshals them into structs, reshapes them, serialises them back to JSON, and only then do they go out. Every one of those hops is code you write, code you test, and one more place where the API’s idea of the data and the database’s idea of it can drift apart.</p>\n<p><strong>Task.</strong> The idea was to skip the relay race. If the database could return the finished response, the API could just pass it along, and the frontend could depend on the database’s shape directly instead of on a hand‑maintained copy of it living in Go.</p>\n<p><strong>Action.</strong> So it was built as a straight passthrough. The PostgreSQL functions assemble the whole response as JSON — the shaping is a SQL concern, done where the data already is. The Go handler takes that back as json.RawMessage and forwards it untouched; it never decomposes it, never re‑encodes it. A small QueryJSON helper made that pattern the path of least resistance rather than something you had to remember to do. What fell out was all the intermediate machinery a conventional layered API collects — the DTOs, the mappers, the response structs.</p>\n<p><strong>Result.</strong> Handler code got dramatically shorter, and more to the point the frontend stopped being coupled to Go. Change what a function returns and the new shape flows straight through to the client without anyone editing a line of handler code. Fewer moving parts, and one whole category of layer‑to‑layer drift simply doesn’t exist here.</p>\n", "date_published": "2026-10-21T18:38:05+02:00", "date_modified": "2026-10-21T18:38:05+02:00", "language": "en", "tags": [ "APIs & Integration", "Backend Engineering", "Databases", "Performance Tuning", "Platform Architecture", "PostgreSQL", "SQL", "AI Agent Tools & MCP Servers", "Backend & API Development", "Database Design & Modeling", "Platform & Solution Architecture" ] }, { "id": "https://engineer.company/portfolio/migrated-the-api-to-huma-v2-at-100-parity/", "url": "https://engineer.company/portfolio/migrated-the-api-to-huma-v2-at-100-parity/", "title": "Migrated the API to Huma v2 at 100% parity", "summary": "Migrated the HTTP API from Fiber to Huma v2 — 649 paths, 760 operations — at 100% parity between registered routes and the published OpenAPI description.", "content_html": "<p><strong>Situation.</strong> The API started life on Fiber, with request validation written out by hand, endpoint by endpoint. That’s fine when there are a handful of endpoints. It stops being fine as the surface grows: the hand‑rolled validation turns into a maintenance tax, and small inconsistencies creep in because every endpoint’s checks are their own little snowflake. And there was no single description of the API’s shape anywhere.</p>\n<p><strong>Task.</strong> The goal was validation coming from the types instead of from hand‑written checks, and an actual contract describing the API — without stopping to do a big‑bang rewrite.</p>\n<p><strong>Action.</strong> The HTTP layer moved onto Huma v2, sitting on top of Fiber, so the existing runtime stayed. Each endpoint gets input and output structs, and Huma generates the request validation and response modelling from those types. An OpenAPI description comes out of it for free, which means the documentation tracks the code instead of rotting in a wiki. Everything new got written against Huma and the existing routes migrated over, with exactly two endpoints left on raw Fiber — the WebSocket ones, where you genuinely want the socket and Huma’s request/response model doesn’t fit. What that has grown into is 649 paths carrying 760 operations, and a check in CI that compares the routes the server actually registers against the ones the OpenAPI description advertises. Parity is 100%, and it stays there because a route that isn’t described fails the build.</p>\n<p><strong>Result.</strong> New endpoints get validation and current documentation without anyone doing extra work for it, and a whole class of request‑handling bugs — the “oh, we forgot to check that field here” kind — went away. At 760 operations the description is the only practical way anyone reads the API, so the guarantee that it is complete matters more than it did at fifty. The typed contract made the API both safer to change and easier to hand to someone else, because the types tell you what an endpoint expects.</p>\n", "date_published": "2026-10-21T18:38:05+02:00", "date_modified": "2026-10-21T18:38:05+02:00", "language": "en", "tags": [ "APIs & Integration", "Backend Engineering", "Documentation", "Migrations & Modernization", "Platform Architecture", "Testing & QA", "AI Agent Tools & MCP Servers", "Backend & API Development", "Platform & Solution Architecture", "Technical Documentation" ] }, { "id": "https://engineer.company/portfolio/generated-the-api-contract-from-the-database-out/", "url": "https://engineer.company/portfolio/generated-the-api-contract-from-the-database-out/", "title": "Generated the API contract from the database out", "summary": "Generated the API contract from the database outward — OpenAPI, a typed TypeScript client, mock handlers and UI limits — with a guard at every hop.", "content_html": "<p><strong>Situation.</strong> Frontend and backend move at their own pace, and their assumptions about a request payload can drift apart without anyone noticing. The way you usually find out is a 422 in the browser — after the mismatch has already shipped, which is the most expensive moment to learn about it. Writing the two sides by hand from the same document does not fix it; it just moves the drift to whoever forgot to reread the document.</p>\n<p><strong>Task.</strong> The two sides had to be generated from one source rather than agreed between two, with every step of the generation checked instead of trusted.</p>\n<p><strong>Action.</strong> The chain starts at the database and runs outward. The schema and its functions define the shapes; the Go types define the API; Huma emits the OpenAPI description from those; a typed TypeScript client — 44,076 lines of it — is generated from that description; 61 mock handlers are generated alongside it so the frontend’s own tests run against the real contract rather than a hand‑written fixture; and the limits the UI enforces on a form come from the same place instead of being retyped into a validator. Every hop has a guard. A contract check in CI compares what the frontend sends against what the API expects and fails the build on divergence, with a schema probe underneath it that checks the real shapes rather than a description of them. It deliberately covers where drift likes to hide: optional body fields, where “missing” and “null” get confused, and query‑parameter enums, where the two sides can quietly disagree on the allowed values.</p>\n<p><strong>Result.</strong> A field cannot change on one side only — it fails at the first hop that notices, in a build, minutes after the change. That took out a recurring and genuinely annoying class of bug, the kind invisible in code review that only shows up at runtime. The cost is a generation step in the middle of everything: regenerating is a chore, and the chain is only as trustworthy as its least‑guarded link, which is why each hop got one.</p>\n", "date_published": "2026-10-21T18:38:05+02:00", "date_modified": "2026-10-21T18:38:05+02:00", "language": "en", "tags": [ "APIs & Integration", "Automation & CI/CD", "Backend Engineering", "DevOps", "Reliability & Backups", "Testing & QA", "AI Agent Tools & MCP Servers", "Backend & API Development", "DevOps & CI/CD Automation" ] }, { "id": "https://engineer.company/portfolio/modeled-348-tables-across-34-postgres-schemas/", "url": "https://engineer.company/portfolio/modeled-348-tables-across-34-postgres-schemas/", "title": "Modeled 348 tables across 34 Postgres schemas", "summary": "Modeled the maritime domain into 348 normalized tables across 34 PostgreSQL schemas, with SMALLINT lookups, UUID v7 keys and one naming convention.", "content_html": "<p><strong>Situation.</strong> The heart of the platform is a dense maritime domain — professionals, companies, ships, jobs, reviews — and these things reference each other constantly. A professional sails on ships, works for companies, leaves reviews; a company owns ships and posts jobs. Nearly every feature is a query across that web, so how well the data is modelled decides how well most of the app performs and how sane it is to extend.</p>\n<p><strong>Task.</strong> That domain had to be modelled so it stayed fast and kept its integrity, and so that adding the next entity type didn’t mean fighting the schema.</p>\n<p><strong>Action.</strong> It’s laid out as normalized PostgreSQL schemas organised by domain — 348 tables across 34 of them by now. The many small, stable enumerations — statuses, types, categories — became SMALLINT lookup tables, which keeps the rows compact and the joins cheap instead of storing text codes everywhere. Entities get UUID v7 primary keys, so they’re globally unique but still time‑ordered in the index. One naming convention runs throughout — plural table names inside singular‑named schemas — applied without exception, which as a bonus sidesteps a lot of reserved‑word collisions. And the relationships are held up by real foreign keys and constraints, so integrity is the database’s job, not something the application has to remember to do.</p>\n<p><strong>Result.</strong> What came out is a data model that’s consistent and quick, and predictable to work in because the same rules hold everywhere — there aren’t special cases to memorise. Adding a feature usually means extending the schema along the existing grain rather than working against it, which is the only reason 34 schemas is a structure rather than a sprawl. It’s the floor the rest of the platform stands on.</p>\n", "date_published": "2026-10-21T18:38:05+02:00", "date_modified": "2026-10-21T18:38:05+02:00", "language": "en", "tags": [ "Data Engineering", "Data Governance", "Databases", "Performance Tuning", "Platform Architecture", "PostgreSQL", "SQL", "AI Data Readiness", "Data Governance & Quality", "Database Design & Modeling", "Platform & Solution Architecture" ] }, { "id": "https://engineer.company/portfolio/built-a-268-check-guard-engine-for-the-repo/", "url": "https://engineer.company/portfolio/built-a-268-check-guard-engine-for-the-repo/", "title": "Built a 268‑check guard engine for the repo", "summary": "Built a guard engine of 268 registered commit checks, 277 lint rules and 15 custom ESLint rules, with 146 tests of the guards themselves.", "content_html": "<p><strong>Situation.</strong> Standards written down in a contributing guide are suggestions. Everyone agrees with them, and then it is Friday, the change is small, and the guide loses. The zero‑warnings bar was only ever going to hold if something other than goodwill was holding it — and the linters that ship with each language stop well short of the project‑specific rules that actually matter, the ones about how this codebase in particular is meant to work.</p>\n<p><strong>Task.</strong> The rules the project cared about had to be executable, so that breaking one failed a commit rather than waiting for a reviewer with the time and the memory to catch it.</p>\n<p><strong>Action.</strong> What grew out of that is a guard engine. There are 274 check scripts, 268 of them registered into the commit hooks, alongside 277 JavaScript linters and 96 shell and 17 Python validators covering the things off‑the‑shelf tooling has no opinion about — that a migration can be rolled back, that a translation key exists in both locales, that a registered route appears in the OpenAPI description, that nobody quietly added an inline suppression. Fifteen custom ESLint rules hold the house patterns in TypeScript. One rule is worth naming on its own: any commit prefixed fix: has to carry a test that fails without it, so a bug that is fixed stays fixed. And because a broken guard is worse than no guard at all — it passes everything and nobody finds out — the guards have 146 tests of their own.</p>\n<p><strong>Result.</strong> Review time moved off mechanics and onto design, because the mechanical objections were already made by a machine before the branch was pushed. The trade‑off is real and worth stating: committing is slow, and a badly written guard is genuinely infuriating to work around. Those 146 tests exist because that happened.</p>\n", "date_published": "2026-10-21T18:38:05+02:00", "date_modified": "2026-10-21T18:38:05+02:00", "language": "en", "tags": [ "Automation & CI/CD", "DevOps", "Documentation", "Technical Leadership", "Testing & QA", "Custom AI Agent Development", "DevOps & CI/CD Automation", "Technical Documentation", "Technical Leadership & Consulting" ] }, { "id": "https://engineer.company/portfolio/moved-slow-work-onto-a-river-job-queue/", "url": "https://engineer.company/portfolio/moved-slow-work-onto-a-river-job-queue/", "title": "Moved slow work onto a River job queue", "summary": "Moved slow work off the request path onto a River job queue: 15 worker modules, 8 scheduled tasks and 20 pg_cron maintenance jobs.", "content_html": "<p><strong>Situation.</strong> Some work has no business happening while a user waits. Sending mail, rebuilding a search index, generating a document, recalculating standings — do any of it inside the request and the user watches a spinner for something they never asked to see. Do it in a goroutine instead and it disappears the moment the process restarts, which it will, mid‑deploy, with no record that it was ever supposed to happen.</p>\n<p><strong>Task.</strong> Background work needed somewhere durable to live: a queue that survives a restart, retries a failure, and can be looked at when something has not happened.</p>\n<p><strong>Action.</strong> River was the choice, largely because it keeps its queue in PostgreSQL — the database is already the system of record, so a job and the rows it touches commit or roll back together, and there is no second piece of infrastructure to run and reason about. There are 15 worker modules behind it and 8 scheduled tasks. Underneath, 20 pg_cron jobs handle the maintenance the database is better placed to do itself: pruning partitions, rotating salts, refreshing aggregates. Anything slow enough to notice was moved off the request path and onto one of the two.</p>\n<p><strong>Result.</strong> Requests return quickly and the slow work still finishes, with retries and a visible history when it does not. Keeping the queue in Postgres rather than a dedicated broker is a deliberate limit: it will not scale forever, and at some volume it becomes the wrong answer. For a platform whose bottleneck is the database anyway, one less moving part was worth more than headroom that was not going to be used.</p>\n", "date_published": "2026-10-21T18:38:05+02:00", "date_modified": "2026-10-21T18:38:05+02:00", "language": "en", "tags": [ "Automation & CI/CD", "Backend Engineering", "Databases", "Performance Tuning", "PostgreSQL", "Reliability & Backups", "AI Agent Tools & MCP Servers", "Backend & API Development", "Database Design & Modeling", "Site Reliability & Monitoring" ] }, { "id": "https://engineer.company/portfolio/built-first-party-error-monitoring-and-tracing/", "url": "https://engineer.company/portfolio/built-first-party-error-monitoring-and-tracing/", "title": "Built first‑party error monitoring and tracing", "summary": "Built first-party error monitoring and OpenTelemetry tracing — sanitising, spike detection, symbolication and a heartbeat — behind 11 operator views.", "content_html": "<p><strong>Situation.</strong> The difference between a platform that is up and a platform that is working is whether anyone would know. An error a user hits at eleven at night, on a page nobody tests, is invisible unless something goes and collects it. The usual answer is to buy a hosted error tracker, which is a good answer — and it also means the platform’s own errors, stack traces and user context leave for a third party.</p>\n<p><strong>Task.</strong> Errors and traces had to be collected, grouped and made actionable, without the platform’s internals leaving the platform.</p>\n<p><strong>Action.</strong> Two pieces got built. OpenTelemetry handles tracing over OTLP, so a slow request can be followed across the frontend, the API and the database rather than guessed at. Alongside it sits an in‑house error pipeline — an errmon service and an ingest service — that sanitises payloads before storage, groups errors into recurring problems rather than a flat list, detects spikes and regressions with a cooldown so one bad deploy does not page anyone forty times, symbolicates minified frontend stack traces back into readable code, and runs a synthetic heartbeat to prove that the pipeline itself is alive. It all lands in the database as error events, error groups, an inbox, API latency and stack samples, and it surfaces through 11 operator views, including one for service level objectives.</p>\n<p><strong>Result.</strong> Errors turn into a queue that someone can work through, and a regression announces itself instead of being discovered by a user. Building rather than buying cost real time and means this is one more thing to maintain — a bought tracker would have been running the same afternoon. What it bought was that nothing sensitive leaves, and that the alerting rules fit this platform rather than a generic one.</p>\n", "date_published": "2026-10-21T18:38:05+02:00", "date_modified": "2026-10-21T18:38:05+02:00", "language": "en", "tags": [ "Backend Engineering", "DevOps", "Infrastructure", "Monitoring & Observability", "Reliability & Backups", "Security", "Backend & API Development", "Custom AI Agent Development", "DevOps & CI/CD Automation", "Site Reliability & Monitoring" ] }, { "id": "https://engineer.company/portfolio/1022-migrations-behind-a-schema-drift-gate/", "url": "https://engineer.company/portfolio/1022-migrations-behind-a-schema-drift-gate/", "title": "1,022 migrations behind a schema drift gate", "summary": "Kept the schema honest across 1,022 migrations with a CI gate that builds the database both ways and fails when the two disagree.", "content_html": "<p><strong>Situation.</strong> A schema is described twice in most projects: once by the initial setup that builds it from scratch, and once by the accumulated migrations that grew it. Both are supposed to produce the same database. Nothing checks that they do, so they drift — and the drift is invisible until a fresh environment behaves differently from production, usually at the worst moment.</p>\n<p><strong>Task.</strong> The two descriptions had to be provably identical, automatically, rather than periodically believed to be.</p>\n<p><strong>Action.</strong> The database is versioned as 1,022 migrations, numbered from 036 through 1102, and the ordering discipline around them is unglamorous and non‑negotiable. What makes it hold is a CI job that builds the database twice on every change: once from the fresh initial schema, once from the initial schema plus every migration replayed in order — and then compares the two. Not just the structure, which is the easy half, but the seeded data too, because a migration that backfills a lookup table wrongly is exactly as damaging as one that forgets a column, and only one of those shows up in a schema diff. Any disagreement fails the build with the difference printed.</p>\n<p><strong>Result.</strong> A fresh environment and a long‑lived one are the same database, and that is checked rather than assumed. The cost lands on whoever writes a migration: it has to work replayed and it has to work from cold, which is more thought than a quick ALTER usually gets. That is the point — the alternative is finding out during a restore, when the answer matters and there is no time to work it out.</p>\n", "date_published": "2026-10-21T18:38:05+02:00", "date_modified": "2026-10-21T18:38:05+02:00", "language": "en", "tags": [ "Data Governance", "Databases", "DevOps", "Migrations & Modernization", "PostgreSQL", "Reliability & Backups", "Testing & QA", "AI Data Readiness", "Database Administration (DBA)", "Database Migration & Modernization", "DevOps & CI/CD Automation" ] }, { "id": "https://engineer.company/portfolio/built-fail-closed-rate-limiting-and-anti-abuse/", "url": "https://engineer.company/portfolio/built-fail-closed-rate-limiting-and-anti-abuse/", "title": "Built fail‑closed rate limiting and anti‑abuse", "summary": "Built fail-closed abuse controls: 22 Redis-backed rate limiters, Cloudflare Turnstile, request idempotency and an origin lock on the front door.", "content_html": "<p><strong>Situation.</strong> A public directory of companies and professionals is a target the day it goes live. Scrapers want the data, spam accounts want the reach, and an endpoint that costs the platform real money to serve — search, export, anything that touches an external API — is worth abusing simply because it is free to call. None of that is malice aimed at this platform in particular; it is background weather on the open internet.</p>\n<p><strong>Task.</strong> The expensive and abusable paths needed limits that hold under pressure, including the pressure of the limiter’s own dependency being unavailable.</p>\n<p><strong>Action.</strong> There are 22 rate limiters, each constructed for the path it protects rather than one global cap, because a login attempt, a search and a bulk export are abusive at wildly different rates. State lives in Redis so a limit is shared across instances instead of being per‑process and trivially escaped. The important decision is what happens when Redis is not there: the limiters fail closed. Traffic is refused rather than waved through, which is the less convenient answer and the only defensible one. Around them sit Cloudflare Turnstile on the paths worth challenging, 351 lines of idempotency middleware so a retried write does not become two, an origin lock that refuses requests not arriving through the front door, and a bespoke challenge on the directory itself.</p>\n<p><strong>Result.</strong> Abuse gets expensive for the abuser and cheap for the platform, and an outage in the limiter’s own store degrades into refusal rather than into an open door. Failing closed does mean a Redis problem becomes a user‑visible problem — accepted deliberately, because the alternative is a Redis problem becoming a billing one.</p>\n", "date_published": "2026-10-21T18:38:05+02:00", "date_modified": "2026-10-21T18:38:05+02:00", "language": "en", "tags": [ "APIs & Integration", "Backend Engineering", "Networking & VPN", "Performance Tuning", "Reliability & Backups", "Security", "AI Agent Security", "Backend & API Development", "Security & Access Management", "Site Reliability & Monitoring" ] }, { "id": "https://engineer.company/portfolio/built-the-operator-back-office-for-the-platform/", "url": "https://engineer.company/portfolio/built-the-operator-back-office-for-the-platform/", "title": "Built the operator back‑office for the platform", "summary": "Built the operator back-office — around 40 admin routes and 128 components for claims, moderation, feature flags, cache and diagnostics.", "content_html": "<p><strong>Situation.</strong> Every platform quietly grows a second application, and it is usually the one nobody plans. Somebody has to approve a claim, hide a review, turn a feature on for a subset of users, clear a cache, or work out why one account is seeing something odd. When that application does not exist, the answer is an engineer with a database console — which is slow, unlogged, and one typo away from an incident.</p>\n<p><strong>Task.</strong> Running the platform day to day had to be possible without a shell, so operations belonged to whoever was on duty rather than to whoever had the credentials.</p>\n<p><strong>Action.</strong> The back‑office came to roughly 40 admin routes built from 128 components, and it covers the work as it actually arrives: adjudicating company ownership claims, moderating reviews and posts, flipping feature flags, inspecting and clearing caches, reading diagnostics, and the reporting the CEO asks for. It runs on the same design system and the same generated API client as the public product, which was the deciding choice — an internal tool built on its own stack becomes the part nobody updates, and then the part nobody trusts.</p>\n<p><strong>Result.</strong> The people who run the platform can run it, and every action goes through the same authorization and leaves the same trail as anything else. It is a large surface to keep tested and accessible for a small internal audience, and that cost is ongoing. It is still cheaper than the alternative, which is an engineer typing UPDATE against production at nine on a Sunday evening.</p>\n", "date_published": "2026-10-21T18:38:05+02:00", "date_modified": "2026-10-21T18:38:05+02:00", "language": "en", "tags": [ "Backend Engineering", "Frontend Engineering", "Full‑Stack Development", "Product & Requirements", "System Administration", "UX / UI Design", "AI Agent Tools & MCP Servers", "Frontend Development", "Full‑Stack Product Development", "Product Strategy & Requirements" ] }, { "id": "https://engineer.company/portfolio/ran-a-continuous-security-scanning-programme/", "url": "https://engineer.company/portfolio/ran-a-continuous-security-scanning-programme/", "title": "Ran a continuous security scanning programme", "summary": "Established a continuous security programme — code scanning, DAST, dependency checks, SBOM, secret scanning and pinned actions — plus 21 audits.", "content_html": "<p><strong>Situation.</strong> The security work in the application — the content security policy, the least‑privilege database roles, the entitlement re‑checks — protects the platform at runtime. None of it says anything about the thing being shipped: whether a dependency picked up a known vulnerability last Tuesday, whether a credential was committed and reverted, or whether a third‑party action pinned to a tag has quietly become a different piece of code.</p>\n<p><strong>Task.</strong> Supply chain and code security had to be continuous and automated, so the state of it was a build result rather than an opinion.</p>\n<p><strong>Action.</strong> Static analysis runs through CodeQL, dynamic testing against a live instance through ZAP, and Go dependencies are checked with govulncheck. A software bill of materials is generated on every build with Anchore and Syft, so what shipped is known rather than reconstructed later. Gitleaks scans history for credentials. Every third‑party GitHub Action is pinned to a commit hash rather than a tag, which is the unglamorous control that stops a tag being moved under you. Dependabot watches 6 ecosystems. Alongside the automation sit 21 written security audits, including a threat model and an assessment against the OWASP testing guide — because scanners find the classes of problem someone has already described, and a threat model is where the ones specific to this platform get named.</p>\n<p><strong>Result.</strong> A vulnerability disclosed upstream shows up as a failing build rather than as news. The volume of findings is the real cost — a scanner that reports everything trains people to ignore it, and keeping the signal usable takes ongoing triage rather than a one‑time setup.</p>\n", "date_published": "2026-10-21T18:38:05+02:00", "date_modified": "2026-10-21T18:38:05+02:00", "language": "en", "tags": [ "Automation & CI/CD", "DevOps", "Documentation", "Reliability & Backups", "Security", "AI Agent Security", "DevOps & CI/CD Automation", "Security & Access Management", "Technical Documentation" ] }, { "id": "https://engineer.company/portfolio/three-locks-on-the-door-none-of-them-locked/", "url": "https://engineer.company/portfolio/three-locks-on-the-door-none-of-them-locked/", "title": "Three locks on the door, none of them locked", "summary": "Three protections that had never once fired now do, and the class of defect they belong to — a control whose failure mode is that it keeps reporting healthy —…", "content_html": "<p><strong>Situation.</strong> A hardening review of the production host in August 2026 asked a question that usually gets a confident answer: are the SSH brute‑force protections working. All three were configured, all three appeared in every report anyone looked at, and all three had been inert since the day the host was built.</p>\n<p><strong>Task.</strong> The controls had to be checked against what the kernel actually does with a packet, rather than against the configuration files that describe what should happen to one.</p>\n<p><strong>Action.</strong> Reading the configuration would have confirmed the wrong answer three times, so the review read the running system instead. The intrusion‑banning jail was watching port 22 while the daemon had been moved to 1986 during the initial hardening — every ban it wrote named a port nothing listened on. The firewall’s rate limit was worse in a subtler way: the rule existed, and it sat below a broader rule that matched first. Firewall user rules are evaluated top down and the first match wins, so a broad allow above a rate limit makes the limit dead code that still prints in every status listing. The third was the quietest of them: the ban action shells out to a packet‑filter binary that the package system only recommends rather than requires, so on a host without it the jail starts, counts and decides, and then fails at the one moment it tries to ban. All three fixes were small. What came out of it was not the fix but two rules that now govern the repository: a security control gets an assertion rather than a comment, and a firewall is verified by rule position rather than by rule presence.</p>\n<p><strong>Result.</strong> Three protections that had never once fired now do, and the class of defect they belong to — a control whose failure mode is that it keeps reporting healthy — is the class the platform’s checks are now built to catch. All three had been inert from bootstrap. All three printed healthy in every place anyone looked, which is the whole reason they lasted.</p>\n", "date_published": "2026-10-21T18:38:05+02:00", "date_modified": "2026-10-21T18:38:05+02:00", "language": "en", "tags": [ "Infrastructure", "Linux & Servers", "Monitoring & Observability", "Networking & VPN", "Security", "System Administration", "AI Agent Security", "Networking & VPN Setup", "Security & Access Management", "Site Reliability & Monitoring" ] }, { "id": "https://engineer.company/portfolio/proved-the-intrusion-ban-works-every-run/", "url": "https://engineer.company/portfolio/proved-the-intrusion-ban-works-every-run/", "title": "Proved the intrusion ban works every run", "summary": "A banning path that stops working now fails a converge instead of continuing to report healthy, which is the only property that mattered.", "content_html": "<p><strong>Situation.</strong> The intrusion‑banning service had already been caught banning a port the SSH daemon did not listen on. Fixing the port closed that instance. It did nothing about the reason the fault survived so long, which is that a ban path has no visible failure: the service runs, the jail is listed as active, and nothing anywhere says whether a ban ever reaches the kernel.</p>\n<p><strong>Task.</strong> The ban path had to be exercised on every converge, against the live rule set, rather than inferred from the service being up.</p>\n<p><strong>Action.</strong> The converge now bans an address from the block the standards reserve for documentation, reads back the resulting rule in the kernel’s packet filter, and unbans it in a cleanup block that runs whether the check passed or failed. The reserved range is the load‑bearing choice — the test address belongs to nobody, so a stray ban that outlives a failed run cannot lock out a real network. The kernel read‑back is the other half: the service’s own status output would report success for a ban that produced no rule, which is the exact failure being tested for. Alongside it, the ban mechanism is pinned rather than left to the service’s detection, and the log source is set to detect because the operating system ships no traditional authentication log and the wrong choice fails silently by watching a file that never appears.</p>\n<p><strong>Result.</strong> A banning path that stops working now fails a converge instead of continuing to report healthy, which is the only property that mattered. It costs a few seconds on every run and it writes and withdraws a firewall rule against production every time, which is a real intrusion into a live system and was accepted on the grounds that the alternative had already been demonstrated to be worse.</p>\n", "date_published": "2026-10-21T18:38:05+02:00", "date_modified": "2026-10-21T18:38:05+02:00", "language": "en", "tags": [ "Automation & CI/CD", "Infrastructure", "Linux & Servers", "Reliability & Backups", "Security", "Testing & QA", "AI Agent Security", "Security & Access Management", "Site Reliability & Monitoring", "System Administration" ] }, { "id": "https://engineer.company/portfolio/added-a-one-second-preflight-for-secrets/", "url": "https://engineer.company/portfolio/added-a-one-second-preflight-for-secrets/", "title": "Added a one‑second preflight for secrets", "summary": "A secret that does not resolve now costs a two-second refusal on a laptop instead of a partly-applied change to a live server.", "content_html": "<p><strong>Situation.</strong> Operational secrets had just been split so that each host carries its own backup repository, passphrase and alert switch. The repository code was correct. The operator’s encrypted vault still held the previous single‑host value, and nothing could see the mismatch, because the vault is deliberately local to the operator’s machine and invisible to the repository’s own quality gate.</p>\n<p><strong>Task.</strong> That class of failure needed somewhere to fail cheaply, because it had just failed expensively.</p>\n<p><strong>Action.</strong> The first converge after the change connected to the live host, ran eight tasks and refused on the ninth — correctly, but from inside a run that had already written swap settings to production. A half‑applied converge is a worse answer than a refusal, so a preflight play was written: it reaches no host at all, runs locally, gathers no facts, and executes the same two task files the real converge uses against the operator’s vault. It answers in about a second, and provisioning runs it first. The load‑bearing decision is that it is the same code rather than a second implementation of the same rule, because a check that restates a rule in another language eventually disagrees with it and disagrees silently. Writing it surfaced a trap it nearly created: facts set during a play outlive that play, so chaining the preflight and the converge into one invocation would have handed the real role a passphrase the preflight had already resolved, and passed the check on a vault that was still wrong. That was reproduced before it was guarded, in both task files.</p>\n<p><strong>Result.</strong> A secret that does not resolve now costs a two‑second refusal on a laptop instead of a partly‑applied change to a live server. The preflight is deliberately not tagged to always run and is deliberately not serialised, and both of those are recorded as decisions rather than defaults.</p>\n", "date_published": "2026-10-21T18:38:05+02:00", "date_modified": "2026-10-21T18:38:05+02:00", "language": "en", "tags": [ "Automation & CI/CD", "DevOps", "Infrastructure", "Reliability & Backups", "Security", "Testing & QA", "AI Agent Security", "DevOps & CI/CD Automation", "Infrastructure as Code", "Security & Access Management" ] }, { "id": "https://engineer.company/portfolio/wrote-a-scope-rule-after-a-credential-leak/", "url": "https://engineer.company/portfolio/wrote-a-scope-rule-after-a-credential-leak/", "title": "Wrote a scope rule after a credential leak", "summary": "The scope boundary is a written rule with a stated test, the residue is visible rather than buried, and the specific shape of credential that got through now…", "content_html": "<p><strong>Situation.</strong> Work done for a different company rode along through a repository restructure and stayed. What came with it was not abstract: a scratch log holding live plaintext credentials, which sat in the tree past every guard for most of the repository’s history; a stale inventory naming that company’s host; and a firewall task file opening ports to sixteen of its client networks. None of it ran. That is exactly why it survived — nothing that runs nowhere is ever reviewed again.</p>\n<p><strong>Task.</strong> The boundary had to become a rule with a test attached, rather than an intention, and the residue had to be dealt with in a way that did not simply hide it.</p>\n<p><strong>Action.</strong> The rule is now the opening section of the repository’s instruction set: this repository manages one company’s infrastructure and only that, and another party’s host, inventory, firewall allowance, DNS zone or credential does not belong in it — not even disabled, commented out, or parked in a file no playbook imports. The practical test is written next to it: would this company still be responsible for this if the relationship ended. The residue was quarantined into a clearly‑named directory rather than deleted, so the history stays legible, and the quarantine is deliberately partial — the two structural linters skip it, and the secret scanner, the vault guard and the emoji check deliberately still read it, because those are the three that would catch the thing that got in. The leak itself produced a linter rule: a custom pattern that flags a credential passed as a command‑line flag, which is the shape the leaked one had and which the standard rule set did not match.</p>\n<p><strong>Result.</strong> The scope boundary is a written rule with a stated test, the residue is visible rather than buried, and the specific shape of credential that got through now fails a commit. The lesson recorded alongside it is the general one: the dangerous artefact is not the one that runs, it is the one that does not, because that is the one nobody reads again.</p>\n", "date_published": "2026-10-21T18:38:05+02:00", "date_modified": "2026-10-21T18:38:05+02:00", "language": "en", "tags": [ "Automation & CI/CD", "Data Governance", "Documentation", "Infrastructure", "Security", "AI Agent Security", "Data Governance & Quality", "Security & Access Management", "Technical Documentation" ] }, { "id": "https://engineer.company/portfolio/split-every-operational-secret-per-host/", "url": "https://engineer.company/portfolio/split-every-operational-secret-per-host/", "title": "Split every operational secret per host", "summary": "Secrets are per host by construction, and the two failure modes that would have followed from sharing them are written down where the next person edits the…", "content_html": "<p><strong>Situation.</strong> The platform was built for one host and was about to have two. Several operational secrets had been written as single values on that assumption: one backup repository, one passphrase, one alert switch. Extending them to a second host by sharing them is the path of least resistance and it is wrong in two separate ways, both of which fail quietly.</p>\n<p><strong>Task.</strong> Each secret had to become a map keyed by host, and the reason had to be written down, because the shared version looks correct and costs nothing until the day it matters.</p>\n<p><strong>Action.</strong> Two arguments settled it, and both are recorded next to the configuration. A dead‑man’s switch shared by two hosts alerts less than two switches, not more: either host still pinging keeps the check green while the other one is dead, so adding a host to a shared switch actively reduces the coverage of the one that was already there. And a backup repository string is only a location — the passphrase is the whole of the encryption, so two hosts sharing a passphrase are not two repositories with a common secret, they are one repository with two directories in it. Each value became a map keyed by inventory hostname, resolved per host by a shared task file that both the converge and the preflight run. The existing host was then named in all four maps with its repository and both switches left empty, which is the truthful state and keeps the two open initiatives honest about owing the operator half of the work rather than the code.</p>\n<p><strong>Result.</strong> Secrets are per host by construction, and the two failure modes that would have followed from sharing them are written down where the next person edits the file. The empty entries are the part worth keeping: they say the wiring exists and the value does not, which is a different and more useful statement than a missing key.</p>\n", "date_published": "2026-10-21T18:38:05+02:00", "date_modified": "2026-10-21T18:38:05+02:00", "language": "en", "tags": [ "Automation & CI/CD", "Infrastructure", "Linux & Servers", "Reliability & Backups", "Security", "Solution Architecture", "AI Agent Security", "Backup & Disaster Recovery", "Infrastructure as Code", "Security & Access Management", "Site Reliability & Monitoring" ] }, { "id": "https://engineer.company/portfolio/built-a-22-linter-commit-gate/", "url": "https://engineer.company/portfolio/built-a-22-linter-commit-gate/", "title": "Built a 22‑linter commit gate", "summary": "Mechanical objections are made by a machine before a commit exists, and the gate is the only reviewer this project has.", "content_html": "<p><strong>Situation.</strong> A contributing guide is a set of suggestions. Everyone agrees with it, and then it is Friday, the change is small, and the guide loses. On a one‑person project that is worse rather than better, because there is no reviewer at all — the only thing between a bad change and production is the person who wrote it, at the moment they are least inclined to argue with themselves.</p>\n<p><strong>Task.</strong> The standards had to be executable, so that breaking one fails a commit rather than waiting to be noticed.</p>\n<p><strong>Action.</strong> What grew is a gate of 22 linters with no warning tier: every diagnostic is an error, and the site generator itself runs with warnings promoted to failures so even a deprecation stops the build. The obvious ones are there — HTML validation, CSS, JavaScript, Python, YAML, Markdown, shell, spelling, secrets, dead links. The interesting ones are the project‑specific checks that no off‑the‑shelf tool has an opinion about: that both colour themes paint every layered surface with the same number of layers, that no photograph is displayed wider than half its source pixels, that the deployed tree contains no private path or hostname, that the prose obeys the tone rules in all three languages, that a page has a Markdown twin and a valid alternate‑protocol representation. Inline suppressions are prohibited outright — no ignore comment, no disable directive, no bypassing the hook, and no renaming a file to dodge a matcher. The rule that keeps that honest is that a pre‑existing failure is not an excuse: a check that surfaces a defect nobody introduced gets fixed in the same pass.</p>\n<p><strong>Result.</strong> Mechanical objections are made by a machine before a commit exists, and the gate is the only reviewer this project has. The cost is stated rather than hidden: committing is slow, and a badly written guard is genuinely infuriating to work around, which is why the guards themselves were later brought under a formatter and a linter of their own.</p>\n", "date_published": "2026-10-21T18:38:05+02:00", "date_modified": "2026-10-21T18:38:05+02:00", "language": "en", "tags": [ "Automation & CI/CD", "DevOps", "Documentation", "Frontend Engineering", "Python", "Testing & QA", "Custom AI Agent Development", "DevOps & CI/CD Automation", "Product Strategy & Requirements", "Technical Documentation" ] }, { "id": "https://engineer.company/portfolio/generated-495-pages-from-a-sqlite-export/", "url": "https://engineer.company/portfolio/generated-495-pages-from-a-sqlite-export/", "title": "Generated 495 pages from a SQLite export", "summary": "Four hundred and ninety-five pages generate from one source, correcting a statement costs nothing, and the addresses this site has ever published continue to…", "content_html": "<p><strong>Situation.</strong> The company’s portfolio content is authored in a separate database — the same one that produces the CV — and the website has to publish it as pages, in three languages, without the two copies drifting apart. The naive approach, writing the pages by hand and keeping them in step, fails on the first correction.</p>\n<p><strong>Task.</strong> The website had to generate its content from the database as a build input, with page addresses that survive the sentences on them being rewritten.</p>\n<p><strong>Action.</strong> An exporter reads the database read‑only and writes one page per achievement per language — 495 pages — plus the taxonomy and services data the templates need. It uses only the standard library, so the site’s build has no dependency on the generator’s environment, and the committed output means the site builds standalone. The single most consequential decision in it concerns addresses. The site used to derive a page’s URL from the leading words of its English statement, so correcting a sentence silently moved the page and broke every link to it — a site whose argument for itself is that it corrects things, charging itself a dead link every time it did. The address is now authored as data: one row per address per achievement, the first is current and every later one is a retired address the site emits as a redirect. Two other traps are recorded from the same work. A period comparison against a text column silently matched all thirty‑two rows because of how the database assigns type affinity across a comparison. And the language list is now the single axis the write loop, the data files and the queries all derive from, so adding a fourth language is one map entry rather than a search.</p>\n<p><strong>Result.</strong> Four hundred and ninety‑five pages generate from one source, correcting a statement costs nothing, and the addresses this site has ever published continue to answer. The exporter also owns exactly one presentation decision — how services group into themed sections — and that is deliberate: everything else it writes is the database’s, and a group heading missing a language fails the export rather than rendering English over translated content.</p>\n", "date_published": "2026-10-21T18:38:05+02:00", "date_modified": "2026-10-21T18:38:05+02:00", "language": "en", "tags": [ "Data Pipelines (ETL/ELT)", "Databases", "Frontend Engineering", "Internationalization", "Python", "SQL", "Web Development", "AI Data Readiness", "Brand, Marketing & SEO", "Data Pipeline Development (ETL/ELT)", "Internationalization & Localization", "Website Development & CMS" ] }, { "id": "https://engineer.company/portfolio/replaced-963-metadata-blocks-with-one-graph/", "url": "https://engineer.company/portfolio/replaced-963-metadata-blocks-with-one-graph/", "title": "Replaced 963 metadata blocks with one graph", "summary": "One graph with stable identity replaces 963 anonymous restatements, and the pages carry less rather than more.", "content_html": "<p><strong>Situation.</strong> The site emitted structured data the way most sites do: a block per page, each one describing the organisation again from scratch. Across the site that came to 963 blocks restating the same company 1,671 times, anonymously — no stable identifier anywhere, so nothing consuming it could tell that the organisation on one page was the organisation on another.</p>\n<p><strong>Task.</strong> The structured data had to become one graph with stable identity, rather than a pile of blocks that happen to contain the same words.</p>\n<p><strong>Action.</strong> Identifiers are now minted from the site’s origin — one for the organisation, one for the site, one for the person — and every block references them rather than restating their contents. The identifiers are origin‑wide rather than per language, because the company is the same company in Danish. Sixteen types are in play, covering the service catalogue and its offers, the reviews and the work they describe, the collections and their breadcrumbs. Two decisions kept the weight down. List pages publish their items by URL only rather than inlining them, which was measured: naming them on the portfolio index would have meant 107 full statements and a 52 per cent increase in the compressed page. And the check that guards it does not merely validate syntax — it asserts that every block parses, that every identifier resolves, and that every URL the graph names was actually built, so a graph pointing at a page that does not exist fails the gate.</p>\n<p><strong>Result.</strong> One graph with stable identity replaces 963 anonymous restatements, and the pages carry less rather than more. The measurement that started the work is worth keeping in mind: the site had been emitting the same organisation description 1,671 times without anything being able to join them up.</p>\n", "date_published": "2026-10-21T18:38:05+02:00", "date_modified": "2026-10-21T18:38:05+02:00", "language": "en", "tags": [ "APIs & Integration", "Automation & CI/CD", "Brand & Marketing", "Data Governance", "Frontend Engineering", "Web Development", "AI Agent Readiness", "AI Data Readiness", "Brand, Marketing & SEO", "Data Governance & Quality", "Website Development & CMS" ] }, { "id": "https://engineer.company/portfolio/wrote-tests-for-the-checkers-themselves/", "url": "https://engineer.company/portfolio/wrote-tests-for-the-checkers-themselves/", "title": "Wrote tests for the checkers themselves", "summary": "Every checker in the project now has a test that proves it fails on bad input, and the range assertions read from the source of truth.", "content_html": "<p><strong>Situation.</strong> The project runs a set of custom checkers over its own content — a spell check, an identifier‑range check, a voice check, a figure‑consistency check. Each of them had run green for months. A checker that has only ever seen clean input and only ever reported clean is indistinguishable from a checker that reads nothing at all, and there was no test in the suite that could tell the two apart.</p>\n<p><strong>Task.</strong> The checkers had to be made to prove they can fail, and the fixtures they check against had to stop being hand‑maintained copies of the thing they describe.</p>\n<p><strong>Action.</strong> The pattern applied throughout is to plant the defect the checker exists to find and require the checker to find it. The spell check is fed a deliberately misspelled word and the test fails if the run comes back clean. The voice check is fed prose in the first person and must reject it. The buzzword check is fed a banned word. Each of these is a small test and each one closed a real blind spot, because two of the checkers turned out to be reading a narrower set of files than their documentation claimed and had been silently skipping content. The second change is about where a test gets its expectations: the identifier‑range check previously compared against a number written in the test file, which meant every content addition required editing a test, and an editor who updated the number without looking had disabled the check. It now derives the range from the database, so the check describes the data rather than a stale memory of it.</p>\n<p><strong>Result.</strong> Every checker in the project now has a test that proves it fails on bad input, and the range assertions read from the source of truth. The uncomfortable part is what this exposed: a green check had been meaningless in at least two places for an unknown length of time, and there is no way to find out retroactively what passed through.</p>\n", "date_published": "2026-10-21T18:38:05+02:00", "date_modified": "2026-10-21T18:38:05+02:00", "language": "en", "tags": [ "Automation & CI/CD", "Data Governance", "Databases", "Python", "Testing & QA", "Custom AI Agent Development", "Data Governance & Quality", "DevOps & CI/CD Automation", "Technical Leadership & Consulting" ] }, { "id": "https://engineer.company/portfolio/made-six-collectors-state-what-they-skipped/", "url": "https://engineer.company/portfolio/made-six-collectors-state-what-they-skipped/", "title": "Made six collectors state what they skipped", "summary": "A page of this platform's numbers now carries its own blind spots beside its findings. None of the self-measurement is a gate, and that is stated rather than…", "content_html": "<p><strong>Situation.</strong> Six read‑only collectors rendered a live host into Markdown, and not one of them recorded its own runtime, the files it could not open, or the inputs it skipped. Several had failure paths that swallowed the failure outright, which produces a specific and dangerous shape of report: a page of numbers that looks complete and healthy whether or not anything was actually read. A rotated log that arrives truncated removes its whole span from every figure on the page, and a week of missing traffic reads exactly like a quiet week. A mirror whose daemon changed its log format produces the same zero as a mirror nobody visited. Every probe in the security report sits behind a test for the tool, so a host missing the firewall, socket and ban tools rendered no rules, no sockets and no jails — the visual signature of a locked‑down host, and in fact a blind one.</p>\n<p><strong>Task.</strong> Two things had to become impossible to mistake for health. A report had to state what counts as bad rather than leave a human to decide, and every collector had to state what its own collection cost and how much of its input it could not use.</p>\n<p><strong>Action.</strong> Thresholds were fitted to the 116 samples the metrics report had already measured, never to a guess, with the observed ranges recorded next to the bounds. The judging finds every column by name rather than by position and refuses to judge a row whose value count does not match its label count, which is what protects it from a clock‑format change that shifts every label by one. The report then distinguishes three outcomes rather than two: a breach, no breach, and not judged because nothing measured it. On the collection side each one now reports its runtime and its blindness — files read against files unreadable, journal lines read against journal lines recognised, repositories git refused to read counted rather than rendered as a zero, sources asked against sources that answered, probes asked against probes not installed — and every one of those rows is printed even when the answer is zero, because a row that disappears when it is empty is the same silence in a different form. Every template gained an explicit branch for output written by a collector older than itself.</p>\n<p><strong>Result.</strong> A page of this platform’s numbers now carries its own blind spots beside its findings. None of the self‑measurement is a gate, and that is stated rather than implied: no bound is fitted to a collector’s runtime, so a collection that doubles is visible and stops nobody, and fitting one would need a baseline from the real host that this work deliberately did not invent. The bounds that do exist are daily averages, so a host that touches a ceiling for a minute crosses nothing here — the fifteen‑minute switch catches that, neither replaces the other, and both reports say so on the page.</p>\n", "date_published": "2026-10-21T18:38:05+02:00", "date_modified": "2026-10-21T18:38:05+02:00", "language": "en", "tags": [ "Automation & CI/CD", "Documentation", "Monitoring & Observability", "Python", "Reliability & Backups", "Testing & QA", "Custom AI Agent Development", "Site Reliability & Monitoring", "System Administration", "Technical Documentation" ] }, { "id": "https://engineer.company/portfolio/managed-8-wireguard-and-ipsec-vpn-connections/", "url": "https://engineer.company/portfolio/managed-8-wireguard-and-ipsec-vpn-connections/", "title": "Managed 8 WireGuard and IPsec VPN connections", "summary": "Managed and troubleshot 8 WireGuard and IPsec VPN connections across Google Cloud and Linux, ending recurring connectivity outages.", "content_html": "<p><strong>Situation.</strong> Secure connectivity between the cloud and the on‑premises Linux systems ran over several VPN tunnels, and they were fragile and awkward to diagnose when they dropped. A dead tunnel could cut communication between environments, and troubleshooting one was slow and uncertain — you were never quite sure you’d found the real cause.</p>\n<p><strong>Task.</strong> Managing and troubleshooting those connections, to guarantee secure and uninterrupted communication, was the job.</p>\n<p><strong>Action.</strong> The VPN estate came under control — eight WireGuard and IPsec tunnels across Google Cloud and the Linux systems, managed and troubleshot as a set rather than eight separate mysteries. Their configuration was standardised so they were consistent and understandable instead of each being its own special case, and their health was monitored, with the recurring routing and key‑exchange problems tackled at the root rather than papered over with a restart.</p>\n<p><strong>Result.</strong> All eight tunnels ran securely and reliably, communication between environments stayed protected, and the recurring connectivity incidents that used to interrupt work stopped happening. Fixing the root causes rather than nursing the symptoms is what turned them from a recurring headache into something that just worked.</p>\n", "date_published": "2026-10-21T12:20:26+02:00", "date_modified": "2026-10-21T12:20:26+02:00", "language": "en", "tags": [ "Cloud", "Infrastructure", "Linux & Servers", "Networking & VPN", "Reliability & Backups", "Security", "System Administration", "Networking & VPN Setup", "Security & Access Management" ] }, { "id": "https://engineer.company/portfolio/one-infrastructure-plan-for-14-departments/", "url": "https://engineer.company/portfolio/one-infrastructure-plan-for-14-departments/", "title": "One infrastructure plan for 14 departments", "summary": "Designed a comprehensive infrastructure framework for a research institute across 14 departments — modular, with unified data pipelines and support strategies.", "content_html": "<p><strong>Situation.</strong> At a research institute comprising 14 diverse research groups, each group worked with varying data sources, formats, scales, and software tools. The technical expertise and available IT resources varied widely across the groups. While a few had managed to create and deploy custom IT solutions, many struggled with the complexity of their data infrastructure needs, diverting valuable time and focus from their core research work.</p>\n<p><strong>Task.</strong> The task was to devise a solution that would allow researchers to focus on their scientific work rather than IT challenges. The goal was to design and implement a scalable, institute‑wide data infrastructure that could accommodate the broad and differing requirements of the majority of research groups.</p>\n<p><strong>Action.</strong> A robust, forward‑thinking infrastructure plan was developed that balanced flexibility and standardization. The plan outlined key components such as modular architecture, integration pathways for diverse data sources, user‑friendly interfaces tailored to varying technical skill levels, and scalable storage and processing solutions. It also included strategies for onboarding, support, and governance to ensure adoption and sustainability.</p>\n<p><strong>Result.</strong> The resulting infrastructure plan was both technically sound and strategically aligned with the institute’s research goals. It unified the vision for data management across the organization, provided a clear path to reducing IT burden on researchers, and laid the foundation for a shared, efficient, and future‑ready research data environment. The plan was well received for its inclusivity, clarity, and adaptability, setting a strong direction for the institute’s data infrastructure transformation.</p>\n", "date_published": "2026-10-21T10:44:46+02:00", "date_modified": "2026-10-21T10:44:46+02:00", "language": "en", "tags": [ "Data Engineering", "Data Governance", "Data Pipelines (ETL/ELT)", "Documentation", "Infrastructure", "Platform Architecture", "Solution Architecture", "Stakeholder & Reporting", "Technical Leadership", "Data Governance & Quality", "Data Pipeline Development (ETL/ELT)", "Platform & Solution Architecture", "Technical Documentation", "Technical Leadership & Consulting" ] }, { "id": "https://engineer.company/portfolio/a-mapping-product-carried-end-to-end/", "url": "https://engineer.company/portfolio/a-mapping-product-carried-end-to-end/", "title": "A mapping product, carried end to end", "summary": "Architected, built and ran the infrastructure, data processing and map app for two years non-stop — the dependable backbone of the product.", "content_html": "<p><strong>Situation.</strong> An early‑stage green‑energy startup depended on a single platform to track, monitor, and optimize renewable energy assets, yet had neither a dedicated infrastructure team nor an established engineering organization to build and operate it. The entire technical foundation — cloud infrastructure, data‑processing pipelines, and the customer‑facing GIS map application — had to be created and kept running continuously, in a market where any downtime or data gap directly eroded customer trust and revenue.</p>\n<p><strong>Task.</strong> The task was to single‑handedly architect, build, and operate the whole system end to end, spanning platform and data engineering, DevOps, and site reliability. Beyond writing the software, this meant owning production: provisioning and hardening infrastructure, designing the data‑processing layer that fed the map, and guaranteeing the application stayed available around the clock for a growing customer base — all within the constraints and relentless pace of a fast‑moving startup.</p>\n<p><strong>Action.</strong> For two years the infrastructure, data pipelines, and map application were designed, implemented, and supported without interruption — no weekends, holidays, or vacations, often ten to fourteen hours a day. A pragmatic, modular architecture was chosen to keep a one‑person operation maintainable, with automated provisioning, monitoring, and alerting so issues could be detected and resolved quickly. Data processing was continuously tuned for reliability and performance, releases were shipped incrementally, and every layer — from servers to the user‑facing map — was personally maintained and improved in response to real customer usage.</p>\n<p><strong>Result.</strong> The platform stayed continuously available and evolved from a fragile early prototype into the dependable backbone of the product, sustaining the company through its critical growth phase on the strength of a single engineer’s ownership. This hands‑on stewardship kept infrastructure, data, and the map application reliable enough to support upsells, data licensing, and new‑customer acquisition, and demonstrated a rare degree of commitment, breadth, and end‑to‑end accountability across the full stack.</p>\n", "date_published": "2026-10-21T10:44:46+02:00", "date_modified": "2026-10-21T10:44:46+02:00", "language": "en", "tags": [ "Data Engineering", "Data Pipelines (ETL/ELT)", "Full‑Stack Development", "GIS / Geospatial", "Infrastructure", "Platform Architecture", "Reliability & Backups", "Data Pipeline Development (ETL/ELT)", "Full‑Stack Product Development", "GIS & Geospatial Solutions", "Platform & Solution Architecture", "Site Reliability & Monitoring" ] }, { "id": "https://engineer.company/portfolio/managed-5000-hours-of-map-development/", "url": "https://engineer.company/portfolio/managed-5000-hours-of-map-development/", "title": "Managed 5,000 hours of map development", "summary": "Planned and ran 5,000 hours of map development with Scrum and Jira, keeping work visible and aligned to the roadmap.", "content_html": "<p><strong>Situation.</strong> The map wasn’t built in a sprint. It was thousands of hours of work spread across a lot of people and a lot of months, and that kind of effort drifts if nobody is holding the line on scope and schedule. Left alone, it quietly becomes late.</p>\n<p><strong>Task.</strong> The responsibility was designing and running the development effort so it delivered on purpose rather than by luck — keeping the priorities honest and the progress visible to anyone who wanted to look.</p>\n<p><strong>Action.</strong> It ran on Agile: proper Scrum, with the ceremonies actually used rather than performed, and the work tracked in Jira so people could see where things stood without having to ask. Somewhere around 5,000 hours of map development went through that process. The emphasis was less about ceremony for its own sake and more about keeping priorities pointed at what mattered and catching drift early, while it was still cheap to fix.</p>\n<p><strong>Result.</strong> The 5,000 hours landed in a controlled, visible way instead of disappearing into a black box. The project management did what it’s meant to do — and what mostly goes unnoticed when it works: it kept the work aligned to the goals and roughly on the timeline, without heroics at the end.</p>\n", "date_published": "2026-10-21T08:22:55+02:00", "date_modified": "2026-10-21T08:22:55+02:00", "language": "en", "tags": [ "Agile & Scrum", "Product & Requirements", "Project Management", "Stakeholder & Reporting", "Team Leadership", "Product Strategy & Requirements", "Project Management (Agile)", "Team Building & Mentoring" ] }, { "id": "https://engineer.company/portfolio/managed-4000-computer-repairs/", "url": "https://engineer.company/portfolio/managed-4000-computer-repairs/", "title": "Managed 4,000 computer repairs", "summary": "Handled 4,000 computer repairs with fast, effective resolution of hardware and software issues, upholding a name for dependable service.", "content_html": "<p><strong>Situation.</strong> There was a constant stream of computer repairs coming through — four thousand of them over time — and every one was someone waiting to get their machine back and get on with their day. Hardware faults, software faults, the whole mix, and clients judged the shop on how fast and how properly they got fixed.</p>\n<p><strong>Task.</strong> Managing those repairs was the job — keeping problems getting resolved quickly and, just as importantly, correctly.</p>\n<p><strong>Action.</strong> Four thousand computer repairs went through — diagnosing the hardware and software faults, organising the workflow so machines moved through instead of piling up, and holding the quality so a repair was actually done right rather than sent back out to fail again next week. A repair that comes back is worse than a slow one; it costs the client a second trip and costs the shop the trust. So the throughput mattered, but never at the expense of fixing it the first time.</p>\n<p><strong>Result.</strong> All four thousand got resolved, and quickly. People got their machines back working and got on with things, and the company’s name for dependable service held up — which in a repair shop is the whole business.</p>\n", "date_published": "2026-10-21T08:22:55+02:00", "date_modified": "2026-10-21T08:22:55+02:00", "language": "en", "tags": [ "Project Management", "System Administration", "Hardware Repair & Data Recovery", "IT Support & Helpdesk" ] }, { "id": "https://engineer.company/portfolio/designed-30-websites-from-photoshop-to-html/", "url": "https://engineer.company/portfolio/designed-30-websites-from-photoshop-to-html/", "title": "Designed 30 websites from Photoshop to HTML", "summary": "Designed 30 websites, converting Photoshop designs to HTML into distinctive, polished and maintainable web presences.", "content_html": "<p><strong>Situation.</strong> Clients came in with a look they wanted — often a finished visual design — and needed a website built faithfully from it. The gap between a design file and a working site is where a lot of quality is won or lost: it’s easy to ship something that’s roughly right and subtly wrong.</p>\n<p><strong>Task.</strong> Designing the sites and converting the designs into accurate, flexible implementations was the job.</p>\n<p><strong>Action.</strong> Thirty websites got designed and the Photoshop designs converted into HTML by hand. Faithful was the standard — the spacing, the type, the details the designer actually intended, not an approximation of them — but so was flexible, because a site that matches the mockup pixel‑for‑pixel and then falls apart the moment the content changes hasn’t really been built well. So the aim was markup that stayed true to the design and stayed maintainable afterwards.</p>\n<p><strong>Result.</strong> The thirty sites matched their designs and stayed workable — distinctive, polished web presences that didn’t break the first time someone edited them. Faithful to the design and still maintainable is the balance that mattered, and that’s where these landed.</p>\n", "date_published": "2026-10-21T08:22:55+02:00", "date_modified": "2026-10-21T08:22:55+02:00", "language": "en", "tags": [ "Design Systems & UI", "Frontend Engineering", "UX / UI Design", "Web Development", "Frontend Development", "UI/UX Design & Design Systems", "Website Development & CMS" ] }, { "id": "https://engineer.company/portfolio/built-the-next-js-16-frontend-ssr-ssg-and-csr/", "url": "https://engineer.company/portfolio/built-the-next-js-16-frontend-ssr-ssg-and-csr/", "title": "Built the Next.js 16 frontend: SSR, SSG and CSR", "summary": "Built the Next.js 16 frontend with SSR/SSG/CSR and a server-shell prefetch-and-hydrate pattern that eliminated N+1 fetches.", "content_html": "<p><strong>Situation.</strong> The authenticated dashboard needed to feel quick and stay genuinely interactive, and those two goals pull against each other if you’re naïve about it. Fetch everything on the client and the first load drags, and worse, you get the N+1 pattern where every component wakes up and fires its own request, so a single page turns into a cascade of round‑trips.</p>\n<p><strong>Task.</strong> Each part of the app needed rendering the way that actually suited it, without giving up the client‑side interactivity where it mattered.</p>\n<p><strong>Action.</strong> The Next.js 16 frontend uses the right mode per surface instead of one blanket choice. Marketing and public pages are statically generated — they don’t change per user, so there’s no reason to render them on every request. The genuinely interactive parts stay client‑rendered. And where the N+1 cascade actually bites — the authenticated dashboard and the big directory listings — the fix was factored rather than hand‑rolled: createPrefetchedServerPage fetches the page’s data on the server and hands it to the client already populated, so the components come up with their data instead of each going off to ask for it. A cache‑invalidation strategy is documented for each data request, so data stays fresh without the app re‑fetching things it already has.</p>\n<p><strong>Result.</strong> The pages moved onto the factory come up in one round‑trip instead of a storm of them, and because it is a factory rather than a pattern people copy by hand, the next page moved over inherits the behaviour for free. The rest of the authenticated app is still client‑rendered and still queued behind it — a migration with a working mechanism and an obvious next page, rather than a finished sweep.</p>\n", "date_published": "2026-10-21T08:22:55+02:00", "date_modified": "2026-10-21T08:22:55+02:00", "language": "en", "tags": [ "Frontend Engineering", "Full‑Stack Development", "Performance Tuning", "Platform Architecture", "Web Development", "Frontend Development", "Full‑Stack Product Development", "Website Development & CMS" ] }, { "id": "https://engineer.company/portfolio/built-the-entire-core-infrastructure-from-scratch/", "url": "https://engineer.company/portfolio/built-the-entire-core-infrastructure-from-scratch/", "title": "Built the entire core infrastructure from scratch", "summary": "As one of the first hires, designed and built a green-energy SaaS startup's entire core infrastructure from scratch, enabling rapid growth.", "content_html": "<p><strong>Situation.</strong> This was a green‑energy SaaS startup with a promising idea and essentially no technical foundation under it yet. Coming in as one of the first hires meant the stage where there’s nothing to maintain because nothing exists — building the ground everyone else will stand on.</p>\n<p><strong>Task.</strong> Building the core infrastructure and the processes around it, from scratch, was the job.</p>\n<p><strong>Action.</strong> The entire core infrastructure and its supporting processes were designed and built — the servers, the networks, the data flow, the security, the operations side. Doing that at a startup means making decisions that are hard to unmake later, so the goal wasn’t just “get something running,” it was to lay a foundation that could take the weight of rapid growth without needing to be ripped out the moment the company got bigger. Early infrastructure choices either become the thing that lets you scale or the thing you spend a year undoing; the aim was firmly the first kind.</p>\n<p><strong>Result.</strong> The startup came away with a solid technical foundation, and it’s what let the business grow quickly afterward. Being the person who builds that base from nothing is a particular kind of responsibility — get it right and nobody notices, get it wrong and everyone does — and this one held up.</p>\n", "date_published": "2026-10-21T08:22:55+02:00", "date_modified": "2026-10-21T08:22:55+02:00", "language": "en", "tags": [ "Cloud", "DevOps", "Infrastructure", "Platform Architecture", "Security", "Solution Architecture", "System Administration", "Cloud Infrastructure & Migration", "DevOps & CI/CD Automation", "Platform & Solution Architecture" ] }, { "id": "https://engineer.company/portfolio/automated-team-collaboration/", "url": "https://engineer.company/portfolio/automated-team-collaboration/", "title": "Automated team collaboration", "summary": "Automated collaboration, password, task and time management and built a semi-automatic project-showcase system, raising team productivity.", "content_html": "<p><strong>Situation.</strong> A lot of the team’s operational work — coordinating, managing passwords, tracking tasks and time — was being done by hand, and manual coordination is a quiet tax: it’s never the thing you notice, but it steadily eats hours that could go somewhere better.</p>\n<p><strong>Task.</strong> Automating that repetitive operational work was the goal.</p>\n<p><strong>Action.</strong> The pieces that lent themselves to it got automated — team collaboration, password management, task management, time management — with a semi‑automatic project‑showcase system on top. The idea across all of it was to take the routine coordination off people’s plates so it ran itself, and let them spend the recovered attention on work that actually needed a human. The showcase system was the same instinct applied to something more visible: make presenting the work mostly automatic rather than a manual chore each time.</p>\n<p><strong>Result.</strong> Operational efficiency went up and the team got more productive, because the routine coordination that used to need constant human attention now largely ran on its own. The time that was leaking into busywork went back into the actual work.</p>\n", "date_published": "2026-10-21T08:22:55+02:00", "date_modified": "2026-10-21T08:22:55+02:00", "language": "en", "tags": [ "Automation & CI/CD", "DevOps", "Project Management", "Security", "Technical Leadership", "DevOps & CI/CD Automation", "Project Management (Agile)", "Security & Access Management" ] }, { "id": "https://engineer.company/portfolio/diagnosed-and-repaired-laptop-hardware/", "url": "https://engineer.company/portfolio/diagnosed-and-repaired-laptop-hardware/", "title": "Diagnosed and repaired laptop hardware", "summary": "Diagnosed and repaired laptop hardware — screens, hinges, keyboards, trackpads, motherboards, power — and fixed software on Linux, Windows and Mac.", "content_html": "<p><strong>Situation.</strong> Laptops came in with the full range of things that go wrong with laptops — cracked screens, broken hinges, dead keyboards, failing trackpads, motherboard faults, power problems — plus the software side, across Linux, Windows and Mac. Basically, whatever was wrong with it, it landed on the bench.</p>\n<p><strong>Task.</strong> Diagnosing and fixing them reliably was the job — reliably being the operative word, because a laptop repair that doesn’t hold is just a delayed second visit.</p>\n<p><strong>Action.</strong> The hardware was repaired end to end — screens, hinges, keyboards, trackpads, motherboards, power — which at the motherboard level is genuinely fiddly, close work, and the software problems resolved across all three operating systems. The diagnosis is usually the real skill: a laptop that won’t power on could be the charger, the board, or the battery, and the repair is only as good as the guess about what’s actually broken. So the care went into finding the real fault before touching anything.</p>\n<p><strong>Result.</strong> Machines went back to their owners working and dependable, hardware and software both sorted. A repair that holds up is the only kind worth doing, and getting the diagnosis right first is what made these hold.</p>\n", "date_published": "2026-10-21T08:22:55+02:00", "date_modified": "2026-10-21T08:22:55+02:00", "language": "en", "tags": [ "Linux & Servers", "Reliability & Backups", "System Administration", "Hardware Repair & Data Recovery", "IT Support & Helpdesk" ] }, { "id": "https://engineer.company/portfolio/held-the-generator-to-981-tests-and-92-coverage/", "url": "https://engineer.company/portfolio/held-the-generator-to-981-tests-and-92-coverage/", "title": "Held the generator to 981 tests and 92% coverage", "summary": "The pipeline can be re-run against a live database without fear, which is what makes incremental content work possible at all.", "content_html": "<p><strong>Situation.</strong> A generator that assembles a database from scratch has a particular kind of bug: it works the first time and corrupts the second. Steps that insert without checking, steps that depend on the order of a previous step’s output, steps that are safe alone and not together. None of that shows up in a test that starts from nothing and runs once.</p>\n<p><strong>Task.</strong> The build had to be proved repeatable rather than merely working, and the test suite had to be large enough and strict enough that a regression could not pass through it quietly.</p>\n<p><strong>Action.</strong> The idempotence test is the blunt one and the most useful: build the entire database from an empty file, snapshot it, run the whole nineteen‑step pipeline again over the result, and require the second pass to change nothing. Row counts, contents and identifiers all have to match. Around it sit 383 test functions — 981 cases once the parameterised ones expand — across forty‑five files and 7,944 lines, covering the pipeline, the renderers, the content loaders, the targeting logic and the checkers. Branch coverage carries a floor of ninety‑two percent enforced in the build rather than reported in a summary, and the suite currently measures about 95 percent, so the floor has headroom without being decorative. Warnings are configured as failures, which is the setting that matters most in practice — a deprecation notice that prints for two years is a deprecation notice nobody reads, and the run that turns it into a red test is the run that gets it fixed.</p>\n<p><strong>Result.</strong> The pipeline can be re‑run against a live database without fear, which is what makes incremental content work possible at all. The floor is a floor, not a target, and it is worth saying that ninety‑two percent branch coverage still leaves branches nothing has ever taken — the number bounds the risk, it does not remove it, and two of the defects found later in the project were in code the coverage report showed as covered.</p>\n", "date_published": "2026-10-21T08:22:55+02:00", "date_modified": "2026-10-21T08:22:55+02:00", "language": "en", "tags": [ "Automation & CI/CD", "Databases", "DevOps", "Python", "Reliability & Backups", "Testing & QA", "Backend & API Development", "DevOps & CI/CD Automation", "Technical Leadership & Consulting" ] }, { "id": "https://engineer.company/portfolio/a-brand-200-times-better-known/", "url": "https://engineer.company/portfolio/a-brand-200-times-better-known/", "title": "A brand 200 times better known", "summary": "Grew brand reach roughly 200x across offline and online platforms, turning an unknown newcomer into a recognised name.", "content_html": "<p><strong>Situation.</strong> Engineer ApS was a brand‑new consultancy with real technical depth and almost nobody who’d heard of it. That’s a specific kind of frustrating: the skill is there, the work would be good, but none of it matters if the clients and partners who’d want it don’t know you exist. A young firm has to be seen before it can win anything.</p>\n<p><strong>Task.</strong> The task was to build a brand people would actually recognise — across both the offline and online sides — and to turn the firm’s engineering credibility into visible market presence, rather than leaving it as a well‑kept secret.</p>\n<p><strong>Action.</strong> The brand was built deliberately and kept consistent. It started with a clear identity — a voice, a visual language, and a portfolio that led with concrete engineering outcomes instead of the vague “we deliver value” language everyone else uses. Then it ran across the channels that matter for this kind of firm — the website, LinkedIn, GitHub, in‑person events — with every touchpoint saying the same thing rather than each drifting off on its own. The throughline was leading with real case studies and real results, so the credibility was something you could see evidence for, not just a claim.</p>\n<p><strong>Result.</strong> Brand reach grew around 200‑fold across the offline and online platforms — an unknown newcomer turned into something people actually recognised. And it wasn’t vanity reach; the visibility started producing a steady flow of inbound conversations and opportunities that simply hadn’t been there before.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "Brand & Marketing", "Product & Requirements", "Stakeholder & Reporting", "Brand, Marketing & SEO", "Product Strategy & Requirements" ] }, { "id": "https://engineer.company/portfolio/twice-the-engagement-from-listening-first/", "url": "https://engineer.company/portfolio/twice-the-engagement-from-listening-first/", "title": "Twice the engagement, from listening first", "summary": "Doubled brand engagement and loyalty (+100%) using market-trend analysis and consumer-behaviour insight to win a returning audience.", "content_html": "<p><strong>Situation.</strong> As the visibility climbed, Engineer ApS was reaching more people — but the engagement was thin. People noticed and moved on; the early interest wasn’t turning into relationships that lasted. Reach without engagement is just noise, and the brand was making noise more than connections.</p>\n<p><strong>Task.</strong> The aim was to deepen the engagement and the loyalty, and to do it by actually looking at what the market and the audience were responding to rather than trusting gut instinct.</p>\n<p><strong>Action.</strong> So the brand became data‑informed instead of intuition‑led. That meant looking at the market trends and how the audience actually behaved across the channels — not what anyone assumed they’d like, but what they demonstrably engaged with. It surfaced the topics and formats that drew real attention, and the content and outreach got steered toward those. The important part was tightening the loop: watch what landed, publish more of that shape next time, and let each cycle be a bit better‑aimed than the last.</p>\n<p><strong>Result.</strong> Engagement and loyalty doubled — a 100% improvement — with an audience that came back and engaged rather than glancing once and leaving, and noticeably stronger relationships with prospects and partners. The brand stopped broadcasting into the void and started building something that returned.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "Brand & Marketing", "Data Analytics", "Product & Requirements", "Stakeholder & Reporting", "Brand, Marketing & SEO", "Data Analytics & BI Dashboards", "Product Strategy & Requirements" ] }, { "id": "https://engineer.company/portfolio/a-month-of-meter-readings-in-180-milliseconds/", "url": "https://engineer.company/portfolio/a-month-of-meter-readings-in-180-milliseconds/", "title": "A month of meter readings in 180 milliseconds", "summary": "Built a Python/SQL/Bash electricity-consumption pipeline aggregating 30-day datasets in 180ms across heterogeneous JSONL sources.", "content_html": "<p><strong>Situation.</strong> The company processes large volumes of heterogeneous electricity data coming from multiple data sources, each with its own reporting frequency — ranging from hourly intervals to 15‑minute intervals, and in some cases, irregular timestamps. This variability poses a challenge when trying to create a coherent, comparable dataset. To support accurate energy analytics, this data needs to be normalized into consistent hourly consumption values, aggregated per zone.</p>\n<p><strong>Task.</strong> The objective was to take raw event data from a historical dataset (in JSONL format) and convert it into hourly‑aligned electricity consumption figures, structured as one row per hour and per zone. Specifically, the task involved:</p>\n<ul>\n<li>Aligning timestamped data to strict hourly intervals,</li>\n<li>Aggregating total electricity production within each interval by summing mix values,</li>\n<li>Incorporating cross‑border exchanges by adding imports and subtracting exports,</li>\n<li>Outputting the final values in a structured, scalable format suitable for further analysis.</li>\n<li>The solution also needed to be efficient enough to scale for large time windows (30+ days) and across multiple countries/zones.</li>\n</ul>\n<p><strong>Action.</strong> To approach this task, three different variants of the solution were implemented using Python, JQ (for command‑line JSON processing), and SQL, each optimized for different contexts:</p>\n<p>Python was chosen for its flexibility, ease of data manipulation, and ability to handle in‑memory transformations efficiently. Using pure Python functions, a pipeline was built that:</p>\n<ul>\n<li>Parsed the JSONL files into data frames</li>\n<li>Resampled the time series data to hourly intervals</li>\n<li>Aggregated production and calculated net electricity consumption (production + imports − exports)</li>\n<li>Exported the results as CSV or loaded them into a lightweight SQLite database for inspection.</li>\n</ul>\n<p>JQ was used to create a quick, minimal‑dependency solution for command‑line environments, built as a JQ filter chain.</p>\n<p>In PostgreSQL the JSONL data was imported, normalized tables created, and a series of SQL queries written.</p>\n<p>To evaluate performance, each solution was benchmarked with both 1‑day and 30‑day historical datasets.</p>\n<p><strong>Result.</strong> The Python implementation emerged as the fastest and most scalable solution, completing:</p>\n<ul>\n<li>1‑day data processing in just 34ms</li>\n<li>30‑day dataset in 180ms</li>\n</ul>\n<p>This confirmed its suitability for handling larger timeframes while maintaining sub‑second performance. It also offered clear, maintainable code that could be easily extended to multi‑zone processing or integrated into an ETL pipeline.</p>\n<p>Overall, the multi‑tool approach demonstrated flexibility in tool usage, strong performance optimization, and robust handling of time‑alignment and aggregation challenges in real‑world electricity data pipelines.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "Automation & CI/CD", "Data Analytics", "Data Engineering", "Data Pipelines (ETL/ELT)", "Databases", "Performance Tuning", "PostgreSQL", "Python", "SQL", "Data Analytics & BI Dashboards", "Data Pipeline Development (ETL/ELT)", "Database Performance Tuning" ] }, { "id": "https://engineer.company/portfolio/improved-gis-map-performance-10x-on-postgresql/", "url": "https://engineer.company/portfolio/improved-gis-map-performance-10x-on-postgresql/", "title": "Improved GIS map performance 10x on PostgreSQL", "summary": "Improved a GIS map application 10x by migrating MSSQL to PostgreSQL — spatial queries dropped from 2.5s to under 250ms.", "content_html": "<p><strong>Situation.</strong> The organization’s geographical map application, which supported real‑time spatial queries for many users, was experiencing severe performance bottlenecks. Latency in rendering map layers and querying location‑based data impacted both user experience and backend service reliability. The system was backed by a legacy Microsoft SQL Server (MSSQL) database that lacked native geospatial indexing.</p>\n<p><strong>Task.</strong> Improve the performance, scalability, and security of the map application, with a specific goal of reducing query latency and increasing throughput for spatial operations.</p>\n<p><strong>Action.</strong> Led a strategic migration of the database from MSSQL to PostgreSQL with the PostGIS extension to enable native geospatial support.</p>\n<ul>\n<li>Designed a new schema optimized for spatial data, introducing GiST and SP‑GiST indexes on geometry and geography columns for faster querying.</li>\n<li>Defined strict foreign key and check constraints to ensure relational integrity and enforce data validation rules on spatial coordinates.</li>\n<li>Migrated over 50 million spatial records using ETL pipelines with data transformation steps to conform to new SRID standards (EPSG:4326).</li>\n<li>Tuned PostgreSQL configuration parameters (e.g., work_mem, effective_cache_size) for optimal I/O performance under concurrent access.</li>\n<li>Implemented role‑based access controls (RBAC) and row‑level security to enforce data protection policies across multiple user groups.</li>\n</ul>\n<p><strong>Result.</strong> Achieved a 10x improvement in spatial query performance, reducing average response time from 2.5 seconds to under 250 milliseconds. Backend CPU load dropped by 65%, and system availability improved during peak usage. Security posture was also strengthened with granular access policies and data validation constraints, reducing potential for spatial data corruption.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "Backend Engineering", "Data Engineering", "Data Pipelines (ETL/ELT)", "Databases", "GIS / Geospatial", "Migrations & Modernization", "Performance Tuning", "PostgreSQL", "Security", "SQL", "Database Migration & Modernization", "Database Performance Tuning", "GIS & Geospatial Solutions" ] }, { "id": "https://engineer.company/portfolio/resolved-1000-issues-in-geographical-data/", "url": "https://engineer.company/portfolio/resolved-1000-issues-in-geographical-data/", "title": "Resolved 1,000 issues in geographical data", "summary": "Resolved 1,000+ issues in geographic and time-series big data with GDAL, PostGIS, ArcGIS, Mapbox, QGIS and SQL — 35% faster queries.", "content_html": "<p><strong>Situation.</strong> While working on a large‑scale geospatial analytics project, the team encountered numerous inconsistencies and anomalies within the geographical and time‑series datasets. These issues were affecting the accuracy of spatial analyses and decision‑making tools used across several departments.</p>\n<p><strong>Task.</strong> The responsibility was to identify, resolve, and optimize over 1,000 data quality issues within these complex datasets to ensure the integrity and performance of downstream applications and visualizations.</p>\n<p><strong>Action.</strong> Spatial errors were systematically diagnosed and corrected using a combination of tools, including GDAL, QGIS, and ArcGIS, with automated workflows implemented in Bash scripting to streamline recurring data cleaning tasks. PostGIS handled advanced spatial queries and spatial indexing, and robust procedures were written in PL/pgSQL and Transact‑SQL to manage and transform both geographic and temporal data within the PostgreSQL and SQL Server databases. Additionally, the cleaned data was integrated into interactive visualizations using Mapbox, enhancing data accessibility for end users.</p>\n<p><strong>Result.</strong> Through these efforts, over 1,000 critical issues were resolved, significantly improving data accuracy and processing speed. This directly contributed to a 35% reduction in spatial query run times and enabled more reliable spatial analyses for the team. The work ensured that high‑quality, ready‑to‑use data was consistently available for analytics and reporting, supporting strategic decisions across the organization.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "Automation & CI/CD", "Data Engineering", "Data Governance", "Data Pipelines (ETL/ELT)", "Databases", "GIS / Geospatial", "Performance Tuning", "PostgreSQL", "SQL", "Data Governance & Quality", "Data Pipeline Development (ETL/ELT)", "GIS & Geospatial Solutions" ] }, { "id": "https://engineer.company/portfolio/built-the-companys-first-observability-dashboard/", "url": "https://engineer.company/portfolio/built-the-companys-first-observability-dashboard/", "title": "Built the company's first observability dashboard", "summary": "Built the company's first observability dashboard for real-time system insight, helping teams detect and resolve incidents 40% faster.", "content_html": "<p><strong>Situation.</strong> The company was facing challenges in monitoring real‑time system performance, which often led to delayed incident responses and reduced visibility into infrastructure health. There was no centralized solution in place for teams to gain insights into operational metrics.</p>\n<p><strong>Task.</strong> The task was to develop a solution that would enable technical and non‑technical stakeholders to monitor key system metrics in real time, with a focus on accessibility, clarity, and proactive issue detection.</p>\n<p><strong>Action.</strong> The company’s first observability dashboard was designed and implemented, collecting all essential system metrics — such as CPU, RAM, HDD, temperature, and more — from remote Linux servers via SSH. Even Docker containers were monitored using this method. Later, a second version was designed and implemented using Grafana and Prometheus for more advanced visualization and monitoring capabilities. Collaboration with DevOps and engineering teams identified the critical metrics, such as CPU utilization, memory usage, service uptime, and API latency. Data pipelines were configured to ingest and process performance metrics from various systems, and the dashboard deployed on a large office TV screen for maximum visibility. Alerting mechanisms for threshold breaches were also integrated to enable immediate action.</p>\n<p><strong>Result.</strong> The dashboard significantly improved system transparency and response time to operational issues. Teams were able to detect and resolve incidents 40% faster. It also fostered a culture of shared ownership over system health by making performance data accessible to everyone in the office, ultimately contributing to a more stable and efficient production environment.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "Automation & CI/CD", "Containers (Docker/Kubernetes)", "Data Analytics", "DevOps", "Infrastructure", "Linux & Servers", "Monitoring & Observability", "Reliability & Backups", "Data Analytics & BI Dashboards", "Site Reliability & Monitoring" ] }, { "id": "https://engineer.company/portfolio/delivered-8-power-bi-projects-with-manuals/", "url": "https://engineer.company/portfolio/delivered-8-power-bi-projects-with-manuals/", "title": "Delivered 8 Power BI projects with manuals", "summary": "Delivered 8 Power BI analytics projects (Node.js API, Python FastAPI) with full manuals, improving report generation efficiency by 60%.", "content_html": "<p><strong>Situation.</strong> The organization needed dynamic, visual insights into complex datasets involving electrical grid performance and geographical distribution metrics to support decision‑making across technical and strategic teams.</p>\n<p><strong>Task.</strong> Develop interactive dashboards and reporting solutions that could effectively present both real‑time and historical geographical and electrical data, enabling stakeholders to quickly identify trends, anomalies, and performance indicators.</p>\n<p><strong>Action.</strong> Integrated Microsoft Power BI with a custom backend stack using NodeJS API and Python FastAPI to streamline data ingestion, transformation, and visualization. Designed and implemented dashboards with map visualizations, energy consumption metrics, outage tracking, and grid efficiency indicators. Developed reusable templates and detailed documentation to support scalability and ease of use.</p>\n<p><strong>Result.</strong> Successfully delivered 8 data analytics projects, improving report generation efficiency by 60% and enabling cross‑functional teams to make faster, data‑driven decisions. Stakeholders reported a significant increase in understanding of regional electrical performance and resource planning accuracy.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "APIs & Integration", "Backend Engineering", "Data Analytics", "Data Engineering", "Documentation", "GIS / Geospatial", "Product & Requirements", "Python", "Backend & API Development", "Data Analytics & BI Dashboards", "Technical Documentation" ] }, { "id": "https://engineer.company/portfolio/released-500-electricity-and-gis-data-reports/", "url": "https://engineer.company/portfolio/released-500-electricity-and-gis-data-reports/", "title": "Released 500 electricity and GIS data reports", "summary": "Published 500 electricity and GIS data-analysis reports — a cross-department reference for load balancing, efficiency and anomaly detection.", "content_html": "<p><strong>Situation.</strong> The team managed vast datasets generated by smart‑meters installed across multiple geographic regions. These smart‑meters produced granular, time‑series electrical consumption data used by energy analysts, engineers, and regional planners for operational and strategic decision‑making.</p>\n<p><strong>Task.</strong> The responsibility was to produce high‑quality, transparent, and reproducible analytical reports that could uncover patterns in energy consumption, detect anomalies, and identify regional usage trends, while ensuring non‑technical stakeholders could easily interpret and reuse the findings.</p>\n<p><strong>Action.</strong> 500+ in‑depth data analysis reports were created and delivered, using pure SQL to perform all data extraction, transformation, and analysis tasks, working directly within cloud‑based environments such as PostgreSQL and BigQuery. The data included geolocation coordinates, meter IDs, timestamped energy usage, and environmental metadata. The SQL scripts featured CTEs, window functions, subqueries, and geospatial joins, allowing for scalable and efficient processing.</p>\n<p>Each report included annotated SQL code, enabling colleagues and collaborators to fully reproduce and audit the research, which significantly reduced the time needed for follow‑up analysis. Troubleshooting notes were also added and common data quality issues documented, such as missing GPS coordinates or corrupted meter values, with recommended handling procedures.</p>\n<p><strong>Result.</strong> The reports became a standard reference across departments, aiding in regional load balancing, energy efficiency planning, and anomaly detection. By ensuring full transparency and reproducibility, the work helped improve stakeholder trust in the data and contributed to more accurate forecasting models and a 10–15% improvement in operational planning efficiency.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "Data Analytics", "Data Governance", "Data Warehousing", "Databases", "Documentation", "GIS / Geospatial", "PostgreSQL", "SQL", "Stakeholder & Reporting", "Data Analytics & BI Dashboards", "Data Governance & Quality", "GIS & Geospatial Solutions" ] }, { "id": "https://engineer.company/portfolio/ran-100-postgresql-ms-sql-bigquery-warehouses/", "url": "https://engineer.company/portfolio/ran-100-postgresql-ms-sql-bigquery-warehouses/", "title": "Ran 100 PostgreSQL, MS SQL, BigQuery warehouses", "summary": "Architected and ran 100 PostgreSQL, MS SQL and BigQuery data-warehouse databases for GIS and time-series data — query times down 40–60%.", "content_html": "<p><strong>Situation.</strong> In a rapidly growing tech company, there was a critical need to create, manage, and optimize a diverse portfolio of 100+ databases across PostgreSQL, Microsoft SQL Server, and Google BigQuery. These databases primarily handled complex datasets, including geographic information systems (GIS) data (e.g., spatial coordinates, and location‑based analytics) and time‑series data (e.g., sensor readings, logs, and real‑time metrics). The existing infrastructure faced challenges with scalability, query performance, and data consistency, particularly as the volume of data increased exponentially. The organization required a robust architecture to ensure data reliability, and reduce operational costs.</p>\n<p><strong>Task.</strong> The primary objective was to design, implement, and manage a scalable, high‑performance data warehouse ecosystem tailored for GIS and time‑series data. This involved:</p>\n<ul>\n<li>Addressing performance bottlenecks in complex spatial and time‑based queries.</li>\n<li>Ensuring scalability to handle growing data volumes while maintaining cost efficiency.</li>\n<li>Collaborating with cross‑functional teams (e.g., data scientists, product managers) to align database design with business needs.</li>\n</ul>\n<p><strong>Action.</strong> To achieve these goals:</p>\n<ol>\n<li>Designed Scalable Architectures:</li>\n</ol>\n<ul>\n<li>Created normalized and denormalized schemas for PostgreSQL and SQL Server, leveraging spatial indexing (e.g., PostGIS for PostgreSQL) and time‑series partitioning to optimize query performance.</li>\n<li>Utilized BigQuery’s time‑partitioned and clustered tables for efficient handling of large‑scale time‑series data.</li>\n</ul>\n<ol start=\"2\">\n<li>Implemented Optimization Strategies:</li>\n</ol>\n<ul>\n<li>Introduced query optimization techniques, such as indexing, materialized views, and caching, to reduce latency for GIS and time‑series queries.</li>\n<li>Applied data compression and columnar storage in BigQuery to minimize storage costs and improve scan speeds.</li>\n</ul>\n<ol start=\"3\">\n<li>Collaborated on Cross‑Platform Integration:</li>\n</ol>\n<ul>\n<li>Documented best practices for GIS and time‑series data modeling to guide teams in future projects.</li>\n</ul>\n<p><strong>Result.</strong> The initiatives led to significant improvements:</p>\n<ul>\n<li>Performance Gains: Query response times for GIS and time‑series data decreased by 40–60%, enabling faster analytics and decision‑making.</li>\n<li>Operational Reliability: Automated monitoring reduced downtime by 50%, while standardized processes improved team productivity and reduced errors.</li>\n<li>Business Impact: The optimized infrastructure enabled the company to launch new data‑driven products (e.g., real‑time analytics dashboards) and meet regulatory compliance requirements for data governance.</li>\n</ul>\n<p>This work solidified the organization’s ability to handle complex data challenges.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "Data Engineering", "Data Governance", "Data Warehousing", "Databases", "GIS / Geospatial", "Performance Tuning", "Platform Architecture", "PostgreSQL", "Reliability & Backups", "SQL", "Data Warehouse Design", "Database Administration (DBA)", "Database Design & Modeling", "Database Performance Tuning", "GIS & Geospatial Solutions" ] }, { "id": "https://engineer.company/portfolio/ran-10-postgresql-and-ms-sql-servers-on-ubuntu/", "url": "https://engineer.company/portfolio/ran-10-postgresql-and-ms-sql-servers-on-ubuntu/", "title": "Ran 10 PostgreSQL and MS SQL servers on Ubuntu", "summary": "Designed and maintained 10 PostgreSQL and MS SQL servers on Ubuntu Linux VPS at 99.9% uptime, with 30% faster queries via tuning.", "content_html": "<p><strong>Situation.</strong> As a DevOps engineer at a mid‑sized tech company, the task was managing and optimizing database infrastructure to support a growing user base and critical business applications. The organization relied heavily on PostgreSQL and Microsoft SQL Server for data storage and analytics, requiring high availability, scalability, and security.</p>\n<p><strong>Task.</strong> The primary responsibility was to architect, deploy, configure, and maintain 10 PostgreSQL and MS SQL Server instances on Ubuntu Linux VPS environments. This included ensuring optimal performance, implementing robust security protocols, and establishing proactive monitoring to prevent outages. Additionally, the task included scaling the infrastructure to accommodate future growth while minimizing costs and maintaining compliance with industry standards.</p>\n<p><strong>Action.</strong> 1. Deployment & Configuration:</p>\n<ul>\n<li>Installed and configured PostgreSQL 14 and MS SQL Server 2019 on Ubuntu 20.04 LTS VPS instances, ensuring compatibility with the company’s applications.</li>\n<li>Set up automated backups using <code>pg_dump</code> for PostgreSQL and SQL Server Agent jobs for MS SQL, with retention policies and offsite storage.</li>\n<li>Optimized server configurations (e.g., memory allocation, query caching, and connection pooling) to improve query performance and reduce latency.</li>\n</ul>\n<ol start=\"2\">\n<li>\n<p>Monitoring & Maintenance:</p>\n<ul>\n<li>Implemented monitoring tools like Prometheus, Grafana, to track CPU, memory, disk I/O, and query performance metrics in real time.</li>\n<li>Conducted regular patching and updates for both databases and the Ubuntu OS to address security vulnerabilities and ensure compliance.</li>\n<li>Created custom scripts for log analysis.</li>\n</ul>\n</li>\n<li>\n<p>Security & Scalability:</p>\n<ul>\n<li>Configured firewalls (UFW), enforced role‑based access control (RBAC) to protect sensitive data.</li>\n<li>Documented procedures for disaster recovery, including point‑in‑time restores and failover protocols.</li>\n</ul>\n</li>\n</ol>\n<p><strong>Result.</strong> - Achieved 99.9% uptime across all 10 database servers.</p>\n<ul>\n<li>Improved query response times by 30% through configuration tuning and index optimization, enhancing application performance.</li>\n<li>Reduced manual maintenance tasks by 50% via automation, freeing up 10+ hours per month for strategic projects.</li>\n<li>Successfully scaled the infrastructure to support a 40% increase in user traffic without service degradation, contributing to a 20% revenue growth in the following quarter.</li>\n<li>Received recognition from the CTO for implementing security best practices that prevented potential data breaches.</li>\n</ul>\n<p>This experience solidified deep expertise in database management, DevOps automation, and infrastructure optimization, delivering reliable, secure, and scalable solutions for complex enterprise environments.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "Automation & CI/CD", "Databases", "DevOps", "Infrastructure", "Linux & Servers", "Monitoring & Observability", "Performance Tuning", "PostgreSQL", "Reliability & Backups", "Security", "System Administration", "Database Administration (DBA)", "Site Reliability & Monitoring" ] }, { "id": "https://engineer.company/portfolio/accelerated-postgresql-performance-by-10x/", "url": "https://engineer.company/portfolio/accelerated-postgresql-performance-by-10x/", "title": "Accelerated PostgreSQL performance by 10x", "summary": "Accelerated PostgreSQL 10x via indexing, partitioning and query tuning for user, tenant, geospatial and time-series data — resource use down 40%.", "content_html": "<p><strong>Situation.</strong> The PostgreSQL database served a mid‑sized application managing user accounts, tenant information, geographical data (e.g., locations, regions), and daily electrical consumption metrics. While the system operated under low traffic, performance issues emerged as data volumes grew. Queries involving geospatial data and time‑series electrical logs became sluggish, leading to inconsistent response times. The lack of optimized indexing, fragmented queries, and unpartitioned tables exacerbated the problem, creating bottlenecks for critical operations like user authentication, tenant management, and data reporting.</p>\n<p><strong>Task.</strong> The goal was to enhance database performance by 10x without overhauling the existing architecture. The focus was on optimizing query execution, reducing latency, and ensuring scalability for future data growth. Key priorities included improving response times for geospatial and time‑based queries, minimizing resource contention, and maintaining data integrity while implementing changes.</p>\n<p><strong>Action.</strong> 1. <strong>Indexing:</strong> Analyzed frequently queried columns (e.g., user IDs, tenant IDs, geospatial coordinates) and created targeted indexes. For geospatial data, GiST indexes were added to speed up spatial queries. Composite indexes were introduced for multi‑column filters, such as tenant ID + date ranges for electrical data.\n2. <strong>Partitioning:</strong> Implemented time‑based range partitioning for the electrical data table, splitting it by day/month. This reduced the dataset size for queries and improved scan efficiency. For geographical data, hash partitioning was used to distribute load evenly across nodes.\n3. <strong>Query Optimization:</strong> Rewrote complex queries to avoid full table scans, leveraging CTEs (Common Table Expressions) and materialized views for frequently accessed datasets. The <code>EXPLAIN ANALYZE</code> tool identified inefficient joins and subqueries, which were restructured for better execution plans. Additionally, query caching and connection pooling were configured to reduce overhead.</p>\n<p><strong>Result.</strong> Post‑optimization, query response times improved by 10x, with critical operations (e.g., user authentication, geospatial lookups) executing in milliseconds. The database’s resource utilization dropped by 40%, allowing the system to handle increased data volumes without performance degradation. Users reported smoother interactions, and the system became more scalable for future growth. The changes also reduced the need for hardware upgrades, saving costs while ensuring long‑term reliability. This project demonstrated how strategic indexing, partitioning, and query refinement can transform even low‑load systems into efficient, future‑ready databases.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "Data Engineering", "Databases", "GIS / Geospatial", "Performance Tuning", "PostgreSQL", "SQL", "Database Administration (DBA)", "Database Performance Tuning" ] }, { "id": "https://engineer.company/portfolio/automated-gis-saas-deployment-with-github-actions/", "url": "https://engineer.company/portfolio/automated-gis-saas-deployment-with-github-actions/", "title": "Automated GIS SaaS deployment with GitHub Actions", "summary": "Automated GIS SaaS deployment, data processing and reporting with GitHub Actions CI/CD, Python, Bash and SQL — shorter, reliable releases.", "content_html": "<p><strong>Situation.</strong> Getting the GIS SaaS app deployed, processing its data and producing the reports were all manual steps — and manual steps are both slow and quietly dangerous. Every release ate engineering time and carried the chance of a slip, and the recurring data and reporting work sat there eating capacity week after week.</p>\n<p><strong>Task.</strong> Automating the whole path from code to production, plus the recurring data‑processing and reporting, was the task — the goal being releases that were fast, safe and repeatable rather than a careful manual ritual each time.</p>\n<p><strong>Action.</strong> The whole path got automated. GitHub Actions workflows took over the test‑build‑deploy cycle, so a release stopped depending on someone remembering the steps. The recurring data processing and the reports moved into scheduled Python, Bash and SQL jobs, so they just ran instead of being someone’s chore. And the configuration and secrets were standardised so every environment behaved identically — which is what kills the “works on my machine” surprises, because there stops being a “my machine” that’s different from production.</p>\n<p><strong>Result.</strong> Deployment, data processing and reporting all became automated and reliable, the manual toil came off the team’s plate, and the release cycle got shorter. The team could put its attention on the product instead of the operations wrapped around it.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "Automation & CI/CD", "Cloud", "Data Engineering", "Data Pipelines (ETL/ELT)", "DevOps", "GIS / Geospatial", "Python", "Reliability & Backups", "SQL", "Data Pipeline Development (ETL/ELT)", "DevOps & CI/CD Automation", "GIS & Geospatial Solutions" ] }, { "id": "https://engineer.company/portfolio/automated-delivery-of-20-gis-data-pipelines/", "url": "https://engineer.company/portfolio/automated-delivery-of-20-gis-data-pipelines/", "title": "Automated delivery of 20 GIS data pipelines", "summary": "Automated 20 GIS data pipelines and app-data ETL processes, streamlining infrastructure automation and delivering dependable, current data.", "content_html": "<p><strong>Situation.</strong> The platform ran on a lot of GIS data pipelines and application‑data ETL processes, and they were being delivered and watched by hand. Hand‑run pipelines create bottlenecks, they drift out of consistency, and worst of all they carry a constant low risk that one quietly fails and nobody notices until the data’s already wrong downstream.</p>\n<p><strong>Task.</strong> Automating the delivery of those pipelines and ETL processes — so the data flowed reliably and predictably without someone shepherding it — was the task.</p>\n<p><strong>Action.</strong> Twenty GIS data pipelines and the application‑data ETL came under automated delivery, end to end. The scheduling, the logging and the failure handling got standardised, so every pipeline behaved the same way and, crucially, you could see when one didn’t — a silent failure is only silent if nothing’s watching. And they were folded into the existing infrastructure automation and reporting, so they were part of one coherent system rather than a drawer full of scripts someone had to remember to run.</p>\n<p><strong>Result.</strong> All twenty pipelines and their ETL ran automatically and predictably, and the whole infrastructure‑automation and reporting picture got tidier for it. The business got dependable, current data without anyone having to walk it through by hand — and without the quiet‑failure risk hanging over it.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "Automation & CI/CD", "Data Engineering", "Data Pipelines (ETL/ELT)", "DevOps", "GIS / Geospatial", "Monitoring & Observability", "Reliability & Backups", "Data Pipeline Development (ETL/ELT)", "DevOps & CI/CD Automation", "GIS & Geospatial Solutions" ] }, { "id": "https://engineer.company/portfolio/automated-100-critical-data-backups-using-barman/", "url": "https://engineer.company/portfolio/automated-100-critical-data-backups-using-barman/", "title": "Automated 100 critical data backups using Barman", "summary": "Automated 100 critical database backups with Barman, Google Cloud, Bash and Python — turning data recoverability into a tested fact.", "content_html": "<p><strong>Situation.</strong> The critical GIS data, the application instances and the databases were scattered across systems with backups that were inconsistent and partly manual. For a product that lives on its data, that’s not a risk you can leave sitting — and the day you actually need a backup is precisely the worst day to find out it was incomplete.</p>\n<p><strong>Task.</strong> The task was to guarantee that all the critical data could be recovered, which meant automating comprehensive, verified backups across the whole estate — verified being the word that matters.</p>\n<p><strong>Action.</strong> The backup regime was built end to end. A hundred critical data backups got automated, with Barman handling the PostgreSQL side and Google Cloud holding the offsite copies, and the whole thing was orchestrated and validated with Bash and Python — because a backup you’ve taken but never checked isn’t really a backup, it’s a hope. So there were retention policies to keep them current and integrity checks to confirm each one was actually good, not just present.</p>\n<p><strong>Result.</strong> Backups ran automatically and were verifiable across every database, which turned data recoverability from an assumption into something tested. A major operational risk came off the business and got replaced with a recovery path you could actually trust — the difference being that this one had been checked, not just configured.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "Automation & CI/CD", "Cloud", "Databases", "DevOps", "Infrastructure", "PostgreSQL", "Python", "Reliability & Backups", "Backup & Disaster Recovery", "Database Administration (DBA)", "DevOps & CI/CD Automation" ] }, { "id": "https://engineer.company/portfolio/ran-20-docker-apps-on-google-cloud-and-aws/", "url": "https://engineer.company/portfolio/ran-20-docker-apps-on-google-cloud-and-aws/", "title": "Ran 20 Docker apps on Google Cloud and AWS", "summary": "Deployed and maintained 20 Docker containerized apps across Google Cloud and AWS, troubleshooting with Podman and Kubernetes for stable releases.", "content_html": "<p><strong>Situation.</strong> There was a growing set of containerised applications — including some R‑based analytics apps — running across both Google Cloud and AWS. Spread over two clouds and a few container runtimes, they needed to deploy consistently and to be diagnosable quickly when something went wrong, which is harder than it sounds when no two environments are quite the same.</p>\n<p><strong>Task.</strong> Deploying and maintaining those workloads reliably, and being able to diagnose problems fast across the runtimes and clouds, was the job.</p>\n<p><strong>Action.</strong> The containerised estate was managed across both clouds — twenty Docker applications deployed and maintained with consistent configuration and monitoring, so they weren’t each their own snowflake. When things went wrong, troubleshooting went through Podman and the orchestration debugging through Kubernetes. The R‑based analytics apps got dedicated attention in the Google Cloud and AWS production environments, kept stable and reproducible, which for analytics matters — a result you can’t reproduce isn’t much of a result.</p>\n<p><strong>Result.</strong> The containerised estate ran reliably across both clouds, problems got diagnosed faster, and deployments stayed stable and reproducible. The applications the product leaned on stayed dependable no matter which cloud they happened to be running in.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "Automation & CI/CD", "Cloud", "Containers (Docker/Kubernetes)", "Data Analytics", "DevOps", "Infrastructure", "Reliability & Backups", "Cloud Infrastructure & Migration", "Containerization & Orchestration", "DevOps & CI/CD Automation" ] }, { "id": "https://engineer.company/portfolio/managed-30-ubuntu-linux-vps-instances/", "url": "https://engineer.company/portfolio/managed-30-ubuntu-linux-vps-instances/", "title": "Managed 30 Ubuntu Linux VPS instances", "summary": "Managed 30 Ubuntu Linux VPS instances with tested disaster-recovery strategies and solid networking — a resilient foundation to grow on.", "content_html": "<p><strong>Situation.</strong> The company ran on a fleet of Ubuntu Linux VPS instances whose setup had grown organically over time — which is a polite way of saying it had accumulated rather than been designed. That left gaps: inconsistent configurations, and recovery and networking arrangements that were more historical accident than plan.</p>\n<p><strong>Task.</strong> The task was to get the fleet under proper management, harden the disaster‑recovery side, and make the network configuration consistent and sensible across every instance.</p>\n<p><strong>Action.</strong> The thirty instances came under deliberate management — treated as one coherent fleet rather than thirty individual pets. Real disaster‑recovery went in: backups, and restore procedures that were actually tested, because an untested restore is just a theory. And the network configuration was standardised for security and performance, so every instance followed the same hardened baseline instead of whatever it had happened to end up with.</p>\n<p><strong>Result.</strong> The infrastructure became resilient and consistent, with recovery paths that had been tested and networking you could rely on. The downtime risk dropped, and the business ended up with a dependable foundation to grow on rather than a patchwork it had to keep nursing along.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "DevOps", "Infrastructure", "Linux & Servers", "Networking & VPN", "Reliability & Backups", "Security", "System Administration", "Backup & Disaster Recovery", "Networking & VPN Setup" ] }, { "id": "https://engineer.company/portfolio/cut-operational-risk-with-grafana-and-prometheus/", "url": "https://engineer.company/portfolio/cut-operational-risk-with-grafana-and-prometheus/", "title": "Cut operational risk with Grafana and Prometheus", "summary": "Built a Grafana + Prometheus monitoring dashboard that catches problems before they escalate, shifting the team from firefighting to prevention.", "content_html": "<p><strong>Situation.</strong> Problems usually got noticed after they’d already hit users, because there was no single view of how the systems were doing. Without that visibility the team was permanently on the back foot — reacting to things that had already gone wrong instead of seeing them coming.</p>\n<p><strong>Task.</strong> The aim was to cut the operational risk by giving the team real‑time visibility into the systems they depended on.</p>\n<p><strong>Action.</strong> The observability layer was built out. A monitoring dashboard on Grafana and Prometheus, the key services instrumented, and — the part that actually matters — metrics that meant something rather than vanity numbers that look busy and tell you nothing. Then alert thresholds set on those, surfaced where the team would actually see them and could act while there was still time to act.</p>\n<p><strong>Result.</strong> Problems started getting caught and dealt with before they escalated, and system reliability improved for it. The team shifted from reactive firefighting to something calmer and more proactive — catching issues while they were still small enough to be boring.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "Automation & CI/CD", "DevOps", "Infrastructure", "Monitoring & Observability", "Reliability & Backups", "DevOps & CI/CD Automation", "Site Reliability & Monitoring" ] }, { "id": "https://engineer.company/portfolio/led-a-gis-map-application-to-10x-revenue-growth/", "url": "https://engineer.company/portfolio/led-a-gis-map-application-to-10x-revenue-growth/", "title": "Led a GIS map application to 10x revenue growth", "summary": "Led development of a GIS map application that became a platform cornerstone, driving a 10x revenue increase via upsells and data licensing.", "content_html": "<p><strong>Situation.</strong> The core challenge was to build a platform that could track, monitor, and optimize renewable energy assets like solar panels and wind turbines. However, the initial solution lacked robust geospatial capabilities, making it difficult for clients to visualize asset locations, analyze spatial data, or derive actionable insights. Recognizing this gap, the leadership team prioritized developing a GIS (Geographic Information System) map application to enhance the platform’s value and meet evolving customer needs.</p>\n<p><strong>Task.</strong> The task was to lead the development of the GIS application, a critical component to differentiate the product in the competitive green energy market. The role extended beyond software development — spanning tech leader, devops engineer, data engineer, and SRE (Site Reliability Engineer), ensuring the solution aligned with the company’s growth trajectory. The goal was to create a scalable, intuitive GIS tool that integrated seamlessly with the SaaS platform, enabling users to visualize asset locations, track performance metrics, and leverage spatial data for decision‑making. This required balancing technical innovation with the constraints of a growing startup, while ensuring the product could evolve alongside the company.</p>\n<p><strong>Action.</strong> It began with collaborating with stakeholders to define the GIS application’s core functionalities, focusing on integration with the existing SaaS platform and real‑time data visualization. Given the small team size, a modular architecture was designed using open‑source GIS libraries to keep the system lightweight and scalable. CI/CD, automated infrastructure provisioning, and monitoring tools were also implemented to ensure reliability. As the team expanded, new engineers were mentored, cross‑functional collaboration facilitated, and user feedback prioritized to refine the application iteratively. Over time, the GIS tool evolved from a basic prototype into a sophisticated platform, incorporating advanced analytics and custom dashboards to meet customer demands.</p>\n<p><strong>Result.</strong> The GIS application became a cornerstone of the SaaS platform, driving a 10x increase in revenue through upsells, data licensing, and new customer acquisitions. Its ability to visualize green energy assets in real time improved operational efficiency for clients, while the tool’s continuous refinement positioned it as a primary data asset. The success of the project not only solidified the company’s reputation in the renewable energy sector but also demonstrated the value of a multi‑disciplinary approach in a fast‑paced startup environment. By combining technical expertise with strategic vision, the GIS application became a key differentiator, fueling long‑term growth and innovation.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "DevOps", "Full‑Stack Development", "GIS / Geospatial", "Mentoring & Coaching", "Platform Architecture", "Product & Requirements", "Reliability & Backups", "Solution Architecture", "Team Leadership", "Technical Leadership", "Full‑Stack Product Development", "GIS & Geospatial Solutions", "Platform & Solution Architecture", "Product Strategy & Requirements", "Technical Leadership & Consulting" ] }, { "id": "https://engineer.company/portfolio/directed-full-stack-gis-map-development/", "url": "https://engineer.company/portfolio/directed-full-stack-gis-map-development/", "title": "Directed full‑stack GIS map development", "summary": "Directed full-stack GIS map development (PostgreSQL, Mapbox, React, Node.js), delivering one integrated, extensible core of the platform.", "content_html": "<p><strong>Situation.</strong> By this stage the GIS map had stopped being a feature and become the reason customers logged in. The trouble was that it had grown up in pieces. Spatial data lived in PostgreSQL, the map itself was drawn with Mapbox, and the application around it was ReactJS on the front with NodeJS behind. Each part worked on its own. They just hadn’t been built to fit together, and the seams were starting to show.</p>\n<p><strong>Task.</strong> The role covered the full‑stack development of the map and the technical direction that came with it: the data model, the rendering, the API and the React front end. The goal was to turn four things that happened to share a repository into one product worth standing behind.</p>\n<p><strong>Action.</strong> The architecture was set first, then the work stayed close to the code rather than steering from a distance. On the data side, the PostgreSQL spatial model was kept tidy so queries didn’t slow to a crawl as the datasets grew. Mapbox did the drawing; the job was feeding it the right data at the right zoom levels instead of everything at once. On the application side, the ReactJS and NodeJS work got reviewed, the team was pushed toward shared conventions, and responsibilities kept getting moved back to the layer they belonged in whenever one started leaking into the next. A fair amount of it was unglamorous work: catching the small inconsistencies before they hardened into architecture.</p>\n<p><strong>Result.</strong> What came out was a single integrated map application, with data, rendering and interface finally pulling the same way. It became a core part of the platform, and something the team could keep extending without it buckling every time a layer got added.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "APIs & Integration", "Backend Engineering", "Databases", "Frontend Engineering", "Full‑Stack Development", "GIS / Geospatial", "Performance Tuning", "Platform Architecture", "PostgreSQL", "Technical Leadership", "Backend & API Development", "Frontend Development", "Full‑Stack Product Development", "GIS & Geospatial Solutions", "Technical Leadership & Consulting" ] }, { "id": "https://engineer.company/portfolio/saved-4000-hours-growing-a-team-from-2-to-18/", "url": "https://engineer.company/portfolio/saved-4000-hours-growing-a-team-from-2-to-18/", "title": "Saved 4,000 hours growing a team from 2 to 18", "summary": "Saved ~4,000 hours by growing and mentoring a team from 2 to 18, redesigning workflows and building durable engineering capability.", "content_html": "<p><strong>Situation.</strong> A team of two couldn’t keep up anymore. The product was pulling in more work than a pair could deliver, and the way the work happened — knowledge in people’s heads, no real conventions — wasn’t going to survive being scaled up. Adding bodies to a team that loose usually just makes the chaos bigger.</p>\n<p><strong>Task.</strong> The task was to grow the team and, at the same time, build the structure that would let a bigger group move faster instead of slower. Mentoring the new people was half of it. Fixing the workflows so nobody stalled waiting on someone else was the other half.</p>\n<p><strong>Action.</strong> The team grew from 2 to 18 over time, with the hiring and the mentoring treated as the same job: everyone who joined needed to be able to work unsupervised. Shared standards meant code and process looked the same regardless of who wrote them, and real effort went into the handovers between specialities, because that is where teams quietly lose their days. When something kept tripping people up, the fix went to the process rather than the symptom.</p>\n<p><strong>Result.</strong> The bigger, better‑mentored team, running on workflows that had actually been designed, saved on the order of 4,000 hours. But the number isn’t really the point. What got built was durable engineering capability — a group that could carry the work with or without any one person in the room.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "Agile & Scrum", "Mentoring & Coaching", "Project Management", "Team Leadership", "Technical Leadership", "Project Management (Agile)", "Team Building & Mentoring", "Technical Leadership & Consulting" ] }, { "id": "https://engineer.company/portfolio/optimized-forecasting-and-investment-strategies/", "url": "https://engineer.company/portfolio/optimized-forecasting-and-investment-strategies/", "title": "Optimized forecasting and investment strategies", "summary": "Optimised forecasting and investment strategy for 11 electricity grid operators with data-driven GIS — trustworthy, targeted planning.", "content_html": "<p><strong>Situation.</strong> Electricity grid operators live and die on decisions about where to reinforce the network and where to put their money, and eleven of them were making those calls without much geospatial analysis underneath. They had the operational data. What they didn’t have was a way to see it on the map, where the patterns actually live.</p>\n<p><strong>Task.</strong> The job was to sharpen their forecasting and their investment strategies with GIS — to turn tables of readings into something that showed them where capacity was getting tight, where risk was building, and where the next pound was best spent.</p>\n<p><strong>Action.</strong> Their operational data was brought together with geospatial modelling so the two reinforced each other. Instead of forecasting in the abstract, the grid could be looked at spatially and asked concrete questions: which stretches were heading toward their limits, which areas justified investment first. For eleven operators that meant fitting the analysis to how each of them actually ran their network, not handing everyone the same template and hoping it fit.</p>\n<p><strong>Result.</strong> The operators came away with forecasts they could trust and investment decisions that were aimed rather than hopeful. Grounding the planning in what the map showed made the whole thing more efficient — money and attention went where the data pointed instead of where habit did.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "Data Analytics", "Data Engineering", "GIS / Geospatial", "Product & Requirements", "Stakeholder & Reporting", "Data Analytics & BI Dashboards", "GIS & Geospatial Solutions", "Product Strategy & Requirements" ] }, { "id": "https://engineer.company/portfolio/presented-200-ui-ux-improvements/", "url": "https://engineer.company/portfolio/presented-200-ui-ux-improvements/", "title": "Presented 200 UI/UX improvements", "summary": "Delivered 200 UI/UX improvements to a GIS map application, contributing to a 10x rise in revenue — careful UX as a commercial driver.", "content_html": "<p><strong>Situation.</strong> The map interface had grown powerful and, along the way, complicated. There were places where you could feel customers not getting the value that was sitting right there in front of them — good functionality trapped behind clumsy interactions. That gap between what the product could do and what people found easy to do was quietly costing us.</p>\n<p><strong>Task.</strong> The job was finding those gaps and pushing the fixes through: the usability work that would make the product easier to get value from and, not by coincidence, more valuable commercially.</p>\n<p><strong>Action.</strong> Rather than guess at what was wrong, the work went into the feedback and the usage data, and out of that came a backlog of 200 concrete UI/UX improvements. They weren’t treated as equal — ranked by impact, with the ones that mattered argued for and taken through the team to ship as real features instead of a wishlist that sat in a document going stale.</p>\n<p><strong>Result.</strong> The interface got noticeably better to use, and the product’s value followed: the work contributed to a tenfold rise in revenue. It’s a case worth coming back to, because it makes the point cleanly — careful UX isn’t cosmetic, it shows up on the invoice.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "Data Analytics", "Design Systems & UI", "Frontend Engineering", "Product & Requirements", "Stakeholder & Reporting", "UX / UI Design", "Frontend Development", "Product Strategy & Requirements", "UI/UX Design & Design Systems" ] }, { "id": "https://engineer.company/portfolio/wrote-50000-words-of-software-documentation/", "url": "https://engineer.company/portfolio/wrote-50000-words-of-software-documentation/", "title": "Wrote 50,000 words of software documentation", "summary": "Wrote 50,000 words of software documentation in GitHub, Craft and Confluence, making knowledge durable, transparent and auditable.", "content_html": "<p><strong>Situation.</strong> Most of what mattered about the product and how the team worked lived in people’s heads. That’s fine right up until someone new joins, or someone leaves — and then onboarding crawls and a chunk of institutional memory is one resignation away from being gone for good.</p>\n<p><strong>Task.</strong> The aim was to get that knowledge out of heads and into documentation people would actually keep and use: clear enough to be read, structured enough to be maintained rather than abandoned after a month.</p>\n<p><strong>Action.</strong> Much of it was written by hand, around 50,000 words by the end, in Markdown across GitHub, Craft and Confluence depending on where each piece belonged. It covered the architecture, the processes and the practical how‑to material — the questions people kept asking. It was kept version‑controlled and, just as importantly, findable, because documentation nobody can locate might as well not exist.</p>\n<p><strong>Result.</strong> The knowledge stopped being fragile. New people got up to speed faster, the process became something you could point at instead of reconstruct from memory, and the whole thing turned auditable: you could see how and why work was done rather than take it on trust.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "Documentation", "Mentoring & Coaching", "Team Leadership", "Technical Leadership", "Technical Documentation", "Technical Leadership & Consulting" ] }, { "id": "https://engineer.company/portfolio/gathered-and-analyzed-business-requirements/", "url": "https://engineer.company/portfolio/gathered-and-analyzed-business-requirements/", "title": "Gathered and analyzed business requirements", "summary": "Turned business requirements into clear features and user stories aligned with data-governance standards, cutting ambiguity and rework.", "content_html": "<p><strong>Situation.</strong> Requirements tended to arrive as conversations — someone wanted something, roughly, and it fell to the developers to guess at the edges. Guessing means rework, and rework is about the most expensive way there is to build anything.</p>\n<p><strong>Task.</strong> The role sat between the business and the engineering, translating one into the other: turning loose needs into work a developer could pick up without guessing, and keeping it lined up with the data‑governance standards along the way.</p>\n<p><strong>Action.</strong> The requirements got worked out with the stakeholders directly, with the awkward questions asked early instead of discovered late, then written up as features and user stories that actually said what “done” meant. Each one was checked against the governance rules, because a feature that’s useful but mishandles data isn’t really finished. The whole aim was that someone could read a story and build the right thing the first time.</p>\n<p><strong>Result.</strong> Development ran off clear, agreed features instead of half‑understood asks. Ambiguity fell, and rework fell with it, and the delivery stayed pointed at the actual business goals — inside the data‑governance lines rather than tidied up to fit them afterward.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "Agile & Scrum", "Data Governance", "Documentation", "Product & Requirements", "Project Management", "Stakeholder & Reporting", "Data Governance & Quality", "Product Strategy & Requirements", "Project Management (Agile)" ] }, { "id": "https://engineer.company/portfolio/managed-30-successful-gis-projects-to-delivery/", "url": "https://engineer.company/portfolio/managed-30-successful-gis-projects-to-delivery/", "title": "Managed 30 successful GIS projects to delivery", "summary": "Delivered 30 successful GIS projects — a track record of consistent, innovative delivery that earned lasting client trust.", "content_html": "<p><strong>Situation.</strong> The company made its money delivering GIS projects, and there were a lot of them running at once. The company’s growth rode on getting them out the door reliably — not one flagship project done brilliantly, but a steady stream of them landing on time and holding together, which only happens when the coordination across the team is actually working.</p>\n<p><strong>Task.</strong> The responsibility was getting that portfolio delivered — keeping scope, people and timelines lined up across all of it, and giving the team the direction to ship.</p>\n<p><strong>Action.</strong> Across those years, the delivery of thirty GIS projects ran through this role. In practice that meant holding the scope steady when it wanted to creep, pointing the right people at the right work, and staying close enough to each project to catch trouble while it was still small. When something was going to slip, better to know early and reshuffle than find out at the deadline. A lot of the job was just keeping the plates spinning and the client’s expectations honest about what was coming and when.</p>\n<p><strong>Result.</strong> All thirty came in. That consistency mattered more than any single project would have — a client who’s seen you deliver thirty times over doesn’t worry about the thirty‑first, and that reputation is a good part of why the company kept growing.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "Agile & Scrum", "GIS / Geospatial", "Project Management", "Stakeholder & Reporting", "Team Leadership", "GIS & Geospatial Solutions", "Project Management (Agile)", "Team Building & Mentoring" ] }, { "id": "https://engineer.company/portfolio/led-company-growth-from-4-to-14-employees/", "url": "https://engineer.company/portfolio/led-company-growth-from-4-to-14-employees/", "title": "Led company growth from 4 to 14 employees", "summary": "Led company growth from 4 to 14 employees with Agile practices, scaling the team without losing quality or cohesion.", "content_html": "<p><strong>Situation.</strong> The company was ready to grow, and there’s a specific danger in that moment: you add people faster than you add process, and both the quality and the shared sense of how things are done start to fray. Four people who all know what everyone else is doing is a very different thing from fourteen who don’t.</p>\n<p><strong>Task.</strong> Leading that growth was the job — bringing people in while keeping delivery disciplined and the team pulling in the same direction.</p>\n<p><strong>Action.</strong> The company went from four people to fourteen. That was hiring and onboarding done deliberately rather than in a panic, but the bigger piece was putting in the ways of working that let a team that size not trip over itself — Agile practices, real project management, the habits that keep everyone’s work visible to everyone else. The tenth and fourteenth hires needed to walk into something that already had a shape, not to have to guess at how things were done.</p>\n<p><strong>Result.</strong> The team got to fourteen without the quality dropping or splintering into people who didn’t know what the others were up to. The Agile side of it is what kept a bigger group productive — the process grew with the headcount instead of lagging behind it.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "Agile & Scrum", "Mentoring & Coaching", "Project Management", "Team Leadership", "Technical Leadership", "Project Management (Agile)", "Team Building & Mentoring", "Technical Leadership & Consulting" ] }, { "id": "https://engineer.company/portfolio/improved-team-communication-and-collaboration/", "url": "https://engineer.company/portfolio/improved-team-communication-and-collaboration/", "title": "Improved team communication and collaboration", "summary": "Saved ~8,000 hours by rolling out Slack, Mattermost, 1Password and Jira, turning chased information and redone work into real delivery.", "content_html": "<p><strong>Situation.</strong> As the team got bigger, the communication and the tooling hadn’t kept up, and it showed. Things got said in one place and missed by the people who needed them, work got duplicated because nobody could see what someone else had already done, and coordinating anything took longer than the work itself. That kind of friction is invisible day to day, but it adds up to a lot of lost time.</p>\n<p><strong>Task.</strong> The aim was to fix how the team communicated and worked together, and claw back the time being quietly bled to all that friction.</p>\n<p><strong>Action.</strong> The tools got brought in and standardised, and — this is the part that actually matters — the practices for using them were set so they didn’t just become another place to check. Slack and Mattermost for communication, 1Password so shared secrets weren’t being passed around in ways nobody could track, Jira so work was tracked in one place instead of living in people’s heads and inboxes. The tools were the easy bit; getting everyone to actually use them the same way was the real work.</p>\n<p><strong>Result.</strong> Communication and collaboration got noticeably better, and the streamlined setup saved something on the order of 8,000 hours of labour — time that had been going into chasing information and redoing work, now going into actual delivery.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "Agile & Scrum", "Automation & CI/CD", "Documentation", "Project Management", "Security", "Team Leadership", "Project Management (Agile)", "Security & Access Management", "Team Building & Mentoring" ] }, { "id": "https://engineer.company/portfolio/led-30-gis-projects-from-build-to-support/", "url": "https://engineer.company/portfolio/led-30-gis-projects-from-build-to-support/", "title": "Led 30+ GIS projects from build to support", "summary": "Led development, deployment and support of 30+ GIS projects with PostgreSQL, Python, GDAL, ArcGIS, PostGIS and Mapbox across the lifecycle.", "content_html": "<p><strong>Situation.</strong> The company’s whole output was made‑to‑measure GIS — custom mapping and spatial systems built to a client’s specific problem, then kept running once they were live. Building the thing is only half of it; a geospatial project that ships and then falls over in production hasn’t really been delivered.</p>\n<p><strong>Task.</strong> These projects ran end to end through this role — the development, the deployment, and the support once they were live — with the technical direction across a fairly wide stack.</p>\n<p><strong>Action.</strong> Delivery ran on more than thirty GIS projects, hands‑on across the stack the whole way. PostgreSQL with PostGIS underneath for the spatial data, GDAL/OGR for moving it between formats — shapefiles, GeoJSON, GeoTIFF, KML, vector tiles — and QGIS and JOSM for the data work itself. On the front of it, web maps built on Mapbox GL and Leaflet, sometimes against the ArcGIS or HERE APIs, with the app layer in JavaScript, Python, PHP and SQL. The work ranged from 2D and 3D digital mapping through LiDAR processing, georectification and vectorisation to indoor mapping and navigation. And the responsibility ran past the point of shipping — the deployment and the ongoing support in production were part of it too, so problems weren’t handed off; the decisions had to be lived with.</p>\n<p><strong>Result.</strong> Thirty‑plus projects built, deployed and supported across that whole range. Being on the hook for the full lifecycle rather than just the build is what kept the quality honest — you design differently when you know you’re the one getting the call if it breaks.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "Backend Engineering", "Data Engineering", "Databases", "Full‑Stack Development", "GIS / Geospatial", "PostgreSQL", "Python", "Reliability & Backups", "SQL", "Team Leadership", "Technical Leadership", "Web Development", "Backend & API Development", "Data Pipeline Development (ETL/ELT)", "Full‑Stack Product Development", "GIS & Geospatial Solutions", "Technical Leadership & Consulting" ] }, { "id": "https://engineer.company/portfolio/contributed-to-user-interface-design-processes/", "url": "https://engineer.company/portfolio/contributed-to-user-interface-design-processes/", "title": "Contributed to user interface design processes", "summary": "Contributed to UI design, delivering intuitive, polished and user-friendly interfaces by asking usability questions during design.", "content_html": "<p><strong>Situation.</strong> The interfaces across our projects were uneven. Some were fine, some had clearly been built by engineers thinking about the data model rather than the person who’d have to use it, and the design decisions weren’t always made with the end user in the room. On a web‑map product especially, the map is the easy part — it’s the controls, the filtering and the flow around it where people get lost.</p>\n<p><strong>Task.</strong> Getting involved in the UI design process was part of the role — to help make the interfaces something people actually found intuitive and pleasant to use, not just functional.</p>\n<p><strong>Action.</strong> The role wasn’t the designer’s, but it sat in the design process and brought an engineering perspective — pushing on layout, on the flow through a task, on whether a screen was actually clear or just familiar to the people who’d built it. These were React, Angular and Vue front‑ends sitting over Mapbox and Leaflet maps, and a lot of the usability lived in the details: how you filtered a dataset, how you moved between floors on an indoor map, whether the thing told you what it was doing. Mostly it meant asking the dumb‑user questions early, while they were still cheap to fix, instead of after release when the confusion came back as support tickets.</p>\n<p><strong>Result.</strong> The interfaces got more intuitive and more polished, which end users felt directly, and it lifted the overall quality of what got put out. Getting the usability questions asked during design rather than after release is most of what made the difference.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "Design Systems & UI", "Frontend Engineering", "GIS / Geospatial", "UX / UI Design", "Web Development", "Frontend Development", "UI/UX Design & Design Systems" ] }, { "id": "https://engineer.company/portfolio/guided-company-projects-through-development/", "url": "https://engineer.company/portfolio/guided-company-projects-through-development/", "title": "Guided company projects through development", "summary": "Guided execution of many company projects with expert support through the hardest software-development phases, steadying delivery.", "content_html": "<p><strong>Situation.</strong> There were a lot of projects running at the same time, each in the middle of its development phase, and they all needed steady technical guidance to stay on the rails. Left alone, projects drift — a wrong approach taken early gets expensive by the time anyone notices.</p>\n<p><strong>Task.</strong> Guiding that execution was the job — being the technical support the teams could lean on through the development phases.</p>\n<p><strong>Action.</strong> The work stayed hands‑on across a lot of the company’s projects at once. That meant unblocking people when they were stuck, looking hard at an approach before too much got built on top of it, and generally staying close enough to catch a project heading the wrong way while it was still a course correction and not a rebuild. The idea was to be available rather than a gate — to keep things moving, not to make everything wait on one person.</p>\n<p><strong>Result.</strong> Projects moved through their tricky development stages more smoothly with someone experienced to lean on at the right moments, and that steadied delivery right across the company’s portfolio.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "Mentoring & Coaching", "Project Management", "Team Leadership", "Technical Leadership", "Project Management (Agile)", "Team Building & Mentoring", "Technical Leadership & Consulting" ] }, { "id": "https://engineer.company/portfolio/overhauled-internal-processes-saving-8000-hours/", "url": "https://engineer.company/portfolio/overhauled-internal-processes-saving-8000-hours/", "title": "Overhauled internal processes, saving 8,000 hours", "summary": "Overhauled internal processes to save ~8,000 hours, improving software architecture, systems and scheduling efficiency.", "content_html": "<p><strong>Situation.</strong> The way things were done internally had accumulated the usual cruft — software architecture that had grown by accretion rather than design, systems that worked but not efficiently, scheduling that left people either waiting or slammed. None of it was on fire, which is exactly why it had been left alone, but it was quietly costing a lot of time.</p>\n<p><strong>Task.</strong> The aim was to overhaul those processes — to go find the waste and take it out rather than keep paying for it.</p>\n<p><strong>Action.</strong> The internal processes got reworked across three fronts: the software architecture, so it was something you could reason about and build on instead of work around; the systems, streamlined so the routine work stopped taking longer than it should; and the scheduling, so capacity was actually matched to the work. And the changes were made to stick — embedded in how the team operated rather than left as a memo everyone nodded at and forgot — because process improvements that aren’t made permanent just decay back to the old way.</p>\n<p><strong>Result.</strong> The overhaul saved roughly 8,000 hours by making the architecture, systems and scheduling meaningfully more efficient. That’s capacity that went straight back into higher‑value work instead of into overhead nobody had thought to question.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "Automation & CI/CD", "Infrastructure", "Performance Tuning", "Platform Architecture", "Project Management", "Solution Architecture", "Technical Leadership", "DevOps & CI/CD Automation", "Platform & Solution Architecture", "Project Management (Agile)", "Technical Leadership & Consulting" ] }, { "id": "https://engineer.company/portfolio/administered-network-infrastructure/", "url": "https://engineer.company/portfolio/administered-network-infrastructure/", "title": "Administered network infrastructure", "summary": "Administered network infrastructure for 1,000+ servers, ensuring reliable deployment, solid security and prompt troubleshooting.", "content_html": "<p><strong>Situation.</strong> The company’s operations sat on top of a big server estate — over a thousand of them — and an estate that size doesn’t stay reliable on its own. Deployment, security and the steady stream of things that go wrong all need someone running them with actual discipline, or the whole thing gets flaky and nobody’s quite sure why.</p>\n<p><strong>Task.</strong> Administering that network infrastructure was the job — keeping it secure, keeping it reliable, keeping it consistent at a scale where inconsistency is what kills you.</p>\n<p><strong>Action.</strong> The network infrastructure ran across more than a thousand servers. How systems got deployed was standardised, so a server came up the same predictable way instead of each one being a little bit bespoke; the security was hardened rather than trusting that nobody would come looking; and the troubleshooting got handled when something did break. At that scale the standardisation is what saves you — a thousand snowflakes is unmanageable, a thousand of the same thing is just work.</p>\n<p><strong>Result.</strong> The estate ran with reliable deployment, solid security and problems that got dealt with promptly instead of festering. That’s the kind of infrastructure work that’s invisible when it’s going well, which is the point — it was the stable backbone everything else at the company ran on.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "Automation & CI/CD", "Infrastructure", "Linux & Servers", "Networking & VPN", "Reliability & Backups", "Security", "System Administration", "Networking & VPN Setup", "Security & Access Management" ] }, { "id": "https://engineer.company/portfolio/established-a-technical-support-department/", "url": "https://engineer.company/portfolio/established-a-technical-support-department/", "title": "Established a Technical Support department", "summary": "Established a Technical Support department serving 10,000+ clients, turning support from reactive fixes into a scalable company strength.", "content_html": "<p><strong>Situation.</strong> The client base was growing and needed dependable technical support, and there just wasn’t a dedicated function to give it to them at any real scale. Support was happening ad hoc, which works for a handful of clients and quietly falls apart as the numbers climb.</p>\n<p><strong>Task.</strong> Standing up an actual technical‑support capability — one that could serve a large and still‑growing client base — was the job.</p>\n<p><strong>Action.</strong> A Technical Support department got built from nothing. That meant deciding how it would actually work before hiring into it — the process for how a request came in and got resolved, the tooling to handle volume, the standard for what good support looked like — and then building the capacity to deliver IT support and troubleshooting to a lot of clients at once. Starting from scratch was the advantage, honestly; it could be designed for the scale the company was heading toward instead of patching something that had grown up by accident.</p>\n<p><strong>Result.</strong> The department ended up serving over ten thousand clients with reliable support and troubleshooting. Support went from a thing done reactively to a genuine strength of the company — something that scaled with the client base instead of buckling under it.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "Mentoring & Coaching", "Product & Requirements", "Project Management", "Stakeholder & Reporting", "System Administration", "Team Leadership", "IT Support & Helpdesk", "Project Management (Agile)", "Team Building & Mentoring" ] }, { "id": "https://engineer.company/portfolio/automated-ssl-tls-certs-for-100-docker-apps/", "url": "https://engineer.company/portfolio/automated-ssl-tls-certs-for-100-docker-apps/", "title": "Automated SSL/TLS certs for 100 Docker apps", "summary": "Automated SSL/TLS certificates for 100 Docker apps on Ubuntu Linux, ending manual renewals and the outages expired certs cause.", "content_html": "<p><strong>Situation.</strong> A hundred Docker applications all needed SSL/TLS certificates, and certificates are the kind of thing that’s fine right up until they aren’t. Issuing and renewing a hundred of them by hand is slow, it’s dull, and it’s exactly the sort of manual job where one forgotten renewal takes an app down with an expiry error at the worst possible moment.</p>\n<p><strong>Task.</strong> The goal was certificate creation and renewal automated — every app with valid, trusted encryption, and nobody having to remember to do anything.</p>\n<p><strong>Action.</strong> An ACME‑based workflow handled the whole lifecycle for the hundred Docker applications — creating the certificates and renewing them before they lapsed — and deployed them automatically across the Ubuntu Linux hosts, which were running a mix of Apache and Nginx. The whole aim was to take the human out of it, because the human is the part that forgets.</p>\n<p><strong>Result.</strong> All hundred applications kept valid certificates and secure connections on their own. The manual certificate work just disappeared, and with it the whole category of outage where something breaks not because it failed but because a cert quietly expired and nobody noticed.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "Automation & CI/CD", "Containers (Docker/Kubernetes)", "DevOps", "Infrastructure", "Linux & Servers", "Reliability & Backups", "Security", "Containerization & Orchestration", "DevOps & CI/CD Automation", "Security & Access Management" ] }, { "id": "https://engineer.company/portfolio/streamlined-ci-cd-processes-saving-4000-hours/", "url": "https://engineer.company/portfolio/streamlined-ci-cd-processes-saving-4000-hours/", "title": "Streamlined CI/CD processes, saving 4,000 hours", "summary": "Streamlined CI/CD to save ~4,000 hours, making releases faster and more reliable so the team could ship with confidence.", "content_html": "<p><strong>Situation.</strong> Getting software out the door relied on manual, inconsistent steps — someone remembering the sequence, doing it slightly differently each time — and it slowed releases down and ate engineering hours that should have gone into building things.</p>\n<p><strong>Task.</strong> The aim was to streamline the CI/CD process and automate it end to end, so releases stopped being a manual ritual.</p>\n<p><strong>Action.</strong> Build, test and deployment were automated, so the path from a change to it running in production was standardised instead of improvised. The repetitive manual steps — the ones that were slow and, worse, done differently depending on who was doing them — came out. Once the automation is doing it the same way every time, a whole class of “it worked on my machine” and half‑remembered deploy steps just stops happening.</p>\n<p><strong>Result.</strong> The automation gave back roughly 4,000 hours and made releases both faster and more reliable. The team could ship without bracing for it — the confidence came from the process being consistent, not from everyone being careful.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "Automation & CI/CD", "DevOps", "Reliability & Backups", "Technical Leadership", "Testing & QA", "DevOps & CI/CD Automation", "Technical Leadership & Consulting" ] }, { "id": "https://engineer.company/portfolio/saved-4000-hours-with-github-and-gitlab-ci-cd/", "url": "https://engineer.company/portfolio/saved-4000-hours-with-github-and-gitlab-ci-cd/", "title": "Saved 4,000 hours with GitHub and GitLab CI/CD", "summary": "Saved ~4,000 hours by introducing GitHub, GitLab, Bash and Python CI/CD, speeding data analysis and development while adding consistency.", "content_html": "<p><strong>Situation.</strong> Both the data‑analysis work and the software development were being held back by the same thing: manual processes. Work moved from development to delivery in a slow, inconsistent way, and the analysis side had its own pile of repetitive steps someone was doing by hand every time.</p>\n<p><strong>Task.</strong> The aim was to streamline both by bringing modern automation and CI/CD practices to workflows that hadn’t had them.</p>\n<p><strong>Action.</strong> CI/CD practices went in, built on GitHub and GitLab, with Bash and Python doing the automation work underneath. The repetitive steps across both the data‑analysis and the development workflows got automated, and how work moved from development through to delivery got standardised so it was the same every time rather than reinvented per project. Bringing the analysis side into the same disciplined process as the dev work was a big part of it — it had been treated as a separate, more manual world.</p>\n<p><strong>Result.</strong> The streamlined processes saved roughly 4,000 hours and sped up both the data analysis and the software development, and just as usefully made what got shipped more consistent — fewer surprises from work that had been done a slightly different way each time.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "Automation & CI/CD", "Data Analytics", "Data Engineering", "Data Pipelines (ETL/ELT)", "DevOps", "Python", "Data Analytics & BI Dashboards", "Data Pipeline Development (ETL/ELT)", "DevOps & CI/CD Automation" ] }, { "id": "https://engineer.company/portfolio/developed-a-data-analytics-reporting-system/", "url": "https://engineer.company/portfolio/developed-a-data-analytics-reporting-system/", "title": "Developed a Data Analytics reporting system", "summary": "Built a Python data-analytics reporting system that raised quarterly software revenue by 400% with clear, timely PDF reports.", "content_html": "<p><strong>Situation.</strong> Stakeholders weren’t getting analytics in any timely, readable form. The data existed, but turning it into something you could actually make a decision from was slow and manual, so visibility into how things were performing lagged, and the commercial decisions lagged with it.</p>\n<p><strong>Task.</strong> Building a data‑analytics reporting system — one that turned raw data into clear, regular insight without someone hand‑assembling it each time — was the job.</p>\n<p><strong>Action.</strong> A reporting system generated PDF reports in Python, automating the whole chain: pulling the data, running the analysis, and presenting it in a clean, consistent format stakeholders could actually read. The point was regularity and clarity — the same professional report landing predictably, so the numbers became something people looked at as a matter of course rather than something they had to go and dig for.</p>\n<p><strong>Result.</strong> That reporting is what drove quarterly software revenue up by 400%. Making the analytics better and faster wasn’t a back‑office nicety — put clear, timely numbers in front of the people making commercial decisions and the decisions get better, and here that showed up directly on the revenue.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "Automation & CI/CD", "Backend Engineering", "Data Analytics", "Data Engineering", "Product & Requirements", "Python", "Stakeholder & Reporting", "Backend & API Development", "Data Analytics & BI Dashboards", "Product Strategy & Requirements" ] }, { "id": "https://engineer.company/portfolio/automated-data-processing-in-python-and-pl-pgsql/", "url": "https://engineer.company/portfolio/automated-data-processing-in-python-and-pl-pgsql/", "title": "Automated data processing in Python and PL/pgSQL", "summary": "Automated data-processing tasks with Shell, PL/pgSQL, Python and Transact-SQL, lifting productivity and ending small recurring errors.", "content_html": "<p><strong>Situation.</strong> There was a steady load of recurring data‑processing work being done by hand. Manual data work has two problems at once: it eats time, and it’s inconsistent — do the same task by hand enough times and it’ll get done slightly differently, and some of those differences are errors.</p>\n<p><strong>Task.</strong> The aim was to automate these tasks, both to get the time back and to make them reliable.</p>\n<p><strong>Action.</strong> The data‑processing work got automated across the databases and systems it touched, using whatever fit the job — Shell scripting for the glue, PL/pgSQL and Transact‑SQL down in the databases, Python where it needed more than SQL could give. Manual steps got replaced with jobs that ran the same way every time, which is the whole point: a script doesn’t get bored, doesn’t skip a step, and doesn’t do it differently on a Friday afternoon.</p>\n<p><strong>Result.</strong> Productivity and efficiency both went up, the manual effort came off people’s plates, and the data processing became consistent and dependable instead of a source of small, recurring errors.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "Automation & CI/CD", "Data Engineering", "Data Pipelines (ETL/ELT)", "Databases", "PostgreSQL", "Python", "SQL", "Data Pipeline Development (ETL/ELT)", "Database Administration (DBA)", "DevOps & CI/CD Automation" ] }, { "id": "https://engineer.company/portfolio/saved-150-hours-a-month-across-30-projects/", "url": "https://engineer.company/portfolio/saved-150-hours-a-month-across-30-projects/", "title": "Saved 150 hours a month across 30 projects", "summary": "Saved ~150 hours per month across 30 projects by optimising workflows and resource management — a recurring, portfolio-wide gain.", "content_html": "<p><strong>Situation.</strong> Across a portfolio of around thirty projects, time was being lost every single month to workflows that had never been optimised and resource management that was uneven — some people underused, some overloaded, work planned differently from one project to the next.</p>\n<p><strong>Task.</strong> The aim was to make the portfolio more efficient and recover the time that was going missing month after month.</p>\n<p><strong>Action.</strong> Going through the thirty projects, the workflows and the resource management got worked on together — taking out the bottlenecks, balancing the workloads so the same few people weren’t always the constraint, and standardising how work got planned and run so each project wasn’t its own special case. Thirty projects each losing a bit of time adds up; the fix was mostly about making the good practices consistent rather than inventing anything clever.</p>\n<p><strong>Result.</strong> The changes saved around 150 hours a month across the portfolio. That’s a recurring monthly saving, not a one‑off — thirty projects running leaner, month in and month out.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "Agile & Scrum", "Automation & CI/CD", "Performance Tuning", "Project Management", "Team Leadership", "Project Management (Agile)", "Team Building & Mentoring" ] }, { "id": "https://engineer.company/portfolio/managed-a-team-delivering-it-support/", "url": "https://engineer.company/portfolio/managed-a-team-delivering-it-support/", "title": "Managed a team delivering IT support", "summary": "Led a team delivering IT support, data recovery and hardware repair to 1,000+ clients, building a reputation for reliable service.", "content_html": "<p><strong>Situation.</strong> The company was the place a large base of clients came to when their IT broke — support, data recovery, hardware repair, the full spread. Meeting that kind of steady, unglamorous demand isn’t about heroics; it’s about having a team that runs well day after day, because the work never really stops coming.</p>\n<p><strong>Task.</strong> Managing the team that delivered all that was the job, and keeping the quality consistent whether it was a quiet week or everything arriving at once.</p>\n<p><strong>Action.</strong> The team ran IT support, data recovery and hardware repair for over a thousand clients. A lot of that was the organising underneath — making sure work got picked up and not dropped, setting a standard for what “fixed” actually meant so people weren’t getting half‑repairs back, and keeping the team effective when the backlog was long. Data recovery especially is work you can’t be casual about; it’s usually someone’s photos or their business sitting on that drive, and they’re already having a bad day by the time they reach you.</p>\n<p><strong>Result.</strong> The team served over a thousand clients and built a real reputation for reliable service. In that kind of business the reputation is everything — people come back, and they tell others, precisely because the last time something broke you actually sorted it.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "Mentoring & Coaching", "Stakeholder & Reporting", "System Administration", "Team Leadership", "Hardware Repair & Data Recovery", "IT Support & Helpdesk", "Team Building & Mentoring" ] }, { "id": "https://engineer.company/portfolio/configured-and-deployed-1000-wi-fi-routers/", "url": "https://engineer.company/portfolio/configured-and-deployed-1000-wi-fi-routers/", "title": "Configured and deployed 1,000 Wi‑Fi routers", "summary": "Configured and deployed 1,000 Wi-Fi routers with a standard setup, giving clients dependable wireless access and performance.", "content_html": "<p><strong>Situation.</strong> Clients needed wireless that just worked, and that came down to configuring and rolling out a large number of Wi‑Fi routers — and doing it the same careful way every time, because a router set up sloppily is either insecure or slow, and usually you find out which one later.</p>\n<p><strong>Task.</strong> Configuring and deploying those routers to give clients better network access and performance was the job.</p>\n<p><strong>Action.</strong> A thousand Wi‑Fi routers got configured and deployed. The trick at that number is standardising the setup — a consistent, secure, sensible configuration — rather than tuning each one from scratch on the day, because a thousand hand‑crafted routers is a thousand different things to support later. So they were set up for security and performance the same way each time, and rolled out reliably across client sites.</p>\n<p><strong>Result.</strong> The thousand routers gave clients dependable wireless — better access, better performance — and did it consistently, because the setup was standard rather than improvised. A router nobody has to think about again is the goal; most of these got there.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "Infrastructure", "Networking & VPN", "Reliability & Backups", "Security", "System Administration", "IT Support & Helpdesk", "Networking & VPN Setup" ] }, { "id": "https://engineer.company/portfolio/administered-100-bare-bone-servers/", "url": "https://engineer.company/portfolio/administered-100-bare-bone-servers/", "title": "Administered 100 Bare Bone servers", "summary": "Administered 100 bare-bone servers, physical networks and IP telephony, providing the robust foundation the company grew on.", "content_html": "<p><strong>Situation.</strong> Underneath everything the company did was the physical stuff — servers, the actual networks, the IP‑telephony — and it all had to just run. That layer is invisible when it works and extremely visible the moment it doesn’t, and the company’s growth was resting on it staying dependable.</p>\n<p><strong>Task.</strong> Administering that infrastructure and keeping it stable as the company grew was the job.</p>\n<p><strong>Action.</strong> A hundred bare‑bone servers, along with the physical networks and the IP‑telephony systems, were looked after — the setup, the maintenance, the troubleshooting when something went wrong. Bare‑bone servers mean dealing with the hardware directly, so there’s a hands‑on, physical side to it: the cabling, the boxes, the phone system that everyone notices the second a call drops. The job was to keep all of it boring, in the good sense.</p>\n<p><strong>Result.</strong> The servers, the networks and the telephony ran reliably, and that dependable physical foundation is what let the company keep growing without the ground shifting underneath it.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "Infrastructure", "Linux & Servers", "Networking & VPN", "Reliability & Backups", "System Administration", "Networking & VPN Setup" ] }, { "id": "https://engineer.company/portfolio/developed-100-web-applications-using-html-html5/", "url": "https://engineer.company/portfolio/developed-100-web-applications-using-html-html5/", "title": "Developed 100 web applications using HTML/HTML5", "summary": "Developed 100 web applications with HTML5, CSS/SCSS, Django, WordPress and Joomla, matching each solution to the client's real needs.", "content_html": "<p><strong>Situation.</strong> Clients wanted web applications, and they wanted very different things — different scales, different budgets, different levels of “just get me online” versus “build me something custom.” Meeting that meant being versatile rather than forcing every client down the same technology.</p>\n<p><strong>Task.</strong> Building web applications that gave each client a diverse, effective online presence was the job.</p>\n<p><strong>Action.</strong> A hundred web applications got built, and the point was matching the tool to the job rather than having a favourite. Where a client needed something custom, that was HTML/HTML5, CSS/SCSS and Django; where they needed something more standard that they could also manage themselves, WordPress or Joomla was the faster, more sensible answer. Part of the skill was knowing which was which — talking a client out of a bespoke build they didn’t need, or into one they did.</p>\n<p><strong>Result.</strong> The hundred applications gave clients a varied, capable online presence, each delivered on the technology that actually fit it. Matching the approach to the client rather than the other way round is what made them effective rather than just delivered.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "Backend Engineering", "Frontend Engineering", "Full‑Stack Development", "Python", "Web Development", "Frontend Development", "Full‑Stack Product Development", "Website Development & CMS" ] }, { "id": "https://engineer.company/portfolio/administered-40-websites-on-apache-and-nginx/", "url": "https://engineer.company/portfolio/administered-40-websites-on-apache-and-nginx/", "title": "Administered 40 websites on Apache and Nginx", "summary": "Administered 40 websites on Ubuntu Linux with Apache and Nginx at high availability and performance — hosting clients never had to think about.", "content_html": "<p><strong>Situation.</strong> There was a portfolio of live websites that needed to stay up and stay fast — and hosting is another of those jobs that’s invisible until a site goes down, at which point it’s the only thing anyone cares about.</p>\n<p><strong>Task.</strong> Administering those sites and keeping them highly available and quick was the job.</p>\n<p><strong>Action.</strong> Forty websites ran on Ubuntu Linux hosting servers, on a mix of Apache and Nginx — the configuration, the performance tuning, the ongoing maintenance to keep them reliable under real traffic. Real traffic is the operative bit: a site that’s fine when nobody’s using it and falls over when they are hasn’t been administered, it’s just been left alone. So the work was keeping them healthy under actual load.</p>\n<p><strong>Result.</strong> All forty ran with high availability and good performance, which gave clients hosting they didn’t have to think about. Stable and dependable under real use is the entire point of hosting, and that’s what these delivered.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "Infrastructure", "Linux & Servers", "Performance Tuning", "Reliability & Backups", "System Administration", "Web Development", "Site Reliability & Monitoring", "Website Development & CMS" ] }, { "id": "https://engineer.company/portfolio/engineered-600-pl-pgsql-etl-elt-pipelines/", "url": "https://engineer.company/portfolio/engineered-600-pl-pgsql-etl-elt-pipelines/", "title": "Engineered 600 PL/pgSQL ETL/ELT pipelines", "summary": "Engineered 600 PL/pgSQL ETL/ELT pipelines across PostgreSQL dev and prod, cutting average execution time 35% and lifting throughput 50%+.", "content_html": "<p><strong>Situation.</strong> The organization was managing large volumes of transactional and analytical data across multiple PostgreSQL development and production environments. The existing data pipelines were fragmented, lacked consistency, and were causing performance bottlenecks, leading to delays in business‑critical reporting and decision‑making.</p>\n<p><strong>Task.</strong> The task was to design and implement robust, scalable, and efficient ETL/ELT pipelines using PL/pgSQL to streamline complex data ingestion, transformation, and loading workflows. A key objective was to improve query performance and ensure data integrity across all environments.</p>\n<p><strong>Action.</strong> Over 600 PL/pgSQL‑based ETL/ELT pipelines were engineered to automate the extraction, transformation, and loading of data from various sources. Key technical implementations included:</p>\n<ul>\n<li>Leveraged partitioned tables and materialized views to optimize read performance for large datasets.</li>\n<li>Applied primary and foreign key constraints to maintain referential integrity during transformations.</li>\n<li>Designed unique and composite indexes to speed up JOIN operations and complex filtering criteria.</li>\n<li>Incorporated exception handling and transactional control to ensure fault tolerance and rollback in case of failures.</li>\n<li>Enabled incremental loads using change data capture (CDC) mechanisms and timestamp‑based deltas, reducing processing time by over 60%.</li>\n<li>Developed automated logging and auditing procedures to track pipeline execution, monitor anomalies, and facilitate debugging.</li>\n</ul>\n<p><strong>Result.</strong> The new ETL/ELT framework significantly improved the consistency, reliability, and performance of data processing workflows:</p>\n<ul>\n<li>Achieved a 35% reduction in average pipeline execution time.</li>\n<li>Increased data pipeline throughput by over 50%, enabling near real‑time data availability for reporting.</li>\n<li>Reduced data quality issues by eliminating over 90% of transformation errors through better constraint enforcement and validation logic.</li>\n<li>Enhanced maintainability and scalability, supporting future data model changes with minimal rework.</li>\n</ul>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "Automation & CI/CD", "Data Engineering", "Data Governance", "Data Pipelines (ETL/ELT)", "Databases", "Performance Tuning", "PostgreSQL", "SQL", "Data Governance & Quality", "Data Pipeline Development (ETL/ELT)", "Database Administration (DBA)" ] }, { "id": "https://engineer.company/portfolio/a-tenth-of-the-cloud-bill-and-nothing-lost/", "url": "https://engineer.company/portfolio/a-tenth-of-the-cloud-bill-and-nothing-lost/", "title": "A tenth of the cloud bill, and nothing lost", "summary": "Cut infrastructure budget 10x with zero productivity loss for an international client by re-imagining the stack and moving off AWS.", "content_html": "<p><strong>Situation.</strong> An international client was carrying badly bloated infrastructure costs. Their AWS setup had been over‑provisioned and had collected services they no longer used, so the cloud bill had drifted completely out of proportion to what the business actually needed. It’s a common story — nobody sets out to overspend, it just accretes when no one’s watching the meter.</p>\n<p><strong>Task.</strong> The engagement was to cut the costs substantially without losing any productivity, which meant rethinking the infrastructure properly rather than trimming round the edges — edge‑trimming rarely moves a bill that’s structurally too big.</p>\n<p><strong>Action.</strong> So the work went end to end. First came an audit of what was actually being used — which is where the duplicated and unnecessary services show themselves — and those got cut. Then whatever was left was right‑sized to match real demand instead of the worst‑case guesses the original setup had been built on. And the big move was relocating the workloads off AWS entirely, onto a more cost‑effective hosting arrangement — done carefully, in stages, so the running business never felt the migration happening underneath it.</p>\n<p><strong>Result.</strong> The budget costs fell roughly ten‑fold, with zero loss in productivity — the same capability at a fraction of what they’d been paying. It freed up a real amount of money that had quietly been leaking into an oversized cloud bill month after month, which for the business was money straight back to the bottom line.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "Cloud", "DevOps", "Infrastructure", "Migrations & Modernization", "Platform Architecture", "Solution Architecture", "Cloud Infrastructure & Migration", "DevOps & CI/CD Automation", "Platform & Solution Architecture" ] }, { "id": "https://engineer.company/portfolio/a-maritime-platform-built-in-clean-layers/", "url": "https://engineer.company/portfolio/a-maritime-platform-built-in-clean-layers/", "title": "A maritime platform built in clean layers", "summary": "Architected a layered maritime platform — Next.js PWA, a Go (Huma/Fiber) API and a PostgreSQL function layer holding all business logic.", "content_html": "<p><strong>Situation.</strong> The product was going to be a maritime platform — professional networking, company reviews, sponsored education — and it was a young product, which is a polite way of saying the requirements were going to move around a lot. The thing to avoid was an architecture where changing a business rule meant touching the frontend, the API and the database all at once. On a small codebase that grows fast, that kind of coupling is what turns a two‑line change into an afternoon.</p>\n<p><strong>Task.</strong> As the architect, the call to make up front was where each kind of logic lived, with boundaries obvious enough to hold up under pressure instead of blurring the first time someone was in a hurry.</p>\n<p><strong>Action.</strong> It settled into three layers, one job each. The Next.js frontend does presentation and interactivity and nothing else. The Go API — Huma over Fiber — is deliberately thin: it routes, validates the request, applies the security filtering and orchestrates, but it holds no business logic at all. The business logic all lives in PostgreSQL functions, which assemble the full result and hand it back for the API to forward. So when a rule changes, it changes in one layer, in SQL, and the other two don’t need to know. The boundaries were written down and then enforced in review, because a convention nobody polices stops being one.</p>\n<p><strong>Result.</strong> The thing stayed easy to hold in your head. Business logic sits in one place you can actually audit, the API is a boring adapter in the good sense, and the frontend doesn’t care when the schema shifts underneath it. That separation is what let the product keep bolting on features without the architecture quietly rotting.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "APIs & Integration", "Backend Engineering", "Databases", "Frontend Engineering", "Full‑Stack Development", "Platform Architecture", "PostgreSQL", "Solution Architecture", "Technical Leadership", "Backend & API Development", "Database Design & Modeling", "Frontend Development", "Full‑Stack Product Development", "Platform & Solution Architecture" ] }, { "id": "https://engineer.company/portfolio/built-organization-context-switching-for-users/", "url": "https://engineer.company/portfolio/built-organization-context-switching-for-users/", "title": "Built organization context switching for users", "summary": "Built organization context-switching with localStorage and server cookie mirroring, letting users act as managed orgs under least-privilege auth.", "content_html": "<p><strong>Situation.</strong> On the platform a maritime professional can manage organizations — companies, academies — that don’t have their own logins. The person is the account; the organization is something they act on behalf of. So a user needs to move through the whole app as any organization they manage, switching between them freely, and that convenience can’t turn into a hole in the authorization.</p>\n<p><strong>Task.</strong> Context‑switching had to be quick and unobtrusive for the user while making sure the active context could never, on its own, hand someone access they weren’t entitled to.</p>\n<p><strong>Action.</strong> An OrganizationContext handles it, with storage on both sides. On the client, localStorage is the source of truth for which organization you’re currently acting as, so switching is instant — no round‑trip. A server‑side cookie mirrors it so that server‑rendered pages resolve the same context during SSR; there’s a getServerViewMode on the server that reads it. The important part is that none of that is trusted for access decisions. Authorization gets re‑checked on the server on every request. The frontend context is there for the experience — showing you the right thing — and the server is the only authority on what you’re allowed to do.</p>\n<p><strong>Result.</strong> A user can act as any organization they manage without friction, and the interface stays in sync on both client and server. But because permissions are verified server‑side every time, none of that convenience weakens the security. Someone tampering with what’s in localStorage changes what their own UI shows them and nothing more — the server still says no.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "APIs & Integration", "Backend Engineering", "Frontend Engineering", "Full‑Stack Development", "Platform Architecture", "Security", "Backend & API Development", "Frontend Development", "Platform & Solution Architecture", "Security & Access Management" ] }, { "id": "https://engineer.company/portfolio/built-email-with-failover-and-delivery-logging/", "url": "https://engineer.company/portfolio/built-email-with-failover-and-delivery-logging/", "title": "Built email with failover and delivery logging", "summary": "Built email as a platform capability: three providers with failover, delivery webhooks, send and delivery logging, templating and campaign broadcasts.", "content_html": "<p><strong>Situation.</strong> Email carries a lot of weight on the platform — verification, notifications, digests, campaigns, the things a user actually waits for. And a mail provider is exactly the kind of dependency that fails quietly: the config looks fine, the app boots, and you only find out something’s broken when a real person never gets the message they were promised. That’s the worst way to learn about it. One provider makes it worse, because the failure is total and someone else’s to fix.</p>\n<p><strong>Task.</strong> Email had to be treated as a capability the platform owns rather than a client library it calls — able to survive a provider outage, able to say what happened to a given message, and loud at startup in the environments where silence is dangerous.</p>\n<p><strong>Action.</strong> Three providers sit behind one interface — SendGrid as primary, with SMTP2GO and Azure Communication Services behind it — and failover between them is automatic rather than a configuration change made under pressure. Delivery is not assumed: inbound webhooks report what each provider did with a message, and both sides are recorded, in a send log and a delivery event table, so “did this person get their verification mail” is a query rather than a guess. Templating keeps the message bodies out of the code, and a separate broadcast schema — 5 tables and 24 functions — carries campaigns to segments of users, which is a different problem from transactional mail and was built as one. In front of all of it, a startup check sends a real message through the stack, behind a flag: in development it logs a warning and carries on, because nobody wants their laptop refusing to start over an expired sandbox key, and in staging and production a failure is fatal and the process exits rather than deploy a build that cannot send mail. The send path itself goes through an SSRF‑protected client with a 30‑second timeout, and the async delivery path has retries and backoff so a momentary blip doesn’t drop a message.</p>\n<p><strong>Result.</strong> A whole category of silent failure moved from “a user notices days later” to “the deploy stops”, and a provider having a bad afternoon became a degraded path rather than an outage. The cost is three integrations to keep working instead of one, and delivery logs that grow and have to be pruned — both accepted, because email is the channel the platform cannot route around.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "APIs & Integration", "Backend Engineering", "Cloud", "Reliability & Backups", "Security", "Backend & API Development", "Security & Access Management", "Site Reliability & Monitoring" ] }, { "id": "https://engineer.company/portfolio/designed-a-function-first-postgresql-data-layer/", "url": "https://engineer.company/portfolio/designed-a-function-first-postgresql-data-layer/", "title": "Designed a function‑first PostgreSQL data layer", "summary": "Designed a PostgreSQL function-first data layer — 1,275 stored functions across 34 schemas — with the access boundary enforced by the database itself.", "content_html": "<p><strong>Situation.</strong> Business logic has a way of leaking. A bit ends up in the API, a bit in some SQL a handler runs inline, and before long the same rule is written two or three slightly different ways and there’s nowhere you can point to and say “this is what the system does with its data.” That’s how bugs and security holes get in.</p>\n<p><strong>Task.</strong> The goal was one home for all of it: every read and write going through the database, the API a thin adapter that doesn’t know the business rules, and the whole thing lockable down tight.</p>\n<p><strong>Action.</strong> The data layer is function‑first. The schema is split by domain — identity, organization, review, message, notification and 29 more, 34 in all — and every operation the app can do is one of 1,275 PostgreSQL functions it calls; there’s no direct table access from Go at all. Then the database enforces it. The role the API logs in as, mariner, has EXECUTE on the app functions and USAGE on the schemas and nothing else — no SELECT, no INSERT, no way to touch a table directly — which comes to roughly 4,000 explicit grants rather than a blanket one. The functions run SECURITY DEFINER, owned by a separate non‑login function_owner role with a pinned search_path, and the superuser account stays reserved for migrations and cron, well away from the running app.</p>\n<p><strong>Result.</strong> The logic lives in one place you can actually audit, the API stays thin and boring in the good way, and the access boundary is enforced by Postgres itself rather than by everyone remembering the rules. If the API were somehow compromised, it still couldn’t do anything the functions don’t allow. At 1,275 functions the discipline costs something real — a new field is a migration and a function change, not a line in a query — and that friction is the price of the boundary holding.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "Backend Engineering", "Data Governance", "Databases", "Platform Architecture", "PostgreSQL", "Security", "SQL", "Backend & API Development", "Data Governance & Quality", "Database Design & Modeling", "Platform & Solution Architecture" ] }, { "id": "https://engineer.company/portfolio/quicker-answers-from-a-better-choice-of-key/", "url": "https://engineer.company/portfolio/quicker-answers-from-a-better-choice-of-key/", "title": "Quicker answers from a better choice of key", "summary": "Adopted UUID v7 time-ordered keys (PostgreSQL 18) to cut B-tree index fragmentation and speed inserts and range queries across every table.", "content_html": "<p><strong>Situation.</strong> Every entity needs a unique id, and the reflex choice is a random UUID. The trouble is that random ids land all over a B‑tree index. Inserts scatter, the index fragments, and as tables grow both writes and range scans pay for it. On a platform meant to keep growing, that’s a slow leak you’d rather not build in.</p>\n<p><strong>Task.</strong> Keep the global uniqueness of a UUID but drop the fragmentation that comes with the randomness.</p>\n<p><strong>Action.</strong> The standard became UUID v7, which is time‑ordered — the leading bits are a timestamp, so new rows sort into the index instead of peppering it. PostgreSQL 18 has this natively as uuidv7(), wrapped in a small uuid_generate_v7() function so the same call behaves cleanly on Azure’s Flexible Server, then made the default for entity primary keys across the schema. Nothing exotic about it; it’s the kind of decision that’s cheap if you make it early and a pain to retrofit later.</p>\n<p><strong>Result.</strong> Ids stayed globally unique, the index stopped fragmenting the way random UUIDs cause, and time‑ordered inserts and range queries got quicker — evenly, across every table, without anyone having to think about it again. As a bonus, every entity ends up with a key you can sort by time for free.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "Backend Engineering", "Data Engineering", "Databases", "Performance Tuning", "PostgreSQL", "Database Design & Modeling", "Database Performance Tuning" ] }, { "id": "https://engineer.company/portfolio/implemented-a-catalogue-driven-deep-merge/", "url": "https://engineer.company/portfolio/implemented-a-catalogue-driven-deep-merge/", "title": "Implemented a catalogue‑driven deep‑merge", "summary": "Implemented a catalogue-driven deep-merge for stored JSON preferences, preventing missing-key crashes as the schema evolves.", "content_html": "<p><strong>Situation.</strong> The platform stores JSON preferences — notification settings and the like — as a user’s saved values laid over a set of defaults. The original merge did that at a single level. The problem shows up later: add a new key to the defaults, and rows saved before that key existed simply don’t have it. Then some client code reads that field, gets undefined, and falls over — for exactly the users who’ve been around longest.</p>\n<p><strong>Task.</strong> Evolving the preference schema had to be safe, so that adding a setting could never break the people who signed up before it existed.</p>\n<p><strong>Action.</strong> The single‑level merge was replaced with a deep‑merge driven by the defaults as a catalogue. The defaults are treated as the authoritative list of every key that should exist; the user’s stored values are merged recursively on top, so anything in the catalogue is guaranteed to come out present, whether or not it was in the saved blob. Add a key to the defaults and it appears in every existing row’s effective preferences automatically, nested keys included. It rolled in through a migration so existing data got the benefit straight away rather than waiting to be rewritten.</p>\n<p><strong>Result.</strong> Preferences can grow without fear. Adding a setting no longer risks an undefined‑field crash in the client, and the frontend stopped needing defensive checks scattered around every place it reads a preference. It also gave the rest of the platform a dependable way to extend any stored‑JSON blob — the pattern, not just the one fix.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "Backend Engineering", "Data Governance", "Databases", "Migrations & Modernization", "PostgreSQL", "Backend & API Development", "Data Governance & Quality", "Database Design & Modeling" ] }, { "id": "https://engineer.company/portfolio/provisioned-azure-infrastructure-as-code/", "url": "https://engineer.company/portfolio/provisioned-azure-infrastructure-as-code/", "title": "Provisioned Azure infrastructure as code", "summary": "Provisioned Azure infrastructure as code with Bicep — Container Apps, PostgreSQL, Front Door/WAF — reproducible across dev, staging and prod.", "content_html": "<p><strong>Situation.</strong> The platform lives on Azure, and Azure done by hand — clicking through the portal, tweaking a setting here and there — is a trap. It drifts, nobody remembers why something is the way it is, and rebuilding it after a bad day is slow and nerve‑wracking. With more than one environment to keep in step, that only gets worse.</p>\n<p><strong>Task.</strong> Put the whole thing in code, so an environment is something you can read, review and recreate rather than a pile of manual state.</p>\n<p><strong>Action.</strong> The estate is defined in Bicep. Each environment — testing, staging, product — comes out of the same templates: the api and www running as Azure Container Apps on a managed environment, a PostgreSQL Flexible Server, Redis for caching, Front Door with a WAF policy out in front, and the networking underneath it (VNet, NSG, private DNS), with Log Analytics wired in for diagnostics. Images are pulled from the project’s Azure Container Registry. Because it’s all parameterised, standing up a fresh environment or changing an existing one is a pull request, not a support ticket to yourself.</p>\n<p><strong>Result.</strong> The environments became reproducible and reviewable. Drift stopped being a mystery, because the source of truth is the code, and bringing infrastructure up or back is a matter of applying the templates rather than remembering what got clicked last time. It’s the difference between infrastructure you own and infrastructure that owns you.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "Automation & CI/CD", "Cloud", "DevOps", "Infrastructure", "Networking & VPN", "Platform Architecture", "Reliability & Backups", "Security", "Cloud Infrastructure & Migration", "Infrastructure as Code", "Networking & VPN Setup", "Security & Access Management" ] }, { "id": "https://engineer.company/portfolio/built-github-actions-ci-cd-pipelines/", "url": "https://engineer.company/portfolio/built-github-actions-ci-cd-pipelines/", "title": "Built GitHub Actions CI/CD pipelines", "summary": "Built GitHub Actions CI/CD with a distroless production frontend image and multi-environment promotion, making releases routine and boring.", "content_html": "<p><strong>Situation.</strong> Shipping shouldn’t depend on someone remembering the steps, and what ends up running in production shouldn’t be a fat general‑purpose container carrying a shell and a package manager it’ll never use — that’s just attack surface sitting there for no reason.</p>\n<p><strong>Task.</strong> Make the path from commit to running‑in‑Azure automatic, and keep the production images as small and locked‑down as each workload allows.</p>\n<p><strong>Action.</strong> The automation runs on GitHub Actions. Separate workflows handle the code‑quality gate, the tests, and the per‑environment deploys, with CodeQL, dependency review and an SBOM step alongside them, so nothing reaches an environment without passing the checks first. The images are multi‑stage builds, and the base for each part was picked on its merits rather than as one blanket choice: the frontend ships on a distroless image (gcr.io/distroless/cc‑debian13 — no shell, no package manager), the Go API on a slim Alpine, and the database image on postgres‑slim. Promotion moves a build through the environments along a defined route rather than by hand.</p>\n<p><strong>Result.</strong> Releases stopped being a careful manual ritual and became a routine, boring event, which is exactly what you want from releases. The production frontend runs on about as little as you can give it, the checks catch problems before they land, and “deploy” is something the workflows do rather than something anyone sweats through.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "Automation & CI/CD", "Cloud", "Containers (Docker/Kubernetes)", "DevOps", "Reliability & Backups", "Security", "Testing & QA", "Containerization & Orchestration", "DevOps & CI/CD Automation" ] }, { "id": "https://engineer.company/portfolio/authored-578-go-task-automation-targets/", "url": "https://engineer.company/portfolio/authored-578-go-task-automation-targets/", "title": "Authored 578 go‑task automation targets", "summary": "Authored 578 go-task automation targets across native, Docker and HTTPS modes — linting, testing, database and deployment behind one toolchain.", "content_html": "<p><strong>Situation.</strong> The codebase is a polyglot monorepo — Go, TypeScript, SQL, Python, shell — and every one of those brings its own way to build, test, lint and run. Left alone, that means everyone carrying a mental cheat‑sheet of tool‑specific commands, and newcomers spending their first day just working out how to make things go.</p>\n<p><strong>Task.</strong> Give the whole project one front door: a single, consistent way to run anything, whatever language it happens to be written in.</p>\n<p><strong>Action.</strong> That’s built out with go‑task — a Taskfile layer that has grown to 578 named targets, 50 in the root file and 528 in namespaced files beneath it. There are the development modes (native, Docker, an HTTPS variant for testing PWA and mobile), the code‑quality side (lint, format, test, fix across all the languages), database management, and the environment‑specific build and deploy tasks. There’s even a low‑memory mode for machines that can’t spare the RAM to build the frontend the usual way. The point was never to have a lot of tasks; it was that you never have to know the underlying command.</p>\n<p><strong>Result.</strong> Anyone can run task –list and see the whole toolchain laid out, and run any part of it the same way regardless of what’s under the hood. Onboarding got shorter, and the small, stupid mistakes — wrong flag, wrong directory, half‑remembered command — mostly went away. The number is also a warning: 578 targets is past what anyone can hold in their head, so the naming and the namespacing are what keep it usable rather than the count being something to be proud of.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "Automation & CI/CD", "DevOps", "Documentation", "Technical Leadership", "Testing & QA", "DevOps & CI/CD Automation", "Technical Documentation" ] }, { "id": "https://engineer.company/portfolio/owned-end-to-end-platform-deployments-to-azure/", "url": "https://engineer.company/portfolio/owned-end-to-end-platform-deployments-to-azure/", "title": "Owned end‑to‑end platform deployments to Azure", "summary": "Owned end-to-end Azure deployments, moving releases through dev, staging and production along a defined, repeatable path.", "content_html": "<p><strong>Situation.</strong> The platform had to reach users across several Azure environments, and deployment is the seam where the infrastructure, the build system and the application all meet. It’s also where a small mistake stops being a bug and becomes an outage, so it’s the part you least want to be doing by hand and half from memory.</p>\n<p><strong>Task.</strong> Deployment was owned end to end, so that a change moved out to each environment the same predictable way every time.</p>\n<p><strong>Action.</strong> Releases move along a fixed route — development, then staging, then production — rather than anyone pushing straight to a live environment. CI/CD builds the images and ships them, and the Bicep templates keep the target infrastructure identical from one environment to the next, so a build isn’t promoted into a subtly different place each time. Configuration that differs per environment is kept separate from the secrets, which means the same built artefact can be promoted through the environments and just picks up the right settings where it lands, instead of being rebuilt for each one.</p>\n<p><strong>Result.</strong> Changes reach each environment predictably, along a defined path, with no ad‑hoc manual deploys in the mix. Releasing turned into a controlled, repeatable step instead of a held‑breath moment, and that’s a big part of what kept the live platform stable while it was still changing quickly underneath.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "Automation & CI/CD", "Cloud", "DevOps", "Infrastructure", "Reliability & Backups", "Security", "Cloud Infrastructure & Migration", "DevOps & CI/CD Automation", "Site Reliability & Monitoring" ] }, { "id": "https://engineer.company/portfolio/implemented-backups-and-disaster-recovery-as-code/", "url": "https://engineer.company/portfolio/implemented-backups-and-disaster-recovery-as-code/", "title": "Implemented backups and disaster recovery as code", "summary": "Implemented database backups and an infrastructure-as-code disaster-recovery strategy for fast, reproducible recovery.", "content_html": "<p><strong>Situation.</strong> A product that lives on its data can’t afford to lose any, and “there are backups somewhere” is a hope, not a recovery plan. The only backup worth having is one you know restores, into an environment you know you can rebuild. The product had neither half written down.</p>\n<p><strong>Task.</strong> Get the recoverable position defined instead of assumed — the data, the environment around it, and an honest account of how far that actually reaches today.</p>\n<p><strong>Action.</strong> Backup retention is configured in the Bicep beside the database it protects, so point‑in‑time restore is a property of the template rather than a setting someone once clicked in a portal. The environment around it is defined as infrastructure‑as‑code too, which is the quiet half people forget: restoring a database into an environment you’d have to rebuild by hand from memory isn’t really recovery. Then the position was audited and written up — the restore procedure, the drill that would measure it, and the gaps still open: geo‑redundancy is switched off, and the recovery‑time objective is proposed rather than measured, because no drill has been run yet.</p>\n<p><strong>Result.</strong> Recovery stopped being a vague reassurance and became a documented position with its gaps named. That reads less impressively than “disaster recovery: done”, and it is worth considerably more — whoever touches it next knows what is covered, what isn’t, and exactly which drill closes the difference. A named gap is one you can close; an unnamed one is discovered during an incident.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "Cloud", "Databases", "DevOps", "Infrastructure", "PostgreSQL", "Reliability & Backups", "Backup & Disaster Recovery", "Database Administration (DBA)", "Infrastructure as Code" ] }, { "id": "https://engineer.company/portfolio/delivered-full-progressive-web-app-support/", "url": "https://engineer.company/portfolio/delivered-full-progressive-web-app-support/", "title": "Delivered full Progressive Web App support", "summary": "Delivered full Progressive Web App support — installable and offline-capable — with Workbox runtime caching via next-pwa.", "content_html": "<p><strong>Situation.</strong> A lot of the platform’s users are at sea. Seafarers and field staff, on phones, frequently offline or hanging off a bad connection — not people sitting at a desk on reliable office wifi. Building as if everyone had a fast, constant network would have quietly excluded a big part of the actual audience.</p>\n<p><strong>Task.</strong> The app had to be installable like a native one, and still usable when the network drops out.</p>\n<p><strong>Action.</strong> It’s delivered as a full Progressive Web App. It installs to the home screen with proper icons across the various sizes, a splash screen and theme colours, so it looks and launches like an app rather than a bookmark. The service worker is set up through the next‑pwa plugin, and Workbox runtime caching uses CacheFirst for the things that don’t change per request — fonts, images, audio, video, CDN assets — alongside sensible HTTP cache‑control headers. Because service workers only really behave over HTTPS, HTTPS‑based testing validated the offline and install behaviour on real devices instead of hoping it worked.</p>\n<p><strong>Result.</strong> Users can install the app and keep using it offline, and repeat loads come back fast from cache instead of over the wire. The platform behaves like a native app on the hardware its users actually carry, which for this audience isn’t a nice‑to‑have — it’s the difference between the app being usable at sea and not.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "Frontend Engineering", "Full‑Stack Development", "Performance Tuning", "Reliability & Backups", "Web Development", "Frontend Development", "Website Development & CMS" ] }, { "id": "https://engineer.company/portfolio/designed-an-ios-26-liquid-glass-design-system/", "url": "https://engineer.company/portfolio/designed-an-ios-26-liquid-glass-design-system/", "title": "Designed an iOS 26 'Liquid Glass' design system", "summary": "Designed an iOS 26 'Liquid Glass' design system and a canonical component inventory enforced by lint rules to stop UI divergence.", "content_html": "<p><strong>Situation.</strong> A UI without a shared visual language and a fixed set of components drifts, and it drifts fast. Every new screen reinvents its own buttons and badges and cards, each a little different, and those small inconsistencies pile up until the product looks incoherent and every change means touching five bespoke versions of the same thing.</p>\n<p><strong>Task.</strong> The design system had to be coherent enough to look deliberate, and enforceable enough that it wouldn’t erode the moment the team was busy.</p>\n<p><strong>Action.</strong> The visual language and the component system were designed as one thing. The language is an iOS 26 “Liquid Glass” look — translucent panels with backdrop blur, layered shadows, a bit of refraction, spring animations — built out of Tailwind utilities. On top of it sits a canonical set of components every screen is supposed to compose from: Card, Label, Button, GlassIconButton, DirectoryGrid and the rest. The part that makes it stick is the linting: rules that reject a hand‑rolled badge or chip and point you at the canonical component instead, so the system is held up by tooling rather than by whoever’s reviewing that day remembering to care. And the docs are paired with concrete mistake‑to‑resolution notes, so the guidance is “here’s the wrong way and the right way,” not an abstract principle.</p>\n<p><strong>Result.</strong> The UI stays consistent and on‑brand, and the one‑off components that would otherwise spread get caught before they do. Visual consistency stopped being a matter of everyone’s discipline and became something the tooling holds the line on, which is the only version of it that survives a deadline.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "Design Systems & UI", "Documentation", "Frontend Engineering", "UX / UI Design", "Frontend Development", "UI/UX Design & Design Systems" ] }, { "id": "https://engineer.company/portfolio/designed-the-products-ui-and-ux-end-to-end/", "url": "https://engineer.company/portfolio/designed-the-products-ui-and-ux-end-to-end/", "title": "Designed the product's UI and UX end to end", "summary": "Designed the product's UI/UX end to end — directory grids, card/table views, live validators and breadcrumbs — for dense maritime data.", "content_html": "<p><strong>Situation.</strong> The platform puts a lot of different entities in front of people — professionals, companies, ships, jobs, reviews — and the interface had to be two things that fight each other: good‑looking and genuinely usable. Dense enough to show real maritime data, but not so dense it turns into a wall you bounce off.</p>\n<p><strong>Task.</strong> The product’s UI and UX were owned end to end — the layouts, the interaction patterns, and the smaller stuff like how someone gets walked through a form without feeling nagged.</p>\n<p><strong>Action.</strong> A few decisions did most of the work. The directory grids have a card/table toggle, so you can browse visually or scan a dense table, and the app remembers which you picked. Forms use live requirement validators that sit above the input and show each rule in green when it’s met and amber when it isn’t, so you’re guided while you type instead of scolded after you submit. Navigation is consistent breadcrumbs and two‑column entity layouts, so pages feel like the same product rather than a set of unrelated screens. And the error philosophy is guidance over failure — no red walls, redirects instead of dead ends, the app trying to keep you moving rather than stopping you.</p>\n<p><strong>Result.</strong> What came out is a polished, consistent experience that makes dense maritime data approachable and steers people through the complicated parts. It reads as considered and trustworthy, which isn’t cosmetic on a platform people are using for their actual careers — if it looked slapdash, they’d trust the data less, and they’d be right to.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "Design Systems & UI", "Frontend Engineering", "Product & Requirements", "UX / UI Design", "Web Development", "Frontend Development", "Product Strategy & Requirements", "UI/UX Design & Design Systems" ] }, { "id": "https://engineer.company/portfolio/hardened-the-application-with-nonce-based-csp/", "url": "https://engineer.company/portfolio/hardened-the-application-with-nonce-based-csp/", "title": "Hardened the application with nonce‑based CSP", "summary": "Hardened the app with nonce-based CSP, HSTS, SameSite cookies, least-privilege database roles and server-side entitlement re-checks.", "content_html": "<p><strong>Situation.</strong> The platform holds professional and organizational data, the kind people expect to be handled properly, so a single line of defence was never going to be enough. The working assumption has to be that the client is hostile — that anything the browser enforces can be switched off by whoever’s holding the browser — and the security has to hold up anyway.</p>\n<p><strong>Task.</strong> The platform had to be hardened at every layer — frontend, API, database — so that security was enforced by the server independently of whatever the interface happened to allow.</p>\n<p><strong>Action.</strong> On the frontend, the Next.js proxy middleware — proxy.ts — sets a Content‑Security‑Policy with a per‑request nonce and strict‑dynamic, plus HSTS and SameSite cookies, so the browser is locked down about what it’ll run and send. At the API, there’s rate limiting, CORS, request‑size limits, input validation before anything touches the database, and logging of the security‑relevant events. In the database, the API logs in as a least‑privilege role that can only EXECUTE the app functions, the functions run SECURITY DEFINER, and everything is parameterised. And the entitlements — tier, role, organization, ship scoping — are re‑checked on the server on every request, with the frontend gates treated as UX only. The gates decide what you see; the server decides what you can do.</p>\n<p><strong>Result.</strong> Security doesn’t depend on the UI behaving. The protections are layered so that getting past one doesn’t get you past the rest, and the whole thing is built on the assumption that the client can’t be trusted — which is the right assumption for data people are handing over in confidence.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "APIs & Integration", "Backend Engineering", "Databases", "Frontend Engineering", "Platform Architecture", "Security", "Backend & API Development", "Security & Access Management" ] }, { "id": "https://engineer.company/portfolio/built-an-english-danish-i18n-system/", "url": "https://engineer.company/portfolio/built-an-english-danish-i18n-system/", "title": "Built an English/Danish i18n system", "summary": "Established an English/Danish i18n system carrying 12,027 messages per locale across 458 files, with linter-enforced vocabulary and a size budget.", "content_html": "<p><strong>Situation.</strong> The platform runs in English and Danish, and translation systems have a way of sprawling into a mess. The files grow without limit, keys leak — present in one locale, missing in the other — and the language‑specific conventions get applied unevenly, so one locale ends up reading like it was translated by a committee that didn’t talk to each other. For a professional product that’s not a small blemish; it reads as carelessness.</p>\n<p><strong>Task.</strong> The internationalization setup had to scale — keeping both languages consistent and correct, and the files something a person could still maintain a year in.</p>\n<p><strong>Action.</strong> It’s built on next‑intl with rules the tooling actually enforces. The Danish side has a locked vocabulary and style — literal æøå, the informal “du”, the right imperative accents, and semantic mappings for compound nouns so they’re translated by meaning rather than word‑for‑word — and a linter holds it to that. There’s a hard 400‑line budget per namespace file, with a split‑and‑merge approach so a big area divides into nested files that merge back cleanly instead of one file growing forever; that budget is why 12,027 messages per locale live in 458 files rather than a handful of enormous ones. A linter check compares keys across locales so nothing leaks or goes missing. And stored overrides are merged catalogue‑driven rather than with fragile top‑level fallbacks — the same deep‑merge idea used for preferences.</p>\n<p><strong>Result.</strong> Both locales stay correct and consistent at 24,054 messages between them, and the files stay maintainable as the string count climbs. Language quality became something the tooling guarantees on every commit rather than something that quietly degrades each time someone adds a string in a hurry. The budget is the load‑bearing part: without a limit per file, 458 files would have been twelve, and nobody would open them.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "Automation & CI/CD", "Documentation", "Frontend Engineering", "Full‑Stack Development", "Internationalization", "Frontend Development", "Internationalization & Localization" ] }, { "id": "https://engineer.company/portfolio/set-a-zero-warnings-bar-across-six-languages/", "url": "https://engineer.company/portfolio/set-a-zero-warnings-bar-across-six-languages/", "title": "Set a zero‑warnings bar across six languages", "summary": "Set a zero-warnings quality bar across Go, TypeScript, SQL, Python, Shell and Markdown, enforced by pre-commit hooks.", "content_html": "<p><strong>Situation.</strong> Warnings that pile up are quietly corrosive. Every diagnostic you ignore lowers the bar a little, and once the build spits out forty of them nobody reads any of them, and a real problem sits in that list in plain sight because “warnings” have become background noise. In a polyglot codebase there are that many more sources of noise to let it happen.</p>\n<p><strong>Task.</strong> The repo needed one uncompromising quality bar across every language, so things got fixed instead of accumulating.</p>\n<p><strong>Action.</strong> A zero‑warnings policy went in, with the tooling as the thing that enforces it, because a policy that relies on everyone’s vigilance loses to the first busy week. Every linter diagnostic is an error — there’s no “warn” tier to hide in — and it’s the same across the whole stack: Go with golangci‑lint, TypeScript with ESLint, SQL with SQLFluff, Python with Ruff, shell with ShellCheck, Markdown with markdownlint. Inline suppressions are banned, so you can’t paper over a diagnostic; you have to actually fix the thing. Pre‑commit and pre‑push hooks run the linters and the tests, so a commit that would introduce a problem doesn’t get made in the first place. There are even function- and file‑length limits, to keep modules from sprawling past the point of being readable.</p>\n<p><strong>Result.</strong> Problems get fixed at the source instead of deferred into a backlog nobody clears, and the codebase stays clean by default rather than by periodic heroics. The standard is identical whatever language you’re in, and it’s the tooling holding it — not anyone’s willpower — which is why it actually holds.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "Automation & CI/CD", "DevOps", "Documentation", "Technical Leadership", "Testing & QA", "DevOps & CI/CD Automation", "Technical Leadership & Consulting" ] }, { "id": "https://engineer.company/portfolio/set-the-platforms-founding-technical-decisions/", "url": "https://engineer.company/portfolio/set-the-platforms-founding-technical-decisions/", "title": "Set the platform's founding technical decisions", "summary": "Set the platform's founding decisions in November 2025 — the layering, database-first data access and the zero-warnings bar — and they still hold.", "content_html": "<p><strong>Situation.</strong> The repository opened on 25 November 2025 with nothing in it. What gets decided in the first few weeks of a project like that is disproportionate: the layering, where the business logic is allowed to live, what the quality bar is. Those choices are cheap to make on day three and close to impossible to reverse by month six, by which point everything written since assumes them.</p>\n<p><strong>Task.</strong> The founding decisions had to be made deliberately and early, and made in a form that could survive being handed to other people and to a much larger codebase than existed at the time.</p>\n<p><strong>Action.</strong> Three decisions did most of the work. The stack was layered so each part has one job — a Next.js frontend, a Go API that validates and forwards, a PostgreSQL function layer that owns the business rules — rather than logic being placed wherever it was convenient that afternoon. Data access was put behind stored functions from the start, which is the choice everything else in the database record follows from; retrofitting it later would have meant rewriting every handler. And a zero‑warnings bar went in before there was much code to hold to it, because a standard introduced at commit 5,000 is a cleanup project, whereas the same standard at commit 50 is just how the repository works. None of the three were the easy option at the time, and all three cost momentum in the first month.</p>\n<p><strong>Result.</strong> Nine months and several thousand commits later, all three still hold: the layers have not blurred, no handler talks to a table directly, and the build still has no warnings in it. That is the check worth applying to a founding decision — not whether it sounded right, but whether it survived contact with the volume of work that came after, which is the point at which convenient choices usually get quietly abandoned.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "Backend Engineering", "Databases", "DevOps", "Frontend Engineering", "Full‑Stack Development", "Platform Architecture", "Security", "Solution Architecture", "Team Leadership", "Technical Leadership", "Backend & API Development", "Frontend Development", "Full‑Stack Product Development", "Platform & Solution Architecture", "Technical Leadership & Consulting" ] }, { "id": "https://engineer.company/portfolio/delivered-analysis-and-regular-progress-reports/", "url": "https://engineer.company/portfolio/delivered-analysis-and-regular-progress-reports/", "title": "Delivered analysis and regular progress reports", "summary": "Delivered analysis and regular progress reports to the CEO, translating engineering metrics and delivery status into confident decisions.", "content_html": "<p><strong>Situation.</strong> As the platform came together, its progress and its health had to be visible to leadership in terms they could actually do something with. Raw engineering signals — build status, delivery pace, incidents — don’t mean much on their own to someone making product and investment calls; they’re at the wrong altitude. Someone had to translate.</p>\n<p><strong>Task.</strong> That translation was the job: taking the engineering reality — where delivery stood, what the system metrics were saying — and reporting it to the CEO clearly and regularly, so decisions rested on facts instead of guesswork.</p>\n<p><strong>Action.</strong> A steady reporting rhythm went in, rather than reporting when asked, which is always a bit too late. Delivery progress, scope, risks and system health got tracked and turned into plain, decision‑oriented updates — what’s on track, what’s at risk, and what a given priority would actually cost in trade‑offs. The thing to avoid was handing over raw numbers and leaving the interpretation to someone without the context; each report came with concrete recommendations, and where it helped the narrative was backed with the underlying analysis, so leadership could drill in if they wanted rather than having to take it on trust.</p>\n<p><strong>Result.</strong> Leadership ended up with a clear, honest, current picture of the engineering, and could steer product priorities and investment with some confidence instead of flying blind. Reporting stopped being a status ritual nobody reads and became something decisions actually got made from — which kept the technical work and the business direction pointed the same way.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "Data Analytics", "Product & Requirements", "Project Management", "Stakeholder & Reporting", "Technical Leadership", "Data Analytics & BI Dashboards", "Project Management (Agile)", "Technical Leadership & Consulting" ] }, { "id": "https://engineer.company/portfolio/ran-24-7-support-for-1000-iptv-ott-servers/", "url": "https://engineer.company/portfolio/ran-24-7-support-for-1000-iptv-ott-servers/", "title": "Ran 24/7 support for ~1,000 IPTV/OTT servers", "summary": "Provided 24/7 infrastructure support for an IPTV/OTT streaming platform — ~1,000 servers plus client systems across China, the US and Germany.", "content_html": "<p><strong>Situation.</strong> This was an IPTV/OTT streaming platform with customers spread across China, the US and Germany, which meant there was no quiet hour to do maintenance in — someone, somewhere, was always watching. Downtime on a platform like that isn’t an abstract metric; it’s someone’s television just stopping, and they don’t care why.</p>\n<p><strong>Task.</strong> Keeping the platform’s infrastructure available around the clock was the job — genuinely around the clock, not “business hours plus an on‑call rota nobody answers.”</p>\n<p><strong>Action.</strong> 24/7 support ran for roughly a thousand servers, plus a good number of client‑owned systems on top of that — administering them, monitoring them, keeping them secured and configured across the whole streaming estate. Because customers sat in three very different time zones, “after hours” didn’t really exist; a problem at 3am local was primetime for someone else, so it got treated as primetime. A lot of the work was noticing something drifting before it became an outage, because on a live streaming platform you don’t get to fix things quietly after the fact.</p>\n<p><strong>Result.</strong> The platform stayed continuously available for a global audience, with problems caught and dealt with at whatever hour they turned up, before they reached a viewer’s screen. On a 24/7 service that’s the whole job — success looks like nothing happening, which is exactly what the viewers wanted.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "Cloud", "Infrastructure", "Linux & Servers", "Monitoring & Observability", "Networking & VPN", "Reliability & Backups", "System Administration", "IT Support & Helpdesk", "Site Reliability & Monitoring" ] }, { "id": "https://engineer.company/portfolio/kept-iptv-streaming-signals-running-non-stop/", "url": "https://engineer.company/portfolio/kept-iptv-streaming-signals-running-non-stop/", "title": "Kept IPTV streaming signals running non‑stop", "summary": "Ensured uninterrupted IPTV streaming between suppliers and clients, monitoring and maintaining the streaming network and IP telephony 24/7.", "content_html": "<p><strong>Situation.</strong> IPTV lives or dies on the signal getting through. The streams flow from suppliers, through the platform, to the clients, and any break anywhere in that chain is a black screen for someone. The IP telephony sat alongside it, with the same requirement: it just had to work.</p>\n<p><strong>Task.</strong> Guaranteeing that the signal delivery and the telephony stayed uninterrupted was the job.</p>\n<p><strong>Action.</strong> The streaming network and the IP telephony were monitored, troubleshot and maintained around the clock. The point of watching it constantly is that streaming problems announce themselves as degradation before they become an outright drop — a stream that starts stuttering, a link that’s getting flaky — and if you’re paying attention you can catch it at the stutter instead of the black screen. So a lot of it was staying ahead of the signal rather than reacting to complaints about it.</p>\n<p><strong>Result.</strong> The streams and the calls stayed reliable across the platform, with problems detected and fixed before they turned into service anyone noticed dropping. Keeping a signal flowing between suppliers and end clients without a visible gap is quiet, constant work, and quiet is what it should be.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "Infrastructure", "Linux & Servers", "Monitoring & Observability", "Networking & VPN", "Reliability & Backups", "System Administration", "Networking & VPN Setup", "Site Reliability & Monitoring" ] }, { "id": "https://engineer.company/portfolio/built-and-maintained-django-apps-for-iptv/", "url": "https://engineer.company/portfolio/built-and-maintained-django-apps-for-iptv/", "title": "Built and maintained Django apps for IPTV", "summary": "Developed and maintained Django web applications for an IPTV platform, shipping features while improving performance and stability.", "content_html": "<p><strong>Situation.</strong> The IPTV platform had web applications built on Django around it — the parts people actually clicked on — and they needed to keep moving forward: new features, and the performance and stability that a platform running around the clock demands.</p>\n<p><strong>Task.</strong> Keeping those applications feature‑rich, fast and stable was the task.</p>\n<p><strong>Action.</strong> The Django web applications were developed and maintained — building new features, fixing the bugs, and pushing on performance and stability, worked across the team rather than in a corner. On something serving customers continuously, the stability side isn’t a nice‑to‑have alongside the features; it’s the constraint the features have to respect. A flashy feature that makes the thing wobble isn’t worth much when the thing can’t afford to wobble.</p>\n<p><strong>Result.</strong> The applications kept gaining capability while staying reliable for both the internal teams and the customers. Adding to something without destabilising it is the balance that mattered here, and that’s what the work held to.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "APIs & Integration", "Backend Engineering", "Frontend Engineering", "Full‑Stack Development", "Python", "Web Development", "Backend & API Development", "Full‑Stack Product Development", "Website Development & CMS" ] }, { "id": "https://engineer.company/portfolio/built-infrastructure-that-still-runs-years-later/", "url": "https://engineer.company/portfolio/built-infrastructure-that-still-runs-years-later/", "title": "Built infrastructure that still runs years later", "summary": "Planned and built infrastructure for internal and external systems durable enough to still run years later with minimal change.", "content_html": "<p><strong>Situation.</strong> As the organisation grew, its internal and external systems kept needing new capabilities bolted on. The easy way to do that is whatever’s quickest today; the trouble with the easy way is you’re back fixing it in six months.</p>\n<p><strong>Task.</strong> Planning and building infrastructure functionality that would actually last was the task — not just work now, but keep working.</p>\n<p><strong>Action.</strong> New infrastructure functionality was planned and implemented across the internal and external systems, designed to be durable — the kind of thing you build once, properly, so it keeps running for years with minimal touching rather than needing constant attention. That’s a deliberate choice each time: spend a bit more thought up front so you’re not signing yourself up to babysit it forever.</p>\n<p><strong>Result.</strong> The systems stayed functional and effective long after they were built, running for years with barely any change. That longevity is the real measure of infrastructure work — anyone can make something that works today; making something that’s still quietly working years later is the harder and more useful thing.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "Infrastructure", "Platform Architecture", "Reliability & Backups", "Solution Architecture", "System Administration", "Platform & Solution Architecture", "Site Reliability & Monitoring" ] }, { "id": "https://engineer.company/portfolio/maintained-a-legacy-hugo-site-and-improved-its-ui/", "url": "https://engineer.company/portfolio/maintained-a-legacy-hugo-site-and-improved-its-ui/", "title": "Maintained a legacy Hugo site and improved its UI", "summary": "Maintained and improved a legacy Hugo static website while contributing UI/UX refinements to the core asset-management product.", "content_html": "<p><strong>Situation.</strong> There was a legacy website built on Hugo — a static‑site generator — running alongside the company’s main product, an asset‑management system. The website was the old, established thing; the product was where the real value sat.</p>\n<p><strong>Task.</strong> Keeping the site healthy while also improving the product’s experience was the remit.</p>\n<p><strong>Action.</strong> The Hugo static site was maintained and enhanced — kept current and working — while UI/UX improvements and feedback fed into the primary asset‑management product at the same time. Splitting attention between a legacy site and the flagship product is mostly about not letting the old thing rot while you’re focused on the new one; both represent the company to someone, so both had to be kept decent.</p>\n<p><strong>Result.</strong> The website stayed current instead of quietly ageing, and the main product’s usability improved through steady, informed refinement — the kind that comes from actually using and thinking about the thing rather than redesigning it in one big swing.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "Frontend Engineering", "Full‑Stack Development", "UX / UI Design", "Web Development", "Frontend Development", "UI/UX Design & Design Systems", "Website Development & CMS" ] }, { "id": "https://engineer.company/portfolio/designed-a-time-management-and-reporting-system/", "url": "https://engineer.company/portfolio/designed-a-time-management-and-reporting-system/", "title": "Designed a time‑management and reporting system", "summary": "Designed a time-management and reporting system that stayed in production for years without significant modification.", "content_html": "<p><strong>Situation.</strong> The team didn’t have a dependable way to manage time and report progress. Which usually means it’s happening in a scatter of spreadsheets and memory, and the reporting turns into a scramble at the end of each period rather than something that just falls out of how people work.</p>\n<p><strong>Task.</strong> Building a system for both that would actually last was the task.</p>\n<p><strong>Action.</strong> A time‑management and reporting system was designed around how the team actually worked, rather than imposing some off‑the‑shelf process they’d route around. That’s the whole trick with internal tools — if it fits the real workflow, people use it; if it fights the workflow, they quietly abandon it and you’re back to spreadsheets. So it was built to match reality rather than an ideal.</p>\n<p><strong>Result.</strong> It stayed in production use for years without any significant modification. That’s the compliment you want for an internal tool — not that it was impressive, but that it just kept working and nobody ever needed to replace it. Something that survives years of daily use untouched was clearly built to fit.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "Automation & CI/CD", "Product & Requirements", "Project Management", "Technical Leadership", "Data Analytics & BI Dashboards", "Product Strategy & Requirements", "Project Management (Agile)" ] }, { "id": "https://engineer.company/portfolio/integrated-a-company-wide-password-manager/", "url": "https://engineer.company/portfolio/integrated-a-company-wide-password-manager/", "title": "Integrated a company‑wide password manager", "summary": "Integrated a company-wide password-management system, strengthening security and streamlining access control across the organisation.", "content_html": "<p><strong>Situation.</strong> Credentials were being handled inconsistently — different people storing and sharing them in different, ad‑hoc ways — and that inconsistency is itself the security risk. It’s rarely a dramatic breach; it’s a password in a chat message, a shared login nobody rotates, the slow accumulation of small exposures.</p>\n<p><strong>Task.</strong> Centralising the credentials and making them secure was the task.</p>\n<p><strong>Action.</strong> A company‑wide password‑management system went in, so there was one consistent, secure way credentials got stored and shared instead of everyone’s personal habit. The value of company‑wide is exactly that it’s not optional per person — a password manager only half the team uses barely helps, because the risk lives in the half that didn’t. So the point was to make the secure way the default way, everywhere.</p>\n<p><strong>Result.</strong> Security improved and access control got simpler and more consistent across the organisation. Once the credentials all live in one managed place, a whole set of small, boring exposures just stop being possible — which is most of what real‑world security actually is.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "Automation & CI/CD", "Infrastructure", "Security", "System Administration", "Security & Access Management" ] }, { "id": "https://engineer.company/portfolio/combined-web-development-with-seo-and-content/", "url": "https://engineer.company/portfolio/combined-web-development-with-seo-and-content/", "title": "Combined web development with SEO and content", "summary": "Drove client web success by combining custom website development and design with SEO, content strategy and copywriting.", "content_html": "<p><strong>Situation.</strong> Clients didn’t really want a website; they wanted the thing a website is supposed to do for them — to be found, to bring in customers, to actually work as a channel. A beautiful site nobody can find is a failure that looks like a success.</p>\n<p><strong>Task.</strong> Combining the build and the growth side into one offering was the task, rather than handing over a site and wishing them luck.</p>\n<p><strong>Action.</strong> The two halves were put together — custom website development and design on one side, SEO, content strategy and copywriting on the other — so a client got something that was both well‑built and actually discoverable. Those usually get treated as separate jobs, which is how you end up with a gorgeous site that ranks nowhere, or a well‑optimised site that’s unpleasant to use. Doing both meant the site was designed from the start to be found, not optimised as an afterthought.</p>\n<p><strong>Result.</strong> Clients ended up with stronger visibility and more engagement — their sites working as genuine growth channels rather than online brochures. Building the thing and making it findable in one go is what turned a website from a cost into something that actually earned its keep.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "Brand & Marketing", "Frontend Engineering", "Product & Requirements", "UX / UI Design", "Web Development", "Brand, Marketing & SEO", "UI/UX Design & Design Systems", "Website Development & CMS" ] }, { "id": "https://engineer.company/portfolio/recovered-data-from-sd-cards-hdds-ssds-and-raid/", "url": "https://engineer.company/portfolio/recovered-data-from-sd-cards-hdds-ssds-and-raid/", "title": "Recovered data from SD cards, HDDs, SSDs and RAID", "summary": "Recovered critical data across SD cards, HDDs, SSDs, RAID arrays, external drives and Mac systems — files back from devices written off as lost.", "content_html": "<p><strong>Situation.</strong> People turned up with storage that had failed or been damaged, and data on it they urgently needed back — and by the time someone’s carrying a dead drive into a shop, they’re usually past worried and into panicking. Photos, business files, the only copy of something that mattered.</p>\n<p><strong>Task.</strong> Recovering their data, across whatever media they walked in with, was the job.</p>\n<p><strong>Action.</strong> Data recovery ran across pretty much every common storage type — SD cards, spinning HDDs, SSDs, RAID arrays, external drives, Mac systems. Each of those fails and gives up its data differently: an SSD is a different problem from a platter drive, a RAID array is a different problem again, and a Mac filesystem has its own quirks. So the work was as much about knowing the right approach for the specific medium as it was about any single technique.</p>\n<p><strong>Result.</strong> Clients got critical data back from devices they’d already written off as gone, across every common kind of storage. There’s a specific relief on someone’s face when you hand back the drive with their files intact — that was the point of the work, and it happened across the whole range of media.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "Reliability & Backups", "System Administration", "Hardware Repair & Data Recovery", "IT Support & Helpdesk" ] }, { "id": "https://engineer.company/portfolio/built-a-test-suite-across-go-sql-and-browser/", "url": "https://engineer.company/portfolio/built-a-test-suite-across-go-sql-and-browser/", "title": "Built a test suite across Go, SQL and browser", "summary": "Built a layered automated test suite — 981 Go tests, 543 frontend specs and 494 SQL behavioural tests — with mutation, property and accessibility gates.", "content_html": "<p><strong>Situation.</strong> A platform that keeps its business logic in the database has a testing problem most projects do not. The logic is not in the language the test framework is good at — it is in SQL, behind function boundaries, and SQL is exactly the sort of code that goes untested because testing it is awkward. Add a Go API and a Next.js frontend on top of that, and “the important parts are covered” quietly turns into “the parts that were easy to cover are covered.”</p>\n<p><strong>Task.</strong> Each layer needed its behaviour checked where that behaviour actually lives, rather than everything being checked from the outside through a browser.</p>\n<p><strong>Action.</strong> Four layers got four kinds of test. The Go API carries 981 test functions across 310 files. The frontend carries 543 Vitest and Playwright specs, including 48 end‑to‑end files and 30 browser specs. The database carries 494 behavioural test files — 116,876 lines of SQL asserting through 6,654 raised exceptions — so a stored function is tested in the database instead of through three layers of application above it. Above those sit the tests that test the tests: Stryker mutation testing deliberately breaks a line and fails when nothing notices, and fast‑check generates inputs nobody thought to write down. An axe‑core gate asserts zero WCAG 2.0 and 2.1 A and AA violations, which makes accessibility a build failure rather than an audit finding months later. Coverage thresholds only ever move upward. The whole suite runs as 10 jobs in a 612‑line workflow.</p>\n<p><strong>Result.</strong> Changing something structural stopped being frightening, which is the only thing that keeps a codebase this size from calcifying. The honest cost is time — the suite is slow, it taxes every change, and at this size it needs maintenance of its own. What it buys is the ability to keep moving quickly, and that is worth more than the minutes it takes.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "Automation & CI/CD", "Backend Engineering", "DevOps", "Frontend Engineering", "PostgreSQL", "Reliability & Backups", "Testing & QA", "Backend & API Development", "DevOps & CI/CD Automation", "Technical Leadership & Consulting" ] }, { "id": "https://engineer.company/portfolio/built-stripe-and-in-app-purchase-entitlements/", "url": "https://engineer.company/portfolio/built-stripe-and-in-app-purchase-entitlements/", "title": "Built Stripe and in‑app purchase entitlements", "summary": "Built the payments and entitlements layer — Stripe with Apple and Google in-app purchase — gating directory, search and export from an access model.", "content_html": "<p><strong>Situation.</strong> Money is the part of a platform nobody gets to be casual about. A subscription has to survive a card expiring, a refund, a plan change, a webhook that arrives twice and a webhook that arrives out of order. The moment payment can happen on three storefronts — a card on the web, Apple in one app store, Google in the other — there are three different accounts of what somebody bought, and the product still needs one answer to one question: what is this person allowed to do right now?</p>\n<p><strong>Task.</strong> Taking money and granting permission had to be two systems rather than one, so that adding a storefront would not mean rewriting every gate in the product.</p>\n<p><strong>Action.</strong> Stripe handles cards and subscriptions through 18 Go files, and Apple and Google in‑app purchases arrive through their own receipt verification. All three converge on a payments schema of 9 tables and 34 functions — and then stop there. What the product actually asks is a separate access schema, 11 tables and 34 functions, which answers “may this account do this?” without knowing or caring which storefront paid for it. That answer gates the company directory, search and data export, and it is re‑checked on the server on every request, because a hidden button is a courtesy and not a control.</p>\n<p><strong>Result.</strong> Adding a storefront now touches the payments side and leaves the gates alone, and a support question about someone’s access has one table to look at rather than three. The cost is two schemas where a smaller product would want one, plus an entitlement lookup on requests that would otherwise have been free.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "APIs & Integration", "Backend Engineering", "Databases", "PostgreSQL", "Product & Requirements", "Security", "Backend & API Development", "Database Design & Modeling", "Product Strategy & Requirements", "Security & Access Management" ] }, { "id": "https://engineer.company/portfolio/built-first-party-analytics-inside-postgresql/", "url": "https://engineer.company/portfolio/built-first-party-analytics-inside-postgresql/", "title": "Built first‑party analytics inside PostgreSQL", "summary": "Built first-party product analytics in PostgreSQL — 47 functions over self-pruning partitioned tables — pseudonymised and gated on consent.", "content_html": "<p><strong>Situation.</strong> Product decisions need numbers, and the ordinary way to get them is to put a third‑party tag on every page and let somebody else’s servers watch the users. That is fast, it is free at small volume, and it means visitor behaviour on a professional networking platform — who looked at which company, who searched for what — becomes an asset held by an advertising business. For a platform whose users are identifiable maritime professionals, that is a poor trade.</p>\n<p><strong>Task.</strong> The product team needed funnels, retention and event data, collected in a way the platform could stand over and a user could refuse.</p>\n<p><strong>Action.</strong> Analytics went into PostgreSQL, in a schema of 7 tables and 47 functions. Event tables are partitioned — 22 partition definitions — and prune themselves on a schedule, because the cost of first‑party analytics is not collecting the events, it is holding them forever. Identity is pseudonymised behind a salt that rotates, so a visitor cannot be followed across the rotation boundary even from inside the database; that is a deliberate ceiling on what the data can answer. The client is consent‑aware and sends nothing before consent is given, rather than sending and filtering afterwards. Conversion funnels are computed in the database, next to the data, instead of exported to somewhere else to be joined back.</p>\n<p><strong>Result.</strong> Questions about how the product is used are answered from the platform’s own tables, with nothing leaving and no tag in the page. The limits are the honest part: rotating the salt costs long‑horizon cohort analysis, and none of this arrives with the dashboards a hosted tool gives you free. Both were accepted knowingly.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "Data Analytics", "Data Engineering", "Data Governance", "Databases", "Performance Tuning", "PostgreSQL", "SQL", "Data Analytics & BI Dashboards", "Data Governance & Quality", "Data Pipeline Development (ETL/ELT)", "Database Design & Modeling" ] }, { "id": "https://engineer.company/portfolio/built-company-ownership-claims-and-adjudication/", "url": "https://engineer.company/portfolio/built-company-ownership-claims-and-adjudication/", "title": "Built company ownership claims and adjudication", "summary": "Built company ownership claims end to end: a user claims, an administrator adjudicates, and approval rewrites the authorization graph.", "content_html": "<p><strong>Situation.</strong> A directory seeded from public sources has a structural problem: the companies in it did not put themselves there. Sooner or later somebody from one of them turns up wanting to correct their own entry — and there is no relationship between that person and that record, only an assertion that one exists. Grant it too readily and a competitor edits your page. Grant it too slowly and the directory stays wrong.</p>\n<p><strong>Task.</strong> There had to be a route from “this is my company” to genuine authority over the record, with a human decision in the middle and a trail behind it.</p>\n<p><strong>Action.</strong> Claims were built end to end, across 108 commits and both applications. A user submits a claim with evidence; it enters a queue in the back‑office; an administrator reviews it and approves or rejects it with a reason that goes back to the claimant. The interesting part is what approval does — it is not a flag on a row. Approval rewrites the authorization graph, so the account gains a real relationship to the organization, which is the same relationship every permission check in the platform already consults. The gate is the adjudication, not the code path, and no feature had to learn about claiming in order to respect it.</p>\n<p><strong>Result.</strong> Companies can take over and correct their own entries without anyone editing the database by hand, and every grant of authority has a named approver and a reason attached. Human review is the bottleneck by design; an automated check on a domain name would be faster and would be wrong in exactly the cases that matter most.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "Backend Engineering", "Frontend Engineering", "Full‑Stack Development", "Product & Requirements", "Security", "Full‑Stack Product Development", "Product Strategy & Requirements", "Security & Access Management" ] }, { "id": "https://engineer.company/portfolio/built-a-loyalty-wallet-leagues-and-badges/", "url": "https://engineer.company/portfolio/built-a-loyalty-wallet-leagues-and-badges/", "title": "Built a loyalty wallet, leagues and badges", "summary": "Built the loyalty and reputation system — 67 functions over a 31-table ledger, leagues, badges and a shop — with row locking on the balance.", "content_html": "<p><strong>Situation.</strong> Professional networking has a cold start problem: the platform is worth using once other people are already using it, and until then there is little reason to come back. The usual lever is a rewards system — points for contributing, standing that reflects reputation — which is easy to describe and treacherous to build, because the moment points can be spent they are money, and every mistake money makes is available to be made here.</p>\n<p><strong>Task.</strong> Contribution had to be measurable and rewardable, with a balance that could not be spent twice.</p>\n<p><strong>Action.</strong> The loyalty schema runs to 31 tables and 67 functions, with standing in a further 5 tables and 32 functions, and discovery and personalization schemas alongside them to decide what a given member sees. On top of the ledger sit leagues, badges and a redemption shop where a balance turns into something real. The part that took the care is the oldest bug in the book: check the balance, then spend it, and two requests arriving together both pass the check. Every mutation takes a row lock on the wallet before reading it, so the second request waits for the first to finish rather than racing it. That the wallet is in the same database as everything else is what makes it possible at all — the balance and the thing it bought commit together or not at all.</p>\n<p><strong>Result.</strong> Contribution is measured and rewarded, and a balance is arithmetic rather than an approximation. Row locking is the slower answer and was chosen anyway: contention on a wallet means members wait their turn, and the alternative is a member spending the same points twice and someone reconciling it by hand afterwards.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "Backend Engineering", "Databases", "Performance Tuning", "PostgreSQL", "Product & Requirements", "SQL", "Backend & API Development", "Database Design & Modeling", "Product Strategy & Requirements" ] }, { "id": "https://engineer.company/portfolio/built-the-social-layer-posts-feed-and-groups/", "url": "https://engineer.company/portfolio/built-the-social-layer-posts-feed-and-groups/", "title": "Built the social layer: posts, feed and groups", "summary": "Built the platform's social layer — posts, feed, groups, mentions, a follower graph and an occasions digest — on the same function-first data layer.", "content_html": "<p><strong>Situation.</strong> A directory of companies is a reference work. People check it and leave. What makes a professional platform worth returning to is other people being on it — which means posts, groups and a reason to come back that is not an email telling you to.</p>\n<p><strong>Task.</strong> A social layer had to be added without it becoming a second system with its own rules, its own permissions and its own way of storing things.</p>\n<p><strong>Action.</strong> Posts, a timeline, groups, mentions, a follower graph and an occasions digest went in across 188 commits, all of it on the same function‑first data layer as the rest of the platform. That constraint did most of the work: the follower graph is tables and functions like everything else, a mention resolves through the same identity schema the directory uses, and a post inherits the moderation queue already built for reviews. Nothing here needed its own store or its own permission model. The timeline is the one place that pushed back, because assembling a personalised timeline efficiently is a genuinely different problem from fetching a row, and it is where the personalization and discovery work earns its place.</p>\n<p><strong>Result.</strong> The platform has a reason to be opened on a day when nobody needs to look a company up, and it arrived without a parallel stack to maintain. Whether a social layer is the right investment for a maritime directory is a product question rather than an engineering one — it is here because the product asked for it, and it is built the same way as everything around it.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "Backend Engineering", "Databases", "Frontend Engineering", "Full‑Stack Development", "Product & Requirements", "Web Development", "Backend & API Development", "Full‑Stack Product Development", "Product Strategy & Requirements" ] }, { "id": "https://engineer.company/portfolio/built-the-hiring-and-seafarer-career-workspaces/", "url": "https://engineer.company/portfolio/built-the-hiring-and-seafarer-career-workspaces/", "title": "Built the hiring and seafarer career workspaces", "summary": "Built the hiring marketplace and seafarer career workspace — 151 stored functions across 48 tables — matching vacancies to documented qualifications.", "content_html": "<p><strong>Situation.</strong> The commercial argument for a maritime professional network is hiring: companies need crew and officers, and seafarers need berths. Both halves already existed on the platform in the wrong shape — companies were in the directory, professionals had profiles, and there was nothing connecting a vacancy to the person qualified to fill it. Qualification is the hard part, because in this industry it is certificates, ranks and documented sea time rather than a job title.</p>\n<p><strong>Task.</strong> Vacancies, applications and verifiable seafarer credentials had to be modelled properly rather than as free text on a profile.</p>\n<p><strong>Action.</strong> Two domains got built. The hiring side runs to 32 tables and 118 functions covering vacancies, applications, shortlisting and the employer’s view of its candidates. The career workspace carries 16 tables and 33 functions holding certificates, rank progression and sea time, with document upload and a reviewer queue so a credential is checked rather than claimed. Modelling progression as a graph rather than a list is what makes the matching useful — a rank is reachable from another rank given certain certificates and enough recorded time at sea, and that structure is what lets a vacancy be matched against a career rather than against a keyword. The career workspace ships behind a feature flag and has not been fully released; the hiring side is live.</p>\n<p><strong>Result.</strong> A vacancy can be matched against documented qualifications instead of a self‑described job title, which is the whole difference between a job board and a hiring tool in this industry. Verification is the bottleneck, deliberately — a reviewer queue does not scale the way an automated check would, and an automated check would certify people who should not be certified.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "Backend Engineering", "Data Governance", "Databases", "Full‑Stack Development", "PostgreSQL", "Product & Requirements", "Backend & API Development", "Database Design & Modeling", "Full‑Stack Product Development", "Product Strategy & Requirements" ] }, { "id": "https://engineer.company/portfolio/servers-you-can-rebuild-from-scratch/", "url": "https://engineer.company/portfolio/servers-you-can-rebuild-from-scratch/", "title": "Servers you can rebuild from scratch", "summary": "The estate is reproducible from the repository, and the parts of it that were only ever true because somebody remembered them are now assertions that fail a…", "content_html": "<p><strong>Situation.</strong> Engineer ApS runs its own estate — a web presence, a git forge, a database, backups, mail transport and DNS — and there was nobody to hand the operations to. A one‑person company has the same failure modes as a large one and none of the redundancy, which makes the usual answer, a person who remembers how the host was set up, the least available option there is.</p>\n<p><strong>Task.</strong> The whole estate had to be described in a repository rather than in a head, and described in a form that converges a real host rather than documenting one.</p>\n<p><strong>Action.</strong> What grew from that is 19 playbooks and 34 roles across 12,065 lines of YAML. The shape matters more than the size. Composition is data rather than flags: a host sits in a tier group whose variables declare which roles it runs, so provisioning with no arguments converges every host to its declared state. Idempotence is a contract rather than an aspiration — a converged host reports zero changes, and a play that cannot say that is not finished. Four command namespaces keep the promises apart: a gate that touches no host, reports that read one and never change it, provisioning that changes a host to match the repository, and verification that changes a host on purpose and returns a verdict.</p>\n<p><strong>Result.</strong> The estate is reproducible from the repository, and the parts of it that were only ever true because somebody remembered them are now assertions that fail a run. The cost is real: every change is slower to make than editing a file on the server would be, and a converge that half‑applies is worse than one that refuses, which is why a preflight play was later added in front of it. That trade was made deliberately and it has held.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "Automation & CI/CD", "DevOps", "Documentation", "Infrastructure", "Linux & Servers", "Platform Architecture", "Reliability & Backups", "System Administration", "Cloud Infrastructure & Migration", "DevOps & CI/CD Automation", "Infrastructure as Code" ] }, { "id": "https://engineer.company/portfolio/a-whole-company-on-one-small-server/", "url": "https://engineer.company/portfolio/a-whole-company-on-one-small-server/", "title": "A whole company on one small server", "summary": "The whole company runs on a machine that costs less per month than a lunch, and the design is better for the discipline rather than merely cheaper.", "content_html": "<p><strong>Situation.</strong> The company’s production host is a single‑core cloud instance with 512 MB of memory and 10 GB of disk, of which about 464 MB is usable. Everything the business runs in public sits on it: the web server terminating TLS for seven domains, the git forge, a Tor onion service, a Gemini server, a Gopher server, encrypted backups and intrusion banning. The usual reaction to that list is to buy a larger box.</p>\n<p><strong>Task.</strong> The constraint had to be treated as an architectural input rather than a problem to spend money on, because the honest question was not whether a bigger machine would work but whether the design needed one.</p>\n<p><strong>Action.</strong> Memory became the argument that settled decisions. There is no monitoring agent, no metrics pipeline and no dashboard — reporting is a pull, seven commands that read the host and render Markdown, changing nothing and running only when asked. The supervisor is systemd rather than a second process manager layered on top of it, and the container plane is Quadlet units under the same supervisor rather than a daemon with its own. Web‑panel platforms were ruled out at the design stage for the same reason. When the question came up of whether the host could carry an onion service, the answer came from a day of measured samples rather than an opinion: available memory never fell below about 310 MB of 464, swap sat at 2.6 per cent and the processor was 99.7 per cent idle.</p>\n<p><strong>Result.</strong> The whole company runs on a machine that costs less per month than a lunch, and the design is better for the discipline rather than merely cheaper. What it cost is headroom for anything careless — mail is deliberately not on this box at all — it is written as an install scaffold awaiting a host of its own, because a mail server needs headroom this machine has already spent, and that is written down rather than discovered later.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "Infrastructure", "Linux & Servers", "Performance Tuning", "Platform Architecture", "Reliability & Backups", "Solution Architecture", "System Administration", "Cloud Infrastructure & Migration", "Infrastructure as Code", "Platform & Solution Architecture" ] }, { "id": "https://engineer.company/portfolio/hardened-ssh-to-24-validated-directives/", "url": "https://engineer.company/portfolio/hardened-ssh-to-24-validated-directives/", "title": "Hardened SSH to 24 validated directives", "summary": "The SSH posture is now the daemon's answer rather than the repository's claim, and the difference is not theoretical — it was already two settings wide when…", "content_html": "<p><strong>Situation.</strong> SSH is the only interactive way into the company’s host, and its configuration is written by an automation role that had been running clean for months. An access report in September 2026 read the daemon’s own resolved settings and found two of them disagreeing with what the role had been writing on every single converge.</p>\n<p><strong>Task.</strong> The hardening had to become something the daemon confirms rather than something the repository asserts, because the gap between the two had already been open for months without anyone noticing.</p>\n<p><strong>Action.</strong> The cause was configuration ordering. The operating system ships its own defaults uncommented, above where a drop‑in file lands, and for the two settings in question the first occurrence wins. The fix was a filename prefix that sorts ahead of the vendor’s, which is a one‑character class of change and exactly the sort that stays broken because nobody thinks to look. What was built around it matters more: three stages of validation on every converge. The candidate file is syntax‑checked before it is installed, so an invalid config never reaches the host. The assembled configuration is checked after installation. Then the daemon’s own resolved output is read back and 24 directives are asserted against it, so a setting that is written but overridden fails the run. Two directives were deliberately left out, both because the daemon no longer implements them and writing them would only look thorough.</p>\n<p><strong>Result.</strong> The SSH posture is now the daemon’s answer rather than the repository’s claim, and the difference is not theoretical — it was already two settings wide when the check was written. A converge that cannot see whether a control took effect has not verified it, and grepping for a directive only proves it was written.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "Automation & CI/CD", "Infrastructure", "Linux & Servers", "Security", "System Administration", "Testing & QA", "Infrastructure as Code", "Security & Access Management" ] }, { "id": "https://engineer.company/portfolio/verified-firewall-rules-by-position/", "url": "https://engineer.company/portfolio/verified-firewall-rules-by-position/", "title": "Verified firewall rules by position", "summary": "The firewall posture is now checked the way a packet experiences it. The most useful outcome was not the check but what it revealed about the reporting that…", "content_html": "<p><strong>Situation.</strong> A firewall status listing shows a set of rules. A packet filter evaluates an ordered list and stops at the first match. Those are different things, and the difference is invisible in every tool that prints a summary — which is how the host ran for a year with a rate limit that no packet ever reached, sitting below a broader rule that matched first.</p>\n<p><strong>Task.</strong> Firewall verification had to read position rather than membership, because presence had already been demonstrated to prove nothing.</p>\n<p><strong>Action.</strong> The checks now read the numbered rule listing and the live filter chain, and assert against order. Exactly one rule exists per port and always with a protocol, because a rule without one silently widens the surface. The SSH port carries a rate limit and never an allow beside it, since an allow above a limit is precisely the shadowing that was found. The declared public surface is a short list of five ports, asserted as a whole rather than checked individually, so a port that appears without being declared fails rather than being noticed. The security report renders the rules in match order with a warning at the top of the section explaining how to read it, because the next person to open that file will otherwise read a set.</p>\n<p><strong>Result.</strong> The firewall posture is now checked the way a packet experiences it. The most useful outcome was not the check but what it revealed about the reporting that preceded it: every tool involved had been printing the rate limit for a year, correctly, and none of them had been asked the only question that mattered.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "Automation & CI/CD", "Infrastructure", "Linux & Servers", "Networking & VPN", "Security", "Testing & QA", "Networking & VPN Setup", "Security & Access Management", "System Administration" ] }, { "id": "https://engineer.company/portfolio/backups-that-have-actually-been-restored/", "url": "https://engineer.company/portfolio/backups-that-have-actually-been-restored/", "title": "Backups that have actually been restored", "summary": "The company can lose the host and get its data back, and that sentence rests on a restore that ran last month rather than on a backup that ran last night.", "content_html": "<p><strong>Situation.</strong> Everything the company holds — the git repositories, the database, the site — sat on one cloud instance whose provider snapshot was the entire recovery position. A provider snapshot is a fine thing to have and a poor thing to rely on: it is in the same account as the machine it protects, it is not encrypted by anyone here, and nobody had ever restored from one.</p>\n<p><strong>Task.</strong> Backups had to be encrypted, off‑host, pruned on a retention policy, and — the part that is usually skipped — actually restored from, on a schedule, without a person remembering to do it.</p>\n<p><strong>Action.</strong> The backup runs on a systemd timer: a database dump where one exists, then an encrypted deduplicated snapshot to storage at a different provider over SFTP, then a retention prune, then an integrity check. A dead‑man’s switch is pinged on success only, which is the distinction that makes it an alarm rather than a log — a run that fails says nothing, and saying nothing is what raises the alert. Separately, a restore drill runs monthly: it pulls a known file out of the repository and compares it, so the thing being checked is a restore rather than a backup. The passphrase is written to a file the units read rather than passed through the environment, because the supervisor processes escapes in environment values and a passphrase containing a backslash would have been silently different from the one that created the repository. Afterwards the recovery position was audited and written up, and the remaining gaps were named in the document rather than left to be discovered during an incident.</p>\n<p><strong>Result.</strong> The company can lose the host and get its data back, and that sentence rests on a restore that ran last month rather than on a backup that ran last night. The audit’s most valuable output was the list of things still not covered, which is the part a green backup report is structurally unable to tell anyone.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "Automation & CI/CD", "Documentation", "Infrastructure", "Linux & Servers", "Reliability & Backups", "Security", "Backup & Disaster Recovery", "Infrastructure as Code", "Site Reliability & Monitoring", "System Administration" ] }, { "id": "https://engineer.company/portfolio/built-dead-mans-switch-host-monitoring/", "url": "https://engineer.company/portfolio/built-dead-mans-switch-host-monitoring/", "title": "Built dead‑man's‑switch host monitoring", "summary": "The host now has an alarm whose failure mode is to fire, and the naming that would have destroyed it has been corrected.", "content_html": "<p><strong>Situation.</strong> A 464 MB host running a forge, a database and a web server has two realistic ways to die: it runs out of memory, or it runs out of disk. Neither announces itself. Both are entirely predictable a few hours in advance if anything is looking, and nothing was.</p>\n<p><strong>Task.</strong> The host needed an alarm that works when the host does not — which rules out anything that has to send a message at the moment of failure.</p>\n<p><strong>Action.</strong> The answer is a dead‑man’s switch on a timer. Every fifteen minutes with jitter, a small script measures available memory and free disk and pings an external service only if both are above their thresholds. Silence is the alert. A machine that has run out of memory, lost its network or stopped booting produces exactly the same signal as one that is unhealthy, which is the correct behaviour and the reason this shape was chosen over an agent that reports a status. The measurement is available memory, not free memory, and that distinction turned into the most instructive part of the work: the script had been reading the right column all along while six variable names around it said “free”. On a healthy Linux box free memory is near zero because the kernel uses idle memory for cache, so a reader checking those names against a ten per cent threshold would see twelve megabytes free on a 464 MB machine, conclude the check was broken, and fix it with a one‑character change that converts a working alarm into one that breaches permanently and gets muted inside a week.</p>\n<p><strong>Result.</strong> The host now has an alarm whose failure mode is to fire, and the naming that would have destroyed it has been corrected. The switch’s real limitation is stated in its own documentation: it proves the box is healthy, not that the site is serving, and those are different questions.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "Automation & CI/CD", "Infrastructure", "Linux & Servers", "Monitoring & Observability", "Performance Tuning", "Reliability & Backups", "Infrastructure as Code", "Site Reliability & Monitoring", "System Administration" ] }, { "id": "https://engineer.company/portfolio/made-ansible-check-mode-tell-the-truth/", "url": "https://engineer.company/portfolio/made-ansible-check-mode-tell-the-truth/", "title": "Made Ansible check mode tell the truth", "summary": "A dry run now either measures something or says it did not, and neither of those is the third option it used to have.", "content_html": "<p><strong>Situation.</strong> A dry run against production is supposed to be the safe way to find out what a change will do. In September 2026 a dry run failed on an assertion that was simply wrong about the host, and advised the operator to set a flag that would have relaxed a security sandbox. The dry run had not read the host. It had read a value the host never gave and drawn a conclusion from it.</p>\n<p><strong>Task.</strong> Every probe whose result feeds a decision had to be made honest under check mode, and the ones that could not be had to say so out loud instead of staying quiet.</p>\n<p><strong>Action.</strong> The cause is a property of the tool that is documented and easy to forget: the command module does not execute under check mode, and what it registers is not an empty result — it is a success with empty output. Any conditional reading that register therefore decides on a value that was never measured, and it decides in whichever direction its own logic happens to point. A sweep of every registered probe found six of them, each lying differently: one reported that the supervisor accepts every unit directive without having asked the supervisor, another reported nothing to do on a host with the firewall switched off. Each was given one of two shapes. A probe that reads pre‑existing state the run has not touched is marked to run even under check mode. A probe that cannot run is skipped, and a message names what was specifically not verified — because silence in a run log reads exactly like a pass.</p>\n<p><strong>Result.</strong> A dry run now either measures something or says it did not, and neither of those is the third option it used to have. The general rule went into the development guide in the same change: check mode must not lie, and a probe that cannot see is required to announce its blindness rather than infer a verdict from it.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "Automation & CI/CD", "DevOps", "Infrastructure", "Linux & Servers", "Reliability & Backups", "Testing & QA", "DevOps & CI/CD Automation", "Infrastructure as Code", "Site Reliability & Monitoring" ] }, { "id": "https://engineer.company/portfolio/reconciled-a-20-record-dns-zone-as-code/", "url": "https://engineer.company/portfolio/reconciled-a-20-record-dns-zone-as-code/", "title": "Reconciled a 20‑record DNS zone as code", "summary": "The zone is versioned, comparable and exportable, and the one decision that went against the obvious default is written down with its reasoning so that nobody…", "content_html": "<p><strong>Situation.</strong> The company’s DNS carried about twenty records across a web presence, a git subdomain, two personal redirects, mail routing through a hosted mailbox provider, a transactional sending domain and the authentication records for both. All of it lived in a provider’s web console, which means the current state was whatever the last person to click had left behind.</p>\n<p><strong>Task.</strong> The zone had to become a declaration in the repository, with a way to compare that declaration against what the provider is actually serving.</p>\n<p><strong>Action.</strong> The zone is written as data — live records, application records, transport‑policy records and a separate list of records pending removal, which is the honest way to record a deletion that has not happened yet. One play reconciles it against the provider’s API. Two more entry points sit behind explicit opt‑in tags: an audit that reports the difference without changing anything, and an export that writes the zone out in the standard zone‑file format so it can be read by something that is not this repository. The most interesting decision was a reversal. Proxying the two web faces through the provider’s content network was built, worked, and was then turned back off — because it costs the one sentence the company’s privacy page is about. With a proxy in front, another company handles every visitor’s address and every URL they request, before we do, under their policy rather than ours. For a business whose distinguishing claim is that nobody is watching, that is a worse trade than the attacks it defends against.</p>\n<p><strong>Result.</strong> The zone is versioned, comparable and exportable, and the one decision that went against the obvious default is written down with its reasoning so that nobody re‑litigates it by accident. The audit path is the part that gets used most, because knowing the difference is more often what is wanted than closing it.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "APIs & Integration", "Automation & CI/CD", "Infrastructure", "Networking & VPN", "Python", "Security", "Infrastructure as Code", "Networking & VPN Setup", "Security & Access Management", "System Administration" ] }, { "id": "https://engineer.company/portfolio/cut-systemd-sandbox-exposure-on-every-unit/", "url": "https://engineer.company/portfolio/cut-systemd-sandbox-exposure-on-every-unit/", "title": "Cut systemd sandbox exposure on every unit", "summary": "Every unit runs with the privileges it needs and not the ones it does not, the numbers are measured rather than asserted, and the class of defect where a typo…", "content_html": "<p><strong>Situation.</strong> Thirteen unit files were installed by this repository and run on the production host with the privileges the supervisor gives by default, which is most of them. A hardening score put the dead‑man’s‑switch service at 9.6 and called it unsafe; the internet‑facing git forge scored 8.3 and exposed. Both numbers were accurate and neither had prompted anything, because a score with no threshold attached is a number people learn to skip.</p>\n<p><strong>Task.</strong> Every unit needed a sandbox proportional to what it actually does, and the sandboxes needed a check, because the failure mode of a wrong sandbox is that the unit starts and then breaks somewhere the parser cannot see.</p>\n<p><strong>Action.</strong> Privilege restrictions went into every unit template — a privilege ceiling fixed at the unit’s own, a system‑call filter, restricted address families, and memory that is writable or executable but never both. The measured results: the dead‑man’s switch went from 9.6 to 1.5, the forge from 8.3 to 1.5, and both backup units from 9.6 to 2.3 and 2.5. Writing them turned up something better than the scores. One directive had been misspelled in three unit templates for as long as those roles had existed — a plausible‑looking name that does not exist, which the supervisor answers by logging that it does not recognise the key and starting the unit anyway. Grepping for a directive proves it was written; only the parser proves it took effect. So a verification role now runs every unit through the supervisor’s own verifier during the converge, and is included by every role that installs a unit. What that still cannot see is stated plainly in the same documents: every directive capable of breaking these units breaks them at runtime, not at parse time, and a score cannot tell whether a script still reaches its last line.</p>\n<p><strong>Result.</strong> Every unit runs with the privileges it needs and not the ones it does not, the numbers are measured rather than asserted, and the class of defect where a typo becomes a silently absent control now fails a run. The limits of the measurement are recorded next to it, which is the part that keeps the next reader from over‑trusting a green band.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "Automation & CI/CD", "Containers (Docker/Kubernetes)", "Infrastructure", "Linux & Servers", "Security", "Testing & QA", "Containerization & Orchestration", "Infrastructure as Code", "Security & Access Management", "System Administration" ] }, { "id": "https://engineer.company/portfolio/built-7-read-only-host-reporting-roles/", "url": "https://engineer.company/portfolio/built-7-read-only-host-reporting-roles/", "title": "Built 7 read‑only host reporting roles", "summary": "The estate can be described from the repository on demand, and the reports have found real things: the two dead security controls, an undeclared listening…", "content_html": "<p><strong>Situation.</strong> The host had no dashboard and was not getting one, because a metrics stack does not fit on 464 MB and would not have been worth its cost if it did. That left an honest gap: there was no way to answer questions like who has access, how much has this repository grown, what does the traffic look like, or is the hardening actually enforced.</p>\n<p><strong>Task.</strong> Those questions needed answers that could be produced on demand, cost the host nothing while nobody was asking, and could never change the thing they were describing.</p>\n<p><strong>Action.</strong> Seven read‑only roles, each rendering a live host into Markdown in the repository. A facts snapshot covering operating system, hardware, storage, network, services, packages, listening sockets and processor utilisation computed from two samples of the kernel’s own counter. An access report covering accounts, sudo scope, key fingerprints, forge users and database roles. A git report per repository. A metrics report over the day’s collected samples. A traffic report built from the web server’s privacy‑masked log. A provider inventory across four provider surfaces — DNS, two cloud APIs and a dedicated‑server API. And a security report that answers whether hardening is enforced rather than configured, printing the firewall rules in match order and the live ban rule set. The governing rule across all seven is that no number is printed that the run did not measure — a report that fills a gap with a plausible figure is worse than one that leaves it blank, because only the plausible one gets believed.</p>\n<p><strong>Result.</strong> The estate can be described from the repository on demand, and the reports have found real things: the two dead security controls, an undeclared listening port, and the observation that the traffic archive is only as old as the last time somebody remembered to harvest it. That last one is written up as a limitation with a specific cost recorded — sixteen days of one site’s history that rotated away between harvests and do not exist anywhere.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "Automation & CI/CD", "Documentation", "Infrastructure", "Linux & Servers", "Monitoring & Observability", "Stakeholder & Reporting", "Site Reliability & Monitoring", "System Administration", "Technical Documentation" ] }, { "id": "https://engineer.company/portfolio/your-code-on-your-own-hardware/", "url": "https://engineer.company/portfolio/your-code-on-your-own-hardware/", "title": "Your code on your own hardware", "summary": "The company hosts its own code on its own hardware, and the page in front of it inherits every check the main site passes rather than drifting away from it in…", "content_html": "<p><strong>Situation.</strong> The company’s source code lived on a third‑party hosting service, which is a reasonable place for it and a poor fit for a business whose argument to clients is that it does not hand their data to intermediaries. Running a forge instead means running a forge: authentication, access control, storage, backups, and a public face for it.</p>\n<p><strong>Task.</strong> A canonical git server had to be stood up on the existing host, with the smallest possible attack surface and no web administration panel, and a landing page in front of it that does not rot.</p>\n<p><strong>Action.</strong> The forge is a single binary supervised by the operating system, installed from its vendor’s own package source, deliberately chosen for being SSH‑first with no administrative web interface — the less interface, the less to defend. It is private by default: anonymous access is refused, keyless access is refused, and every declared repository is marked private rather than relying on obscurity. Its SSH listener binds only to the loopback interface on port 23231 and is reached from outside through a jump host, so the firewall’s declared surface does not grow. A protocol multiplexer that would put HTTPS and git SSH on one public port by inspecting the first bytes of a connection is written and ready behind a master switch, and that switch is off: multi‑user git over the public port is not needed yet, and a listener nobody uses is surface. The landing page in front of it was the more interesting problem. It had been a hand‑maintained copy of the main site’s design in its own repository, and every single difference ever found between the two turned out to be an accident rather than a decision: a type scale rendering the wordmark about nine per cent too large, a font declaration that collapsed two weights to one on any machine with the family installed, ornaments hidden below a certain width so they were absent on every phone, a weight used with no font shipped for it, and no main landmark or top‑level heading on any page. Five for five, and not one visible in a screenshot. The copy was deleted; the landing page is now built by the main site’s own templates and delivered as an artefact.</p>\n<p><strong>Result.</strong> The company hosts its own code on its own hardware, and the page in front of it inherits every check the main site passes rather than drifting away from it in ways only a measurement can see. Divergence is still available and now has to be written down.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "Automation & CI/CD", "DevOps", "Infrastructure", "Linux & Servers", "System Administration", "Web Development", "DevOps & CI/CD Automation", "Infrastructure as Code", "Website Development & CMS" ] }, { "id": "https://engineer.company/portfolio/ran-containers-on-podman-and-quadlet/", "url": "https://engineer.company/portfolio/ran-containers-on-podman-and-quadlet/", "title": "Ran containers on Podman and Quadlet", "summary": "Containers run under the supervisor that was already trusted, behind the firewall that was already declared, and a routine release needs no privileged access.", "content_html": "<p><strong>Situation.</strong> The platform needed somewhere to run application containers. The obvious choice was the industry default, and the obvious choice was wrong for this host in a specific way: it rewrites the kernel’s packet‑filter rules and publishes container ports above the firewall rules the hardening role installs, so a container quietly becomes reachable from the internet regardless of what the firewall was told.</p>\n<p><strong>Task.</strong> A container plane had to be chosen that does not bypass the firewall, does not add a second supervisor beside the one already trusted, and does not require a person to be root to ship a release.</p>\n<p><strong>Action.</strong> The plane is a daemonless container engine driving units generated by the operating system’s own supervisor. There is no second process manager: containers are services, they start the way services start, and they are described in the same declarative form as everything else. The containers use host networking with no published ports at all, which removes the firewall‑bypass question rather than mitigating it. Units run at the system level rather than rootless, and that is recorded as a trade — it keeps the automation simple and normal, at the cost of the stricter isolation rootless would give, and the note says which way to move if container escape ever matters more than automation simplicity. Deployment was separated from provisioning: root sets up the deployment path once, and thereafter an unprivileged user redeploys by running one fixed script through a scoped rule that permits that command and no arguments. Build the image, restart the service. No root shell, no arbitrary commands.</p>\n<p><strong>Result.</strong> Containers run under the supervisor that was already trusted, behind the firewall that was already declared, and a routine release needs no privileged access. The choice against the default is written down with its reason, which matters more than the choice — the next person will be told to use the default by every article they read.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "Automation & CI/CD", "Containers (Docker/Kubernetes)", "DevOps", "Infrastructure", "Linux & Servers", "Security", "Cloud Infrastructure & Migration", "Containerization & Orchestration", "DevOps & CI/CD Automation", "Security & Access Management" ] }, { "id": "https://engineer.company/portfolio/provisioned-a-second-server-from-code/", "url": "https://engineer.company/portfolio/provisioned-a-second-server-from-code/", "title": "Provisioned a second server from code", "summary": "A second host can be created, or re-adopted, from the repository, behind a firewall that already exists, at a cost the file states.", "content_html": "<p><strong>Situation.</strong> A second machine was needed for an application tier, at a different provider from the first, and the first machine’s own history was the argument for how to do it: it had been created by hand, and its firewall had been added afterwards, which leaves a window where a fresh host with a default password policy is reachable from the internet.</p>\n<p><strong>Task.</strong> The machine had to be created from the repository, and it had to be born behind its firewall rather than acquiring one shortly afterwards.</p>\n<p><strong>Action.</strong> Ordering carried the design. The firewall play runs before the server play, so the rule set exists before there is anything for it to protect; on the other provider, tagging runs before the cloud firewall because a firewall that targets tags needs the tags to exist first. Both providers’ firewalls are written directly against their REST APIs through a generic HTTP task rather than through a vendor collection, which removes a dependency and its version drift at the cost of writing the request shapes by hand. The role that creates the machine adopts an existing one rather than duplicating it if it is already there, so re‑running is safe. It is also documented, in its own file, as the one role in the repository that spends money — the instance type, the region, the specification and the monthly cost in euros are all written down, because a play that bills someone should say so where they will read it. That play is also the one that cannot be rehearsed the usual way: the generic HTTP task declares no check‑mode support, so a dry run skips every task in it. The rehearsal is done against the firewall play instead, which is free and reversible.</p>\n<p><strong>Result.</strong> A second host can be created, or re‑adopted, from the repository, behind a firewall that already exists, at a cost the file states. The one thing dry‑run cannot cover is named in the same file rather than left as a surprise.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "APIs & Integration", "Automation & CI/CD", "Cloud", "Infrastructure", "Python", "Security", "Cloud Infrastructure & Migration", "Infrastructure as Code", "Networking & VPN Setup", "Security & Access Management" ] }, { "id": "https://engineer.company/portfolio/built-a-multilingual-hugo-site-in-3-languages/", "url": "https://engineer.company/portfolio/built-a-multilingual-hugo-site-in-3-languages/", "title": "Built a multilingual Hugo site in 3 languages", "summary": "The site serves three languages and four representations off one tree, has no backend to attack and nothing to bill, and the one piece of JavaScript in it is…", "content_html": "<p><strong>Situation.</strong> The company needed a public site that works in three languages, proves capability rather than asserting it, costs nothing to run, and does not hand its visitors to anybody. Most of those are ordinary requirements. Together they rule out almost every content management system, because a runtime backend is a thing to secure, to patch, to pay for, and to explain on a privacy page.</p>\n<p><strong>Task.</strong> The site had to be generated entirely at build time and still behave like a modern one — searchable, installable, syndicated, printable, and readable by a screen reader in every language it ships.</p>\n<p><strong>Action.</strong> It is a static site: 108 template files, of which 53 are partials and 18 are shortcodes, three languages, no runtime backend, and one first‑party script granted as a scoped exception for a portfolio filter whose every control is hidden until it runs. Capability is added at build time rather than in the browser, which is a product decision and is written down as one. Every page is published in four representations from one content tree — HTML, a Markdown twin, a Gemini document and a Gopher menu entry — with the home page adding three syndication feeds and an installable‑application manifest on top. The taxonomies are deliberately two rather than one, and the distinction is load‑bearing: a category is a topic tag on the work, a service is something the company sells, and collapsing them would have made the catalogue a list of skills instead of a list of offerings. The same tree is then built seven more times, once per focused subdomain, by pointing the generator at a different content directory rather than by forking anything.</p>\n<p><strong>Result.</strong> The site serves three languages and four representations off one tree, has no backend to attack and nothing to bill, and the one piece of JavaScript in it is held to a contract a linter enforces. The cost is that everything interactive has to be solved at build time or not at all, which has ruled out several things that would have been easy with a server and is the reason the search index was costed and parked rather than shipped.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "Design Systems & UI", "Frontend Engineering", "Full‑Stack Development", "Internationalization", "Performance Tuning", "Web Development", "AI Agent Readiness", "Frontend Development", "Full‑Stack Product Development", "Internationalization & Localization", "Website Development & CMS" ] }, { "id": "https://engineer.company/portfolio/a-safety-net-that-no-longer-costs-half-an-hour/", "url": "https://engineer.company/portfolio/a-safety-net-that-no-longer-costs-half-an-hour/", "title": "A safety net that no longer costs half an hour", "summary": "The gate went from 1,636 seconds to 615, while the checks got broader rather than thinner — the serial cost went up and the wall clock came down.", "content_html": "<p><strong>Situation.</strong> Eleven of the site’s checks drive a headless browser: layout at every window shape the design draws, type scale, translated‑text expansion, contrast, accessibility rules, forced colours, mascot sizing, motion, print across six paper combinations, console errors and visual regression. Run one at a time they took 1,636 seconds, a little over twenty‑seven minutes. A gate that takes twenty‑seven minutes is a gate that gets skipped, and a skipped check is indistinguishable from a passing one.</p>\n<p><strong>Task.</strong> The wall clock had to come down far enough that running them was the default rather than a decision, without weakening any of them.</p>\n<p><strong>Action.</strong> The work was measurement first. Each check was timed individually on a twelve‑core machine: layout at 405 seconds, type at 301, translation at 282, contrast at 189, accessibility at 178, and so on down to seventeen. Two findings shaped the answer. Running all of them at once was slower than running four at a time — 265 seconds against 224 — because each check is itself a browser doing parallel work, and oversubscribing the machine costs more than the concurrency wins. And ordering by longest processing time first beat alphabetical order by nearly a third, 224 seconds against 320, which is the classic scheduling result and shows up here because the checks vary by a factor of twenty in cost. So the runner is a bounded worker pool, sized from the core count with a floor of two and a ceiling of four, fed longest‑first. Alongside it the checks were widened rather than narrowed: they now share one viewport table of twenty‑two window shapes, derived from every media query the stylesheet actually contains, which took the layout check alone from 95 seconds to 405.</p>\n<p><strong>Result.</strong> The gate went from 1,636 seconds to 615, while the checks got broader rather than thinner — the serial cost went up and the wall clock came down. The measurement is the part worth keeping: two reasonable‑sounding choices, running everything at once and running things in the order they were written, were each measurably worse than the alternative.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "Automation & CI/CD", "DevOps", "Frontend Engineering", "Performance Tuning", "Python", "Testing & QA", "DevOps & CI/CD Automation", "Frontend Development", "Technical Leadership & Consulting" ] }, { "id": "https://engineer.company/portfolio/replaced-27-font-sizes-with-a-6-step-scale/", "url": "https://engineer.company/portfolio/replaced-27-font-sizes-with-a-6-step-scale/", "title": "Replaced 27 font sizes with a 6‑step scale", "summary": "Six sizes where there were twenty-seven, with a stated ratio, a stated measure and a check that refuses the next unplanned value.", "content_html": "<p><strong>Situation.</strong> A measurement of the site’s rendered text found twenty‑seven distinct font sizes in use. Several were separated by less than one per cent — five values between 0.8 and 0.85 of the body size were all live at once, which is a difference no reader can perceive and every future editor will add to. Two of the headings were not sized by the stylesheet at all and were falling through to the browser’s own defaults, a ratio of 1.33 that matched nothing else on the page.</p>\n<p><strong>Task.</strong> The sizes had to become a scale with a stated ratio, and something had to prevent the twenty‑eighth.</p>\n<p><strong>Action.</strong> The scale is a Major Third, ratio 1.25, six named steps from fine print to display, each a token rather than a value. Headings are mapped explicitly onto steps instead of inheriting whatever the browser thinks, which is what fixed the two that had no size of their own. Only the bottom step is floored, so fine print stays legible on a phone without the whole scale being pinned. Logotypes are exempt by name rather than by accident. The linter is the part that makes it hold: it renders the site and fails on the twenty‑eighth distinct size. It has already earned its place twice — it caught a heading arriving at 1.17 of the body size, which is not a step of anything, and named the ratio in the failure message; and it caught inline code at 0.9, which is precisely the kind of value the scale exists to prevent. Alongside the scale went a measure of about seventy‑two characters, replacing an inherited fixed width that produced ninety‑one characters on a review page and a hundred and ten on the contact page.</p>\n<p><strong>Result.</strong> Six sizes where there were twenty‑seven, with a stated ratio, a stated measure and a check that refuses the next unplanned value. The constraint is real and occasionally inconvenient: a design that wants a size between two steps has to move to a step or argue for changing the scale, and that argument has been had and lost more than once.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "Automation & CI/CD", "Design Systems & UI", "Documentation", "Frontend Engineering", "Testing & QA", "UX / UI Design", "Brand, Marketing & SEO", "Frontend Development", "UI/UX Design & Design Systems" ] }, { "id": "https://engineer.company/portfolio/the-accessibility-faults-your-checker-misses/", "url": "https://engineer.company/portfolio/the-accessibility-faults-your-checker-misses/", "title": "The accessibility faults your checker misses", "summary": "Eleven defects fixed and, more usefully, four rules that outlive them: a guard that tests one axis defends one axis, a rule set that does not contain the rule…", "content_html": "<p><strong>Situation.</strong> The site passed its automated accessibility rules. It also had eleven defects that those rules could not see, because each one was a property of how the page rendered rather than of what the markup said — the kind that a validator reports as healthy and a keyboard user hits within seconds.</p>\n<p><strong>Task.</strong> The defects had to be found, fixed, and written up as a ledger with the rule each one produced, so the class of defect closes rather than the instance.</p>\n<p><strong>Action.</strong> They fell into three groups. Sizing: a stylesheet keyword used as if it were relative pinned a whole region to sixteen pixels while the body ran up to twenty‑two; a subscript element was used to mean “smaller”; shrinking text lengthened a line until a title ran to ninety‑six characters; and a measured height cap was outgrown by the very text‑spacing override the site claims to support. Focus and clipping: eight footer links stayed in the tab order behind a property that removes pointer interaction and nothing else; an overflow rule created a scroll container nobody wanted; and a scroll‑driven animation, which is simply inactive on a page too short to scroll, permanently clipped the footer off three pages. And two defects in the guards themselves, which are the ones worth naming — every window shape the layout check opened was nine hundred pixels tall, so a landscape phone at 852 by 393 gave seven pixels between two items and no check had ever looked; and the rule engine had been running seventy of its hundred and five rules, so the rule that would have caught the missing landmark was not in the set.</p>\n<p><strong>Result.</strong> Eleven defects fixed and, more usefully, four rules that outlive them: a guard that tests one axis defends one axis, a rule set that does not contain the rule cannot enforce it, height is a dimension so test it, and prove visibility in pixels rather than in the document. When the layout check was reopened properly it failed a hundred and twenty‑eight times across three languages, including a phone‑sized window where the hero’s closing line sat twenty‑one pixels under the fold.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "Automation & CI/CD", "Design Systems & UI", "Frontend Engineering", "Testing & QA", "UX / UI Design", "Web Development", "Frontend Development", "UI/UX Design & Design Systems", "Website Development & CMS" ] }, { "id": "https://engineer.company/portfolio/closed-the-colour-system-at-28-colours/", "url": "https://engineer.company/portfolio/closed-the-colour-system-at-28-colours/", "title": "Closed the colour system at 28 colours", "summary": "The palette is a closed, measured set that cannot quietly grow, and the two near-duplicate spellings that prompted it are gone.", "content_html": "<p><strong>Situation.</strong> Colour on a site with a dark default, a light theme, a print stylesheet, a forced‑colours mode and an illustrated brand does not stay a small set on its own. It had already started to drift in the way it always does: two spellings of the same colour sitting close enough that nobody could tell them apart, values re‑typed as literals beside the tokens that defined them, and documented colours that nothing painted.</p>\n<p><strong>Task.</strong> The palette had to become a closed set with a stated distinctness threshold, and something had to enforce the closure.</p>\n<p><strong>Action.</strong> Twenty‑eight colours, each documented with the contrast ratio it measures on the surface it appears on, and eleven brand values declared once as tokens. The threshold is numeric rather than editorial: two colours closer than a perceptual distance of 0.02 in a uniform colour space are one colour with two spellings. The linter fails five different ways — a value painted but undocumented, documented but unpainted, recorded with the wrong ratio, within the threshold of one already there, or a brand value re‑spelled as a literal. It found two immediately: a theme colour that existed in two spellings 0.018 apart, and a background colour standing in for a wall it sat 0.021 from. Transparency uses relative colour syntax rather than the mixing function, specifically because the guard cannot see through a mix and a palette guard that can be evaded is not a guard. The rule that goes with it is short: change a token rather than a rule, add a colour only when none fits, and never lower a documented ratio to make a design work.</p>\n<p><strong>Result.</strong> The palette is a closed, measured set that cannot quietly grow, and the two near‑duplicate spellings that prompted it are gone. It is a constraint that occasionally says no — a design wanting a slightly different blue has to take the one that exists or make the case for a twenty‑ninth colour, and that case has to include the ratio.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "Automation & CI/CD", "Design Systems & UI", "Documentation", "Frontend Engineering", "Testing & QA", "UX / UI Design", "Brand, Marketing & SEO", "Frontend Development", "UI/UX Design & Design Systems" ] }, { "id": "https://engineer.company/portfolio/found-the-light-theme-missing-a-whole-layer/", "url": "https://engineer.company/portfolio/found-the-light-theme-missing-a-whole-layer/", "title": "Found the light theme missing a whole layer", "summary": "A layer missing from one theme now fails a commit, and one that had been missing since it was written is painted.", "content_html": "<p><strong>Situation.</strong> The site ships two colour themes. The dark one is the default and gets looked at constantly; the light one is an override that appears only under a system preference, which means it is seen far less often and by nobody who is checking it. Theme parity is a class of defect where a surface is built once and restated once, and the restatement quietly drops a layer.</p>\n<p><strong>Task.</strong> Parity needed an assertion, because the only alternative is a person remembering to switch preference and look.</p>\n<p><strong>Action.</strong> The check is small and structural: for every layered surface, count the layers each theme paints, and fail when the counts differ. It does not compare appearances — themes are supposed to look different — it compares composition, which is the thing that should be identical. It found the defect on its first run. The site’s full‑page cover is three layers in the dark theme, a facet pattern over a scrim over the wall, and the light theme restated it with two. The facet overlay had been absent from the light theme from the day it was written, and nothing had ever said so, because a page missing one of three background layers looks like a design decision rather than a bug. The fix was one rule; the write‑up afterwards recorded what the overlay costs in contrast, so the addition is measured rather than assumed to be free.</p>\n<p><strong>Result.</strong> A layer missing from one theme now fails a commit, and one that had been missing since it was written is painted. This is the shape of defect the whole checking approach is aimed at — nothing was broken, nothing errored, the page rendered correctly, and it had been wrong for months.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "Automation & CI/CD", "Design Systems & UI", "Frontend Engineering", "Testing & QA", "UX / UI Design", "Frontend Development", "UI/UX Design & Design Systems" ] }, { "id": "https://engineer.company/portfolio/made-the-reduced-motion-setting-honest/", "url": "https://engineer.company/portfolio/made-the-reduced-motion-setting-honest/", "title": "Made the reduced‑motion setting honest", "summary": "The preference is honoured in fact rather than in intent, and eleven live animations that a blanket rule appeared to have covered are actually covered.", "content_html": "<p><strong>Situation.</strong> The site honoured the reduce‑motion preference with a single rule turning off every transition. It read as complete, it lints clean, and under an emulated reduce‑motion preference eleven selectors were still animating.</p>\n<p><strong>Task.</strong> The preference had to be honoured in fact, and the reason the obvious rule failed had to become something the next person cannot repeat.</p>\n<p><strong>Action.</strong> The cause is specificity. A universal rule scores at the bottom of the cascade and loses to any rule carrying a class, so a blanket transition‑none is beaten by every considered animation on the page — which is every animation worth turning off. The fix was structural rather than a patch: anything that moves now lives in one late stylesheet part, and a linter fails a transitioned transform anywhere else. The distinction the rule draws is deliberate and narrower than the obvious one: reduce motion, not colour. A colour fade is not motion and turning it off makes interfaces feel broken to people who did not ask for that, so a transition listing colour properties passes and a transition on movement does not. Motion is also required to be expressed as a transition rather than a keyframe animation, because the former is trivially cancellable and the latter is not. Two related traps came out of the same work and are recorded with measurements: a menu ornament was thirty‑one degrees through a sixty‑degree turn before it was half visible, and because the shape is periodic a half‑turn looks identical for a third of the cost; and five different properties each make an element the containing block for anything positioned relative to the viewport, which had silently shrunk a dismiss layer to a fraction of the screen.</p>\n<p><strong>Result.</strong> The preference is honoured in fact rather than in intent, and eleven live animations that a blanket rule appeared to have covered are actually covered. The general lesson is the one written at the top of that document: a rule that loses on specificity fails silently, and every report calls it something else.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "Design Systems & UI", "Frontend Engineering", "Testing & QA", "UX / UI Design", "Web Development", "Frontend Development", "UI/UX Design & Design Systems", "Website Development & CMS" ] }, { "id": "https://engineer.company/portfolio/built-a-637-line-print-stylesheet/", "url": "https://engineer.company/portfolio/built-a-637-line-print-stylesheet/", "title": "Built a 637‑line print stylesheet", "summary": "The site prints on unknown paper in both themes, and the four engine behaviours are written down with the defect each produced.", "content_html": "<p><strong>Situation.</strong> The site’s case studies are the artefact somebody prints and takes into a meeting. That makes paper a real output rather than a courtesy, and paper is a different medium from a narrow screen — the reader’s paper size is unknown, the reader’s orientation is unknown, and the browser’s print behaviour differs between engines in ways no screen preview reveals.</p>\n<p><strong>Task.</strong> The site had to print correctly on unknown paper, in both colour themes, without a separate document being maintained beside the web one.</p>\n<p><strong>Action.</strong> It is one 637‑line stylesheet with a single page rule and one print block. The page rule sets a margin and deliberately sets no paper size, so whatever the reader chooses in the dialog comes through and everything else adapts to it. The root font size is pinned in points so every relative measurement has a physical anchor, and one measure of about seventy‑two characters is applied to the four top‑level blocks — which is the sheet winning over the design, since a landscape page would otherwise run a line to about a hundred and ten characters. Four engine behaviours are documented as traps, each having caused a real defect. Viewport units resolve to the page box in one engine and to the window in another, so a page printed from a wide window has a third of every line cropped off in the second. Fixed‑position elements paint on every sheet, so the decorative ones are dropped and two of them are repurposed as a letterhead and a colophon. The palette defaults to white text because the default theme is dark, which printed the four section run‑in labels white on white — the words were simply absent. And paged media cannot split a flex or grid box, which produced a blank first sheet on an achievement page. A six‑part check covers it, testing six paper and orientation combinations, both colour‑scheme preferences, and the real page count against what the content height implies.</p>\n<p><strong>Result.</strong> The site prints on unknown paper in both themes, and the four engine behaviours are written down with the defect each produced. The known limits are recorded rather than hidden: no page numbers or running heads, one engine ignores widow and orphan control, and drop caps are not universal. A stray comment terminator once made the CSS linter swallow a whole rule and pass clean twice — only the print check noticed.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "Design Systems & UI", "Documentation", "Frontend Engineering", "UX / UI Design", "Web Development", "Frontend Development", "UI/UX Design & Design Systems", "Website Development & CMS" ] }, { "id": "https://engineer.company/portfolio/mirrored-the-site-to-gemini-and-gopher/", "url": "https://engineer.company/portfolio/mirrored-the-site-to-gemini-and-gopher/", "title": "Mirrored the site to Gemini and Gopher", "summary": "The site is readable over four protocols from one build, at 777 documents each and zero HTML change, and the alternate representations cost about twelve per…", "content_html": "<p><strong>Situation.</strong> The site is static, has no tracking and no runtime backend, and its argument is that a document does not need a megabyte of JavaScript to be read. That argument is easy to make and hard to demonstrate. Two small internet protocols demonstrate it directly, because neither of them can carry a script at all.</p>\n<p><strong>Task.</strong> The whole site had to be published over Gemini and Gopher from the same content, without a second content tree and without changing the HTML.</p>\n<p><strong>Action.</strong> Both are output formats of the same build rather than a build of their own. The site generator was given custom media types and output formats, and every page, section and taxonomy term gained two more representations alongside its HTML and its Markdown twin. The result is 777 Gemini documents and 777 Gopher menus produced from the same source, deployed to the same tree, and served by two small daemons on the same host. Two properties made it worth doing rather than a curiosity. Both formats are marked as non‑alternative representations, so nothing about the HTML changed — not one byte, and that was measured rather than assumed. And the Gopher format has no way to put a link inside a sentence, since a menu line is tab‑separated fields, so the prose is hard‑wrapped at sixty‑eight columns during the build; that constraint sharpened the writing in a way the HTML never demanded. A Tor onion mirror sits alongside them, and it is the only public face the platform could add without opening a firewall port, because the daemon dials out and nothing dials in.</p>\n<p><strong>Result.</strong> The site is readable over four protocols from one build, at 777 documents each and zero HTML change, and the alternate representations cost about twelve per cent of the HTML’s own weight on disk. Only one of the four is measured for traffic, and that is stated in the platform’s own statistics document rather than being quietly ignored.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "Automation & CI/CD", "Frontend Engineering", "Infrastructure", "Internationalization", "Web Development", "DevOps & CI/CD Automation", "Internationalization & Localization", "Website Development & CMS" ] }, { "id": "https://engineer.company/portfolio/cut-the-stylesheet-bundle-from-87-kb-to-31-kb/", "url": "https://engineer.company/portfolio/cut-the-stylesheet-bundle-from-87-kb-to-31-kb/", "title": "Cut the stylesheet bundle from 87 KB to 31 KB", "summary": "The bundle is 31 KB instead of 87, 756 KB of dead assets stopped being deployed, and each decision has a measurement attached — including the one that went the…", "content_html": "<p><strong>Situation.</strong> The site’s stylesheet bundle was 87 KB, which for a document site with no framework is most of a page weight spent before any content arrives. The site also published a directory of icons on every deploy, generated once and referenced by nothing since.</p>\n<p><strong>Task.</strong> The weight had to come down without the design changing, and it had to come down for a reason that could be pointed at rather than by general tidying.</p>\n<p><strong>Action.</strong> Measurement first. Two thirds of the largest stylesheet was one base64‑encoded font — about 56 KB of text encoding about 42 KB of font — inlined for a first‑paint benefit that a preloaded external file gets anyway, and paid for on every page whether the weight was needed or not. Taking it out took that file from 84 KB to 28 and the whole bundle from 87 to 31. The three font weights are preloaded instead, and the third joined the list only when field data showed it closing the longest critical chain rather than because three sounded complete. Separately, an audit of what the deploy actually published found twenty‑one generated icons and a duplicate configuration file, 756 KB, shipped every time and referenced from nowhere; and the site’s icon file itself came down from 145 KB to 15. A modern image format was evaluated for the brand mark, measured, and declined — it came out larger, and the selection mechanism picks by order rather than by size, so a modern browser would have taken the heavier file everywhere. Two checks now hold the line: no photograph may be displayed wider than half its source pixels, and every illustration must be published at a size its own pixels support at both standard and high density.</p>\n<p><strong>Result.</strong> The bundle is 31 KB instead of 87, 756 KB of dead assets stopped being deployed, and each decision has a measurement attached — including the one that went the other way, which is the more useful record of the two.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "Automation & CI/CD", "Design Systems & UI", "Frontend Engineering", "Performance Tuning", "Web Development", "Brand, Marketing & SEO", "Frontend Development", "Website Development & CMS" ] }, { "id": "https://engineer.company/portfolio/fixed-a-sitemap-with-one-shared-timestamp/", "url": "https://engineer.company/portfolio/fixed-a-sitemap-with-one-shared-timestamp/", "title": "Fixed a sitemap with one shared timestamp", "summary": "A re-sync after a month of content edits now touches eight files of 186 instead of all of them, and the sitemap says something true.", "content_html": "<p><strong>Situation.</strong> The site’s sitemap told search engines that 172 of its 176 pages had last changed at the same instant. That is not a subtle inaccuracy — a modification date is a promise to a crawler that a page’s content changed, and a site making that promise 172 times simultaneously is either telling the truth about a full rewrite or telling nobody anything useful.</p>\n<p><strong>Task.</strong> The dates had to describe the content rather than the file, without inventing precision the repository does not have.</p>\n<p><strong>Action.</strong> The cause was that the date came from the file’s modification time and the content export rewrites every file on every run, so a single sync stamped the whole corpus. The fix moves the source to the version history, with a fallback chain that tries an explicit field first, then the commit history, then the file. The trap in that fix is worth recording: the literal field name has to appear in the list or the generator never reads it, so a configuration that looks like it prefers an authored date but omits the name silently ignores every authored date. Two other date decisions came out of the same work. The database now carries when each record was written and last revised, kept deliberately separate from when the work happened — those are years apart and conflating them would date a page written this year to a decade ago. And rows in that file are optional and nothing is derived, because the repository’s own history begins after the content did: a missing date leaves the field empty rather than recording a migration, on the principle that a precise wrong number is worse than an absent one, since only the wrong one gets believed.</p>\n<p><strong>Result.</strong> A re‑sync after a month of content edits now touches eight files of 186 instead of all of them, and the sitemap says something true. The general rule went into the metadata document alongside it, because the same trap applies to every field that has a fallback chain.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "Automation & CI/CD", "Brand & Marketing", "Python", "Testing & QA", "Web Development", "AI Agent Readiness", "Brand, Marketing & SEO", "DevOps & CI/CD Automation", "Website Development & CMS" ] }, { "id": "https://engineer.company/portfolio/ran-development-as-51-written-initiatives/", "url": "https://engineer.company/portfolio/ran-development-as-51-written-initiatives/", "title": "Ran development as 51 written initiatives", "summary": "Fifty-one decisions with their reasoning attached, roughly three-quarters closed, and the declined ones are as retrievable as the accepted ones.", "content_html": "<p><strong>Situation.</strong> A one‑person company has no planning meeting, no backlog grooming and nobody to disagree with a decision. What it has instead is a strong tendency to do the work that is currently interesting, and no record afterwards of why anything was chosen — which becomes a problem the first time a decision needs revisiting or defending.</p>\n<p><strong>Task.</strong> The site’s development needed a written record per initiative that survives being reread months later, covering the ones that were declined as well as the ones that were built.</p>\n<p><strong>Action.</strong> Fifty‑one initiatives across fifty‑four numbered and indexed plan documents, each carrying the problem, the measurements taken, the decision and its open boxes. They are not summaries written afterwards. They hold the numbers that settled the argument — the scheduling measurements that shaped the check runner, the image format that was tested and declined because it came out larger, the pricing ladder confirmed by the owner, the audit that found the site proving capability across 371 pages while stating no price, no engagement shape and no minimum anywhere. Declined findings get the same treatment as accepted ones: an agent‑readiness review produced eight findings and six were refused, each with the verdict recorded, specifically so that a later automated suggestion cannot quietly reverse a written decision. The status board reads a plan’s checkboxes rather than its prose, because prose says “done” and boxes say what is open. That distinction was not theoretical — a documentation linter later found six plans marked done while carrying unchecked boxes.</p>\n<p><strong>Result.</strong> Fifty‑one decisions with their reasoning attached, roughly three‑quarters closed, and the declined ones are as retrievable as the accepted ones. What it costs is that every initiative has a write‑up, which is a real tax on small work and has occasionally meant the record is longer than the change it describes.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "Agile & Scrum", "Documentation", "Product & Requirements", "Project Management", "Stakeholder & Reporting", "Technical Leadership", "AI Agent Readiness", "Product Strategy & Requirements", "Project Management (Agile)", "Technical Documentation", "Technical Leadership & Consulting" ] }, { "id": "https://engineer.company/portfolio/put-the-documentation-under-its-own-linter/", "url": "https://engineer.company/portfolio/put-the-documentation-under-its-own-linter/", "title": "Put the documentation under its own linter", "summary": "Twenty-six documents totalling about 2,666 effective lines, each under a budget that cannot grow, with every path and symbol they name checked against the…", "content_html": "<p><strong>Situation.</strong> The repository governs itself with written instructions — twenty‑six documents covering the quality policy, the design system, accessibility, print, metadata, translation and git. Instructions have a specific decay pattern: they get longer, they drift from the code, they start referring to files that moved, and at some size they stop being read. An unread rule is not a rule.</p>\n<p><strong>Task.</strong> The documentation needed the same treatment as the code — a linter with limits that fail rather than advise.</p>\n<p><strong>Action.</strong> Eight policies, each mechanical. A hard ceiling on document length and a softer one that warns. A per‑file line budget that only ever ratchets downward, so a document can shrink and cannot grow back. An index requirement above a threshold, and a section index above a smaller one. A location rule about which document owns which subject. Link validity. Plan honesty — the status a plan claims must match its own checkboxes. A rule that every task gating a commit is named in some document. And a real‑path check, that every directory‑qualified path and every code symbol a document names actually exists. The first run was the argument for the whole exercise: six plans marked done while carrying unchecked boxes, fourteen tasks gating every commit and named in no document at all, three broken links, one orphaned document, and three dead paths across five documents. The budget then refused its own author — writing the summary of this work into the development document pushed it past its limit, which is how a separate document came to exist, which is the policy working exactly as intended.</p>\n<p><strong>Result.</strong> Twenty‑six documents totalling about 2,666 effective lines, each under a budget that cannot grow, with every path and symbol they name checked against the tree. The uncomfortable finding is the one worth repeating: fourteen checks were gating every single commit while appearing in no document, so the rules the machine enforced and the rules the humans read had already come apart.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "Automation & CI/CD", "Documentation", "Project Management", "Python", "Technical Leadership", "Testing & QA", "Project Management (Agile)", "Technical Documentation", "Technical Leadership & Consulting" ] }, { "id": "https://engineer.company/portfolio/linted-the-10242-line-quality-gate-itself/", "url": "https://engineer.company/portfolio/linted-the-10242-line-quality-gate-itself/", "title": "Linted the 10,242‑line quality gate itself", "summary": "The largest and most load-bearing code in the repository is now formatted, linted and type-checked, with thirteen findings fixed and zero suppressions.", "content_html": "<p><strong>Situation.</strong> The site’s quality gate is thirty‑two scripts totalling 10,242 lines of JavaScript. It was, by a wide margin, the largest body of code in the repository, and it was the only body of code nothing read — no formatter, no linter, no type checking. The programs enforcing every rule in the project were the only programs subject to none of them.</p>\n<p><strong>Task.</strong> The checkers had to be held to the standard they exist to enforce, and the formatter had to be fitted to them rather than the other way around.</p>\n<p><strong>Action.</strong> The formatter came first, and the indentation width was the interesting decision. The repository’s default is four spaces; at four spaces the formatter would have rewritten 2,173 lines of the largest checker. An override to two spaces for those files reduced that to 38 lines of genuine drift. The principle recorded with it is that the formatter is fitted to the code, not the code to the formatter — reformatting two thousand lines to satisfy a preference destroys the ability to read the history of the file. Then the linter, which produced thirteen real findings, all fixed and none suppressed. The Python checkers got the same treatment through a type checker configured at a middle strictness with thirty individual strict‑tier rules enabled on top, chosen by measurement: at that setting the tree is silent, and of the thirty candidates twenty‑nine were already silent and one fired — and that one was fixed rather than exempted. The type checker found two real defects the linter had passed clean, both about a value’s shape rather than its syntax.</p>\n<p><strong>Result.</strong> The largest and most load‑bearing code in the repository is now formatted, linted and type‑checked, with thirteen findings fixed and zero suppressions. The reason it matters more than the line count suggests is stated in the plan: a defect in the site’s stylesheet shows up as a page that looks wrong, and a defect in a checker shows up as a check that passes when it should not.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "Automation & CI/CD", "DevOps", "Documentation", "Frontend Engineering", "Python", "Testing & QA", "DevOps & CI/CD Automation", "Frontend Development", "Technical Leadership & Consulting" ] }, { "id": "https://engineer.company/portfolio/built-a-database-driven-cv-generator/", "url": "https://engineer.company/portfolio/built-a-database-driven-cv-generator/", "title": "Built a database‑driven CV generator", "summary": "Six formats, three languages and five themes come out of one database, and a corrected sentence is corrected once.", "content_html": "<p><strong>Situation.</strong> A CV, a reference sheet, a portfolio and a cover letter are the same facts arranged four ways, and keeping them as four documents means every correction is made four times and eventually is not. Three languages multiplies that by three. The failure mode is not that a document is wrong; it is that two documents disagree and nothing says which one is current.</p>\n<p><strong>Task.</strong> One source of facts had to produce every document, in every language, in every format, with the arrangement decided by code rather than by whoever last edited a file.</p>\n<p><strong>Action.</strong> The source is a SQLite database of twenty‑three tables — achievements, titles, companies, regions, categories, services, profiles, references, summaries — assembled by a nineteen‑step pipeline that runs from an empty file to a complete database in one command. The steps are ordered and each is written to be safe to repeat, so the pipeline can be run against an existing database without duplicating a row. Forty‑one Python modules totalling 10,580 lines render six output formats from it: HTML, PDF in two variants, plain text, Markdown and JSON, through six Jinja templates and eight stylesheets. Runtime dependencies were held to exactly two, a template engine and a PDF renderer, on the argument that a document generator which cannot be installed in five years has not preserved anything. Targeting is a first‑class concept rather than a manual edit: a profile selects which achievements appear and in what order, so a document aimed at one kind of reader is a query rather than a copy.</p>\n<p><strong>Result.</strong> Six formats, three languages and five themes come out of one database, and a corrected sentence is corrected once. The cost is that the system is now the only way to produce a document — there is no longer a file to open and edit in a hurry, and adding a language means adding it everywhere before anything builds at all.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "Automation & CI/CD", "Backend Engineering", "Databases", "Full‑Stack Development", "Internationalization", "Python", "Solution Architecture", "SQL", "Backend & API Development", "Database Design & Modeling", "Full‑Stack Product Development", "Internationalization & Localization", "Platform & Solution Architecture" ] }, { "id": "https://engineer.company/portfolio/reached-pdf-ua-1-conformance-at-106-of-106/", "url": "https://engineer.company/portfolio/reached-pdf-ua-1-conformance-at-106-of-106/", "title": "Reached PDF/UA‑1 conformance at 106 of 106", "summary": "Accessibility conformance is proven at full marks and the archival claim is stated honestly as a near-miss with the specific gap named.", "content_html": "<p><strong>Situation.</strong> A PDF that looks correct on screen tells you nothing about whether a screen reader can read it, whether its headings form a structure, whether its language is declared, or whether it will still open in twenty years. Accessible‑PDF and archival‑PDF conformance are both machine‑checkable standards, and a document that has never been run through the validator is a document making an unverified claim.</p>\n<p><strong>Task.</strong> The generated documents had to be measured against both standards, with the result recorded as a number rather than as an intention.</p>\n<p><strong>Action.</strong> Nine documents — the CV in its variants, the reference sheet, the portfolio and the cover letter, across languages — were run through an independent validator for the accessibility profile and the archival profile separately. The accessibility profile passes at 106 of 106 rules, which required tagged structure, declared document language, alternative text on every non‑decorative graphic, a real title in the metadata, and an explicit reading order rather than the one the layout happens to produce. The archival profile is the more interesting result: it fails exactly one rule of 146. That is the shape of result that is easy to misreport. One hundred and forty‑five passes reads like conformance in a summary, and it is not conformance; it is a document that will be rejected by a system enforcing the standard. The failing rule is recorded with what it is and why it has not been closed, rather than rounded away.</p>\n<p><strong>Result.</strong> Accessibility conformance is proven at full marks and the archival claim is stated honestly as a near‑miss with the specific gap named. The limit worth being direct about is that both numbers come from one validator; another implementation may disagree, and a rule that passes is only evidence that this checker had nothing to say about it.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "Automation & CI/CD", "Documentation", "Python", "Testing & QA", "UX / UI Design", "Web Development", "Technical Documentation", "UI/UX Design & Design Systems", "Website Development & CMS" ] }, { "id": "https://engineer.company/portfolio/enabled-every-linter-rule-as-an-error/", "url": "https://engineer.company/portfolio/enabled-every-linter-rule-as-an-error/", "title": "Enabled every linter rule as an error", "summary": "The linter runs at full strength with zero findings, and every deviation is documented at the line where it is taken.", "content_html": "<p><strong>Situation.</strong> Most projects pick a comfortable subset of their linter’s rules, and the subset is chosen by whichever rules were quiet on the day it was configured. That makes the configuration a record of the code’s existing habits rather than a standard the code is held to, and every rule left off is a class of defect nobody will ever be told about.</p>\n<p><strong>Task.</strong> The default had to be inverted — every rule the tool implements enabled as an error — and the resulting backlog worked to zero rather than negotiated down by turning rules back off.</p>\n<p><strong>Action.</strong> Selecting the complete rule set produced 1,815 findings on first run. They were worked through by category rather than by file, because the categories tell you something: unused arguments and shadowed builtins are noise, but the security category, the mutable‑default category and the exception‑handling category each pointed at real behaviour. Genuine incompatibilities exist — a formatter and a linter can disagree about the same line, and a few rules contradict the project’s own deliberate choices — and each of the small number of exemptions carries a written reason at the point of exemption saying what the rule wanted and why this code does otherwise. Two of them go further and are backed by a check rather than a comment, so the exemption cannot quietly widen: the rule is off, and a test asserts the specific property the rule would have enforced. Type checking runs in strict mode alongside it, which is a separate and harder standard, and it is the one that caught defects the linter could not see because they are about what a value is rather than how it is written.</p>\n<p><strong>Result.</strong> The linter runs at full strength with zero findings, and every deviation is documented at the line where it is taken. The cost is real and worth naming: the strictest setting produces findings that are genuinely not worth acting on, and someone has to make that judgement 1,815 times rather than once.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "Automation & CI/CD", "DevOps", "Documentation", "Python", "Testing & QA", "Backend & API Development", "DevOps & CI/CD Automation", "Technical Leadership & Consulting" ] }, { "id": "https://engineer.company/portfolio/vendored-a-qr-encoder-in-522-lines/", "url": "https://engineer.company/portfolio/vendored-a-qr-encoder-in-522-lines/", "title": "Vendored a QR encoder in 522 lines", "summary": "The runtime dependency count stayed at two, and the encoder is the only vendored component in the project.", "content_html": "<p><strong>Situation.</strong> The documents needed a QR code linking to the online version. Every available library does this, and taking one would have added a third runtime dependency to a project that had deliberately held itself to two — a template engine and a PDF renderer — on the argument that a document generator should still install years from now.</p>\n<p><strong>Task.</strong> Either accept the dependency or implement the format, and the implementation had to be verified as correct rather than merely producing something square and black.</p>\n<p><strong>Action.</strong> The encoder is 522 lines and implements the parts of the specification the use actually needs: byte‑mode encoding, Reed‑Solomon error correction over the Galois field of 256 elements with the generator polynomial built at the required degree, the fixed module layout with its finder patterns, timing patterns and alignment patterns, format and version information, and all eight data mask patterns evaluated against the specification’s four penalty rules so the lowest‑scoring mask is chosen rather than a fixed one. The verification is the part that made it defensible. Testing an encoder against its own logic proves nothing, so the finished module matrix is read back by a decoder written independently from the specification’s reading order, and the test asserts that the decoded string equals the input. That turns “it produces a plausible image” into “it produces a code that decodes to the right URL”, and it is checked on every run rather than once by eye with a phone.</p>\n<p><strong>Result.</strong> The runtime dependency count stayed at two, and the encoder is the only vendored component in the project. It should be said plainly that this is not a general‑purpose implementation — it supports the modes and versions the documents use and nothing else, and the honest justification is the dependency budget rather than any claim that the result is better than a mature library.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "Backend Engineering", "Documentation", "Performance Tuning", "Python", "Testing & QA", "Backend & API Development", "Platform & Solution Architecture" ] }, { "id": "https://engineer.company/portfolio/found-the-hooks-and-the-gate-disagreeing/", "url": "https://engineer.company/portfolio/found-the-hooks-and-the-gate-disagreeing/", "title": "Found the hooks and the gate disagreeing", "summary": "The hook and the gate provably run the same checks, and the prose checks now run on every commit.", "content_html": "<p><strong>Situation.</strong> The project had a commit hook that runs checks before a commit is accepted, and a full quality gate run on demand. A document stated that the hook runs the gate, so nothing could reach history without passing everything. Both lists were maintained by hand, in two different files, and nothing compared them.</p>\n<p><strong>Task.</strong> The claim had to be turned into an assertion, which meant enumerating both sets programmatically and failing when they diverge.</p>\n<p><strong>Action.</strong> The test reads the hook configuration and the gate’s task definitions, resolves each to the set of checks it actually invokes, and compares. They did not match. Five checks existed only in the gate and never ran on commit, and the five were not random — they were the ones that read the CV prose itself: the voice check that keeps reviews impersonal, the buzzword check, the figure‑consistency check across languages, the notation check, and the spell check over content. In other words, every check protecting code ran automatically and every check protecting the writing ran only when someone remembered. Given that the writing is the entire product, the exposure was inverted from where anyone would have guessed. The fix was to bring the five into the hook, which required making two of them fast enough to survive a pre‑commit budget, and then to keep the comparison test in place so the two lists cannot drift apart again. The document that had been describing an intention now describes something enforced.</p>\n<p><strong>Result.</strong> The hook and the gate provably run the same checks, and the prose checks now run on every commit. The trade‑off is the commit hook’s runtime, which grew and will keep growing as content grows, and there is a point at which a slow hook gets bypassed — so this fix has a shelf life measured in how long the checks stay fast.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "Automation & CI/CD", "DevOps", "Documentation", "Python", "Testing & QA", "DevOps & CI/CD Automation", "Technical Documentation", "Technical Leadership & Consulting" ] }, { "id": "https://engineer.company/portfolio/rehearsed-the-forge-ci-hook-found-2-defects/", "url": "https://engineer.company/portfolio/rehearsed-the-forge-ci-hook-found-2-defects/", "title": "Rehearsed the forge CI hook, found 2 defects", "summary": "The hook works on both paths and both defects were found before any real push met them. The limit is that a rehearsal is still a simulation: it proves the hook…", "content_html": "<p><strong>Situation.</strong> The self‑hosted git forge accepts pushes and can run a hook on the server side to reject work that does not pass the gate. A server‑side hook is the one piece of automation that cannot be tested by running it locally: it executes in a bare repository, with no working tree, under an environment the forge sets, reading the pushed references from its standard input. Reading the script and concluding it is correct is a guess.</p>\n<p><strong>Task.</strong> The hook had to be rehearsed against a real bare repository and a real push before being trusted, rather than deployed and discovered.</p>\n<p><strong>Action.</strong> The rehearsal creates a bare repository, installs the hook, clones it, commits, pushes, and asserts on both the accept and the reject path. It found two defects, neither of which was visible in the file. The first was a fallback in the argument handling: when the hook could not determine a reference it substituted a placeholder that was not a resolvable object, and because the value arrives on the hook’s standard input rather than as a parameter, the failure surfaced as an unrelated error much further down. The second is the one worth remembering. Git exports an environment variable naming the repository directory when it invokes a hook, and that variable is inherited by everything the hook runs. The gate checks out the pushed revision and runs the test suite inside it, and the test suite inherited the pointer to the bare repository — so nineteen tests that touch git resolved against the wrong repository and went red, on code that was correct. The environment has to be cleared at the boundary, and that boundary is invisible unless the thing is actually run.</p>\n<p><strong>Result.</strong> The hook works on both paths and both defects were found before any real push met them. The limit is that a rehearsal is still a simulation: it proves the hook survives one push shape, and the forge in production sees pushes this rehearsal does not construct, including forced updates and branch deletions.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "Automation & CI/CD", "DevOps", "Infrastructure", "Linux & Servers", "Python", "Testing & QA", "DevOps & CI/CD Automation", "Infrastructure as Code", "System Administration" ] }, { "id": "https://engineer.company/portfolio/moved-every-string-into-a-content-tree/", "url": "https://engineer.company/portfolio/moved-every-string-into-a-content-tree/", "title": "Moved every string into a content tree", "summary": "The prose is now editable without touching code and comparable across languages by counting. The cost is that adding a language is now unambiguous rather than…", "content_html": "<p><strong>Situation.</strong> Every user‑facing string in the generator — every achievement, every review, every section heading, every summary, in three languages — lived inside Python source. That makes editing a sentence a code change, makes reviewing a translation a diff against source, and makes it impossible to tell at a glance whether a language is complete. It also hides the failure mode that matters: a translation can be present, wrong, and unreachable at the same time.</p>\n<p><strong>Task.</strong> The strings had to move out of code into a content tree that can be counted, compared across languages, and checked without running the renderer.</p>\n<p><strong>Action.</strong> The result is 874 files under a content directory, organised by kind and then by language: achievements, reviews, summaries, titles, companies, regions, categories, services, profiles, references and cover‑letter fragments. Tab‑separated files where the unit is a line with an identifier, individual files where the unit is a paragraph. The loader reads them at build time and the database is assembled from them, so the tree is the source and the database is derived. Two findings came directly out of being able to count. Some translated strings no longer had any English counterpart — dead entries that nothing rendered and nobody could have noticed while they were embedded in code, because an unreferenced dictionary key looks exactly like a referenced one. And a content check that was supposed to grade every achievement was reading only the subset it could resolve, grading thirty‑seven of ninety‑three and reporting success. Making the corpus a directory made both of these visible as a mismatch in file counts.</p>\n<p><strong>Result.</strong> The prose is now editable without touching code and comparable across languages by counting. The cost is that adding a language is now unambiguous rather than gradual — the tree makes an incomplete language obvious, which is the point, but it also means partial translations cannot be shipped quietly while they are being finished.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "Backend Engineering", "Data Governance", "Documentation", "Internationalization", "Migrations & Modernization", "Python", "Backend & API Development", "Data Governance & Quality", "Database Migration & Modernization", "Internationalization & Localization" ] }, { "id": "https://engineer.company/portfolio/built-a-cross-language-figure-check/", "url": "https://engineer.company/portfolio/built-a-cross-language-figure-check/", "title": "Built a cross‑language figure check", "summary": "Eighteen real defects closed and the class closed with them, since every future translation is compared to its English source before it can be committed.", "content_html": "<p><strong>Situation.</strong> The most damaging kind of translation error in a CV is not an awkward phrase. It is a number that disappears. An English sentence claiming a fifty‑fold improvement, translated into a sentence that says “significantly”, is a claim quietly withdrawn in one market and kept in another — and no spell check, grammar check or human reading of the target language alone will ever notice, because the translated sentence is perfectly good prose.</p>\n<p><strong>Task.</strong> A check was needed that reads the languages against each other rather than each one on its own, on the two things that must survive translation: the figures, and the notation each language uses to write them.</p>\n<p><strong>Action.</strong> The figure check extracts every number, percentage, multiplier and unit from the English string and requires each one to appear in every translation of that string, with the multiplier forms mapped per language rather than matched literally — the English fifty‑times form corresponds to a specific Danish phrasing and a specific Ukrainian phrasing, and the check knows the mapping instead of demanding the digits alone. The notation check is the mirror of it: each language has conventions it must use and conventions it must not, including decimal separators, thousands grouping and quotation marks. Ukrainian quotes with guillemets; English double quotes in a Ukrainian sentence are as wrong as a missing figure, and far easier to introduce by copying. The first full run found two Danish descriptions where a metric present in English had been dropped, and sixteen Ukrainian spans quoting in the English style.</p>\n<p><strong>Result.</strong> Eighteen real defects closed and the class closed with them, since every future translation is compared to its English source before it can be committed. What the check cannot do is judge meaning: it proves the number survived and the punctuation is native, and a translation that keeps every figure while getting the claim backwards passes it cleanly.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "Automation & CI/CD", "Data Governance", "Documentation", "Internationalization", "Python", "Testing & QA", "Data Governance & Quality", "Internationalization & Localization", "Technical Documentation" ] }, { "id": "https://engineer.company/portfolio/built-a-native-macos-client-in-swift-6/", "url": "https://engineer.company/portfolio/built-a-native-macos-client-in-swift-6/", "title": "Built a native macOS client in Swift 6", "summary": "A native client that starts as a Mac application, uses the platform's own materials and controls, and carries no embedded browser.", "content_html": "<p><strong>Situation.</strong> The messaging protocol had a well‑tested core written in Rust and clients on several platforms, but the desktop experience on macOS was a cross‑platform shell — it did not look like a Mac application, did not behave like one, and carried a runtime that a native application does not need. The core exposes its capability through a C interface, which means any language that can call C can build on it, and Swift can.</p>\n<p><strong>Task.</strong> A native client had to be built directly on that C interface, in the platform’s current language and current interface framework, with the core linked in rather than shipped alongside.</p>\n<p><strong>Action.</strong> The application is 11,141 lines of Swift across fifty‑three files in the app target, built on SwiftUI with the core linked as a static archive compiled from a pinned upstream revision. Pinning the revision rather than tracking a branch is the decision that makes the build reproducible: the C interface is the contract, and a moving core changes the contract without changing a line of Swift. The layering keeps the unsafe surface small — a thin wrapper owns every pointer and every string that crosses the boundary, models above it are ordinary Swift values, and the interface layer never sees a raw pointer. Everything the C interface returns has an ownership rule, and getting one wrong produces either a leak or a crash with no compiler warning in between, so the wrapper is where the entire project’s memory discipline lives. The build is driven by a task runner with fifty‑six targets covering the core compile, the Swift build, the test suites, the linters and the packaging.</p>\n<p><strong>Result.</strong> A native client that starts as a Mac application, uses the platform’s own materials and controls, and carries no embedded browser. What has not been done should be stated: this is not a shipped release. There is no notarised distribution, no update channel, and one language in the interface, so it is a working application rather than a product anyone else is running.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "APIs & Integration", "Design Systems & UI", "Frontend Engineering", "Full‑Stack Development", "Solution Architecture", "UX / UI Design", "Frontend Development", "Full‑Stack Product Development", "Platform & Solution Architecture", "UI/UX Design & Design Systems" ] }, { "id": "https://engineer.company/portfolio/an-app-that-ate-41-mb-a-second-stopped/", "url": "https://engineer.company/portfolio/an-app-that-ate-41-mb-a-second-stopped/", "title": "An app that ate 41 MB a second, stopped", "summary": "Memory stays flat under sustained load and the same session that produced 610,996 log lines produces 1,411.", "content_html": "<p><strong>Situation.</strong> The application filled memory until the operating system killed it. The recorded incident reached 111 GB of compressed pages on a 36 GB machine, climbing at roughly 41 MB a second, and the log file for a single short session held 610,996 lines. A machine in that state is not slow, it is unusable — the kill arrives after the swap has already made everything else on the desktop stop responding.</p>\n<p><strong>Task.</strong> The growth had to be found rather than guessed at, and every unbounded path had to be given a limit, because one bounded queue next to three unbounded ones is not a fix.</p>\n<p><strong>Action.</strong> There were three multiplying causes and the multiplication is why it was so fast. The event stream from the core was consumed without any bound, so events arrived faster than the interface could apply them and the backlog was retained rather than dropped. Every subscriber received every event and filtered afterwards, so the cost of one event was multiplied by the number of listeners, and each listener’s filtering allocated. And logging was unbudgeted, so each event produced log lines — which is the compounding term, because the volume of logging was proportional to the volume of the thing going wrong. The fix addressed all three: bounded buffers with an explicit policy for what happens when they fill, subscription by event type so a listener is only woken for events it wants, and a rate budget on logging that collapses repeats rather than writing each one. A regression test drives a high event rate and asserts the memory ceiling holds, so the bound is a property of the build rather than a comment.</p>\n<p><strong>Result.</strong> Memory stays flat under sustained load and the same session that produced 610,996 log lines produces 1,411. The honest note is that the rate budget on logging discards information: when something goes wrong quickly now, the record of it is deliberately incomplete, and that is a trade made knowingly against the alternative of a machine that stops.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "Frontend Engineering", "Monitoring & Observability", "Performance Tuning", "Reliability & Backups", "Solution Architecture", "Testing & QA", "Frontend Development", "Platform & Solution Architecture", "Site Reliability & Monitoring" ] }, { "id": "https://engineer.company/portfolio/adopted-swift-6-strict-concurrency-over-c/", "url": "https://engineer.company/portfolio/adopted-swift-6-strict-concurrency-over-c/", "title": "Adopted Swift 6 strict concurrency over C", "summary": "The application compiles under complete strict concurrency with no suppressions, and event ordering is a structural property rather than a hope.", "content_html": "<p><strong>Situation.</strong> Swift 6’s complete strict concurrency checking turns data races into compile errors instead of intermittent crashes. Adopting it against a C library is where it gets difficult: the core’s event loop is a blocking call that must run off the main thread forever, and the values it hands back are pointers with no concurrency guarantees at all. The compiler cannot reason about any of it and will refuse everything until the boundary is described explicitly.</p>\n<p><strong>Task.</strong> Complete checking had to be enabled with no escape hatches, which meant designing the crossing from a blocking C loop to the main actor rather than annotating around it.</p>\n<p><strong>Action.</strong> The obvious approach is an actor per subsystem, and it was rejected on measurement rather than taste. Spawning a task per incoming event lets the runtime schedule them in any order, and the core’s event stream is ordered — a message‑changed event that overtakes the message‑created event it refers to produces an interface showing an edit to something that does not exist yet. Actor reentrancy makes this worse, not better, because an actor can suspend mid‑method and process another call. What replaced it is deliberately plain: one producer thread owning the blocking loop, one consumer, one queue between them, and a single hop onto the main actor at the end. Ordering is preserved because there is exactly one path and nothing overtakes anything. The unsafe types crossing that boundary are wrapped in types whose thread‑safety is asserted at the wrapper rather than assumed, and the assertion is documented with why it holds — the pointer is owned by one thread and copied before it is handed over.</p>\n<p><strong>Result.</strong> The application compiles under complete strict concurrency with no suppressions, and event ordering is a structural property rather than a hope. The cost is that the design is less parallel than it could be: everything funnels through one consumer, and if that consumer ever becomes a bottleneck the fix will require re‑deriving which events can be reordered safely, which is exactly the analysis this avoided.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "Backend Engineering", "Frontend Engineering", "Performance Tuning", "Reliability & Backups", "Solution Architecture", "Testing & QA", "Backend & API Development", "Platform & Solution Architecture", "Technical Leadership & Consulting" ] }, { "id": "https://engineer.company/portfolio/wrote-a-parser-verifying-every-ffi-call/", "url": "https://engineer.company/portfolio/wrote-a-parser-verifying-every-ffi-call/", "title": "Wrote a parser verifying every FFI call", "summary": "The class of defect that produced the original crash cannot recur, because a stale reference is now a build failure rather than a runtime one.", "content_html": "<p><strong>Situation.</strong> Early in the project the C interface was represented by a hand‑written header describing the functions the application expected. That header compiled, the application linked, and calls to three functions that no longer existed in the core reached the point of being called and crashed. The compiler and the linker had both been satisfied by a description of the library rather than the library, and the gap only appeared at runtime.</p>\n<p><strong>Task.</strong> The real header — 7,308 lines and 268 declarations — had to become the authority, and every use of it in the Swift code had to be verified against it automatically rather than by review.</p>\n<p><strong>Action.</strong> The check is a parser that reads the actual upstream header and builds the set of functions, enum constants and types it declares, then reads the Swift source and resolves every call site and every constant reference against that set. A call to a function the header does not declare fails the build. A reference to an enum constant that has been renamed fails the build. The current count is 132 of the 268 declarations referenced, and knowing which 136 are unused is itself useful, because it says exactly how much of the core the client has not reached. The parser also enforces a rule the compiler cannot: every Swift class that owns a pointer into the core must be final. A non‑final pointer‑owning class can be subclassed, and a subclass that overrides deinitialisation or adds its own lifetime changes when the pointer is freed — a use‑after‑free with no unsafe keyword anywhere near it. The rule is checked by name across the whole tree.</p>\n<p><strong>Result.</strong> The class of defect that produced the original crash cannot recur, because a stale reference is now a build failure rather than a runtime one. The limitation is that the parser understands the header’s declarations and not its semantics: it proves a function exists with a matching name, and a function whose meaning or ownership rule changed upstream while keeping its signature passes without comment.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "APIs & Integration", "Automation & CI/CD", "Backend Engineering", "Documentation", "Security", "Testing & QA", "Backend & API Development", "DevOps & CI/CD Automation", "Technical Documentation" ] }, { "id": "https://engineer.company/portfolio/took-the-test-suite-to-8-9-seconds/", "url": "https://engineer.company/portfolio/took-the-test-suite-to-8-9-seconds/", "title": "Took the test suite to 8.9 seconds", "summary": "The suite went from six tests over sixty seconds — 74 seconds of wall clock — to 135 tests passing in 8.9, which puts it inside the window where it runs on…", "content_html": "<p><strong>Situation.</strong> The test suite had a sixty‑second time limit and six tests were over it. A test suite that takes over a minute stops being run before every change, and a suite that is not run before every change is a report on the past. The instinct in this situation is to raise the limit, and raising the limit is how a suite gets to ten minutes.</p>\n<p><strong>Task.</strong> The time had to be found rather than budgeted for, which meant profiling the suite instead of reasoning about which tests looked expensive.</p>\n<p><strong>Action.</strong> The profile was taken on the main thread while the suite ran, and the result contradicted the guess. The main thread sat inside two calls for 3,989 samples out of 4,017 — so the slow tests were not the ones doing the most work, and almost every test passed through the same two places. The first was a fixed wait used to let asynchronous work settle before asserting — a sleep, effectively, paid by every test that touched the event path whether or not the work had already finished. It was replaced by waiting on the actual condition with a timeout, so a test that is ready in five milliseconds takes five milliseconds and only a genuinely stuck test pays the full wait. The second was per‑test setup that rebuilt an expensive fixture each time, where the fixture was read‑only and could be built once for the suite. Neither was in a test anyone would have nominated as slow; both were in the shared path, which is why the whole suite was uniformly slow rather than a few tests being outliers.</p>\n<p><strong>Result.</strong> The suite went from six tests over sixty seconds — 74 seconds of wall clock — to 135 tests passing in 8.9, which puts it inside the window where it runs on every save. The caveat is that the shared read‑only fixture is now a coupling point — a test that mutates it will produce a failure in a different test, and the suite’s speed depends on a discipline the compiler does not enforce.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "Automation & CI/CD", "DevOps", "Performance Tuning", "Testing & QA", "DevOps & CI/CD Automation", "Technical Leadership & Consulting" ] }, { "id": "https://engineer.company/portfolio/named-all-17-icons-for-screen-readers/", "url": "https://engineer.company/portfolio/named-all-17-icons-for-screen-readers/", "title": "Named all 17 icons for screen readers", "summary": "All seventeen controls announce their function, and an eighteenth cannot be added without one. The rule's imprecision is real and stated where it is defined:…", "content_html": "<p><strong>Situation.</strong> The interface used system symbols for its controls, and a symbol with no accessibility label is read out by the screen reader as its own internal name. The send button announced as “arrow up circle, button”. So did every other icon‑only control in the application, in its own way — seventeen of them describing their own artwork instead of their function.</p>\n<p><strong>Task.</strong> Every icon‑only control needed a label describing what it does, and the fix had to come with a check, because the next icon added would otherwise reintroduce the defect immediately.</p>\n<p><strong>Action.</strong> Each of the seventeen was given a label naming the action rather than the shape, and where the control’s meaning depends on state the label follows the state instead of being fixed. The check is the part worth describing, because a general “is this accessible” rule does not exist for this. The linter looks for the construction that creates an icon‑only control and then requires an accessibility label within eight lines of it. Eight lines is a deliberately crude heuristic and it was chosen by measurement: it is long enough to span every legitimate way the codebase writes one of these controls with its modifiers, and short enough that a label attached to a different view further down does not accidentally satisfy it. A precise rule here would need to understand SwiftUI’s modifier chains as a tree, and the cheap proximity rule catches the actual mistake — which is not a mislabelled control, it is an unlabelled one.</p>\n<p><strong>Result.</strong> All seventeen controls announce their function, and an eighteenth cannot be added without one. The rule’s imprecision is real and stated where it is defined: it can be satisfied by a label on a neighbouring view, so it proves a label exists nearby rather than proving the label is correct, and correctness still needs someone to listen to it.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "Automation & CI/CD", "Design Systems & UI", "Documentation", "Frontend Engineering", "Testing & QA", "UX / UI Design", "Frontend Development", "UI/UX Design & Design Systems" ] }, { "id": "https://engineer.company/portfolio/built-a-48-token-macos-design-system/", "url": "https://engineer.company/portfolio/built-a-48-token-macos-design-system/", "title": "Built a 48‑token macOS design system", "summary": "Forty-eight tokens describe the whole interface, the appearance follows the system, and none of the three erosions can be committed.", "content_html": "<p><strong>Situation.</strong> An interface built by adding views accumulates values: a corner radius here, a fourteen‑point font there, a two‑tenths‑of‑a‑second animation somewhere else. Individually each is reasonable and collectively they are a design that cannot be changed, because there is no such thing as “the corner radius” to change — there are forty of them, slightly different, spread across fifty files.</p>\n<p><strong>Task.</strong> The visual vocabulary had to be reduced to a named set, expressed against the platform’s own material rather than reinvented, and defended by a check so it stays reduced.</p>\n<p><strong>Action.</strong> The system is forty‑eight tokens in six groups: spacing, typography, colour, radius, elevation and motion. Colour and material are built on the platform’s semantic colours and its glass material rather than fixed values, which is what makes the interface follow the system appearance, the accent colour and the contrast settings without any code that watches for them. Typography maps to the platform’s text styles so it scales with the user’s size preference instead of pinning a point value. The linter rejects three things: a numeric literal where a spacing or radius token belongs, a hardcoded font size anywhere, and an animation declared without honouring the reduce‑motion preference. The third is the one that would otherwise erode fastest, because an animation is added in a moment of polish and the preference check is the part that gets skipped — so the rule makes the animation itself impossible to write without it, rather than asking anyone to remember.</p>\n<p><strong>Result.</strong> Forty‑eight tokens describe the whole interface, the appearance follows the system, and none of the three erosions can be committed. The limit is that a token system constrains consistency and not quality — everything now matches, and matching is not the same as being well designed, which is a judgement no linter in this project makes.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "Automation & CI/CD", "Design Systems & UI", "Frontend Engineering", "Testing & QA", "UX / UI Design", "Brand, Marketing & SEO", "Frontend Development", "UI/UX Design & Design Systems" ] }, { "id": "https://engineer.company/portfolio/reached-the-unused-half-of-the-core/", "url": "https://engineer.company/portfolio/reached-the-unused-half-of-the-core/", "title": "Reached the unused half of the core", "summary": "The previously unreached half of the core is driven by tests against the real library, and the wrapper carries the highest floor in the project.", "content_html": "<p><strong>Situation.</strong> The client used roughly half of what the messaging core offers. The unused half was not obscure — backup transfer between devices, disappearing messages, message editing and resending, verified invitation links, proxy configuration and the encryption policy for a chat. Each is a feature a user would expect and each was an untested region of the C interface, which is the more dangerous fact, because an untested region of a C interface is where the ownership mistakes live.</p>\n<p><strong>Task.</strong> The unreached capability had to be driven and covered, and the tests had to run against the real library rather than a stand‑in.</p>\n<p><strong>Action.</strong> The rule adopted was no mocks for the core. A mock of a C interface encodes the developer’s belief about what the library does, and every defect worth finding here is a place where that belief is wrong — so a passing mock‑based test is evidence about the mock. Instead the tests create real accounts in temporary directories, drive the real library, and assert on what it actually returns, with each suite cleaning up its own state. That is what made the coverage meaningful: exercising backup transfer meant handling a real transfer’s state machine and its failure paths, and exercising verified invitations meant constructing the real link format and having the library parse it. Coverage floors were set per layer rather than as one number, at eighty‑four percent for the core wrapper, seventy‑five for utilities and sixty‑five for models, on the reasoning that the layer touching raw pointers should be held highest and a model that is mostly stored properties should not be padded with tests to hit an average.</p>\n<p><strong>Result.</strong> The previously unreached half of the core is driven by tests against the real library, and the wrapper carries the highest floor in the project. The cost is speed and determinism: real‑library tests are slower than mocks and they can fail for environmental reasons, which is the price of them being able to fail for real ones.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "APIs & Integration", "Automation & CI/CD", "Backend Engineering", "Reliability & Backups", "Security", "Testing & QA", "Backend & API Development", "DevOps & CI/CD Automation", "Security & Access Management" ] }, { "id": "https://engineer.company/portfolio/rewrote-error-messages-to-stop-blaming-users/", "url": "https://engineer.company/portfolio/rewrote-error-messages-to-stop-blaming-users/", "title": "Rewrote error messages to stop blaming users", "summary": "The error surface follows a stated standard and the case that prompted it is covered by a test that fails on the old text.", "content_html": "<p><strong>Situation.</strong> A sign‑in against a major mail provider failed, and the application told the user their password was incorrect. It was not incorrect. That provider requires an application‑specific password for third‑party clients and rejects the account password regardless of how carefully it is typed. The message sent the user to retype something that could never work, and the actual instruction — go and generate a different kind of password — appeared nowhere.</p>\n<p><strong>Task.</strong> The error messages had to be rewritten against a written standard rather than patched one at a time, since this one was the visible instance of a habit running through all of them.</p>\n<p><strong>Action.</strong> The standard has three requirements: say what happened, never imply the user did something wrong when the cause is elsewhere, and give the next action when one exists. Applied across the error surface, most messages violated at least one — several were the underlying library’s error string passed through, which describes a condition to a programmer rather than a situation to a person. The provider case was rewritten to name the provider, state that it requires an app‑specific password for other clients, and say where to create one. The test is what stops it regressing, and it is deliberately specific: it drives a sign‑in failure against that provider and asserts the message contains the provider’s name and the phrase describing the required credential type. A test asserting only that some error appeared would pass on the original wrong message, so the assertion is on the content, which is the only part that was ever broken.</p>\n<p><strong>Result.</strong> The error surface follows a stated standard and the case that prompted it is covered by a test that fails on the old text. What remains unresolved is scale: the standard is enforced by review and by one test on one message, and the other providers with their own particular requirements have no equivalent test, so the class is documented rather than closed.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "Documentation", "Frontend Engineering", "Product & Requirements", "Security", "Testing & QA", "UX / UI Design", "Product Strategy & Requirements", "Technical Documentation", "UI/UX Design & Design Systems" ] }, { "id": "https://engineer.company/portfolio/know-the-licence-of-everything-you-ship/", "url": "https://engineer.company/portfolio/know-the-licence-of-everything-you-ship/", "title": "Know the licence of everything you ship", "summary": "The distribution position is checked on every build, and the check is known to work because it was made to fail rather than because it has never spoken.", "content_html": "<p><strong>Situation.</strong> Linking a Rust core into a shipped application means shipping everything that core depends on. The dependency graph is 644 crates. Each carries a licence, some carry more than one, and a single copyleft crate arriving three levels down through a routine version bump changes what the application as a whole may be distributed under — silently, in a lock file nobody reads line by line.</p>\n<p><strong>Task.</strong> The licence position had to be verified on every build rather than reviewed once, with an explicit allow list, so that a change in the graph is a build failure and not a discovery made later by someone else.</p>\n<p><strong>Action.</strong> The audit resolves the full transitive graph and checks each crate’s licence expression against a list of terms the project accepts, evaluating the boolean expressions properly — a crate offering a choice of two licences is acceptable if either is on the list, and a crate requiring both is only acceptable if both are. Anything unmatched fails the build rather than warning, and adding a term to the allow list is a deliberate edit with a reason. A check that has never failed is indistinguishable from one that cannot fail, so it was made to fail on purpose, twice. Once by rewriting a crate’s licence expression to something the list does not accept, which is the shape of a crate changing its terms upstream between versions — the case no human review catches, because the crate itself is not new. Once by moving the pinned core revision without regenerating the audit, which is the shape of a core bump quietly bringing a new dependency with it. Both provocations failed the build as intended, and the check was left in place rather than widened.</p>\n<p><strong>Result.</strong> The distribution position is checked on every build, and the check is known to work because it was made to fail rather than because it has never spoken. Before it existed, the bundle, the licence file and the project’s own README all made a claim about 644 crates that nothing had verified. The limit is precise and should not be overstated: the check reads the declared licence metadata, and metadata can be wrong or incomplete. It proves nothing about a crate that misdeclares itself, and it is not legal advice.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "Automation & CI/CD", "Data Governance", "DevOps", "Documentation", "Security", "Data Governance & Quality", "DevOps & CI/CD Automation", "Security & Access Management" ] }, { "id": "https://engineer.company/portfolio/built-the-visual-identity-from-2-drawings/", "url": "https://engineer.company/portfolio/built-the-visual-identity-from-2-drawings/", "title": "Built the visual identity from 2 drawings", "summary": "One drawing now produces every published icon, the tab icon included, regeneration is one command, and a stale asset fails the build.", "content_html": "<p><strong>Situation.</strong> A small company’s visual identity is usually bought, generated, or assembled from stock, and the result is an identity that belongs to nobody. The alternative problem is worse: hand‑made artwork that exists only as exported files, where the source drawing is lost, the exports are edited directly, and within a year the versions in use disagree with each other and no original remains to settle it.</p>\n<p><strong>Task.</strong> The identity had to be drawn rather than sourced, and the path from drawing to published asset had to be reproducible, so that every file on the site is derivable rather than kept.</p>\n<p><strong>Action.</strong> The generated artwork began with two hand‑made drawings: the company mark, a raster, and a vector cog drawn for the browser tab on the argument that a bold silhouette survives 16 pixels. Rendered side by side at 16, 32 and 48 pixels over light and dark tab strips, the mark stayed recognisable and the cog washed out, so the tab icon moved to the mark, and every icon the site publishes is now produced from that one drawing. From it, scripts produce every derived form: the favicon set in its required sizes, the touch icons, and the launch images at each device size. The generation is a task anyone can run, so the question “where did this file come from” has an answer that is a command. The staleness check is the piece that makes it hold: it compares the commit date of every derived asset against its source and fails when a derived file was committed first, which catches the exact failure this design exists to prevent — someone edits the source, forgets to regenerate, and the published site keeps showing artwork that no longer matches the original. The rule that derived files are never edited by hand is stated where the sources live.</p>\n<p><strong>Result.</strong> One drawing now produces every published icon, the tab icon included, regeneration is one command, and a stale asset fails the build. The trade‑off is a hard dependency on the toolchain: the derived files are committed so the site builds anywhere, but changing the identity requires the generation tooling to still work, and a source format that stops being readable takes the whole identity with it.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "Automation & CI/CD", "Brand & Marketing", "Design Systems & UI", "Testing & QA", "UX / UI Design", "Web Development", "Brand, Marketing & SEO", "UI/UX Design & Design Systems", "Website Development & CMS" ] }, { "id": "https://engineer.company/portfolio/held-4-repositories-to-one-history-standard/", "url": "https://engineer.company/portfolio/held-4-repositories-to-one-history-standard/", "title": "Held 4 repositories to one history standard", "summary": "Four repositories share one history format and one instruction structure, both enforced by hooks rather than by discipline.", "content_html": "<p><strong>Situation.</strong> Four repositories built over eighteen months by one person is the situation where process is easiest to skip, because there is nobody to coordinate with and the cost of an unreadable history is paid entirely by a future self who has not complained yet. It is also the situation where an inconsistent history is most likely, since each repository can drift into its own habits with nothing pulling them together.</p>\n<p><strong>Task.</strong> One standard for history and one standard for instructions had to apply across every repository the work is authored in, and it had to be enforced mechanically rather than remembered.</p>\n<p><strong>Action.</strong> The commit standard is a conventional prefix naming the kind of change and a scope, a subject under a fixed length, no emojis, and no attribution trailers of any sort — the last of these because a trailer crediting a tool is not a fact about the change, and history is for facts about changes. A commit‑message hook rejects anything that does not conform, in every repository that carries work, so the standard is a property of the repository rather than of whoever is committing. The distribution on the largest repository shows what the work actually was: 156 feature commits, 133 fixes, 128 documentation, 81 chores, 26 refactors, 20 style, 5 performance and 3 test. Documentation is close enough to fixes to be worth noticing, and that is a consequence of the second half of this — 48 instruction documents across the four, each covering one domain, each written as rules rather than description, all reachable from a single entry document per repository so there is one place to start. A documentation linter enforces per‑file size budgets and index membership, so the set stays navigable rather than growing into an archive.</p>\n<p><strong>Result.</strong> Four repositories share one history format and one instruction structure, both enforced by hooks rather than by discipline. What this does not do is make the history good: the format is checked and the content is not, so a conforming subject line that describes nothing passes exactly as well as one that explains the change.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "Agile & Scrum", "Automation & CI/CD", "DevOps", "Documentation", "Project Management", "Technical Leadership", "DevOps & CI/CD Automation", "Project Management (Agile)", "Technical Documentation", "Technical Leadership & Consulting" ] }, { "id": "https://engineer.company/portfolio/measured-13-addresses-with-no-tracking/", "url": "https://engineer.company/portfolio/measured-13-addresses-with-no-tracking/", "title": "Measured 13 addresses with no tracking", "summary": "Thirteen addresses are measured this way, and the site ships no cookie, no tracker and no third-party request, so there is nothing to consent to and nothing…", "content_html": "<p><strong>Situation.</strong> The site had no way of knowing whether anybody read it. Every ordinary answer to that costs the reader something — a cookie, a script, a pixel, a hosted service that learns about a visitor on the way past — and each of those was refused before the first line of this was written. What remained was the web server’s own access log, the one record that exists anyway because a request has to be answered.</p>\n<p><strong>Task.</strong> The question was whether a useful record of readership can be built from a log line that has already had the identifying parts taken out of it, and whether the taking‑out can be proved rather than promised.</p>\n<p><strong>Action.</strong> The masking happens as the line is written, not in a cleanup pass afterwards, because a promise not to store something is broken by storing it and tidying up later. The server derives the client address under two names, and both are masked — masking one reads as correct in the configuration while the full address sits on disk under the other, which only a live probe found. Seven forwarded‑address headers are dropped outright, since the encoder writes the header map exactly as the client sent it and a visitor behind a corporate proxy would otherwise carry their own full address into the file under a name no mask ever looked at. The cookie header, the authorization header and the ephemeral source port go the same way, and query strings are cut before anything reads the path, so neither the words somebody typed into a search engine nor the words they typed into this site’s own search box can reach any file. Collection is a pull: a stdlib‑only script streams the live log and its rolled siblings on the host and prints counts rather than rows, bounded by the number of distinct paths rather than by the size of the log, because the machine has 464 MB. Retention needed two mechanisms rather than one, after a size ceiling and a time ceiling on the same shelf turned out to mean the smaller wins silently — a busy site kept ten days instead of thirty, and a quiet one deleted nothing at all. What outlives the log is an archive of 21 counts a day and 20 facets a month, merged rather than appended on the rule that a window can only under‑observe, with the total stored beside each ranking so a truncated table reports how much it left out.</p>\n<p><strong>Result.</strong> Thirteen addresses are measured this way, and the site ships no cookie, no tracker and no third‑party request, so there is nothing to consent to and nothing for a tag to execute in. The design refuses more than it answers — unique visitors, search terms, a visitor’s path through the site, time on page, clicks, geography and device are all unanswerable here and say so — and the limits are printed beside the numbers rather than kept as fine print, because a coarse figure read as a precise one is worse than no figure.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "Automation & CI/CD", "Data Governance", "Infrastructure", "Monitoring & Observability", "Python", "Security", "Data Governance & Quality", "Security & Access Management", "Site Reliability & Monitoring" ] }, { "id": "https://engineer.company/portfolio/published-a-readable-tor-onion-mirror/", "url": "https://engineer.company/portfolio/published-a-readable-tor-onion-mirror/", "title": "Published a readable Tor onion mirror", "summary": "The site has four front doors onto one build, and this is the only one that is never counted. Gemini requests and Gopher connections are tallied per day with…", "content_html": "<p><strong>Situation.</strong> The site already answered on three protocols from one build. A reader who needed to reach it without revealing that they had reached it was still making an exit‑node round trip to an ordinary address, which is a weaker thing than a front door of its own. The obvious objection to a fourth door is the firewall, and it does not apply: the anonymity daemon reaches the network by making outbound circuits, so nothing dials in, no port opens, and neither firewall layer has anything to synchronise.</p>\n<p><strong>Task.</strong> The mirror had to serve the same pages as the ordinary address, be published wherever the other mirrors are published, carry an address a person can read aloud rather than a random string, and never let the private key that is the address touch this repository or a laptop.</p>\n<p><strong>Action.</strong> The role installs the daemon from the distribution’s own package, writes a configuration with the outbound proxy port switched off, and declines to overwrite an identity it did not generate — so a key placed by hand is simply used. A readable address cannot be chosen, only found: the address is the public key in base32, so a prefix is bought by generating keypairs until one of them happens to encode the letters wanted, and each character costs thirty‑two times the one before it. The prefix that was bought reads as the company’s own name. The address is a declared value in the inventory and every converge asserts the host’s real identity file matches it, so a swapped key with a stale declaration fails the run rather than going quiet. The key is caught by both secret guards, after it was measured that the obvious pattern for a key file matches a deploy key and misses this one. Retiring the random address the mirror had shipped with was a staged cutover rather than a switch, so the published address kept working while the readable one was being published. The step that looked like the last one was not: the key landed correctly and the converge reported no change, because nothing yet named the new directory, so the new address was never published at all — the role now fails on an identity on disk that no service names.</p>\n<p><strong>Result.</strong> The site has four front doors onto one build, and this is the only one that is never counted. Gemini requests and Gopher connections are tallied per day with no address and no path; the onion has no counter and no flag to add one, because counting a reader there is the single thing that protocol exists to prevent, and the blindness is the price of the posture rather than a gap in the reporting. The mirror also paid for itself as a second opinion: its first readers were on a different browser engine, which does not implement the scroll‑driven animation the footer relied on, so every one of them got the colophon painted across the front page. That was a real defect on the ordinary site, found by the audience that had no other way to report it.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "en", "tags": [ "Infrastructure", "Linux & Servers", "Security", "System Administration", "Web Development", "Infrastructure as Code", "Security & Access Management", "Website Development & CMS" ] }, { "id": "https://engineer.company/notes/optimise-before-you-shard/", "url": "https://engineer.company/notes/optimise-before-you-shard/", "title": "Optimise the database before you shard it", "summary": "Most databases that seem to need sharding need reading first. The order we work in: measure, tighten, move reads off, partition, then shard.", "content_html": "<p>When a database slows down under load, the first proposal is usually more\nmachines. Sometimes that is right. More often the slow part is a handful of\nstatements nobody has read yet, and spreading the data across servers spreads\nthose statements over more hardware without making one of them cheaper. A\nproblem that looks unsolvable is often one query plan away from a neat answer,\nand horizontal scaling is sometimes chosen precisely so that nobody has to read\nthe database you already have (read it before you buy more of it).</p>\n<p>This is the order we work in before we split anything: look, tighten, take the\nreads off the primary, partition the tables around the hot spot, and shard only\nwhen one machine is full of good work. The flash sale, where every buyer writes\nto one row, is worked through in\n<a href=\"/notes/selling-limited-stock-without-overselling/\">Selling the last ticket exactly once</a>.\nThis note is the method around it, and its last section takes on the opposite\nproposal: sell from every server, and apologise for the units sold twice.</p>\n<h2 id=\"look-first\">Look first</h2>\n\n<p>PostgreSQL already records most of what there is to know. Before changing\nanything, ask it four questions.</p>\n<p><strong>Which statements cost the most in total?</strong> <code>pg_stat_statements</code>, an extension\nloaded through <code>shared_preload_libraries</code>, keeps every normalised statement with\nits call count and its time. Sort by total, not by mean: a 2 ms query run a\nmillion times an hour costs more than a two-second report run twice.</p>\n<div class=\"highlight\"><pre tabindex=\"0\" class=\"chroma\"><code class=\"language-sql\" data-lang=\"sql\"><span class=\"line\"><span class=\"cl\"><span class=\"k\">SELECT</span><span class=\"w\"> </span><span class=\"n\">calls</span><span class=\"p\">,</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"n\">round</span><span class=\"p\">(</span><span class=\"n\">total_exec_time</span><span class=\"p\">)</span><span class=\"w\"> </span><span class=\"k\">AS</span><span class=\"w\"> </span><span class=\"n\">total_ms</span><span class=\"p\">,</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"n\">round</span><span class=\"p\">(</span><span class=\"n\">mean_exec_time</span><span class=\"p\">::</span><span class=\"nb\">numeric</span><span class=\"p\">,</span><span class=\"w\"> </span><span class=\"mi\">1</span><span class=\"p\">)</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">AS</span><span class=\"w\"> </span><span class=\"n\">mean_ms</span><span class=\"p\">,</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">left</span><span class=\"p\">(</span><span class=\"n\">query</span><span class=\"p\">,</span><span class=\"w\"> </span><span class=\"mi\">60</span><span class=\"p\">)</span><span class=\"w\"> </span><span class=\"k\">AS</span><span class=\"w\"> </span><span class=\"n\">query</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">FROM</span><span class=\"w\"> </span><span class=\"n\">pg_stat_statements</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">ORDER</span><span class=\"w\"> </span><span class=\"k\">BY</span><span class=\"w\"> </span><span class=\"n\">total_exec_time</span><span class=\"w\"> </span><span class=\"k\">DESC</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">LIMIT</span><span class=\"w\"> </span><span class=\"mi\">10</span><span class=\"p\">;</span><span class=\"w\">\n</span></span></span></code></pre></div><p><strong>What does the top one actually do?</strong> <code>EXPLAIN (ANALYZE, BUFFERS)</code> runs the\nstatement and shows the plan it chose, the rows each step really returned\nagainst the rows it expected, and how many pages it read. A scan of a million\nrows to return ten, or an estimate off by a factor of a hundred, is usually the\nwhole story. <code>ANALYZE</code> executes what it explains, so wrap a write in <code>BEGIN</code> and\n<code>ROLLBACK</code>.</p>\n<p><strong>Who is waiting, and on whom?</strong></p>\n<div class=\"highlight\"><pre tabindex=\"0\" class=\"chroma\"><code class=\"language-sql\" data-lang=\"sql\"><span class=\"line\"><span class=\"cl\"><span class=\"k\">SELECT</span><span class=\"w\"> </span><span class=\"n\">pid</span><span class=\"p\">,</span><span class=\"w\"> </span><span class=\"n\">wait_event_type</span><span class=\"p\">,</span><span class=\"w\"> </span><span class=\"n\">wait_event</span><span class=\"p\">,</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"n\">pg_blocking_pids</span><span class=\"p\">(</span><span class=\"n\">pid</span><span class=\"p\">)</span><span class=\"w\"> </span><span class=\"k\">AS</span><span class=\"w\"> </span><span class=\"n\">blocked_by</span><span class=\"p\">,</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">left</span><span class=\"p\">(</span><span class=\"n\">query</span><span class=\"p\">,</span><span class=\"w\"> </span><span class=\"mi\">60</span><span class=\"p\">)</span><span class=\"w\"> </span><span class=\"k\">AS</span><span class=\"w\"> </span><span class=\"n\">query</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">FROM</span><span class=\"w\"> </span><span class=\"n\">pg_stat_activity</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">WHERE</span><span class=\"w\"> </span><span class=\"n\">wait_event_type</span><span class=\"w\"> </span><span class=\"o\">=</span><span class=\"w\"> </span><span class=\"s1\">'Lock'</span><span class=\"p\">;</span><span class=\"w\">\n</span></span></span></code></pre></div><p>Rows here in a busy minute mean transactions are queueing for each other, not\nfor hardware, and a bigger machine does not shorten a queue for one row.</p>\n<p><strong>Is the server working or juggling?</strong> Count the active sessions in\n<code>pg_stat_activity</code> against the cores. When many times more are active than\nthere are cores, the server spends its time switching between them, and a\nconnection pool fixes that before any hardware does. Then read <code>n_dead_tup</code> in\n<code>pg_stat_user_tables</code>: a table with more dead rows than live ones is being\nscanned through its own garbage, which is autovacuum falling behind, not a\ndatabase that is too small.</p>\n<p>Write down what you find. It is the baseline every later step is measured\nagainst, and the honest answer to “do we need to shard?” is often in it\nalready.</p>\n<h2 id=\"tighten-what-is-there\">Tighten what is there</h2>\n\n<p>Roughly in order of payoff for the effort.</p>\n<p><strong>Indexes that match the questions.</strong> The plans above name the scans. An index\non the columns a hot query filters by turns a scan into a lookup, and a partial\nindex holds only the rows a query can want — units still for sale, say — so it\nshrinks as a sale goes on instead of growing:</p>\n<div class=\"highlight\"><pre tabindex=\"0\" class=\"chroma\"><code class=\"language-sql\" data-lang=\"sql\"><span class=\"line\"><span class=\"cl\"><span class=\"k\">CREATE</span><span class=\"w\"> </span><span class=\"k\">INDEX</span><span class=\"w\"> </span><span class=\"n\">units_unsold</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">ON</span><span class=\"w\"> </span><span class=\"n\">units</span><span class=\"w\"> </span><span class=\"p\">(</span><span class=\"n\">item_id</span><span class=\"p\">)</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">WHERE</span><span class=\"w\"> </span><span class=\"k\">NOT</span><span class=\"w\"> </span><span class=\"n\">sold</span><span class=\"p\">;</span><span class=\"w\">\n</span></span></span></code></pre></div><p>Every index is also paid for on every write, so drop the ones whose <code>idx_scan</code>\nin <code>pg_stat_user_indexes</code> stays at zero.</p>\n<p><strong>Types that say what the value is.</strong> Schemas written fast, by a person or a\nmodel, reach for <code>TEXT</code> where a <code>SMALLINT</code> or a <code>BOOLEAN</code> would do. A <code>BOOLEAN</code>\nis one byte, a <code>SMALLINT</code> two, and <code>'yes'</code> stored as text is four. PostgreSQL\nalso pads each fixed-width column to its own alignment, so a column order chosen\nby accident costs bytes too. One unit of stock, written fast:</p>\n<div class=\"highlight\"><pre tabindex=\"0\" class=\"chroma\"><code class=\"language-sql\" data-lang=\"sql\"><span class=\"line\"><span class=\"cl\"><span class=\"k\">CREATE</span><span class=\"w\"> </span><span class=\"k\">TABLE</span><span class=\"w\"> </span><span class=\"n\">units</span><span class=\"w\"> </span><span class=\"p\">(</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"n\">sold</span><span class=\"w\"> </span><span class=\"nb\">text</span><span class=\"p\">,</span><span class=\"w\"> </span><span class=\"c1\">-- 'yes' or 'no'\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"n\">id</span><span class=\"w\"> </span><span class=\"nb\">bigint</span><span class=\"p\">,</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"n\">reserved</span><span class=\"w\"> </span><span class=\"nb\">text</span><span class=\"p\">,</span><span class=\"w\"> </span><span class=\"c1\">-- 'yes' or 'no'\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"n\">reserved_at</span><span class=\"w\"> </span><span class=\"n\">timestamptz</span><span class=\"p\">,</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"n\">item_id</span><span class=\"w\"> </span><span class=\"nb\">bigint</span><span class=\"p\">,</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"n\">order_id</span><span class=\"w\"> </span><span class=\"nb\">bigint</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">);</span><span class=\"w\">\n</span></span></span></code></pre></div><p>And the same unit with types that say what the values are, widest first:</p>\n<div class=\"highlight\"><pre tabindex=\"0\" class=\"chroma\"><code class=\"language-sql\" data-lang=\"sql\"><span class=\"line\"><span class=\"cl\"><span class=\"k\">CREATE</span><span class=\"w\"> </span><span class=\"k\">TABLE</span><span class=\"w\"> </span><span class=\"n\">units</span><span class=\"w\"> </span><span class=\"p\">(</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"n\">id</span><span class=\"w\"> </span><span class=\"nb\">bigint</span><span class=\"w\"> </span><span class=\"k\">PRIMARY</span><span class=\"w\"> </span><span class=\"k\">KEY</span><span class=\"p\">,</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"n\">reserved_at</span><span class=\"w\"> </span><span class=\"n\">timestamptz</span><span class=\"p\">,</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"n\">order_id</span><span class=\"w\"> </span><span class=\"nb\">bigint</span><span class=\"p\">,</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"n\">item_id</span><span class=\"w\"> </span><span class=\"nb\">integer</span><span class=\"w\"> </span><span class=\"k\">NOT</span><span class=\"w\"> </span><span class=\"k\">NULL</span><span class=\"p\">,</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"n\">sold</span><span class=\"w\"> </span><span class=\"nb\">boolean</span><span class=\"w\"> </span><span class=\"k\">NOT</span><span class=\"w\"> </span><span class=\"k\">NULL</span><span class=\"w\"> </span><span class=\"k\">DEFAULT</span><span class=\"w\"> </span><span class=\"k\">false</span><span class=\"p\">,</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"n\">reserved</span><span class=\"w\"> </span><span class=\"nb\">boolean</span><span class=\"w\"> </span><span class=\"k\">NOT</span><span class=\"w\"> </span><span class=\"k\">NULL</span><span class=\"w\"> </span><span class=\"k\">DEFAULT</span><span class=\"w\"> </span><span class=\"k\">false</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">);</span><span class=\"w\">\n</span></span></span></code></pre></div><p>On PostgreSQL 18 the first row measures 72 bytes and the second 54, a quarter\nless, so more of the table fits in memory. The types also refuse values such as\n<code>'maybe'</code> that the application would otherwise have to defend against, and a\n<code>CHECK</code> constraint does the same for ranges: <code>quantity smallint CHECK (quantity > 0)</code>.</p>\n<p><strong>Fewer round trips, and the work where the data is.</strong> Every statement the\napplication sends costs a round trip and a parse. A loop that reads a hundred\nrows and updates them one by one is a hundred and one round trips where one\nset-based statement would do, and a page that fetches every unit of an item to\ncount the free ones ships thousands of rows to compute one number. The rule we\nuse: what has to see the data goes to the database — filtering, counting, a\nwrite that depends on what is there — and business rules stay in the\napplication, because the database’s CPU is the hardest one in the system to add\nmore of.</p>\n<p><strong>Updates that stay on their page.</strong> A row that changes many times a minute\nleaves a new version of itself each time. When an update changes no indexed\ncolumn and the page has room, PostgreSQL keeps the new version on the same page\nand touches no index at all: a heap-only tuple, or HOT update. So keep the\ncolumns a frequent update changes out of the indexes, and leave room on the\npages:</p>\n<div class=\"highlight\"><pre tabindex=\"0\" class=\"chroma\"><code class=\"language-sql\" data-lang=\"sql\"><span class=\"line\"><span class=\"cl\"><span class=\"k\">ALTER</span><span class=\"w\"> </span><span class=\"k\">TABLE</span><span class=\"w\"> </span><span class=\"n\">units</span><span class=\"w\"> </span><span class=\"k\">SET</span><span class=\"w\"> </span><span class=\"p\">(</span><span class=\"n\">fillfactor</span><span class=\"w\"> </span><span class=\"o\">=</span><span class=\"w\"> </span><span class=\"mi\">90</span><span class=\"p\">);</span><span class=\"w\">\n</span></span></span></code></pre></div><p>The setting applies to pages written from then on, so set it before the rows\nare loaded. On the table above, 2,000 claims that set <code>reserved</code>, <code>reserved_at</code>\nand <code>order_id</code> were all HOT, and of 2,000 updates that set <code>sold</code>, which the\npartial index reads, none were HOT. Selling happens once per unit; claiming can\nhappen many times. <code>n_tup_hot_upd</code> against <code>n_tup_upd</code> in <code>pg_stat_user_tables</code>\nshows the ratio on a live system.</p>\n<h2 id=\"one-row-says-who-was-first\">One row says who was first</h2>\n\n<p>A unit goes to the first buyer whose statement reaches it, and the claim runs\nout on the database’s clock. Two columns carry it: <code>reserved</code>, a boolean, and\n<code>reserved_at</code>, the moment it was set.</p>\n<div class=\"highlight\"><pre tabindex=\"0\" class=\"chroma\"><code class=\"language-sql\" data-lang=\"sql\"><span class=\"line\"><span class=\"cl\"><span class=\"k\">UPDATE</span><span class=\"w\"> </span><span class=\"n\">units</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">SET</span><span class=\"w\"> </span><span class=\"n\">reserved</span><span class=\"w\"> </span><span class=\"o\">=</span><span class=\"w\"> </span><span class=\"k\">true</span><span class=\"p\">,</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"n\">reserved_at</span><span class=\"w\"> </span><span class=\"o\">=</span><span class=\"w\"> </span><span class=\"n\">now</span><span class=\"p\">(),</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"n\">order_id</span><span class=\"w\"> </span><span class=\"o\">=</span><span class=\"w\"> </span><span class=\"p\">:</span><span class=\"k\">order</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">WHERE</span><span class=\"w\"> </span><span class=\"n\">id</span><span class=\"w\"> </span><span class=\"o\">=</span><span class=\"w\"> </span><span class=\"p\">:</span><span class=\"n\">unit</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">AND</span><span class=\"w\"> </span><span class=\"k\">NOT</span><span class=\"w\"> </span><span class=\"n\">sold</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">AND</span><span class=\"w\"> </span><span class=\"p\">(</span><span class=\"k\">NOT</span><span class=\"w\"> </span><span class=\"n\">reserved</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">OR</span><span class=\"w\"> </span><span class=\"n\">reserved_at</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"o\"><</span><span class=\"w\"> </span><span class=\"n\">now</span><span class=\"p\">()</span><span class=\"w\"> </span><span class=\"o\">-</span><span class=\"w\"> </span><span class=\"nb\">interval</span><span class=\"w\"> </span><span class=\"s1\">'10 minutes'</span><span class=\"p\">)</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"n\">RETURNING</span><span class=\"w\"> </span><span class=\"n\">id</span><span class=\"p\">;</span><span class=\"w\">\n</span></span></span></code></pre></div><p>One row back: this buyer has it. None: someone else was first, or it is sold.\nTwo claims on the same unit queue on its row lock, and on READ COMMITTED,\nPostgreSQL’s default, the second checks the <code>WHERE</code> clause again against the\nrow the first one committed, so exactly one of them gets a row back. A claim older than ten minutes counts as free, so nothing\nhas to sweep it up for the answer to be right.</p>\n<p>That makes the boolean alone an incomplete answer. <code>reserved</code> stays true after\na claim lapses, and only the two columns together say whether a unit is held,\nso every query that asks has to ask both. A periodic sweep that resets lapsed\nclaims keeps the table honest for anyone reading <code>reserved</code> on its own, but\ncorrectness never waits for it. A constraint keeps the pair consistent:</p>\n<div class=\"highlight\"><pre tabindex=\"0\" class=\"chroma\"><code class=\"language-sql\" data-lang=\"sql\"><span class=\"line\"><span class=\"cl\"><span class=\"k\">ALTER</span><span class=\"w\"> </span><span class=\"k\">TABLE</span><span class=\"w\"> </span><span class=\"n\">units</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">ADD</span><span class=\"w\"> </span><span class=\"k\">CONSTRAINT</span><span class=\"w\"> </span><span class=\"n\">reserved_has_time</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">CHECK</span><span class=\"w\"> </span><span class=\"p\">(</span><span class=\"k\">NOT</span><span class=\"w\"> </span><span class=\"n\">reserved</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">OR</span><span class=\"w\"> </span><span class=\"n\">reserved_at</span><span class=\"w\"> </span><span class=\"k\">IS</span><span class=\"w\"> </span><span class=\"k\">NOT</span><span class=\"w\"> </span><span class=\"k\">NULL</span><span class=\"p\">);</span><span class=\"w\">\n</span></span></span></code></pre></div><p>The ticket note stores the deadline, <code>held_until</code>, rather than the start; it is\nthe same design. When the buyer picks the unit — a seat, a numbered print — the\nstatement names it, as here. When any unit will do, that note’s <code>SKIP LOCKED</code>\nform picks one nobody else is holding.</p>\n<h2 id=\"reads-off-the-primary\">Reads off the primary</h2>\n\n<p>Most of a shop’s traffic is reading: the product page, the “3 left”, the\nbasket, the order history. None of it needs the primary, the one machine that\nhas to stay free for writes. A streaming replica takes those reads, and adding\none is configuration where a shard is a project.</p>\n<p>Two cautions come with it. A replica runs a little behind, so a buyer who has\njust claimed a unit and then reads their basket from it may not see the claim:\nread what a user has just written from the primary, or send that user to the\nprimary for a few seconds after each write. And a long report on a replica can\nbe cancelled when it holds up the changes the replica is replaying. The\nPostgreSQL documentation calls these query conflicts and lists the settings\nthat trade replica lag against them.</p>\n<p>The cheapest read is one that never reaches a database. When every unit of an\nitem is claimed, nobody can buy it until a claim lapses or is released, and the\ndatabase can say exactly when that is:</p>\n<div class=\"highlight\"><pre tabindex=\"0\" class=\"chroma\"><code class=\"language-sql\" data-lang=\"sql\"><span class=\"line\"><span class=\"cl\"><span class=\"k\">SELECT</span><span class=\"w\"> </span><span class=\"k\">count</span><span class=\"p\">(</span><span class=\"o\">*</span><span class=\"p\">)</span><span class=\"w\"> </span><span class=\"n\">FILTER</span><span class=\"w\"> </span><span class=\"p\">(</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">WHERE</span><span class=\"w\"> </span><span class=\"k\">NOT</span><span class=\"w\"> </span><span class=\"n\">reserved</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">OR</span><span class=\"w\"> </span><span class=\"n\">reserved_at</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"o\"><</span><span class=\"w\"> </span><span class=\"n\">now</span><span class=\"p\">()</span><span class=\"w\"> </span><span class=\"o\">-</span><span class=\"w\"> </span><span class=\"nb\">interval</span><span class=\"w\"> </span><span class=\"s1\">'10 minutes'</span><span class=\"p\">)</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">AS</span><span class=\"w\"> </span><span class=\"k\">free</span><span class=\"p\">,</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">min</span><span class=\"p\">(</span><span class=\"n\">reserved_at</span><span class=\"p\">)</span><span class=\"w\"> </span><span class=\"o\">+</span><span class=\"w\"> </span><span class=\"nb\">interval</span><span class=\"w\"> </span><span class=\"s1\">'10 minutes'</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">AS</span><span class=\"w\"> </span><span class=\"k\">until</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">FROM</span><span class=\"w\"> </span><span class=\"n\">units</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">WHERE</span><span class=\"w\"> </span><span class=\"n\">item_id</span><span class=\"w\"> </span><span class=\"o\">=</span><span class=\"w\"> </span><span class=\"p\">:</span><span class=\"n\">item</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">AND</span><span class=\"w\"> </span><span class=\"k\">NOT</span><span class=\"w\"> </span><span class=\"n\">sold</span><span class=\"p\">;</span><span class=\"w\">\n</span></span></span></code></pre></div><p>When <code>free</code> is zero, <code>until</code> is the moment the earliest claim lapses: cache\n“fully reserved” with that as its expiry, never longer, and every visitor until\nthen is turned away by the cache rather than the database. When <code>free</code> is zero\nand <code>until</code> is empty, every unit is sold and the answer is final. A claim\nreleased early — a declined card, an abandoned basket — deletes the cache\nentry, so the unit is back on sale at once.</p>\n<p>The cache only ever says no. A yes always comes from the conditional <code>UPDATE</code>\nabove, so a stale cache can cost a sale for a few seconds and can never sell a\nunit twice. It is the ticket note’s cached “sold out”, with a deadline.</p>\n<h2 id=\"partition-the-tables-around-the-hot-row\">Partition the tables around the hot row</h2>\n\n<p>Partitioning splits one table into several by a key — a month, a tenant —\nbehind one name. It is worth being plain about what it does not do: it does not\nmake one contended row any faster. That row still lives in one partition, on\none machine, behind one lock.</p>\n<p>What it does is keep the tables around the hot spot small. Orders, claim\nhistory and events grow for ever, and most queries want the recent part:</p>\n<div class=\"highlight\"><pre tabindex=\"0\" class=\"chroma\"><code class=\"language-sql\" data-lang=\"sql\"><span class=\"line\"><span class=\"cl\"><span class=\"k\">CREATE</span><span class=\"w\"> </span><span class=\"k\">TABLE</span><span class=\"w\"> </span><span class=\"n\">orders</span><span class=\"w\"> </span><span class=\"p\">(</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"n\">id</span><span class=\"w\"> </span><span class=\"nb\">bigint</span><span class=\"w\"> </span><span class=\"k\">NOT</span><span class=\"w\"> </span><span class=\"k\">NULL</span><span class=\"p\">,</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"n\">created_at</span><span class=\"w\"> </span><span class=\"n\">timestamptz</span><span class=\"w\"> </span><span class=\"k\">NOT</span><span class=\"w\"> </span><span class=\"k\">NULL</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">DEFAULT</span><span class=\"w\"> </span><span class=\"n\">now</span><span class=\"p\">(),</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"n\">item_id</span><span class=\"w\"> </span><span class=\"nb\">integer</span><span class=\"w\"> </span><span class=\"k\">NOT</span><span class=\"w\"> </span><span class=\"k\">NULL</span><span class=\"p\">,</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"n\">quantity</span><span class=\"w\"> </span><span class=\"nb\">smallint</span><span class=\"w\"> </span><span class=\"k\">NOT</span><span class=\"w\"> </span><span class=\"k\">NULL</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">CHECK</span><span class=\"w\"> </span><span class=\"p\">(</span><span class=\"n\">quantity</span><span class=\"w\"> </span><span class=\"o\">></span><span class=\"w\"> </span><span class=\"mi\">0</span><span class=\"p\">),</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"n\">paid</span><span class=\"w\"> </span><span class=\"nb\">boolean</span><span class=\"w\"> </span><span class=\"k\">NOT</span><span class=\"w\"> </span><span class=\"k\">NULL</span><span class=\"w\"> </span><span class=\"k\">DEFAULT</span><span class=\"w\"> </span><span class=\"k\">false</span><span class=\"p\">,</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">PRIMARY</span><span class=\"w\"> </span><span class=\"k\">KEY</span><span class=\"w\"> </span><span class=\"p\">(</span><span class=\"n\">id</span><span class=\"p\">,</span><span class=\"w\"> </span><span class=\"n\">created_at</span><span class=\"p\">)</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">)</span><span class=\"w\"> </span><span class=\"n\">PARTITION</span><span class=\"w\"> </span><span class=\"k\">BY</span><span class=\"w\"> </span><span class=\"n\">RANGE</span><span class=\"w\"> </span><span class=\"p\">(</span><span class=\"n\">created_at</span><span class=\"p\">);</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"k\">CREATE</span><span class=\"w\"> </span><span class=\"k\">TABLE</span><span class=\"w\"> </span><span class=\"n\">orders_2026_10</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"n\">PARTITION</span><span class=\"w\"> </span><span class=\"k\">OF</span><span class=\"w\"> </span><span class=\"n\">orders</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">FOR</span><span class=\"w\"> </span><span class=\"k\">VALUES</span><span class=\"w\"> </span><span class=\"k\">FROM</span><span class=\"w\"> </span><span class=\"p\">(</span><span class=\"s1\">'2026-10-01'</span><span class=\"p\">)</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">TO</span><span class=\"w\"> </span><span class=\"p\">(</span><span class=\"s1\">'2026-11-01'</span><span class=\"p\">);</span><span class=\"w\">\n</span></span></span></code></pre></div><p>A query that names a date range reads only the partitions inside it, and this\nmonth’s indexes stay small enough to live in memory. Retention stops being a\n<code>DELETE</code> of millions of rows, with the bloat and the vacuuming that follow, and\nbecomes one statement:</p>\n<div class=\"highlight\"><pre tabindex=\"0\" class=\"chroma\"><code class=\"language-sql\" data-lang=\"sql\"><span class=\"line\"><span class=\"cl\"><span class=\"k\">ALTER</span><span class=\"w\"> </span><span class=\"k\">TABLE</span><span class=\"w\"> </span><span class=\"n\">orders</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"n\">DETACH</span><span class=\"w\"> </span><span class=\"n\">PARTITION</span><span class=\"w\"> </span><span class=\"n\">orders_2025_10</span><span class=\"p\">;</span><span class=\"w\">\n</span></span></span></code></pre></div><p>The primary key has to include the partition key, which is why it is\n<code>(id, created_at)</code>. A query that does not name the key reads every partition,\nso partition by what the queries already filter on.</p>\n<h2 id=\"when-sharding-is-reasonable\">When sharding is reasonable</h2>\n\n<p>There is one limit sharding cannot move: every item has one owner. A unit\ncannot be claimed on two servers at once, so sharding moves whole items, never\nhalf of one. When a single item outgrows its machine, the ticket note’s buckets\nsplit its stock into smaller pieces, and each piece again has exactly one\nowner.</p>\n<p>Within that, we call sharding reasonable when one of these is true, and not\nbefore:</p>\n<ul>\n<li><strong>One machine is full of good work.</strong> The statements are already lean — one\nconditional write each, nothing external inside a transaction, updates that\nstay on their page — and the primary’s CPU or write-ahead log is still\nsaturated, on the largest machine worth buying, across many items rather than\none. One hot item is a bucket problem, not a shard problem.</li>\n<li><strong>The data has outgrown its maintenance.</strong> A backup cannot be restored inside\nthe time promised to the business, an index cannot be built or a table\nvacuumed in the quiet hours, or the working set no longer fits in memory.</li>\n<li><strong>Isolation is the requirement.</strong> A tenant whose contract or regulator says\ntheir data lives apart, or whose sale must not share a disk with anyone\nelse’s.</li>\n</ul>\n<p>The cost lands on everything else. Every migration, backup, restore and report\nruns once per shard; a join across shards becomes application code; and a query\nthat does not know its shard key asks all of them. That is the bill the\nsections above exist to avoid, and once it is paid it is hard to get back.</p>\n<h2 id=\"when-an-apology-is-cheaper-than-a-lock\">When an apology is cheaper than a lock</h2>\n\n<p>There is a serious argument for going the other way. Let every server sell from\nthe stock it can see, accept that now and then a unit goes to two buyers, and\nlet customer support refund one of them with an apology. Business logic becomes\neventually consistent, built for the 99.9% of requests that never collide, and\nan asynchronous loop finds the collisions afterwards and makes them good. In\nreturn the shop never has to say “try again”. Helland and Campbell make the\nstrongest form of the case in <em>Building on Quicksand</em>: a system that\nacknowledges work before its backup has it makes every promise probabilistic,\nand businesses have always settled the promises they could not keep with\napologies.</p>\n<p>We agree with most of that. We disagree about where it is spent.</p>\n<p><strong>Most of a shop should be eventually consistent.</strong> The catalogue, the search\nindex, the “3 left” badge, the basket, the recommendations, the emails, the\nwarehouse export and the reports can all run seconds behind, and making them\nwait for the primary costs speed and buys nothing. Dynamo was built for exactly\nthis: Amazon’s shopping cart never forgets or rejects an “add to cart”, even\nwhile machines fail, and the price is that a merge can bring back an item the\nbuyer had removed. After the claim, the ticket note’s checkout is already\nasynchronous: a saga of local steps, messages delivered at least once, and\nconsumers that are idempotent, keyed by the order. A reconciliation loop belongs\nin any design that talks to a payment provider, because webhooks get lost.</p>\n<p><strong>The claim is the one place it does not hold.</strong> “Rare” assumes collisions are\nrare, and in a flash sale the collisions are the sale. A server that sells from\nits own view keeps selling until it hears what the others sold, so the oversell\nis roughly the attempt rate times the replication lag. In a simple simulation,\ntwo regions split 2,000 attempts a second, each sells from its own count and\nhears of the other’s sales 200 milliseconds later. Across five runs of a\n10,000-unit sale, each run sold between 191 and 205 units that did not exist.\nThe arithmetic predicts 200: the attempt rate, times the lag, times the share of\nsales made in the other region. With one owner per item, the same model sold\nexactly the stock. That happens on every sell-out, whether anything failed or\nnot, and it lands at the climax, when the most buyers are watching.</p>\n<p><strong>The uptime is smaller than it sounds.</strong> Gilbert and Lynch proved the trade\nthat eventual consistency makes: while the network is partitioned, a system can\nanswer or it can be consistent, not both. Abadi’s PACELC adds the other half.\nWhen nothing is partitioned, the trade is latency against consistency, and one\nowner per item, with lean statements, is already fast. Neither design reaches\n100%: the payment provider, DNS and the networks between them have outages of\ntheir own. What the strict design gives up is narrow. When the owner of one\nitem’s stock cannot be reached, that item stops selling for the seconds a\nfailover takes, and the ticket note’s section on pausing the item scopes the\npause to the items on the failed machine. The rest of the shop keeps selling.</p>\n<p><strong>An apology is not a refund.</strong> The refund is the cheap part. The buyer was told\nyes, and may already have booked the train, taken the day off and told their\nfriends. Hearing “sold out” a second too early is a disappointment. Hearing that\na confirmed order has been withdrawn is a broken promise. Support is staffed for\nan ordinary day, and oversells arrive together, in the busiest minute of the\nyear. Before planning to withdraw confirmed orders, ask a lawyer what a\nconfirmation commits the shop to.</p>\n<p><strong>Keep the asynchronous part, and move it before the yes.</strong> What the proposal\ngets right is that the buyer does not have to wait on a lock. Accept every click\ninto a durable, replicated log, one partition per item, and tell the buyer the\nanswer is coming. Accepting a click then depends only on the log, and a log\nreplicated across three machines keeps taking writes when one of them fails. One\nowner per item reads the log in order and decides, at memory speed, and the\nanswer goes back as a message within seconds. When that owner fails over, the\nrequests wait in the log instead of being refused or sold twice. The yes is a\nhold, and payment follows it as in the ticket note’s checkout, so nobody pays\nfor a unit they did not get. The reconciliation still happens, but before the\npromise instead of after it.</p>\n<p><strong>Share the stock, not the count.</strong> When the business needs regions to sell\nwithout asking each other, give each region a quota of the stock and let it sell\nfrom its own quota without asking anyone. A region whose quota runs dry asks for\nmore, and near the end of the sale the quotas collapse back to one owner. Bailis\nand colleagues work out which rules servers can keep without talking to each\nother, and a stock that must not go below zero is not one of them. Quotas do not\nremove that conversation; they move it off the hot path, to the rare moment a\nquota is refilled. That is the eventually consistent design that keeps the\ninvariant. Its one cost is the tail, where a buyer is turned away in one region\nwhile another region still has stock.</p>\n<p><strong>When an oversell is the right policy.</strong> Some goods can be made again: a print\non demand, a T-shirt the supplier still holds, a licence key. Selling past the\ncount is then a backorder with an honest date, and eventual consistency is the\nright design. Airlines overbook on purpose. They know their no-show rates, price\nthe risk and compensate by a rule written in advance. That is the test. An\noversell is acceptable when the business chose its size and its remedy before\nthe sale. It is a defect when it falls on whoever happened to click last.</p>\n<p><strong>If you choose apologies, build them like payments.</strong> The loop the proposal\ndescribes is a refund process, and it needs what anything that moves money\nneeds:</p>\n<ul>\n<li><strong>An id on every sale,</strong> so a retry is the same sale, and an append-only\nledger of sales. The stock number is derived from the ledger, never edited.</li>\n<li><strong>A rule for who keeps the unit</strong> that you could explain to the buyer who\nlost. Order by the log, not by the clocks of different servers, where a few\nmilliseconds of skew would decide who gets the refund.</li>\n<li><strong>Compensations that are idempotent and logged:</strong> cancel, void or refund, tell\nthe buyer. They are the saga’s undo steps, run by a machine.</li>\n<li><strong>A deadline.</strong> The loop has to find every oversell before anything ships, and\nbefore the buyer finds it.</li>\n<li><strong>A budget, measured.</strong> Oversells per sale, against the number the business\nsigned for, next to the invariant on the same dashboard, with an alert when\nthe budget is spent.</li>\n</ul>\n<p>Our answer is to be eventually consistent everywhere around the claim, strict at\nthe claim, and asynchronous in how the answer reaches the buyer. Design for the\ncommon path, but in a flash sale the collision is the common path. If the\nbusiness wants a few oversells in return for something, make it a number the\nbusiness chose, with a remedy written before the sale, and measure it.</p>\n<h2 id=\"what-we-would-tell-the-next-person\">What we would tell the next person</h2>\n\n<ul>\n<li><strong>Read before you buy.</strong> <code>pg_stat_statements</code> and one <code>EXPLAIN</code> answer more\ncapacity questions than a new server does.</li>\n<li><strong>Make the types say what the data is.</strong> A boolean stored as a boolean is\nsmaller, and it refuses nonsense.</li>\n<li><strong>Send the database what has to see the data, and nothing else.</strong> Filtering\nand counting belong there. Business rules do not.</li>\n<li><strong>The cache says no; only the database says yes.</strong> Then a stale cache costs\nseconds, never a unit.</li>\n<li><strong>Partitioning is housekeeping.</strong> It keeps the tables around a hot row small\nand does nothing for the row.</li>\n<li><strong>Shard when one machine is full of good work,</strong> not when it is full of\nwaiting.</li>\n<li><strong>Reconcile before the yes, not after.</strong> Everything around the claim can be\neventually consistent. The claim itself, made that way, oversells by the\nattempt rate times the lag, on every sale.</li>\n</ul>\n<h2 id=\"sources\">Sources</h2>\n\n<ul>\n<li><a href=\"https://www.postgresql.org/docs/current/pgstatstatements.html\">PostgreSQL: pg_stat_statements</a></li>\n<li><a href=\"https://www.postgresql.org/docs/current/using-explain.html\">PostgreSQL: Using EXPLAIN</a></li>\n<li><a href=\"https://www.postgresql.org/docs/current/monitoring-stats.html\">PostgreSQL: the cumulative statistics system</a></li>\n<li><a href=\"https://www.postgresql.org/docs/current/datatype-numeric.html\">PostgreSQL: numeric types</a></li>\n<li><a href=\"https://www.postgresql.org/docs/current/datatype-character.html\">PostgreSQL: character types</a></li>\n<li><a href=\"https://www.postgresql.org/docs/current/datatype-boolean.html\">PostgreSQL: Boolean type</a></li>\n<li><a href=\"https://www.postgresql.org/docs/current/indexes-partial.html\">PostgreSQL: partial indexes</a></li>\n<li><a href=\"https://www.postgresql.org/docs/current/storage-hot.html\">PostgreSQL: heap-only tuples (HOT)</a></li>\n<li><a href=\"https://www.postgresql.org/docs/current/ddl-partitioning.html\">PostgreSQL: table partitioning</a></li>\n<li><a href=\"https://www.postgresql.org/docs/current/hot-standby.html\">PostgreSQL: hot standby and query conflicts</a></li>\n<li><a href=\"https://arxiv.org/abs/0909.1788\">Pat Helland and Dave Campbell, Building on Quicksand</a></li>\n<li><a href=\"https://groups.csail.mit.edu/tds/papers/Gilbert/Brewer2.pdf\">Seth Gilbert and Nancy Lynch, Brewer’s Conjecture and the Feasibility of Consistent, Available, Partition-Tolerant Web Services</a></li>\n<li><a href=\"https://www.cs.umd.edu/~abadi/papers/abadi-pacelc.pdf\">Daniel Abadi, Consistency Tradeoffs in Modern Distributed Database System Design</a></li>\n<li><a href=\"https://www.allthingsdistributed.com/files/amazon-dynamo-sosp2007.pdf\">DeCandia et al., Dynamo: Amazon’s Highly Available Key-value Store</a></li>\n<li><a href=\"https://www.cs.cornell.edu/andru/cs711/2002fa/reading/sagas.pdf\">Hector Garcia-Molina and Kenneth Salem, Sagas</a></li>\n<li><a href=\"https://www.vldb.org/pvldb/vol8/p185-bailis.pdf\">Bailis et al., Coordination Avoidance in Database Systems</a></li>\n</ul>\n", "date_published": "2026-10-14T00:00:00Z", "date_modified": "2026-10-20T11:21:41+02:00", "language": "en" }, { "id": "https://engineer.company/notes/selling-limited-stock-without-overselling/", "url": "https://engineer.company/notes/selling-limited-stock-without-overselling/", "title": "Selling the last ticket exactly once", "summary": "A flash sale oversells on one hot row, not on a big database. What stops it: a conditional write, holds that expire, and a saga.", "content_html": "<p>A flash sale puts ten thousand units in front of a million people at once. The\ninteresting failure is not that the shop goes down. It is that the shop stays\nup and sells ten thousand and forty. The instinct that follows — the database\nis too small, shard it — answers a question nobody asked. The database was\nnever too big. Every buyer was writing to\nthe same row (the whole incident lives in one row).</p>\n<p>We take the problem apart here before it reaches a project: where the ceiling\ncomes from, the one statement that removes most of it, and what to build when a\nsingle database really is not enough.</p>\n<h2 id=\"one-row-every-buyer\">One row, every buyer</h2>\n\n<p>Twenty million rows spread over a cluster is a solved problem. One row that\nevery request has to change is not, and adding machines does not touch it,\nbecause the row lives on one of them. Distributed stores say so plainly:\nDynamoDB gives a single partition about 1,000 write units a second, and every\nwrite to one item lands on that item’s partition however many others there\nare.</p>\n<p>There is a theorem behind the shape. Bailis and colleagues call an invariant\ninvariant-confluent when replicas can accept writes on their own and still merge\ninto a valid state. <code>stock >= 0</code> is the textbook case of one that is not: two\nreplicas that each see one unit left will each sell it, and no merge can take\nthe second sale back. Never overselling needs coordination. The only question\nis how much of it each sale pays for.</p>\n<h2 id=\"the-number-that-decides-it\">The number that decides it</h2>\n\n<p>The ceiling for one item is roughly how many sales one lock carries, divided by\nhow long the lock is held:</p>\n<div class=\"highlight\"><pre tabindex=\"0\" class=\"chroma\"><code class=\"language-text\" data-lang=\"text\"><span class=\"line\"><span class=\"cl\">max sales/s per item ≈ sales per lock ÷ lock hold time\n</span></span></code></pre></div><table>\n\t<thead>\n\t\t\t<tr>\n\t\t\t\t\t<th>What holds the row</th>\n\t\t\t\t\t<th>Ceiling per row</th>\n\t\t\t</tr>\n\t</thead>\n\t<tbody>\n\t\t\t<tr>\n\t\t\t\t\t<td>Lock, then call payment</td>\n\t\t\t\t\t<td>~2/s</td>\n\t\t\t</tr>\n\t\t\t<tr>\n\t\t\t\t\t<td>Lock, then app logic</td>\n\t\t\t\t\t<td>~100–200/s</td>\n\t\t\t</tr>\n\t\t\t<tr>\n\t\t\t\t\t<td>One conditional <code>UPDATE</code></td>\n\t\t\t\t\t<td>~500–2,000/s</td>\n\t\t\t</tr>\n\t\t\t<tr>\n\t\t\t\t\t<td>Batches of 100 per commit</td>\n\t\t\t\t\t<td>~50,000/s</td>\n\t\t\t</tr>\n\t\t\t<tr>\n\t\t\t\t\t<td>One in-memory writer</td>\n\t\t\t\t\t<td>~100,000+/s</td>\n\t\t\t</tr>\n\t</tbody>\n</table>\n<p>These are orders of magnitude, not benchmarks; the hardware moves them. What it\ndoes not move is the shape. Most of a short lock is the commit waiting for its\nlog to reach the disk, which is why batching wins by two orders: a hundred sales\nshare one flush.</p>\n<p>That leaves four levers, and every design below pulls one of them:</p>\n<ol>\n<li><strong>Hold the lock for less time</strong> — one statement, nothing on the network\ninside it.</li>\n<li><strong>Do more per lock</strong> — batch the decrements.</li>\n<li><strong>Have more locks</strong> — split the stock across several keys.</li>\n<li><strong>Send fewer requests to the lock</strong> — turn most buyers away before they\nreach it.</li>\n</ol>\n<p>The last one matters more than it sounds. With a million buyers and ten\nthousand units, 990,000 requests have to fail. Rejections are most of the\ntraffic, so “sold out” has to be the cheapest answer the system gives: a waiting\nroom in front of the sale, a gate that admits about as many buyers as there are\nunits, a <code>sold_out</code> flag the CDN serves without asking anyone, and a rate limit\nper account.</p>\n<h2 id=\"fix-the-one-database-first\">Fix the one database first</h2>\n\n<p>The version that crawls, or oversells, usually looks like this:</p>\n<div class=\"highlight\"><pre tabindex=\"0\" class=\"chroma\"><code class=\"language-sql\" data-lang=\"sql\"><span class=\"line\"><span class=\"cl\"><span class=\"k\">BEGIN</span><span class=\"p\">;</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"k\">SELECT</span><span class=\"w\"> </span><span class=\"n\">available</span><span class=\"w\"> </span><span class=\"k\">FROM</span><span class=\"w\"> </span><span class=\"n\">inventory</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">WHERE</span><span class=\"w\"> </span><span class=\"n\">item_id</span><span class=\"w\"> </span><span class=\"o\">=</span><span class=\"w\"> </span><span class=\"p\">:</span><span class=\"n\">item</span><span class=\"w\"> </span><span class=\"k\">FOR</span><span class=\"w\"> </span><span class=\"k\">UPDATE</span><span class=\"p\">;</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\">-- the application checks the number,\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\">-- and perhaps calls a payment provider\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"k\">UPDATE</span><span class=\"w\"> </span><span class=\"n\">inventory</span><span class=\"w\"> </span><span class=\"k\">SET</span><span class=\"w\"> </span><span class=\"n\">available</span><span class=\"w\"> </span><span class=\"o\">=</span><span class=\"w\"> </span><span class=\"p\">:</span><span class=\"k\">new</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">WHERE</span><span class=\"w\"> </span><span class=\"n\">item_id</span><span class=\"w\"> </span><span class=\"o\">=</span><span class=\"w\"> </span><span class=\"p\">:</span><span class=\"n\">item</span><span class=\"p\">;</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"k\">COMMIT</span><span class=\"p\">;</span><span class=\"w\">\n</span></span></span></code></pre></div><p>The lock is right. What it is held across is wrong: every millisecond the\napplication spends between the <code>SELECT</code> and the <code>UPDATE</code> is a millisecond the\nnext buyer waits. Without the <code>FOR UPDATE</code> it is worse. Two buyers read 1, both\nwrite 0, and one unit has been sold twice.</p>\n<p>The fix makes the check and the decrement one statement:</p>\n<div class=\"highlight\"><pre tabindex=\"0\" class=\"chroma\"><code class=\"language-sql\" data-lang=\"sql\"><span class=\"line\"><span class=\"cl\"><span class=\"k\">UPDATE</span><span class=\"w\"> </span><span class=\"n\">inventory</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">SET</span><span class=\"w\"> </span><span class=\"n\">available</span><span class=\"w\"> </span><span class=\"o\">=</span><span class=\"w\"> </span><span class=\"n\">available</span><span class=\"w\"> </span><span class=\"o\">-</span><span class=\"w\"> </span><span class=\"p\">:</span><span class=\"n\">qty</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">WHERE</span><span class=\"w\"> </span><span class=\"n\">tenant_id</span><span class=\"w\"> </span><span class=\"o\">=</span><span class=\"w\"> </span><span class=\"p\">:</span><span class=\"n\">tenant</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">AND</span><span class=\"w\"> </span><span class=\"n\">item_id</span><span class=\"w\"> </span><span class=\"o\">=</span><span class=\"w\"> </span><span class=\"p\">:</span><span class=\"n\">item</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">AND</span><span class=\"w\"> </span><span class=\"n\">available</span><span class=\"w\"> </span><span class=\"o\">>=</span><span class=\"w\"> </span><span class=\"p\">:</span><span class=\"n\">qty</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"n\">RETURNING</span><span class=\"w\"> </span><span class=\"n\">available</span><span class=\"p\">;</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\">-- one row back: reserved. none: sold out.\n</span></span></span></code></pre></div><p>In PostgreSQL at READ COMMITTED, a second buyer who reaches the row while the\nfirst holds it waits, then evaluates the <code>WHERE</code> clause again against the row\nthe first one committed. If that sale took the last unit, the condition fails\nand the second buyer gets no row back — sold out — rather than a negative\nnumber. At REPEATABLE READ or SERIALIZABLE the second buyer gets a\nserialization failure instead, which is safe but turns contention into retries;\nREAD COMMITTED is the level this statement is written for. MySQL’s InnoDB\nreaches the same answer, because an <code>UPDATE</code> there reads the latest committed\nrow at any level. Add the constraint as well, so a bug somewhere else fails\nloudly instead of selling stock that does not exist:</p>\n<div class=\"highlight\"><pre tabindex=\"0\" class=\"chroma\"><code class=\"language-sql\" data-lang=\"sql\"><span class=\"line\"><span class=\"cl\"><span class=\"k\">ALTER</span><span class=\"w\"> </span><span class=\"k\">TABLE</span><span class=\"w\"> </span><span class=\"n\">inventory</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">ADD</span><span class=\"w\"> </span><span class=\"k\">CONSTRAINT</span><span class=\"w\"> </span><span class=\"n\">available_not_negative</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">CHECK</span><span class=\"w\"> </span><span class=\"p\">(</span><span class=\"n\">available</span><span class=\"w\"> </span><span class=\"o\">>=</span><span class=\"w\"> </span><span class=\"mi\">0</span><span class=\"p\">);</span><span class=\"w\">\n</span></span></span></code></pre></div><p>Then the rest of the single-database work, roughly in order of payoff:</p>\n<ul>\n<li><strong>Nothing external inside the transaction.</strong> No payment call, no HTTP request,\nno message published. Those go before the transaction or after it.</li>\n<li><strong>Touch the hot row last.</strong> Insert the order and everything else first, so the\nlock on the contended row lives only for the final statement and the commit.</li>\n<li><strong>No optimistic locking on a hot counter.</strong> A version column turns contention\ninto retries, and under a sale the retries are the load.</li>\n<li><strong>Serve the reads from somewhere else.</strong> The product page’s “12 left” can come\nfrom a replica or a cache a second behind. Only the sale needs the primary.</li>\n<li><strong>Give inventory a database of its own,</strong> so a sale never waits behind the\ncatalogue for the same disk.</li>\n<li><strong>Pool the connections.</strong> A hundred thousand buyers should be a few dozen\nconnections through PgBouncer, not thousands of backends queueing for one\nlock.</li>\n</ul>\n<p>Seats are a different shape, because every unit is distinct, and they want a\ndifferent statement. Each seat is a row, and a buyer skips the rows another\ntransaction holds instead of queueing behind them:</p>\n<div class=\"highlight\"><pre tabindex=\"0\" class=\"chroma\"><code class=\"language-sql\" data-lang=\"sql\"><span class=\"line\"><span class=\"cl\"><span class=\"k\">UPDATE</span><span class=\"w\"> </span><span class=\"n\">seats</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">SET</span><span class=\"w\"> </span><span class=\"n\">held_by</span><span class=\"w\"> </span><span class=\"o\">=</span><span class=\"w\"> </span><span class=\"p\">:</span><span class=\"k\">order</span><span class=\"p\">,</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"n\">held_until</span><span class=\"w\"> </span><span class=\"o\">=</span><span class=\"w\"> </span><span class=\"n\">now</span><span class=\"p\">()</span><span class=\"w\"> </span><span class=\"o\">+</span><span class=\"w\"> </span><span class=\"nb\">interval</span><span class=\"w\"> </span><span class=\"s1\">'10 min'</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">WHERE</span><span class=\"w\"> </span><span class=\"n\">id</span><span class=\"w\"> </span><span class=\"o\">=</span><span class=\"w\"> </span><span class=\"p\">(</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">SELECT</span><span class=\"w\"> </span><span class=\"n\">id</span><span class=\"w\"> </span><span class=\"k\">FROM</span><span class=\"w\"> </span><span class=\"n\">seats</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">WHERE</span><span class=\"w\"> </span><span class=\"n\">event_id</span><span class=\"w\"> </span><span class=\"o\">=</span><span class=\"w\"> </span><span class=\"p\">:</span><span class=\"n\">event</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">AND</span><span class=\"w\"> </span><span class=\"n\">sold_at</span><span class=\"w\"> </span><span class=\"k\">IS</span><span class=\"w\"> </span><span class=\"k\">NULL</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">AND</span><span class=\"w\"> </span><span class=\"p\">(</span><span class=\"n\">held_until</span><span class=\"w\"> </span><span class=\"k\">IS</span><span class=\"w\"> </span><span class=\"k\">NULL</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">OR</span><span class=\"w\"> </span><span class=\"n\">now</span><span class=\"p\">()</span><span class=\"w\"> </span><span class=\"o\">></span><span class=\"w\"> </span><span class=\"n\">held_until</span><span class=\"p\">)</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">LIMIT</span><span class=\"w\"> </span><span class=\"mi\">1</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">FOR</span><span class=\"w\"> </span><span class=\"k\">UPDATE</span><span class=\"w\"> </span><span class=\"n\">SKIP</span><span class=\"w\"> </span><span class=\"n\">LOCKED</span><span class=\"p\">)</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"n\">RETURNING</span><span class=\"w\"> </span><span class=\"n\">id</span><span class=\"p\">;</span><span class=\"w\">\n</span></span></span></code></pre></div><p>An expired hold is simply available again, so no sweeper is needed for the\nanswer to be right. And <code>now()</code> is the database’s clock, so application servers\nwhose clocks disagree cannot disagree about when a hold ran out.</p>\n<p>For many shops this section is the whole fix. Do it before anything below,\nbecause sharding is a one-way door: easy to walk through, expensive to walk\nback.</p>\n<h2 id=\"claim-first-pay-second\">Claim first, pay second</h2>\n\n<p>A buyer who takes the last unit and then abandons the card form has not bought\nit. So a sale is not one write but a short lifecycle:</p>\n<div class=\"highlight\"><pre tabindex=\"0\" class=\"chroma\"><code class=\"language-text\" data-lang=\"text\"><span class=\"line\"><span class=\"cl\">AVAILABLE → HELD, for minutes → SOLD\n</span></span><span class=\"line\"><span class=\"cl\"> → RELEASED\n</span></span></code></pre></div><ul>\n<li><strong>The hold is keyed by the request, not the attempt.</strong> Every checkout carries\nan idempotency key, and the holds table carries\n<code>UNIQUE (tenant_id, idempotency_key)</code>, so a retried click gets back the hold\nit already has instead of a second unit. One PostgreSQL detail bites here:\n<code>INSERT ... ON CONFLICT DO NOTHING RETURNING id</code> returns nothing when the row\nalready existed, so “no row back” means “look it up”, not “failed”.</li>\n<li><strong>Every transition is conditional.</strong> Confirming a sale moves a hold from\n<code>held</code> to <code>sold</code> only where it is still <code>held</code>, and a release credits the\nstock back only if its own update changed exactly one row. A release that\nruns twice — the timeout and the cancel button, say — gives the unit back\nonce.</li>\n<li><strong>Holds expire in minutes.</strong> Five to ten is usual. Extend the hold once when\npayment starts, so a slow card does not lose the unit it is paying for.</li>\n<li><strong>Cap the units per buyer at the hold,</strong> so one account cannot hold the whole\nstock while it decides.</li>\n</ul>\n<p>Underneath all of it sits one line, the most useful in the design:</p>\n<div class=\"highlight\"><pre tabindex=\"0\" class=\"chroma\"><code class=\"language-text\" data-lang=\"text\"><span class=\"line\"><span class=\"cl\">available + held + sold = total\n</span></span></code></pre></div><p>It holds after every transaction, one query checks it, and when it stops\nholding, something has oversold or leaked a unit — before a customer says so.</p>\n<h2 id=\"when-one-database-is-not-enough\">When one database is not enough</h2>\n\n<p>Sometimes it genuinely is not: many tenants, many sales at once, several\nregions. The structure that scales keeps one rule — every item has exactly one\nowner of its stock, an inventory service, and nothing else decides — and shards\nthat service by <code>(tenant_id, item_id)</code>, so one item’s writes never span two\nshards. The largest tenants get cells of their own, so their sale cannot starve\nanyone else’s. Only the reservation has to be synchronous; everything after it\ncan be a message.</p>\n<p>For the hot item inside a shard there are four tools, and each pulls one of the\nlevers above.</p>\n<p><strong>A gate in front.</strong> A Redis counter, decremented by a Lua script so the check\nand the decrement are one step, admits about as many buyers as there are units\nand turns everyone else away in microseconds. It is not the arbiter. Redis\nreplicates asynchronously, so after a failover the new primary may have\nforgotten some admissions and let a few too many through. The database’s\nconditional <code>UPDATE</code> still decides, and a reconciler resets the gate from the\ndatabase’s number.</p>\n<p><strong>Split the stock.</strong> Ten thousand units can be a hundred buckets of a hundred,\neach its own row or its own shard, with each buyer sent to one: a hundred locks\ninstead of one. The idea is old — O’Neil’s escrow transactions in 1986, then\nBarbará and Garcia-Molina’s demarcation protocol, then the bounded counters of\nBalegas and colleagues — and its one real cost is the tail. A buyer’s bucket\nruns dry while others still have stock, so either try another bucket or move\nthe leftovers, and near the end of a sale collapse back to one. The same idea\ngives each region a quota of its own, so most sales never cross an ocean to\nask.</p>\n<p><strong>One writer per item.</strong> Put every request for an item on one Kafka partition,\nread by one consumer that keeps the count in memory and commits its decisions in\nbatches. One thread and no locks: the LMAX shape. Requests carry an id, and the\nwriter drops one it has already decided, because a queue delivers twice.</p>\n<p><strong>Units as rows.</strong> When units are few and distinct — seats, numbered editions —\ncreate one row each in advance and claim them with <code>SKIP LOCKED</code> as above, or\nkeep the unit ids in a Redis set and <code>SPOP</code> one. There is no counter left to\nfight over.</p>\n<h2 id=\"the-rest-of-checkout\">The rest of checkout</h2>\n\n<p>Payment, the order and the receipt are separate systems, and no transaction\nspans them. Two-phase commit is the textbook answer and the wrong one here: it\nblocks while any participant is slow, it adds round trips to the hottest path,\nand payment providers do not take part in it. A saga does the job, a sequence\nof local steps, each with a step that undoes it:</p>\n<div class=\"highlight\"><pre tabindex=\"0\" class=\"chroma\"><code class=\"language-text\" data-lang=\"text\"><span class=\"line\"><span class=\"cl\">1. reserve stock undo: release the hold\n</span></span><span class=\"line\"><span class=\"cl\">2. pending order undo: cancel the order\n</span></span><span class=\"line\"><span class=\"cl\">3. extend the hold (expires on its own)\n</span></span><span class=\"line\"><span class=\"cl\">4. authorise payment undo: void it\n</span></span><span class=\"line\"><span class=\"cl\">5. confirm HELD → SOLD\n</span></span><span class=\"line\"><span class=\"cl\">6. capture payment\n</span></span></code></pre></div><p>Authorising first and capturing last means a payment that arrives after its\nhold has expired is voided rather than refunded: the money never moved. Each\nstep writes its message to an outbox table in the same transaction as the\nchange it announces, and change data capture publishes it, so nothing is lost\nbetween saved and sent. Every consumer is idempotent, keyed by the order.\nMessages arrive at least once, and idempotency makes that effectively once.</p>\n<p>A distributed lock does not belong anywhere in this. Kleppmann’s point is that a\nlock holder can pause past its lease — a garbage collection, a stalled virtual\nmachine — and keep writing, and the cure is a fencing token the store checks.\nBut a store that can check a token can check <code>available >= :qty</code>, and then it\nnever needed the lock.</p>\n<h2 id=\"pause-the-item-never-oversell-it\">Pause the item, never oversell it</h2>\n\n<p>Consistency and availability are chosen per question, not per system. “Can I\nsell this unit?” has to be consistent: when the owner of an item’s stock cannot\nbe reached, that item stops selling. Everything else — the catalogue, the\n“3 left” badge, the order history — can stay available and be a little stale.</p>\n<p>The quietest oversell comes from a failover. With asynchronous replication a\nprimary acknowledges a sale and dies before its replica has it; the replica is\npromoted, the sale is gone, and the unit is sold again. The fix is to make a\nsale durable on more than one machine before acknowledging it: a synchronous\nstandby in PostgreSQL (<code>synchronous_commit</code> with <code>synchronous_standby_names</code>),\nor a store built on Raft. That costs latency on every commit, which lengthens\nexactly the lock hold time the ceiling divides by, and batching and the gate\nbuy it back. Scope the pause, too: a failed shard should stop the items on it,\nnot the shop.</p>\n<h2 id=\"proving-it\">Proving it</h2>\n\n<p>None of this should be taken on trust, ours included.</p>\n<ul>\n<li><strong>Watch the invariant.</strong> Run <code>available + held + sold = total</code> on a schedule,\nchart it, and page on any drift. Zero oversells is a service level objective\nwith an alert, not a hope.</li>\n<li><strong>Race it on purpose.</strong> Ten thousand concurrent buyers for a hundred units, in\nthe test suite, asserting exactly a hundred sold and no number below zero.</li>\n<li><strong>Break things during the race.</strong> Kill the primary, cut the network, pause a\nprocess — Jepsen-style — and check the invariant afterwards.</li>\n<li><strong>Load-test the rejections.</strong> The sold-out path has to stay fast when it is\n99% of the traffic, and the edge has to absorb it.</li>\n<li><strong>Put the metrics on one dashboard:</strong> lock waits, expired holds, and the\ndistance between the gate’s count and the database’s.</li>\n</ul>\n<h2 id=\"in-what-order\">In what order</h2>\n\n<ol>\n<li>Measure first. <code>pg_stat_statements</code> and the lock waits say whether the hot\nrow is the problem at all.</li>\n<li>Make the write one conditional statement.</li>\n<li>Shed load: a waiting room, a gate, a cached “sold out”.</li>\n<li>Give inventory its own service and its own owner.</li>\n<li>Shard by <code>(tenant_id, item_id)</code>, and only if the first four were not enough.</li>\n<li>Add a hot-item mode, buckets or a single writer, for the sale you know is\ncoming.</li>\n<li>Regions last, with quotas.</li>\n</ol>\n<h2 id=\"what-we-would-tell-the-next-person\">What we would tell the next person</h2>\n\n<ul>\n<li><strong>Sharding answers a different question.</strong> It spreads many rows, and an\noversold sale is one row. Find out which one you have before you build.</li>\n<li><strong>Make the check and the decrement one statement.</strong> Most of the distance\nbetween fine and oversold is the time between reading a number and writing\nit.</li>\n<li><strong>Most of the traffic is the answer no.</strong> Make rejection the cheapest thing\nthe system does.</li>\n<li><strong>Pause before you oversell.</strong> An item that stops selling for thirty seconds\nis an apology. An item sold twice is a refund, a support queue and a customer\nwho remembers.</li>\n<li><strong>Write the invariant down and watch it.</strong> It is one line, and it is the only\nthing that says the design works in production rather than in review.</li>\n</ul>\n<h2 id=\"sources\">Sources</h2>\n\n<ul>\n<li><a href=\"https://www.postgresql.org/docs/current/transaction-iso.html#XACT-READ-COMMITTED\">PostgreSQL: Read Committed isolation</a></li>\n<li><a href=\"https://www.postgresql.org/docs/current/sql-select.html#SQL-FOR-UPDATE-SHARE\">PostgreSQL: <code>FOR UPDATE</code> and <code>SKIP LOCKED</code></a></li>\n<li><a href=\"https://www.postgresql.org/docs/current/warm-standby.html#SYNCHRONOUS-REPLICATION\">PostgreSQL: synchronous replication</a></li>\n<li><a href=\"https://www.vldb.org/pvldb/vol8/p185-bailis.pdf\">Bailis et al., Coordination Avoidance in Database Systems</a></li>\n<li><a href=\"https://arxiv.org/abs/1503.09052\">Balegas et al., Extending Eventually Consistent Cloud Databases for Enforcing Numeric Invariants</a></li>\n<li><a href=\"https://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html\">Martin Kleppmann: How to do distributed locking</a></li>\n<li><a href=\"https://martinfowler.com/articles/lmax.html\">Martin Fowler: The LMAX Architecture</a></li>\n<li><a href=\"https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/bp-partition-key-design.html\">DynamoDB: designing partition keys</a></li>\n<li><a href=\"https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.Partitions.html\">DynamoDB: partitions and data distribution</a></li>\n<li><a href=\"https://microservices.io/patterns/data/saga.html\">microservices.io: Saga</a></li>\n<li><a href=\"https://microservices.io/patterns/data/transactional-outbox.html\">microservices.io: Transactional outbox</a></li>\n<li><a href=\"https://redis.io/docs/latest/operate/oss_and_stack/management/replication/\">Redis: replication</a></li>\n<li><a href=\"https://jepsen.io/\">Jepsen</a></li>\n</ul>\n", "date_published": "2026-10-13T00:00:00Z", "date_modified": "2026-10-19T00:40:12+02:00", "language": "en" }, { "id": "https://engineer.company/notes/safari-underline-font-subset/", "url": "https://engineer.company/notes/safari-underline-font-subset/", "title": "The underline that crossed every letter", "summary": "Safari drew our underlines through every descender. The cause was a Cyrillic font subset on pages that never load it.", "content_html": "<p>Our own site underlines every link, and lets the browser cut a gap where a\nletter’s tail crosses the line — the <code>g</code>, the <code>p</code>, the comma. In Safari it did\nnot. The line ran straight through them. It had been doing so on the English\nand the Danish pages for as long as the site had been\nmultilingual (the bug nobody reports, because it just looks cheap).</p>\n<p>The cause was three font declarations for an alphabet those pages never\ndownload.</p>\n<h2 id=\"what-the-browser-is-supposed-to-do\">What the browser is supposed to do</h2>\n\n<p><code>text-decoration-skip-ink</code> is the property that lifts an underline around a\ndescender. It is on by default, and for years the answer to “my underline looks\nwrong” has been to reach for <code>text-underline-offset</code> and push the line further\ndown. That is the wrong reflex: moving the line changes the design, and the gap\nis what was actually missing.</p>\n<p>We found two real causes before the interesting one, and both are worth knowing.</p>\n<ul>\n<li><strong>A <code>1px</code> underline gets a <code>1px</code>-sized gap.</strong> The break the engine cuts is\nproportional to the thickness of the line doing the cutting. Our stylesheet\npinned the thickness at one pixel, which left about 0.6px of air either side\nof a stem — technically a gap, visually a line straight through the letter.\nSetting <code>text-decoration-thickness: auto</code> scaled both together and took the\ngap from 3.5px to 12.4px without moving the underline a single pixel.</li>\n<li><strong><code>from-font</code> is not the safe default it sounds like.</strong> It reads the\nthickness the typeface itself declares, and Ubuntu declares 0.056em for\nLight, 0.020em for Medium and 0.120em for Bold — a family whose Medium rule\nis under half the weight of its Light one. At large sizes that is a six-pixel\nline beside a two-pixel line on the same page.</li>\n</ul>\n<p>That fixed Chrome. Safari still drew the line through everything.</p>\n<h2 id=\"the-part-that-took-the-longest\">The part that took the longest</h2>\n\n<p>We measured it properly: drive a real Safari, recolour the underline, screenshot\nat eight times scale, and count the pixels where the line stops and starts.</p>\n<table>\n\t<thead>\n\t\t\t<tr>\n\t\t\t\t\t<th></th>\n\t\t\t\t\t<th>painted thickness</th>\n\t\t\t\t\t<th>gap around a descender</th>\n\t\t\t</tr>\n\t</thead>\n\t<tbody>\n\t\t\t<tr>\n\t\t\t\t\t<td>Chrome</td>\n\t\t\t\t\t<td>2.00px</td>\n\t\t\t\t\t<td>5.8 / 12.5 / 6.3px</td>\n\t\t\t</tr>\n\t\t\t<tr>\n\t\t\t\t\t<td>Safari</td>\n\t\t\t\t\t<td>1.38px</td>\n\t\t\t\t\t<td>1.8 / 1.0 / 1.0px</td>\n\t\t\t</tr>\n\t</tbody>\n</table>\n<p>So Safari was cutting a gap. It was just six times narrower than Chrome’s, and\nat reading size a one-pixel gap is not a gap. Worse, the usual lever made it\nworse: at a 3px thickness Safari dropped from three gaps to one, at 4px to\nnone, because it does not scale the gap with the line. It pins it at roughly\n0.07em at every size, where Chrome gives 0.6em.</p>\n<p>We swept about twenty declarations against it — <code>skip-ink: all</code>, both legacy\nspellings of the property, every thickness from half a pixel to four,\n<code>text-underline-position</code>, <code>font-smoothing</code>, <code>text-rendering</code>, <code>paint-order</code>.\nNothing widened it. The honest conclusion at that point was that WebKit simply\ncapped the gap and there was nothing further to do from a stylesheet.</p>\n<p>That conclusion was wrong, and the thing that broke it was\n<a href=\"https://github.com/fontsource/fontsource/issues/1096\">a bug report against a font project</a>:\nSafari ignores skip-ink when a typeface is loaded with a non-Latin subset.</p>\n<h2 id=\"the-actual-cause\">The actual cause</h2>\n\n<p>The site serves Ubuntu in six pieces — three weights of Latin, three of\nCyrillic — so a Ukrainian page is set in the same typeface as an English one\ninstead of falling back to whatever the system offers. Each piece declares the\nrange of characters it covers, and the browser downloads only the pieces a page\nactually needs.</p>\n<p>All six were declared under the one family name, which is the obvious way to\nwrite it. And that is the trigger, recorded as\n<a href=\"https://bugs.webkit.org/show_bug.cgi?id=255159\">WebKit bug 255159</a>: <strong>when one\nface in a family declares a non-Latin character range, Safari degrades skip-ink\nfor every character in that family</strong> — including Latin text on a page that\nnever fetches the other file.</p>\n<p>An English page was rendering its underlines badly because a Ukrainian font\ndeclaration existed somewhere in the stylesheet. Deleting those three lines at\nruntime, and changing nothing else, took the gaps from 1.8/1.0/1.0px to\n4.5/11.0/5.0px.</p>\n<h2 id=\"the-fix\">The fix</h2>\n\n<p>Give the second script its own family name, and name it in the font stack after\nthe first:</p>\n<div class=\"highlight\"><pre tabindex=\"0\" class=\"chroma\"><code class=\"language-css\" data-lang=\"css\"><span class=\"line\"><span class=\"cl\"><span class=\"p\">@</span><span class=\"k\">font-face</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nt\">font-family</span><span class=\"o\">:</span> <span class=\"nt\">UbuntuCyrillic</span><span class=\"o\">;</span> <span class=\"c\">/* was: Ubuntu */</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nt\">src</span><span class=\"o\">:</span> <span class=\"nt\">url</span><span class=\"o\">(</span><span class=\"nt\">ubuntu_300_cyrillic</span><span class=\"p\">.</span><span class=\"nc\">woff2</span><span class=\"o\">)</span> <span class=\"nt\">format</span><span class=\"o\">(</span><span class=\"s1\">'woff2'</span><span class=\"o\">);</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nt\">unicode-range</span><span class=\"o\">:</span> <span class=\"nt\">U</span><span class=\"o\">+</span><span class=\"nt\">0400-045F</span><span class=\"o\">,</span> <span class=\"nt\">U</span><span class=\"o\">+</span><span class=\"nt\">0490-0491</span><span class=\"o\">;</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">}</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">:</span><span class=\"nd\">root</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nv\">--body-font</span><span class=\"p\">:</span> <span class=\"n\">Ubuntu</span><span class=\"p\">,</span> <span class=\"n\">UbuntuCyrillic</span><span class=\"p\">,</span> <span class=\"n\">system-ui</span><span class=\"p\">,</span> <span class=\"kc\">sans-serif</span><span class=\"p\">;</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">}</span>\n</span></span></code></pre></div><p>A Latin letter is found in the first family and never reaches the second. A\nCyrillic one falls past the first, which has no such glyph, and lands in the\nsecond rather than in a system face. The character ranges still decide what\ngets downloaded, so nothing about the loading changes: English and Danish pages\nfetch three Latin files and no Cyrillic ones, Ukrainian pages the reverse.</p>\n<p>Three renamed declarations and one stack entry. Safari’s gaps went to\n4.5/11.0/5.0px, Chrome was untouched, the underline did not move, and Ukrainian\nstill sets in Ubuntu at identical widths.</p>\n<h2 id=\"what-we-would-tell-the-next-person\">What we would tell the next person</h2>\n\n<ul>\n<li><strong>A skip-ink measurement is only true of the engine that produced it.</strong> We\nhad a table of numbers, a correct diagnosis and a real fix, and it was all\nChromium. The screenshot that reopened the case came from a person looking at\nthe page.</li>\n<li><strong>Reach for the thickness before the offset.</strong> Widening the gap keeps the\ndesign; moving the line down replaces it.</li>\n<li><strong>Splitting a typeface by script is right, but the split belongs in the\nfamily name too.</strong> Sharing one name across scripts reads better and costs you\nskip-ink in Safari. Merging them back looks like tidying and is a regression.</li>\n<li><strong>The loading was never the problem.</strong> Every instinct says a page rendering\nbadly because of a Cyrillic font must be downloading something it should not.\nIt was not. The declaration alone was enough.</li>\n</ul>\n<h2 id=\"sources\">Sources</h2>\n\n<ul>\n<li><a href=\"https://bugs.webkit.org/show_bug.cgi?id=255159\">WebKit bug 255159 — <code>text-decoration-skip-ink</code> and font subsets</a></li>\n<li><a href=\"https://github.com/fontsource/fontsource/issues/1096\">Fontsource issue 1096, which named the trigger</a></li>\n<li><a href=\"https://developer.mozilla.org/en-US/docs/Web/CSS/text-decoration-skip-ink\">MDN: <code>text-decoration-skip-ink</code></a></li>\n<li><a href=\"https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/unicode-range\">MDN: <code>unicode-range</code></a></li>\n</ul>\n", "date_published": "2026-09-28T00:00:00Z", "date_modified": "2026-10-20T11:21:41+02:00", "language": "en" }, { "id": "https://engineer.company/notes/solana-mobile-wallet-deeplinks/", "url": "https://engineer.company/notes/solana-mobile-wallet-deeplinks/", "title": "Opening a dApp inside Solana mobile wallets", "summary": "Why Phantom, Solflare and Backpack browse deep links fail on mobile, and the exact formats, trigger rules and fixes that make them work.", "content_html": "<p>A React dApp built on <code>@solana/wallet-adapter-react</code> connects desktop wallets\nwithout trouble, but on a phone the same flow falls apart: the wallet has to\nopen the dApp inside its own in-app browser, and the deep links that should\nmake that happen quietly do not. Backpack lands on a “download the app” page;\nSolflare opens the app but never the site; every variant seems to fail. We took\nthe problem apart, and it turned out to be four separate problems wearing one\nsymptom.</p>\n<h2 id=\"the-four-problems\">The four problems</h2>\n\n<ul>\n<li><strong>The Backpack link was malformed.</strong> The only documented format is\n<code>https://backpack.app/ul/v1/browse/<url>?ref=<ref></code> — a universal link with\nthe target URL in the path and a required <code>ref</code>. A custom-scheme guess like\n<code>backpack://ul/v1/browse?url=...</code> matches no route the app registers, so the\nuser ends on the wallet’s install page.</li>\n<li><strong>Solflare needs its universal link too:</strong>\n<code>https://solflare.com/ul/v1/browse/<url>?ref=<ref></code>, not the bare\n<code>solflare://</code> scheme. A bare scheme can launch the app without routing it —\nwhich is exactly “the app opens, but the site tab has to be opened by hand”.</li>\n<li><strong>Both parameters must be encoded.</strong> <code>url</code> is the full absolute dApp address\nand <code>ref</code> is the requesting origin, each passed through <code>encodeURIComponent</code>.\nAn unencoded <code>?</code> or <code>&</code> in the target corrupts the parse, and the wallet\nopens on its home screen instead of the browser tab.</li>\n<li><strong>The trigger matters as much as the link.</strong> Universal links only switch apps\non a navigation the operating system trusts — and they deliberately do\nnothing when pasted into the address bar, which is also how a perfectly\ncorrect link “fails” during testing.</li>\n</ul>\n<h2 id=\"the-documented-formats\">The documented formats</h2>\n\n<ul>\n<li>Phantom: <code>https://phantom.app/ul/browse/<url>?ref=<ref></code> — no <code>/v1</code> in this\none.</li>\n<li>Solflare: <code>https://solflare.com/ul/v1/browse/<url>?ref=<ref></code></li>\n<li>Backpack: <code>https://backpack.app/ul/v1/browse/<url>?ref=<ref></code></li>\n</ul>\n<p>One pattern serves all three:</p>\n<pre tabindex=\"0\"><code>const WALLET_BROWSE = {\n phantom: (url, ref) =>\n `https://phantom.app/ul/browse/${url}?ref=${ref}`,\n solflare: (url, ref) =>\n `https://solflare.com/ul/v1/browse/${url}?ref=${ref}`,\n backpack: (url, ref) =>\n `https://backpack.app/ul/v1/browse/${url}?ref=${ref}`,\n};\n\nfunction walletBrowseLink(\n walletName,\n targetUrl = window.location.href,\n) {\n const build = WALLET_BROWSE[walletName.toLowerCase()];\n if (!build) return null;\n return build(\n encodeURIComponent(targetUrl),\n encodeURIComponent(window.location.origin),\n );\n}\n</code></pre><h2 id=\"triggering-the-link-so-ios-and-android-accept-it\">Triggering the link so iOS and Android accept it</h2>\n\n<ul>\n<li><strong>Render a real anchor, precomputed.</strong> A plain\n<code><a href={walletBrowseLink('phantom')}></code> is the most reliable trigger on both\nplatforms.</li>\n<li><strong>If it must be programmatic</strong>, assign <code>window.location.href</code> synchronously\ninside the tap handler — no <code>await</code>, no <code>fetch</code>, no <code>setTimeout</code> first. After\nasynchronous work the gesture context is gone, and iOS falls back to the\nwallet’s website. Never <code>window.open</code>.</li>\n<li><strong>Never test by pasting into the address bar.</strong> Universal links deliberately\ndo not fire there; test with a tapped link or a QR code scanned by the\ncamera.</li>\n<li><strong>Mind the messenger webviews.</strong> Opened inside Telegram’s or Instagram’s\nin-app browser, universal links are frequently swallowed and the wallet’s\nplain website loads instead. User-agent detection is heuristic at best, so\nalso give users a visible escape hatch: “open in Safari or Chrome, then\nconnect”.</li>\n</ul>\n<h2 id=\"the-bigger-fix-on-android\">The bigger fix on Android</h2>\n\n<p>Hand-rolled deep links are the iOS story. On Android, Solana Mobile’s Mobile\nWallet Adapter lets a dApp running in the mobile browser connect straight to\nthe installed wallet app, with no in-app-browser detour at all. Recent versions\nof <code>@solana/wallet-adapter-react</code> register the mobile adapter automatically, so\nupgrading the wallet-adapter packages can fix Android by itself. The target\narchitecture: Mobile Wallet Adapter on Android, browse universal links on iOS,\nwhere Apple allows no equivalent.</p>\n<h2 id=\"verifying-on-a-device\">Verifying on a device</h2>\n\n<ol>\n<li>Real device, wallet installed, link opened from the system browser — not\nfrom a messenger.</li>\n<li>Tap a rendered link or scan a QR code; never paste into the address bar.</li>\n<li>Confirm the wallet opens and the dApp loads in its in-app browser tab — the\nsecond half is the part that fails.</li>\n<li>Repeat without the wallet installed: the universal link should degrade to\nthe wallet’s website. That page appearing while the app is installed means\nthe link or the trigger is still wrong.</li>\n<li>Then test the messenger path, and add the “open in browser” hint if it\nfails there.</li>\n</ol>\n<h2 id=\"sources\">Sources</h2>\n\n<ul>\n<li><a href=\"https://docs.phantom.com/phantom-deeplinks/deeplinks-ios-and-android\">Phantom: deep links on iOS and Android</a></li>\n<li><a href=\"https://docs.solflare.com/solflare/technical/deeplinks/other-methods/browse\">Solflare: the Browse deep link</a></li>\n<li><a href=\"https://docs.backpack.app/deeplinks/other-methods/browse\">Backpack: the Browse deep link</a></li>\n<li><a href=\"https://docs.solanamobile.com/mobile-wallet-adapter/mobile-apps\">Solana Mobile: Mobile Wallet Adapter</a></li>\n</ul>\n", "date_published": "2026-08-10T00:00:00Z", "date_modified": "2026-10-19T00:40:12+02:00", "language": "en" } ] }