{"version":"https://jsonfeed.org/version/1.1","title":"Engineer — Advisory — Portfolio and notes","home_page_url":"https://advisory.engineer.company/","feed_url":"https://advisory.engineer.company/feed.json","description":"Engineer ApS — software development \u0026 IT consulting in Copenhagen, Denmark. A portfolio of proven engineering achievements across software, data, cloud and IT.","language":"en","icon":"https://advisory.engineer.company/assets/images/brand/card.webp","favicon":"https://advisory.engineer.company/assets/icons/apple/apple-touch-icon.png","authors":[{"name":"Engineer ApS","url":"https://advisory.engineer.company/"}],"items":[{"id":"https://advisory.engineer.company/portfolio/increased-brand-popularity-by-200x-through-successful-brand-1/","url":"https://advisory.engineer.company/portfolio/increased-brand-popularity-by-200x-through-successful-brand-1/","title":"Increased brand popularity by 200x through successful brand development across offline and online platforms.","summary":"Grew brand reach roughly 200x across offline and online platforms, turning an unknown newcomer into a recognised name.","content_html":"\u003cp\u003e\u003cstrong\u003eSituation.\u003c/strong\u003e Engineer ApS was a brand‑new consultancy with real technical depth and almost nobody who\u0026rsquo;d heard of it. That\u0026rsquo;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\u0026rsquo;d want it don\u0026rsquo;t know you exist. A young firm has to be seen before it can win anything.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eTask.\u003c/strong\u003e The task was to build a brand people would actually recognise — across both the offline and online sides — and to turn the firm\u0026rsquo;s engineering credibility into visible market presence, rather than leaving it as a well‑kept secret.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eAction.\u003c/strong\u003e 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 \u0026ldquo;we deliver value\u0026rdquo; 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.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eResult.\u003c/strong\u003e Brand reach grew around 200‑fold across the offline and online platforms — an unknown newcomer turned into something people actually recognised. And it wasn\u0026rsquo;t vanity reach; the visibility started producing a steady flow of inbound conversations and opportunities that simply hadn\u0026rsquo;t been there before.\u003c/p\u003e\n","date_published":"2026-09-13T01:45:50+02:00","date_modified":"2026-09-13T01:45:50+02:00","language":"en","tags":["Brand \u0026 Marketing","Product \u0026 Requirements","Stakeholder \u0026 Reporting","Brand, Marketing \u0026 SEO","Product Strategy \u0026 Requirements"]},{"id":"https://advisory.engineer.company/portfolio/drove-brand-engagement-and-loyalty-by-100-through-2/","url":"https://advisory.engineer.company/portfolio/drove-brand-engagement-and-loyalty-by-100-through-2/","title":"Drove brand engagement and loyalty by 100% through market trend analysis and consumer behavior insights.","summary":"Doubled brand engagement and loyalty (+100%) using market-trend analysis and consumer-behaviour insight to win a returning audience.","content_html":"\u003cp\u003e\u003cstrong\u003eSituation.\u003c/strong\u003e As the visibility climbed, Engineer ApS was reaching more people — but the engagement was thin. People noticed and moved on; the early interest wasn\u0026rsquo;t turning into relationships that lasted. Reach without engagement is just noise, and the brand was making noise more than connections.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eTask.\u003c/strong\u003e 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.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eAction.\u003c/strong\u003e 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\u0026rsquo;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.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eResult.\u003c/strong\u003e 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.\u003c/p\u003e\n","date_published":"2026-09-13T01:45:50+02:00","date_modified":"2026-09-13T01:45:50+02:00","language":"en","tags":["Brand \u0026 Marketing","Data Analytics","Product \u0026 Requirements","Stakeholder \u0026 Reporting","Brand, Marketing \u0026 SEO","Data Analytics \u0026 BI Dashboards","Product Strategy \u0026 Requirements"]},{"id":"https://advisory.engineer.company/portfolio/designed-a-comprehensive-infrastructure-framework-for-dtu-impacting-3/","url":"https://advisory.engineer.company/portfolio/designed-a-comprehensive-infrastructure-framework-for-dtu-impacting-3/","title":"Designed a comprehensive infrastructure framework for DTU impacting 14 departments, featuring flexible modules, unified data pipelines, and structured support strategies for long‑term adoption.","summary":"Designed a comprehensive infrastructure framework for DTU spanning 14 departments — modular, with unified data pipelines and support strategies.","content_html":"\u003cp\u003e\u003cstrong\u003eSituation.\u003c/strong\u003e 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.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eTask.\u003c/strong\u003e 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.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eAction.\u003c/strong\u003e 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.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eResult.\u003c/strong\u003e 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\u0026rsquo;s data infrastructure transformation.\u003c/p\u003e\n","date_published":"2026-09-13T01:45:50+02:00","date_modified":"2026-09-13T01:45:50+02:00","language":"en","tags":["Data Engineering","Data Governance","Data Pipelines (ETL/ELT)","Documentation","Infrastructure","Platform Architecture","Solution Architecture","Stakeholder \u0026 Reporting","Technical Leadership","Data Governance \u0026 Quality","Data Pipeline Development (ETL/ELT)","Platform \u0026 Solution Architecture","Technical Documentation","Technical Leadership \u0026 Consulting"]},{"id":"https://advisory.engineer.company/portfolio/delivered-8-power-bi-projects-with-comprehensive-manuals-10/","url":"https://advisory.engineer.company/portfolio/delivered-8-power-bi-projects-with-comprehensive-manuals-10/","title":"Delivered 8 Power BI projects with comprehensive manuals, integrating Microsoft Power BI tools with NodeJS API and Python FastAPI for effective data analytics and visualization.","summary":"Delivered 8 Power BI analytics projects (Node.js API, Python FastAPI) with full manuals, improving report generation efficiency by 60%.","content_html":"\u003cp\u003e\u003cstrong\u003eSituation.\u003c/strong\u003e 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.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eTask.\u003c/strong\u003e 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.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eAction.\u003c/strong\u003e 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.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eResult.\u003c/strong\u003e 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.\u003c/p\u003e\n","date_published":"2026-09-13T01:45:50+02:00","date_modified":"2026-09-13T01:45:50+02:00","language":"en","tags":["APIs \u0026 Integration","Backend Engineering","Data Analytics","Data Engineering","Documentation","GIS / Geospatial","Product \u0026 Requirements","Python","Backend \u0026 API Development","Data Analytics \u0026 BI Dashboards","Technical Documentation"]},{"id":"https://advisory.engineer.company/portfolio/led-the-software-development-of-a-gis-map-24/","url":"https://advisory.engineer.company/portfolio/led-the-software-development-of-a-gis-map-24/","title":"Led the software development of a GIS map application, driving revenue growth by 10x and positioning the product as a primary data asset.","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":"\u003cp\u003e\u003cstrong\u003eSituation.\u003c/strong\u003e 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.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eTask.\u003c/strong\u003e 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.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eAction.\u003c/strong\u003e 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 pipelines, 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.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eResult.\u003c/strong\u003e 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.\u003c/p\u003e\n","date_published":"2026-09-13T01:45:50+02:00","date_modified":"2026-09-13T01:45:50+02:00","language":"en","tags":["DevOps","Full‑Stack Development","GIS / Geospatial","Mentoring \u0026 Coaching","Platform Architecture","Product \u0026 Requirements","Reliability \u0026 Backups","Solution Architecture","Team Leadership","Technical Leadership","Full‑Stack Product Development","GIS \u0026 Geospatial Solutions","Platform \u0026 Solution Architecture","Product Strategy \u0026 Requirements","Technical Leadership \u0026 Consulting"]},{"id":"https://advisory.engineer.company/portfolio/directed-full-stack-gis-map-development-overseeing-postgresql-25/","url":"https://advisory.engineer.company/portfolio/directed-full-stack-gis-map-development-overseeing-postgresql-25/","title":"Directed full‑stack GIS map development, overseeing PostgreSQL, Mapbox, ReactJS, and NodeJS to deliver an integrated solution.","summary":"Directed full-stack GIS map development (PostgreSQL, Mapbox, React, Node.js), delivering one integrated, extensible core of the platform.","content_html":"\u003cp\u003e\u003cstrong\u003eSituation.\u003c/strong\u003e 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\u0026rsquo;t been built to fit together, and the seams were starting to show.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eTask.\u003c/strong\u003e 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.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eAction.\u003c/strong\u003e 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\u0026rsquo;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.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eResult.\u003c/strong\u003e 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.\u003c/p\u003e\n","date_published":"2026-09-13T01:45:50+02:00","date_modified":"2026-09-13T01:45:50+02:00","language":"en","tags":["APIs \u0026 Integration","Backend Engineering","Databases","Frontend Engineering","Full‑Stack Development","GIS / Geospatial","Performance Tuning","Platform Architecture","PostgreSQL","Technical Leadership","Backend \u0026 API Development","Frontend Development","Full‑Stack Product Development","GIS \u0026 Geospatial Solutions","Technical Leadership \u0026 Consulting"]},{"id":"https://advisory.engineer.company/portfolio/saved-4-000-hours-by-mentoring-and-growing-26/","url":"https://advisory.engineer.company/portfolio/saved-4-000-hours-by-mentoring-and-growing-26/","title":"Saved 4,000 hours by mentoring and growing a team from 2 to 18 members, optimizing workflows and fostering cross‑departmental collaboration.","summary":"Saved ~4,000 hours by growing and mentoring a team from 2 to 18, redesigning workflows and building durable engineering capability.","content_html":"\u003cp\u003e\u003cstrong\u003eSituation.\u003c/strong\u003e A team of two couldn\u0026rsquo;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\u0026rsquo;s heads, no real conventions — wasn\u0026rsquo;t going to survive being scaled up. Adding bodies to a team that loose usually just makes the chaos bigger.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eTask.\u003c/strong\u003e 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.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eAction.\u003c/strong\u003e 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.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eResult.\u003c/strong\u003e 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\u0026rsquo;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.\u003c/p\u003e\n","date_published":"2026-09-13T01:45:50+02:00","date_modified":"2026-09-13T01:45:50+02:00","language":"en","tags":["Agile \u0026 Scrum","Mentoring \u0026 Coaching","Project Management","Team Leadership","Technical Leadership","Project Management (Agile)","Team Building \u0026 Mentoring","Technical Leadership \u0026 Consulting"]},{"id":"https://advisory.engineer.company/portfolio/optimized-forecasting-and-investment-strategies-for-11-electricity-27/","url":"https://advisory.engineer.company/portfolio/optimized-forecasting-and-investment-strategies-for-11-electricity-27/","title":"Optimized forecasting and investment strategies for 11 electricity grid operators, driving operational efficiency through data‑driven GIS solutions.","summary":"Optimised forecasting and investment strategy for 11 electricity grid operators with data-driven GIS — trustworthy, targeted planning.","content_html":"\u003cp\u003e\u003cstrong\u003eSituation.\u003c/strong\u003e 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\u0026rsquo;t have was a way to see it on the map, where the patterns actually live.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eTask.\u003c/strong\u003e 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.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eAction.\u003c/strong\u003e 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.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eResult.\u003c/strong\u003e 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.\u003c/p\u003e\n","date_published":"2026-09-13T01:45:50+02:00","date_modified":"2026-09-13T01:45:50+02:00","language":"en","tags":["Data Analytics","Data Engineering","GIS / Geospatial","Product \u0026 Requirements","Stakeholder \u0026 Reporting","Data Analytics \u0026 BI Dashboards","GIS \u0026 Geospatial Solutions","Product Strategy \u0026 Requirements"]},{"id":"https://advisory.engineer.company/portfolio/presented-200-ui-ux-improvements-for-the-gis-28/","url":"https://advisory.engineer.company/portfolio/presented-200-ui-ux-improvements-for-the-gis-28/","title":"Presented 200 UI/UX improvements for the GIS map application, boosting revenue by 10x through enhanced software features.","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":"\u003cp\u003e\u003cstrong\u003eSituation.\u003c/strong\u003e 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.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eTask.\u003c/strong\u003e 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.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eAction.\u003c/strong\u003e 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\u0026rsquo;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.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eResult.\u003c/strong\u003e The interface got noticeably better to use, and the product\u0026rsquo;s value followed: the work contributed to a tenfold rise in revenue. It\u0026rsquo;s a case worth coming back to, because it makes the point cleanly — careful UX isn\u0026rsquo;t cosmetic, it shows up on the invoice.\u003c/p\u003e\n","date_published":"2026-09-13T01:45:50+02:00","date_modified":"2026-09-13T01:45:50+02:00","language":"en","tags":["Data Analytics","Design Systems \u0026 UI","Frontend Engineering","Product \u0026 Requirements","Stakeholder \u0026 Reporting","UX / UI Design","Frontend Development","Product Strategy \u0026 Requirements","UI/UX Design \u0026 Design Systems"]},{"id":"https://advisory.engineer.company/portfolio/designed-and-managed-5-000-hours-of-map-29/","url":"https://advisory.engineer.company/portfolio/designed-and-managed-5-000-hours-of-map-29/","title":"Designed and managed 5,000 hours of map development, utilizing Agile methodologies such as Scrum and Jira for effective project management.","summary":"Planned and ran 5,000 hours of map development with Scrum and Jira, keeping work visible and aligned to the roadmap.","content_html":"\u003cp\u003e\u003cstrong\u003eSituation.\u003c/strong\u003e The map wasn\u0026rsquo;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.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eTask.\u003c/strong\u003e 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.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eAction.\u003c/strong\u003e 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.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eResult.\u003c/strong\u003e The 5,000 hours landed in a controlled, visible way instead of disappearing into a black box. The project management did what it\u0026rsquo;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.\u003c/p\u003e\n","date_published":"2026-09-13T01:45:50+02:00","date_modified":"2026-09-13T01:45:50+02:00","language":"en","tags":["Agile \u0026 Scrum","Product \u0026 Requirements","Project Management","Stakeholder \u0026 Reporting","Team Leadership","Product Strategy \u0026 Requirements","Project Management (Agile)","Team Building \u0026 Mentoring"]},{"id":"https://advisory.engineer.company/portfolio/wrote-50-000-words-of-comprehensive-software-documentation-30/","url":"https://advisory.engineer.company/portfolio/wrote-50-000-words-of-comprehensive-software-documentation-30/","title":"Wrote 50,000 words of comprehensive software documentation utilizing Markdown in GitHub, Craft, and Confluence, thereby ensuring the retention of knowledge and the transparency of the process.","summary":"Wrote 50,000 words of software documentation in GitHub, Craft and Confluence, making knowledge durable, transparent and auditable.","content_html":"\u003cp\u003e\u003cstrong\u003eSituation.\u003c/strong\u003e Most of what mattered about the product and how the team worked lived in people\u0026rsquo;s heads. That\u0026rsquo;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.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eTask.\u003c/strong\u003e 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.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eAction.\u003c/strong\u003e 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.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eResult.\u003c/strong\u003e 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.\u003c/p\u003e\n","date_published":"2026-09-13T01:45:50+02:00","date_modified":"2026-09-13T01:45:50+02:00","language":"en","tags":["Documentation","Mentoring \u0026 Coaching","Team Leadership","Technical Leadership","Technical Documentation","Technical Leadership \u0026 Consulting"]},{"id":"https://advisory.engineer.company/portfolio/gathered-and-analyzed-business-requirements-to-translate-into-31/","url":"https://advisory.engineer.company/portfolio/gathered-and-analyzed-business-requirements-to-translate-into-31/","title":"Gathered and analyzed business requirements to translate into actionable features and user stories aligned with data governance standards.","summary":"Turned business requirements into clear features and user stories aligned with data-governance standards, cutting ambiguity and rework.","content_html":"\u003cp\u003e\u003cstrong\u003eSituation.\u003c/strong\u003e 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.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eTask.\u003c/strong\u003e 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.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eAction.\u003c/strong\u003e 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 \u0026ldquo;done\u0026rdquo; meant. Each one was checked against the governance rules, because a feature that\u0026rsquo;s useful but mishandles data isn\u0026rsquo;t really finished. The whole aim was that someone could read a story and build the right thing the first time.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eResult.\u003c/strong\u003e 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.\u003c/p\u003e\n","date_published":"2026-09-13T01:45:50+02:00","date_modified":"2026-09-13T01:45:50+02:00","language":"en","tags":["Agile \u0026 Scrum","Data Governance","Documentation","Product \u0026 Requirements","Project Management","Stakeholder \u0026 Reporting","Data Governance \u0026 Quality","Product Strategy \u0026 Requirements","Project Management (Agile)"]},{"id":"https://advisory.engineer.company/portfolio/managed-the-execution-of-30-successful-gis-projects-32/","url":"https://advisory.engineer.company/portfolio/managed-the-execution-of-30-successful-gis-projects-32/","title":"Managed the execution of 30 successful GIS projects, demonstrating leadership in delivering innovative solutions across the field.","summary":"Delivered 30 successful GIS projects — a track record of consistent, innovative delivery that earned lasting client trust.","content_html":"\u003cp\u003e\u003cstrong\u003eSituation.\u003c/strong\u003e Mappitall made its money delivering GIS projects, and there were a lot of them running at once. The company\u0026rsquo;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.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eTask.\u003c/strong\u003e 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.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eAction.\u003c/strong\u003e 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\u0026rsquo;s expectations honest about what was coming and when.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eResult.\u003c/strong\u003e All thirty came in. That consistency mattered more than any single project would have — a client who\u0026rsquo;s seen you deliver thirty times over doesn\u0026rsquo;t worry about the thirty‑first, and that reputation is a good part of why the company kept growing.\u003c/p\u003e\n","date_published":"2026-09-13T01:45:50+02:00","date_modified":"2026-09-13T01:45:50+02:00","language":"en","tags":["Agile \u0026 Scrum","GIS / Geospatial","Project Management","Stakeholder \u0026 Reporting","Team Leadership","GIS \u0026 Geospatial Solutions","Project Management (Agile)","Team Building \u0026 Mentoring"]},{"id":"https://advisory.engineer.company/portfolio/led-company-growth-from-4-to-14-employees-33/","url":"https://advisory.engineer.company/portfolio/led-company-growth-from-4-to-14-employees-33/","title":"Led company growth from 4 to 14 employees by employing Agile methodologies and effective project management practices.","summary":"Led company growth from 4 to 14 employees with Agile practices, scaling the team without losing quality or cohesion.","content_html":"\u003cp\u003e\u003cstrong\u003eSituation.\u003c/strong\u003e Mappitall was ready to grow, and there\u0026rsquo;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\u0026rsquo;t.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eTask.\u003c/strong\u003e Leading that growth was the job — bringing people in while keeping delivery disciplined and the team pulling in the same direction.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eAction.\u003c/strong\u003e 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\u0026rsquo;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.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eResult.\u003c/strong\u003e The team got to fourteen without the quality dropping or splintering into people who didn\u0026rsquo;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.\u003c/p\u003e\n","date_published":"2026-09-13T01:45:50+02:00","date_modified":"2026-09-13T01:45:50+02:00","language":"en","tags":["Agile \u0026 Scrum","Mentoring \u0026 Coaching","Project Management","Team Leadership","Technical Leadership","Project Management (Agile)","Team Building \u0026 Mentoring","Technical Leadership \u0026 Consulting"]},{"id":"https://advisory.engineer.company/portfolio/improved-team-communication-and-collaboration-by-implementing-slack-34/","url":"https://advisory.engineer.company/portfolio/improved-team-communication-and-collaboration-by-implementing-slack-34/","title":"Improved team communication and collaboration by implementing Slack, Mattermost, 1Password, and Jira, saving 8,000 hours of labor.","summary":"Saved ~8,000 hours by rolling out Slack, Mattermost, 1Password and Jira, turning chased information and redone work into real delivery.","content_html":"\u003cp\u003e\u003cstrong\u003eSituation.\u003c/strong\u003e As the team got bigger, the communication and the tooling hadn\u0026rsquo;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.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eTask.\u003c/strong\u003e The aim was to fix how the team communicated and worked together, and claw back the time being quietly bled to all that friction.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eAction.\u003c/strong\u003e 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\u0026rsquo;t just become another place to check. Slack and Mattermost for communication, 1Password so shared secrets weren\u0026rsquo;t being passed around in ways nobody could track, Jira so work was tracked in one place instead of living in people\u0026rsquo;s heads and inboxes. The tools were the easy bit; getting everyone to actually use them the same way was the real work.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eResult.\u003c/strong\u003e 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.\u003c/p\u003e\n","date_published":"2026-09-13T01:45:50+02:00","date_modified":"2026-09-13T01:45:50+02:00","language":"en","tags":["Agile \u0026 Scrum","Automation \u0026 CI/CD","Documentation","Project Management","Security","Team Leadership","Project Management (Agile)","Security \u0026 Access Management","Team Building \u0026 Mentoring"]},{"id":"https://advisory.engineer.company/portfolio/led-the-development-deployment-and-support-of-over-35/","url":"https://advisory.engineer.company/portfolio/led-the-development-deployment-and-support-of-over-35/","title":"Led the development, deployment, and support of over 30 GIS projects, demonstrating expertise in PostgreSQL, Bash, Python, JavaScript, GDAL, ArcGIS, PostGIS, and Mapbox technologies.","summary":"Led development, deployment and support of 30+ GIS projects with PostgreSQL, Python, GDAL, ArcGIS, PostGIS and Mapbox across the lifecycle.","content_html":"\u003cp\u003e\u003cstrong\u003eSituation.\u003c/strong\u003e The company\u0026rsquo;s whole output was made‑to‑measure GIS — custom mapping and spatial systems built to a client\u0026rsquo;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\u0026rsquo;t really been delivered.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eTask.\u003c/strong\u003e 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.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eAction.\u003c/strong\u003e 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\u0026rsquo;t handed off; the decisions had to be lived with.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eResult.\u003c/strong\u003e 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\u0026rsquo;re the one getting the call if it breaks.\u003c/p\u003e\n","date_published":"2026-09-13T01:45:50+02:00","date_modified":"2026-09-13T01:45:50+02:00","language":"en","tags":["Backend Engineering","Data Engineering","Databases","Full‑Stack Development","GIS / Geospatial","PostgreSQL","Python","Reliability \u0026 Backups","SQL","Team Leadership","Technical Leadership","Web Development","Backend \u0026 API Development","Data Pipeline Development (ETL/ELT)","Full‑Stack Product Development","GIS \u0026 Geospatial Solutions","Technical Leadership \u0026 Consulting"]},{"id":"https://advisory.engineer.company/portfolio/guided-the-execution-of-numerous-company-projects-offering-37/","url":"https://advisory.engineer.company/portfolio/guided-the-execution-of-numerous-company-projects-offering-37/","title":"Guided the execution of numerous company projects, offering expert support in the software development phases.","summary":"Guided execution of many company projects with expert support through the hardest software-development phases, steadying delivery.","content_html":"\u003cp\u003e\u003cstrong\u003eSituation.\u003c/strong\u003e 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.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eTask.\u003c/strong\u003e Guiding that execution was the job — being the technical support the teams could lean on through the development phases.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eAction.\u003c/strong\u003e The work stayed hands‑on across a lot of the company\u0026rsquo;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.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eResult.\u003c/strong\u003e 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\u0026rsquo;s portfolio.\u003c/p\u003e\n","date_published":"2026-09-13T01:45:50+02:00","date_modified":"2026-09-13T01:45:50+02:00","language":"en","tags":["Mentoring \u0026 Coaching","Project Management","Team Leadership","Technical Leadership","Project Management (Agile)","Team Building \u0026 Mentoring","Technical Leadership \u0026 Consulting"]},{"id":"https://advisory.engineer.company/portfolio/overhauled-internal-processes-saving-8-000-hours-by-38/","url":"https://advisory.engineer.company/portfolio/overhauled-internal-processes-saving-8-000-hours-by-38/","title":"Overhauled internal processes, saving 8,000 hours by improving software architecture, systems, and scheduling efficiency.","summary":"Overhauled internal processes to save ~8,000 hours, improving software architecture, systems and scheduling efficiency.","content_html":"\u003cp\u003e\u003cstrong\u003eSituation.\u003c/strong\u003e 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.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eTask.\u003c/strong\u003e The aim was to overhaul those processes — to go find the waste and take it out rather than keep paying for it.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eAction.\u003c/strong\u003e 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\u0026rsquo;t made permanent just decay back to the old way.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eResult.\u003c/strong\u003e The overhaul saved roughly 8,000 hours by making the architecture, systems and scheduling meaningfully more efficient. That\u0026rsquo;s capacity that went straight back into higher‑value work instead of into overhead nobody had thought to question.\u003c/p\u003e\n","date_published":"2026-09-13T01:45:50+02:00","date_modified":"2026-09-13T01:45:50+02:00","language":"en","tags":["Automation \u0026 CI/CD","Infrastructure","Performance Tuning","Platform Architecture","Project Management","Solution Architecture","Technical Leadership","DevOps \u0026 CI/CD Automation","Platform \u0026 Solution Architecture","Project Management (Agile)","Technical Leadership \u0026 Consulting"]},{"id":"https://advisory.engineer.company/portfolio/established-a-technical-support-department-servicing-over-10-40/","url":"https://advisory.engineer.company/portfolio/established-a-technical-support-department-servicing-over-10-40/","title":"Established a Technical Support department, servicing over 10,000 clients with IT support and troubleshooting solutions.","summary":"Established a Technical Support department serving 10,000+ clients, turning support from reactive fixes into a scalable company strength.","content_html":"\u003cp\u003e\u003cstrong\u003eSituation.\u003c/strong\u003e The client base was growing and needed dependable technical support, and there just wasn\u0026rsquo;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.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eTask.\u003c/strong\u003e Standing up an actual technical‑support capability — one that could serve a large and still‑growing client base — was the job.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eAction.\u003c/strong\u003e 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.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eResult.\u003c/strong\u003e 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.\u003c/p\u003e\n","date_published":"2026-09-13T01:45:50+02:00","date_modified":"2026-09-13T01:45:50+02:00","language":"en","tags":["Mentoring \u0026 Coaching","Product \u0026 Requirements","Project Management","Stakeholder \u0026 Reporting","System Administration","Team Leadership","IT Support \u0026 Helpdesk","Project Management (Agile)","Team Building \u0026 Mentoring"]},{"id":"https://advisory.engineer.company/portfolio/streamlined-ci-cd-processes-saving-4-000-hours-42/","url":"https://advisory.engineer.company/portfolio/streamlined-ci-cd-processes-saving-4-000-hours-42/","title":"Streamlined CI/CD processes, saving 4,000 hours by introducing automation in software development pipelines.","summary":"Streamlined CI/CD to save ~4,000 hours, making releases faster and more reliable so the team could ship with confidence.","content_html":"\u003cp\u003e\u003cstrong\u003eSituation.\u003c/strong\u003e 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.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eTask.\u003c/strong\u003e The aim was to streamline the CI/CD process and get automation into the pipelines, so releases stopped being a manual ritual.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eAction.\u003c/strong\u003e Automated build, test and deployment pipelines went in, 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 pipeline is doing it the same way every time, a whole class of \u0026ldquo;it worked on my machine\u0026rdquo; and half‑remembered deploy steps just stops happening.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eResult.\u003c/strong\u003e 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.\u003c/p\u003e\n","date_published":"2026-09-13T01:45:50+02:00","date_modified":"2026-09-13T01:45:50+02:00","language":"en","tags":["Automation \u0026 CI/CD","DevOps","Reliability \u0026 Backups","Technical Leadership","Testing \u0026 QA","DevOps \u0026 CI/CD Automation","Technical Leadership \u0026 Consulting"]},{"id":"https://advisory.engineer.company/portfolio/developed-a-data-analytics-reporting-system-increasing-quarterly-44/","url":"https://advisory.engineer.company/portfolio/developed-a-data-analytics-reporting-system-increasing-quarterly-44/","title":"Developed a Data Analytics reporting system, increasing quarterly software revenue by 400% through Python‑based PDF reports.","summary":"Built a Python data-analytics reporting system that raised quarterly software revenue by 400% with clear, timely PDF reports.","content_html":"\u003cp\u003e\u003cstrong\u003eSituation.\u003c/strong\u003e Stakeholders weren\u0026rsquo;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.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eTask.\u003c/strong\u003e 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.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eAction.\u003c/strong\u003e 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.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eResult.\u003c/strong\u003e That reporting is what drove quarterly software revenue up by 400%. Making the analytics better and faster wasn\u0026rsquo;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.\u003c/p\u003e\n","date_published":"2026-09-13T01:45:50+02:00","date_modified":"2026-09-13T01:45:50+02:00","language":"en","tags":["Automation \u0026 CI/CD","Backend Engineering","Data Analytics","Data Engineering","Product \u0026 Requirements","Python","Stakeholder \u0026 Reporting","Backend \u0026 API Development","Data Analytics \u0026 BI Dashboards","Product Strategy \u0026 Requirements"]},{"id":"https://advisory.engineer.company/portfolio/enhanced-project-efficiency-saving-150-hours-per-month-46/","url":"https://advisory.engineer.company/portfolio/enhanced-project-efficiency-saving-150-hours-per-month-46/","title":"Enhanced project efficiency, saving 150 hours per month across 30 projects by optimizing workflows and resource management.","summary":"Saved ~150 hours per month across 30 projects by optimising workflows and resource management — a recurring, portfolio-wide gain.","content_html":"\u003cp\u003e\u003cstrong\u003eSituation.\u003c/strong\u003e 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.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eTask.\u003c/strong\u003e The aim was to make the portfolio more efficient and recover the time that was going missing month after month.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eAction.\u003c/strong\u003e 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\u0026rsquo;t always the constraint, and standardising how work got planned and run so each project wasn\u0026rsquo;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.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eResult.\u003c/strong\u003e The changes saved around 150 hours a month across the portfolio. That\u0026rsquo;s a recurring monthly saving, not a one‑off — thirty projects running leaner, month in and month out.\u003c/p\u003e\n","date_published":"2026-09-13T01:45:50+02:00","date_modified":"2026-09-13T01:45:50+02:00","language":"en","tags":["Agile \u0026 Scrum","Automation \u0026 CI/CD","Performance Tuning","Project Management","Team Leadership","Project Management (Agile)","Team Building \u0026 Mentoring"]},{"id":"https://advisory.engineer.company/portfolio/managed-a-team-delivering-it-support-data-recovery-47/","url":"https://advisory.engineer.company/portfolio/managed-a-team-delivering-it-support-data-recovery-47/","title":"Managed a team delivering IT support, data recovery, and hardware repair services to over 1,000 clients, ensuring high‑quality service.","summary":"Led a team delivering IT support, data recovery and hardware repair to 1,000+ clients, building a reputation for reliable service.","content_html":"\u003cp\u003e\u003cstrong\u003eSituation.\u003c/strong\u003e 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\u0026rsquo;t about heroics; it\u0026rsquo;s about having a team that runs well day after day, because the work never really stops coming.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eTask.\u003c/strong\u003e 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.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eAction.\u003c/strong\u003e 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 \u0026ldquo;fixed\u0026rdquo; actually meant so people weren\u0026rsquo;t getting half‑repairs back, and keeping the team effective when the queue was long. Data recovery especially is work you can\u0026rsquo;t be casual about; it\u0026rsquo;s usually someone\u0026rsquo;s photos or their business sitting on that drive, and they\u0026rsquo;re already having a bad day by the time they reach you.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eResult.\u003c/strong\u003e 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.\u003c/p\u003e\n","date_published":"2026-09-13T01:45:50+02:00","date_modified":"2026-09-13T01:45:50+02:00","language":"en","tags":["Mentoring \u0026 Coaching","Stakeholder \u0026 Reporting","System Administration","Team Leadership","Hardware Repair \u0026 Data Recovery","IT Support \u0026 Helpdesk","Team Building \u0026 Mentoring"]},{"id":"https://advisory.engineer.company/portfolio/migrated-the-http-api-from-fiber-to-huma-60/","url":"https://advisory.engineer.company/portfolio/migrated-the-http-api-from-fiber-to-huma-60/","title":"Migrated the HTTP API from Fiber to Huma v2 — 649 paths and 760 operations — reaching and holding 100% parity between the routes the server registers and the OpenAPI description it publishes.","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":"\u003cp\u003e\u003cstrong\u003eSituation.\u003c/strong\u003e The API started life on Fiber, with request validation written out by hand, endpoint by endpoint. That\u0026rsquo;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\u0026rsquo;s checks are their own little snowflake. And there was no single description of the API\u0026rsquo;s shape anywhere.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eTask.\u003c/strong\u003e 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.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eAction.\u003c/strong\u003e 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\u0026rsquo;s request/response model doesn\u0026rsquo;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\u0026rsquo;t described fails the build.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eResult.\u003c/strong\u003e New endpoints get validation and current documentation without anyone doing extra work for it, and a whole class of request‑handling bugs — the \u0026ldquo;oh, we forgot to check that field here\u0026rdquo; 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.\u003c/p\u003e\n","date_published":"2026-09-13T01:45:50+02:00","date_modified":"2026-09-13T01:45:50+02:00","language":"en","tags":["APIs \u0026 Integration","Backend Engineering","Documentation","Migrations \u0026 Modernization","Platform Architecture","Testing \u0026 QA","Backend \u0026 API Development","Platform \u0026 Solution Architecture","Technical Documentation"]},{"id":"https://advisory.engineer.company/portfolio/authored-578-go-task-automation-targets-70/","url":"https://advisory.engineer.company/portfolio/authored-578-go-task-automation-targets-70/","title":"Authored 578 go‑task automation targets spanning native, Docker and HTTPS dev modes, linting, testing, database and deployment.","summary":"Authored 578 go-task automation targets across native, Docker and HTTPS modes — linting, testing, database and deployment behind one toolchain.","content_html":"\u003cp\u003e\u003cstrong\u003eSituation.\u003c/strong\u003e NextMariner 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.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eTask.\u003c/strong\u003e Give the whole project one front door: a single, consistent way to run anything, whatever language it happens to be written in.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eAction.\u003c/strong\u003e That\u0026rsquo;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\u0026rsquo;s even a low‑memory mode for machines that can\u0026rsquo;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.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eResult.\u003c/strong\u003e Anyone can run task \u0026ndash;list and see the whole toolchain laid out, and run any part of it the same way regardless of what\u0026rsquo;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.\u003c/p\u003e\n","date_published":"2026-09-13T01:45:50+02:00","date_modified":"2026-09-13T01:45:50+02:00","language":"en","tags":["Automation \u0026 CI/CD","DevOps","Documentation","Technical Leadership","Testing \u0026 QA","DevOps \u0026 CI/CD Automation","Technical Documentation"]},{"id":"https://advisory.engineer.company/portfolio/designed-the-product-s-ui-and-ux-end-76/","url":"https://advisory.engineer.company/portfolio/designed-the-product-s-ui-and-ux-end-76/","title":"Designed the product's UI and UX end to end — directory grids, dual card/table views, live requirement validators and breadcrumb navigation.","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":"\u003cp\u003e\u003cstrong\u003eSituation.\u003c/strong\u003e NextMariner 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.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eTask.\u003c/strong\u003e The product\u0026rsquo;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.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eAction.\u003c/strong\u003e 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\u0026rsquo;s met and amber when it isn\u0026rsquo;t, so you\u0026rsquo;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.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eResult.\u003c/strong\u003e 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\u0026rsquo;t cosmetic on a platform people are using for their actual careers — if it looked slapdash, they\u0026rsquo;d trust the data less, and they\u0026rsquo;d be right to.\u003c/p\u003e\n","date_published":"2026-09-13T01:45:50+02:00","date_modified":"2026-09-13T01:45:50+02:00","language":"en","tags":["Design Systems \u0026 UI","Frontend Engineering","Product \u0026 Requirements","UX / UI Design","Web Development","Frontend Development","Product Strategy \u0026 Requirements","UI/UX Design \u0026 Design Systems"]},{"id":"https://advisory.engineer.company/portfolio/set-a-zero-warnings-quality-bar-across-six-79/","url":"https://advisory.engineer.company/portfolio/set-a-zero-warnings-quality-bar-across-six-79/","title":"Set a zero‑warnings quality bar across six languages — Go, TypeScript, SQL, Python, Shell and Markdown — enforced by pre‑commit hooks.","summary":"Set a zero-warnings quality bar across Go, TypeScript, SQL, Python, Shell and Markdown, enforced by pre-commit hooks.","content_html":"\u003cp\u003e\u003cstrong\u003eSituation.\u003c/strong\u003e 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 \u0026ldquo;warnings\u0026rdquo; have become background noise. In a polyglot codebase there are that many more sources of noise to let it happen.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eTask.\u003c/strong\u003e The repo needed one uncompromising quality bar across every language, so things got fixed instead of accumulating.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eAction.\u003c/strong\u003e A zero‑warnings policy went in, with the tooling as the thing that enforces it, because a policy that relies on everyone\u0026rsquo;s vigilance loses to the first busy week. Every linter diagnostic is an error — there\u0026rsquo;s no \u0026ldquo;warn\u0026rdquo; tier to hide in — and it\u0026rsquo;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\u0026rsquo;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\u0026rsquo;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.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eResult.\u003c/strong\u003e 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\u0026rsquo;re in, and it\u0026rsquo;s the tooling holding it — not anyone\u0026rsquo;s willpower — which is why it actually holds.\u003c/p\u003e\n","date_published":"2026-09-13T01:45:50+02:00","date_modified":"2026-09-13T01:45:50+02:00","language":"en","tags":["Automation \u0026 CI/CD","DevOps","Documentation","Technical Leadership","Testing \u0026 QA","DevOps \u0026 CI/CD Automation","Technical Leadership \u0026 Consulting"]},{"id":"https://advisory.engineer.company/portfolio/set-the-platform-s-founding-decisions-in-november-2025-80/","url":"https://advisory.engineer.company/portfolio/set-the-platform-s-founding-decisions-in-november-2025-80/","title":"Set the platform's founding decisions in the weeks after the repository opened in November 2025 — the layering, database‑first data access and the zero‑warnings bar — and they still hold nine months on.","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":"\u003cp\u003e\u003cstrong\u003eSituation.\u003c/strong\u003e 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.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eTask.\u003c/strong\u003e 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.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eAction.\u003c/strong\u003e 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.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eResult.\u003c/strong\u003e 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.\u003c/p\u003e\n","date_published":"2026-09-13T01:45:50+02:00","date_modified":"2026-09-13T01:45: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 \u0026 API Development","Frontend Development","Full‑Stack Product Development","Platform \u0026 Solution Architecture","Technical Leadership \u0026 Consulting"]},{"id":"https://advisory.engineer.company/portfolio/delivered-analysis-and-regular-progress-reports-to-the-81/","url":"https://advisory.engineer.company/portfolio/delivered-analysis-and-regular-progress-reports-to-the-81/","title":"Delivered analysis and regular progress reports to the CEO, translating engineering metrics and delivery status into decisions.","summary":"Delivered analysis and regular progress reports to the CEO, translating engineering metrics and delivery status into confident decisions.","content_html":"\u003cp\u003e\u003cstrong\u003eSituation.\u003c/strong\u003e 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\u0026rsquo;t mean much on their own to someone making product and investment calls; they\u0026rsquo;re at the wrong altitude. Someone had to translate.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eTask.\u003c/strong\u003e 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.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eAction.\u003c/strong\u003e 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\u0026rsquo;s on track, what\u0026rsquo;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.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eResult.\u003c/strong\u003e 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.\u003c/p\u003e\n","date_published":"2026-09-13T01:45:50+02:00","date_modified":"2026-09-13T01:45:50+02:00","language":"en","tags":["Data Analytics","Product \u0026 Requirements","Project Management","Stakeholder \u0026 Reporting","Technical Leadership","Data Analytics \u0026 BI Dashboards","Project Management (Agile)","Technical Leadership \u0026 Consulting"]},{"id":"https://advisory.engineer.company/portfolio/designed-a-time-management-and-reporting-system-that-88/","url":"https://advisory.engineer.company/portfolio/designed-a-time-management-and-reporting-system-that-88/","title":"Designed a time‑management and reporting system that remained in production use for years without significant modification.","summary":"Designed a time-management and reporting system that stayed in production for years without significant modification.","content_html":"\u003cp\u003e\u003cstrong\u003eSituation.\u003c/strong\u003e The team didn\u0026rsquo;t have a dependable way to manage time and report progress. Which usually means it\u0026rsquo;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.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eTask.\u003c/strong\u003e Building a system for both that would actually last was the task.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eAction.\u003c/strong\u003e A time‑management and reporting system was designed around how the team actually worked, rather than imposing some off‑the‑shelf process they\u0026rsquo;d route around. That\u0026rsquo;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\u0026rsquo;re back to spreadsheets. So it was built to match reality rather than an ideal.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eResult.\u003c/strong\u003e It stayed in production use for years without any significant modification. That\u0026rsquo;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.\u003c/p\u003e\n","date_published":"2026-09-13T01:45:50+02:00","date_modified":"2026-09-13T01:45:50+02:00","language":"en","tags":["Automation \u0026 CI/CD","Product \u0026 Requirements","Project Management","Technical Leadership","Data Analytics \u0026 BI Dashboards","Product Strategy \u0026 Requirements","Project Management (Agile)"]},{"id":"https://advisory.engineer.company/portfolio/automated-team-collaboration-password-management-task-and-time-89/","url":"https://advisory.engineer.company/portfolio/automated-team-collaboration-password-management-task-and-time-89/","title":"Automated team collaboration, password management, task and time management, and built a semi‑automatic project‑showcase system, raising team productivity.","summary":"Automated collaboration, password, task and time management and built a semi-automatic project-showcase system, raising team productivity.","content_html":"\u003cp\u003e\u003cstrong\u003eSituation.\u003c/strong\u003e A lot of the team\u0026rsquo;s operational work — coordinating, managing passwords, tracking tasks and time — was being done by hand, and manual coordination is a quiet tax: it\u0026rsquo;s never the thing you notice, but it steadily eats hours that could go somewhere better.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eTask.\u003c/strong\u003e Automating that repetitive operational work was the goal.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eAction.\u003c/strong\u003e 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\u0026rsquo;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.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eResult.\u003c/strong\u003e 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.\u003c/p\u003e\n","date_published":"2026-09-13T01:45:50+02:00","date_modified":"2026-09-13T01:45:50+02:00","language":"en","tags":["Automation \u0026 CI/CD","DevOps","Project Management","Security","Technical Leadership","DevOps \u0026 CI/CD Automation","Project Management (Agile)","Security \u0026 Access Management"]},{"id":"https://advisory.engineer.company/portfolio/built-a-layered-automated-test-suite-across-four-layers-94/","url":"https://advisory.engineer.company/portfolio/built-a-layered-automated-test-suite-across-four-layers-94/","title":"Built a layered automated test suite — 981 Go tests, 543 frontend and browser specs, 494 SQL behavioural tests — with mutation testing, property‑based tests and an accessibility gate.","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":"\u003cp\u003e\u003cstrong\u003eSituation.\u003c/strong\u003e 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 \u0026ldquo;the important parts are covered\u0026rdquo; quietly turns into \u0026ldquo;the parts that were easy to cover are covered.\u0026rdquo;\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eTask.\u003c/strong\u003e Each layer needed its behaviour checked where that behaviour actually lives, rather than everything being checked from the outside through a browser.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eAction.\u003c/strong\u003e 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.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eResult.\u003c/strong\u003e 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.\u003c/p\u003e\n","date_published":"2026-09-13T01:45:50+02:00","date_modified":"2026-09-13T01:45:50+02:00","language":"en","tags":["Automation \u0026 CI/CD","Backend Engineering","DevOps","Frontend Engineering","PostgreSQL","Reliability \u0026 Backups","Testing \u0026 QA","Backend \u0026 API Development","DevOps \u0026 CI/CD Automation","Technical Leadership \u0026 Consulting"]},{"id":"https://advisory.engineer.company/portfolio/built-the-repository-s-guard-engine-of-268-checks-95/","url":"https://advisory.engineer.company/portfolio/built-the-repository-s-guard-engine-of-268-checks-95/","title":"Built the repository's guard engine — 268 registered commit checks, 277 lint rules and 15 custom ESLint rules — plus 146 tests of the guards themselves, so the build holds the standard, not review.","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":"\u003cp\u003e\u003cstrong\u003eSituation.\u003c/strong\u003e 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.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eTask.\u003c/strong\u003e 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.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eAction.\u003c/strong\u003e 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.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eResult.\u003c/strong\u003e 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.\u003c/p\u003e\n","date_published":"2026-09-13T01:45:50+02:00","date_modified":"2026-09-13T01:45:50+02:00","language":"en","tags":["Automation \u0026 CI/CD","DevOps","Documentation","Technical Leadership","Testing \u0026 QA","DevOps \u0026 CI/CD Automation","Technical Documentation","Technical Leadership \u0026 Consulting"]},{"id":"https://advisory.engineer.company/portfolio/built-the-payments-and-entitlements-layer-96/","url":"https://advisory.engineer.company/portfolio/built-the-payments-and-entitlements-layer-96/","title":"Built the payments and entitlements layer — Stripe alongside Apple and Google in‑app purchase — gating the directory, search and export through an 11‑table access model checked on the server.","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":"\u003cp\u003e\u003cstrong\u003eSituation.\u003c/strong\u003e 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?\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eTask.\u003c/strong\u003e 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.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eAction.\u003c/strong\u003e 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 \u0026ldquo;may this account do this?\u0026rdquo; 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.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eResult.\u003c/strong\u003e Adding a storefront now touches the payments side and leaves the gates alone, and a support question about someone\u0026rsquo;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.\u003c/p\u003e\n","date_published":"2026-09-13T01:45:50+02:00","date_modified":"2026-09-13T01:45:50+02:00","language":"en","tags":["APIs \u0026 Integration","Backend Engineering","Databases","PostgreSQL","Product \u0026 Requirements","Security","Backend \u0026 API Development","Database Design \u0026 Modeling","Product Strategy \u0026 Requirements","Security \u0026 Access Management"]},{"id":"https://advisory.engineer.company/portfolio/built-the-operator-back-office-for-the-platform-102/","url":"https://advisory.engineer.company/portfolio/built-the-operator-back-office-for-the-platform-102/","title":"Built the operator back‑office — around 40 admin routes and 128 components covering claims, moderation, feature flags, cache and diagnostics — so the platform can be run without database access.","summary":"Built the operator back-office — around 40 admin routes and 128 components for claims, moderation, feature flags, cache and diagnostics.","content_html":"\u003cp\u003e\u003cstrong\u003eSituation.\u003c/strong\u003e 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.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eTask.\u003c/strong\u003e 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.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eAction.\u003c/strong\u003e 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.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eResult.\u003c/strong\u003e 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.\u003c/p\u003e\n","date_published":"2026-09-13T01:45:50+02:00","date_modified":"2026-09-13T01:45:50+02:00","language":"en","tags":["Backend Engineering","Frontend Engineering","Full‑Stack Development","Product \u0026 Requirements","System Administration","UX / UI Design","Frontend Development","Full‑Stack Product Development","Product Strategy \u0026 Requirements"]},{"id":"https://advisory.engineer.company/portfolio/built-company-ownership-claims-end-to-end-103/","url":"https://advisory.engineer.company/portfolio/built-company-ownership-claims-end-to-end-103/","title":"Built company ownership claims end to end — a user claims a company, an administrator adjudicates, and an approval rewrites the authorization graph that decides who is allowed to edit what.","summary":"Built company ownership claims end to end: a user claims, an administrator adjudicates, and approval rewrites the authorization graph.","content_html":"\u003cp\u003e\u003cstrong\u003eSituation.\u003c/strong\u003e 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.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eTask.\u003c/strong\u003e There had to be a route from \u0026ldquo;this is my company\u0026rdquo; to genuine authority over the record, with a human decision in the middle and a trail behind it.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eAction.\u003c/strong\u003e 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.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eResult.\u003c/strong\u003e 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.\u003c/p\u003e\n","date_published":"2026-09-13T01:45:50+02:00","date_modified":"2026-09-13T01:45:50+02:00","language":"en","tags":["Backend Engineering","Frontend Engineering","Full‑Stack Development","Product \u0026 Requirements","Security","Full‑Stack Product Development","Product Strategy \u0026 Requirements","Security \u0026 Access Management"]},{"id":"https://advisory.engineer.company/portfolio/established-a-continuous-security-programme-104/","url":"https://advisory.engineer.company/portfolio/established-a-continuous-security-programme-104/","title":"Established a continuous security programme — code scanning, DAST, dependency and vulnerability checks, SBOM generation, secret scanning and SHA‑pinned actions — alongside 21 written security audits.","summary":"Established a continuous security programme — code scanning, DAST, dependency checks, SBOM, secret scanning and pinned actions — plus 21 audits.","content_html":"\u003cp\u003e\u003cstrong\u003eSituation.\u003c/strong\u003e 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.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eTask.\u003c/strong\u003e Supply chain and code security had to be continuous and automated, so the state of it was a build result rather than an opinion.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eAction.\u003c/strong\u003e 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.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eResult.\u003c/strong\u003e 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.\u003c/p\u003e\n","date_published":"2026-09-13T01:45:50+02:00","date_modified":"2026-09-13T01:45:50+02:00","language":"en","tags":["Automation \u0026 CI/CD","DevOps","Documentation","Reliability \u0026 Backups","Security","DevOps \u0026 CI/CD Automation","Security \u0026 Access Management","Technical Documentation"]},{"id":"https://advisory.engineer.company/portfolio/built-the-loyalty-and-reputation-system-105/","url":"https://advisory.engineer.company/portfolio/built-the-loyalty-and-reputation-system-105/","title":"Built the loyalty and reputation system — 67 functions over a 31‑table ledger, with leagues, badges and a redemption shop — taking a row lock on the balance to close the double‑spend window.","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":"\u003cp\u003e\u003cstrong\u003eSituation.\u003c/strong\u003e 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.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eTask.\u003c/strong\u003e Contribution had to be measurable and rewardable, with a balance that could not be spent twice.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eAction.\u003c/strong\u003e 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.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eResult.\u003c/strong\u003e 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 is a queue, and the alternative is a member spending the same points twice and someone reconciling it by hand afterwards.\u003c/p\u003e\n","date_published":"2026-09-13T01:45:50+02:00","date_modified":"2026-09-13T01:45:50+02:00","language":"en","tags":["Backend Engineering","Databases","Performance Tuning","PostgreSQL","Product \u0026 Requirements","SQL","Backend \u0026 API Development","Database Design \u0026 Modeling","Product Strategy \u0026 Requirements"]},{"id":"https://advisory.engineer.company/portfolio/built-the-platform-s-social-layer-106/","url":"https://advisory.engineer.company/portfolio/built-the-platform-s-social-layer-106/","title":"Built the platform's social layer — posts, feed, groups, mentions, a follower graph and an occasions digest — on the same function‑first data layer as the rest of the product.","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":"\u003cp\u003e\u003cstrong\u003eSituation.\u003c/strong\u003e 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.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eTask.\u003c/strong\u003e 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.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eAction.\u003c/strong\u003e Posts, a feed, 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 feed 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.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eResult.\u003c/strong\u003e 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.\u003c/p\u003e\n","date_published":"2026-09-13T01:45:50+02:00","date_modified":"2026-09-13T01:45:50+02:00","language":"en","tags":["Backend Engineering","Databases","Frontend Engineering","Full‑Stack Development","Product \u0026 Requirements","Web Development","Backend \u0026 API Development","Full‑Stack Product Development","Product Strategy \u0026 Requirements"]},{"id":"https://advisory.engineer.company/portfolio/built-the-hiring-marketplace-and-career-workspace-107/","url":"https://advisory.engineer.company/portfolio/built-the-hiring-marketplace-and-career-workspace-107/","title":"Built the hiring marketplace and the seafarer career workspace — 151 stored functions across 48 tables — covering vacancies, applications, certificates, rank progression and verified sea time.","summary":"Built the hiring marketplace and seafarer career workspace — 151 stored functions across 48 tables — matching vacancies to documented qualifications.","content_html":"\u003cp\u003e\u003cstrong\u003eSituation.\u003c/strong\u003e 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.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eTask.\u003c/strong\u003e Vacancies, applications and verifiable seafarer credentials had to be modelled properly rather than as free text on a profile.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eAction.\u003c/strong\u003e Two domains got built. The hiring side runs to 32 tables and 118 functions covering vacancies, applications, shortlisting and the employer\u0026rsquo;s view of a pipeline. 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.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eResult.\u003c/strong\u003e 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.\u003c/p\u003e\n","date_published":"2026-09-13T01:45:50+02:00","date_modified":"2026-09-13T01:45:50+02:00","language":"en","tags":["Backend Engineering","Data Governance","Databases","Full‑Stack Development","PostgreSQL","Product \u0026 Requirements","Backend \u0026 API Development","Database Design \u0026 Modeling","Full‑Stack Product Development","Product Strategy \u0026 Requirements"]},{"id":"https://advisory.engineer.company/portfolio/built-seven-read-only-host-reporting-roles-120/","url":"https://advisory.engineer.company/portfolio/built-seven-read-only-host-reporting-roles-120/","title":"Built seven read‑only reporting roles that render a live host to Markdown — facts, access, git, metrics, traffic, security and provider inventory — under a rule that no number is printed the run did not measure.","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":"\u003cp\u003e\u003cstrong\u003eSituation.\u003c/strong\u003e 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.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eTask.\u003c/strong\u003e 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.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eAction.\u003c/strong\u003e 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\u0026rsquo;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\u0026rsquo;s collected samples. A traffic report built from the web server\u0026rsquo;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.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eResult.\u003c/strong\u003e 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\u0026rsquo;s history that rotated away between harvests and do not exist anywhere.\u003c/p\u003e\n","date_published":"2026-09-13T01:45:50+02:00","date_modified":"2026-09-13T01:45:50+02:00","language":"en","tags":["Automation \u0026 CI/CD","Documentation","Infrastructure","Linux \u0026 Servers","Monitoring \u0026 Observability","Stakeholder \u0026 Reporting","Site Reliability \u0026 Monitoring","System Administration","Technical Documentation"]},{"id":"https://advisory.engineer.company/portfolio/wrote-a-scope-rule-after-a-credential-leak-124/","url":"https://advisory.engineer.company/portfolio/wrote-a-scope-rule-after-a-credential-leak-124/","title":"Wrote a scope rule into the repository after a restructure carried another company's inventory, firewall allowances and prose into it — and kept the quarantined residue under the secret scanner rather than excluding it.","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":"\u003cp\u003e\u003cstrong\u003eSituation.\u003c/strong\u003e 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\u0026rsquo;s history; a stale inventory naming that company\u0026rsquo;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.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eTask.\u003c/strong\u003e 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.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eAction.\u003c/strong\u003e The rule is now the opening section of the repository\u0026rsquo;s instruction set: this repository manages one company\u0026rsquo;s infrastructure and only that, and another party\u0026rsquo;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.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eResult.\u003c/strong\u003e 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.\u003c/p\u003e\n","date_published":"2026-09-13T01:45:50+02:00","date_modified":"2026-09-13T01:45:50+02:00","language":"en","tags":["Automation \u0026 CI/CD","Data Governance","Documentation","Infrastructure","Security","Data Governance \u0026 Quality","Security \u0026 Access Management","Technical Documentation"]},{"id":"https://advisory.engineer.company/portfolio/built-a-22-linter-commit-gate-127/","url":"https://advisory.engineer.company/portfolio/built-a-22-linter-commit-gate-127/","title":"Built a commit gate of 22 one‑line linters plus five that earn a paragraph, with no warning tier and no inline suppressions permitted, covering HTML, CSS, JavaScript, Python, YAML, Markdown, shell, links, spelling, secrets and typography.","summary":"Mechanical objections are made by a machine before a commit exists, and the gate is the only reviewer this project has.","content_html":"\u003cp\u003e\u003cstrong\u003eSituation.\u003c/strong\u003e 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.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eTask.\u003c/strong\u003e The standards had to be executable, so that breaking one fails a commit rather than waiting to be noticed.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eAction.\u003c/strong\u003e 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.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eResult.\u003c/strong\u003e 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.\u003c/p\u003e\n","date_published":"2026-09-13T01:45:50+02:00","date_modified":"2026-09-13T01:45:50+02:00","language":"en","tags":["Automation \u0026 CI/CD","DevOps","Documentation","Frontend Engineering","Python","Testing \u0026 QA","DevOps \u0026 CI/CD Automation","Product Strategy \u0026 Requirements","Technical Documentation"]},{"id":"https://advisory.engineer.company/portfolio/cut-the-visual-quality-gate-to-ten-minutes-128/","url":"https://advisory.engineer.company/portfolio/cut-the-visual-quality-gate-to-ten-minutes-128/","title":"Cut the site's browser‑driven quality gate from 1,636 seconds to 615 by scheduling its checks longest‑first through a worker pool bounded to four lanes, after measuring that alphabetical order cost 320 seconds against 224.","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":"\u003cp\u003e\u003cstrong\u003eSituation.\u003c/strong\u003e Eleven of the site\u0026rsquo;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.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eTask.\u003c/strong\u003e The wall clock had to come down far enough that running them was the default rather than a decision, without weakening any of them.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eAction.\u003c/strong\u003e 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.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eResult.\u003c/strong\u003e 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.\u003c/p\u003e\n","date_published":"2026-09-13T01:45:50+02:00","date_modified":"2026-09-13T01:45:50+02:00","language":"en","tags":["Automation \u0026 CI/CD","DevOps","Frontend Engineering","Performance Tuning","Python","Testing \u0026 QA","DevOps \u0026 CI/CD Automation","Frontend Development","Technical Leadership \u0026 Consulting"]},{"id":"https://advisory.engineer.company/portfolio/ran-development-as-49-written-initiatives-141/","url":"https://advisory.engineer.company/portfolio/ran-development-as-49-written-initiatives-141/","title":"Ran the site's development as 51 written initiatives across 54 plan documents, each carrying its open boxes, its measurements and the decisions it declined — including six of eight agent‑readiness findings refused with the reason recorded.","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":"\u003cp\u003e\u003cstrong\u003eSituation.\u003c/strong\u003e 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.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eTask.\u003c/strong\u003e The site\u0026rsquo;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.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eAction.\u003c/strong\u003e 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\u0026rsquo;s checkboxes rather than its prose, because prose says \u0026ldquo;done\u0026rdquo; and boxes say what is open. That distinction was not theoretical — a documentation linter later found six plans marked done while carrying unchecked boxes.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eResult.\u003c/strong\u003e 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.\u003c/p\u003e\n","date_published":"2026-09-13T01:45:50+02:00","date_modified":"2026-09-13T01:45:50+02:00","language":"en","tags":["Agile \u0026 Scrum","Documentation","Product \u0026 Requirements","Project Management","Stakeholder \u0026 Reporting","Technical Leadership","Product Strategy \u0026 Requirements","Project Management (Agile)","Technical Documentation","Technical Leadership \u0026 Consulting"]},{"id":"https://advisory.engineer.company/portfolio/put-the-documentation-under-its-own-linter-142/","url":"https://advisory.engineer.company/portfolio/put-the-documentation-under-its-own-linter-142/","title":"Put the documentation under its own linter — a per‑file line budget that only ratchets down, an index requirement and a real‑path check — which found six of nine plans marked done while carrying unchecked boxes, and fourteen commit‑gating tasks named in no document.","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":"\u003cp\u003e\u003cstrong\u003eSituation.\u003c/strong\u003e 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.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eTask.\u003c/strong\u003e The documentation needed the same treatment as the code — a linter with limits that fail rather than advise.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eAction.\u003c/strong\u003e 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.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eResult.\u003c/strong\u003e 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.\u003c/p\u003e\n","date_published":"2026-09-13T01:45:50+02:00","date_modified":"2026-09-13T01:45:50+02:00","language":"en","tags":["Automation \u0026 CI/CD","Documentation","Project Management","Python","Technical Leadership","Testing \u0026 QA","Project Management (Agile)","Technical Documentation","Technical Leadership \u0026 Consulting"]},{"id":"https://advisory.engineer.company/portfolio/brought-the-quality-gate-code-under-a-linter-143/","url":"https://advisory.engineer.company/portfolio/brought-the-quality-gate-code-under-a-linter-143/","title":"Brought 10,242 lines of quality‑gate JavaScript under a formatter and a linter after establishing it was the largest body of code in the repository and the only one nothing read, fixing 13 findings and suppressing none.","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":"\u003cp\u003e\u003cstrong\u003eSituation.\u003c/strong\u003e The site\u0026rsquo;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.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eTask.\u003c/strong\u003e 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.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eAction.\u003c/strong\u003e The formatter came first, and the indentation width was the interesting decision. The repository\u0026rsquo;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\u0026rsquo;s shape rather than its syntax.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eResult.\u003c/strong\u003e 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\u0026rsquo;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.\u003c/p\u003e\n","date_published":"2026-09-13T01:45:50+02:00","date_modified":"2026-09-13T01:45:50+02:00","language":"en","tags":["Automation \u0026 CI/CD","DevOps","Documentation","Frontend Engineering","Python","Testing \u0026 QA","DevOps \u0026 CI/CD Automation","Frontend Development","Technical Leadership \u0026 Consulting"]},{"id":"https://advisory.engineer.company/portfolio/held-the-generator-to-964-tests-and-a-coverage-floor-145/","url":"https://advisory.engineer.company/portfolio/held-the-generator-to-964-tests-and-a-coverage-floor-145/","title":"Held the generator to 981 test cases at a 92% branch‑coverage floor with warnings treated as failures, and asserted idempotence by running the whole build pipeline twice from an empty file and requiring the second pass to change nothing.","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":"\u003cp\u003e\u003cstrong\u003eSituation.\u003c/strong\u003e 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\u0026rsquo;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.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eTask.\u003c/strong\u003e 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.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eAction.\u003c/strong\u003e 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 \u0026ndash; 981 cases once the parameterised ones expand \u0026ndash; 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.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eResult.\u003c/strong\u003e 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.\u003c/p\u003e\n","date_published":"2026-09-13T01:45:50+02:00","date_modified":"2026-09-13T01:45:50+02:00","language":"en","tags":["Automation \u0026 CI/CD","Databases","DevOps","Python","Reliability \u0026 Backups","Testing \u0026 QA","Backend \u0026 API Development","DevOps \u0026 CI/CD Automation","Technical Leadership \u0026 Consulting"]},{"id":"https://advisory.engineer.company/portfolio/established-pdf-ua-1-conformance-across-nine-documents-146/","url":"https://advisory.engineer.company/portfolio/established-pdf-ua-1-conformance-across-nine-documents-146/","title":"Established PDF/UA‑1 conformance across nine documents at 106 of 106 rules, and found the archival variant failing one rule of 146 — a near‑miss that reads as a pass to anyone not running the validator.","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":"\u003cp\u003e\u003cstrong\u003eSituation.\u003c/strong\u003e 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.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eTask.\u003c/strong\u003e The generated documents had to be measured against both standards, with the result recorded as a number rather than as an intention.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eAction.\u003c/strong\u003e 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.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eResult.\u003c/strong\u003e 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.\u003c/p\u003e\n","date_published":"2026-09-13T01:45:50+02:00","date_modified":"2026-09-13T01:45:50+02:00","language":"en","tags":["Automation \u0026 CI/CD","Documentation","Python","Testing \u0026 QA","UX / UI Design","Web Development","Technical Documentation","UI/UX Design \u0026 Design Systems","Website Development \u0026 CMS"]},{"id":"https://advisory.engineer.company/portfolio/enabled-every-python-linter-rule-as-an-error-147/","url":"https://advisory.engineer.company/portfolio/enabled-every-python-linter-rule-as-an-error-147/","title":"Selected every rule the Python linter has as an error, working through 1,815 findings to reach zero, with each of the few exemptions carrying a written reason and two of them backed by a checker instead of a comment.","summary":"The linter runs at full strength with zero findings, and every deviation is documented at the line where it is taken.","content_html":"\u003cp\u003e\u003cstrong\u003eSituation.\u003c/strong\u003e Most projects pick a comfortable subset of their linter\u0026rsquo;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\u0026rsquo;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.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eTask.\u003c/strong\u003e 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.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eAction.\u003c/strong\u003e 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\u0026rsquo;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.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eResult.\u003c/strong\u003e 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.\u003c/p\u003e\n","date_published":"2026-09-13T01:45:50+02:00","date_modified":"2026-09-13T01:45:50+02:00","language":"en","tags":["Automation \u0026 CI/CD","DevOps","Documentation","Python","Testing \u0026 QA","Backend \u0026 API Development","DevOps \u0026 CI/CD Automation","Technical Leadership \u0026 Consulting"]},{"id":"https://advisory.engineer.company/portfolio/wrote-tests-for-the-checkers-themselves-149/","url":"https://advisory.engineer.company/portfolio/wrote-tests-for-the-checkers-themselves-149/","title":"Wrote tests for the checkers themselves after establishing that a checker fed only clean input will one day report clean because it read nothing — planting a misspelling to confirm the spell‑check finds it, and taking an id range from the database rather than from a number in the test.","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":"\u003cp\u003e\u003cstrong\u003eSituation.\u003c/strong\u003e 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.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eTask.\u003c/strong\u003e 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.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eAction.\u003c/strong\u003e 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.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eResult.\u003c/strong\u003e 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.\u003c/p\u003e\n","date_published":"2026-09-13T01:45:50+02:00","date_modified":"2026-09-13T01:45:50+02:00","language":"en","tags":["Automation \u0026 CI/CD","Data Governance","Databases","Python","Testing \u0026 QA","Data Governance \u0026 Quality","DevOps \u0026 CI/CD Automation","Technical Leadership \u0026 Consulting"]},{"id":"https://advisory.engineer.company/portfolio/found-the-commit-hooks-and-the-gate-disagreeing-150/","url":"https://advisory.engineer.company/portfolio/found-the-commit-hooks-and-the-gate-disagreeing-150/","title":"Found the commit hooks and the quality gate running different checks while a document promised they were the same, by comparing the two lists in a test — the five that only ever ran by hand were the ones reading the CV prose.","summary":"The hook and the gate provably run the same checks, and the prose checks now run on every commit.","content_html":"\u003cp\u003e\u003cstrong\u003eSituation.\u003c/strong\u003e 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.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eTask.\u003c/strong\u003e The claim had to be turned into an assertion, which meant enumerating both sets programmatically and failing when they diverge.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eAction.\u003c/strong\u003e The test reads the hook configuration and the gate\u0026rsquo;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.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eResult.\u003c/strong\u003e 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\u0026rsquo;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.\u003c/p\u003e\n","date_published":"2026-09-13T01:45:50+02:00","date_modified":"2026-09-13T01:45:50+02:00","language":"en","tags":["Automation \u0026 CI/CD","DevOps","Documentation","Python","Testing \u0026 QA","DevOps \u0026 CI/CD Automation","Technical Documentation","Technical Leadership \u0026 Consulting"]},{"id":"https://advisory.engineer.company/portfolio/built-a-cross-language-figure-and-notation-check-153/","url":"https://advisory.engineer.company/portfolio/built-a-cross-language-figure-and-notation-check-153/","title":"Built a cross‑language content check that fails when a translation drops a figure the English states, and when a language uses notation it does not use — finding two Danish descriptions missing a metric and sixteen Ukrainian spans quoting in the English style.","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":"\u003cp\u003e\u003cstrong\u003eSituation.\u003c/strong\u003e 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 \u0026ldquo;significantly\u0026rdquo;, 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.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eTask.\u003c/strong\u003e 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.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eAction.\u003c/strong\u003e 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 uses low‑nine and high‑six quotation marks; 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.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eResult.\u003c/strong\u003e 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.\u003c/p\u003e\n","date_published":"2026-09-13T01:45:50+02:00","date_modified":"2026-09-13T01:45:50+02:00","language":"en","tags":["Automation \u0026 CI/CD","Data Governance","Documentation","Internationalization","Python","Testing \u0026 QA","Data Governance \u0026 Quality","Internationalization \u0026 Localization","Technical Documentation"]},{"id":"https://advisory.engineer.company/portfolio/adopted-swift-6-strict-concurrency-over-a-c-event-loop-156/","url":"https://advisory.engineer.company/portfolio/adopted-swift-6-strict-concurrency-over-a-c-event-loop-156/","title":"Adopted Swift 6 complete strict concurrency with no actors, bridging a blocking C event loop to the main actor through one producer, one consumer and one ordering — after establishing that a task per event loses the ordering the interface depends on.","summary":"The application compiles under complete strict concurrency with no suppressions, and event ordering is a structural property rather than a hope.","content_html":"\u003cp\u003e\u003cstrong\u003eSituation.\u003c/strong\u003e Swift 6\u0026rsquo;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\u0026rsquo;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.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eTask.\u003c/strong\u003e 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.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eAction.\u003c/strong\u003e 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\u0026rsquo;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.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eResult.\u003c/strong\u003e 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.\u003c/p\u003e\n","date_published":"2026-09-13T01:45:50+02:00","date_modified":"2026-09-13T01:45:50+02:00","language":"en","tags":["Backend Engineering","Frontend Engineering","Performance Tuning","Reliability \u0026 Backups","Solution Architecture","Testing \u0026 QA","Backend \u0026 API Development","Platform \u0026 Solution Architecture","Technical Leadership \u0026 Consulting"]},{"id":"https://advisory.engineer.company/portfolio/wrote-a-parser-that-verifies-every-ffi-call-site-157/","url":"https://advisory.engineer.company/portfolio/wrote-a-parser-that-verifies-every-ffi-call-site-157/","title":"Wrote a parser that reads the real 7,308‑line C header and verifies every call site, every enum constant and that every pointer‑owning class is final, after a hand‑written placeholder header let calls to three removed functions compile, link and crash.","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":"\u003cp\u003e\u003cstrong\u003eSituation.\u003c/strong\u003e 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.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eTask.\u003c/strong\u003e 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.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eAction.\u003c/strong\u003e 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.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eResult.\u003c/strong\u003e 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\u0026rsquo;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.\u003c/p\u003e\n","date_published":"2026-09-13T01:45:50+02:00","date_modified":"2026-09-13T01:45:50+02:00","language":"en","tags":["APIs \u0026 Integration","Automation \u0026 CI/CD","Backend Engineering","Documentation","Security","Testing \u0026 QA","Backend \u0026 API Development","DevOps \u0026 CI/CD Automation","Technical Documentation"]},{"id":"https://advisory.engineer.company/portfolio/took-the-test-suite-under-nine-seconds-158/","url":"https://advisory.engineer.company/portfolio/took-the-test-suite-under-nine-seconds-158/","title":"Took the test suite from six tests over a sixty‑second limit to 135 passing in 8.9 seconds by profiling the main thread and removing the two calls it sat inside for 3,989 samples out of 4,017.","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":"\u003cp\u003e\u003cstrong\u003eSituation.\u003c/strong\u003e 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.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eTask.\u003c/strong\u003e The time had to be found rather than budgeted for, which meant profiling the suite instead of reasoning about which tests looked expensive.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eAction.\u003c/strong\u003e 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 \u0026ndash; 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.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eResult.\u003c/strong\u003e The suite went from six tests over sixty seconds \u0026ndash; 74 seconds of wall clock \u0026ndash; 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\u0026rsquo;s speed depends on a discipline the compiler does not enforce.\u003c/p\u003e\n","date_published":"2026-09-13T01:45:50+02:00","date_modified":"2026-09-13T01:45:50+02:00","language":"en","tags":["Automation \u0026 CI/CD","DevOps","Performance Tuning","Testing \u0026 QA","DevOps \u0026 CI/CD Automation","Technical Leadership \u0026 Consulting"]},{"id":"https://advisory.engineer.company/portfolio/rewrote-the-error-messages-against-a-tone-standard-162/","url":"https://advisory.engineer.company/portfolio/rewrote-the-error-messages-against-a-tone-standard-162/","title":"Rewrote the application's error messages against a written tone standard after a refused sign‑in blamed the user for mistyping when the provider actually required an app‑specific password, and covered it with a test that names the provider.","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":"\u003cp\u003e\u003cstrong\u003eSituation.\u003c/strong\u003e 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.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eTask.\u003c/strong\u003e 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.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eAction.\u003c/strong\u003e 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\u0026rsquo;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\u0026rsquo;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.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eResult.\u003c/strong\u003e 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.\u003c/p\u003e\n","date_published":"2026-09-13T01:45:50+02:00","date_modified":"2026-09-13T01:45:50+02:00","language":"en","tags":["Documentation","Frontend Engineering","Product \u0026 Requirements","Security","Testing \u0026 QA","UX / UI Design","Product Strategy \u0026 Requirements","Technical Documentation","UI/UX Design \u0026 Design Systems"]},{"id":"https://advisory.engineer.company/portfolio/held-five-repositories-to-one-history-standard-165/","url":"https://advisory.engineer.company/portfolio/held-five-repositories-to-one-history-standard-165/","title":"Held four repositories to one history standard — conventional, emoji‑free, no attribution trailers, enforced by a commit‑message hook — alongside 46 instruction documents that govern how the work is done.","summary":"Five repositories share one history format and one instruction structure, both enforced by hooks rather than by discipline.","content_html":"\u003cp\u003e\u003cstrong\u003eSituation.\u003c/strong\u003e Five 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.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eTask.\u003c/strong\u003e 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.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eAction.\u003c/strong\u003e 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 — 46 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.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eResult.\u003c/strong\u003e Five 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.\u003c/p\u003e\n","date_published":"2026-09-13T01:45:50+02:00","date_modified":"2026-09-13T01:45:50+02:00","language":"en","tags":["Agile \u0026 Scrum","Automation \u0026 CI/CD","DevOps","Documentation","Project Management","Technical Leadership","DevOps \u0026 CI/CD Automation","Project Management (Agile)","Technical Documentation","Technical Leadership \u0026 Consulting"]},{"id":"https://advisory.engineer.company/notes/solana-mobile-wallet-deeplinks/","url":"https://advisory.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":"\u003cp\u003eA React dApp built on \u003ccode\u003e@solana/wallet-adapter-react\u003c/code\u003e 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 \u0026ldquo;download the app\u0026rdquo; 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.\u003c/p\u003e\n\u003ch2 id=\"the-four-problems\"\u003eThe four problems\u003c/h2\u003e\n\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eThe Backpack link was malformed.\u003c/strong\u003e The only documented format is\n\u003ccode\u003ehttps://backpack.app/ul/v1/browse/\u0026lt;url\u0026gt;?ref=\u0026lt;ref\u0026gt;\u003c/code\u003e — a universal link with\nthe target URL in the path and a required \u003ccode\u003eref\u003c/code\u003e. A custom-scheme guess like\n\u003ccode\u003ebackpack://ul/v1/browse?url=...\u003c/code\u003e matches no route the app registers, so the\nuser ends on the wallet\u0026rsquo;s install page.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eSolflare needs its universal link too:\u003c/strong\u003e\n\u003ccode\u003ehttps://solflare.com/ul/v1/browse/\u0026lt;url\u0026gt;?ref=\u0026lt;ref\u0026gt;\u003c/code\u003e, not the bare\n\u003ccode\u003esolflare://\u003c/code\u003e scheme. A bare scheme can launch the app without routing it —\nwhich is exactly \u0026ldquo;the app opens, but the site tab has to be opened by hand\u0026rdquo;.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eBoth parameters must be encoded.\u003c/strong\u003e \u003ccode\u003eurl\u003c/code\u003e is the full absolute dApp address\nand \u003ccode\u003eref\u003c/code\u003e is the requesting origin, each passed through \u003ccode\u003eencodeURIComponent\u003c/code\u003e.\nAn unencoded \u003ccode\u003e?\u003c/code\u003e or \u003ccode\u003e\u0026amp;\u003c/code\u003e in the target corrupts the parse, and the wallet\nopens on its home screen instead of the browser tab.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eThe trigger matters as much as the link.\u003c/strong\u003e 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 \u0026ldquo;fails\u0026rdquo; during testing.\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"the-documented-formats\"\u003eThe documented formats\u003c/h2\u003e\n\n\u003cul\u003e\n\u003cli\u003ePhantom: \u003ccode\u003ehttps://phantom.app/ul/browse/\u0026lt;url\u0026gt;?ref=\u0026lt;ref\u0026gt;\u003c/code\u003e — no \u003ccode\u003e/v1\u003c/code\u003e in this\none.\u003c/li\u003e\n\u003cli\u003eSolflare: \u003ccode\u003ehttps://solflare.com/ul/v1/browse/\u0026lt;url\u0026gt;?ref=\u0026lt;ref\u0026gt;\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003eBackpack: \u003ccode\u003ehttps://backpack.app/ul/v1/browse/\u0026lt;url\u0026gt;?ref=\u0026lt;ref\u0026gt;\u003c/code\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eOne pattern serves all three:\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003econst WALLET_BROWSE = {\n  phantom: (url, ref) =\u0026gt;\n    `https://phantom.app/ul/browse/${url}?ref=${ref}`,\n  solflare: (url, ref) =\u0026gt;\n    `https://solflare.com/ul/v1/browse/${url}?ref=${ref}`,\n  backpack: (url, ref) =\u0026gt;\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\u003c/code\u003e\u003c/pre\u003e\u003ch2 id=\"triggering-the-link-so-ios-and-android-accept-it\"\u003eTriggering the link so iOS and Android accept it\u003c/h2\u003e\n\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eRender a real anchor, precomputed.\u003c/strong\u003e A plain\n\u003ccode\u003e\u0026lt;a href={walletBrowseLink('phantom')}\u0026gt;\u003c/code\u003e is the most reliable trigger on both\nplatforms.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eIf it must be programmatic\u003c/strong\u003e, assign \u003ccode\u003ewindow.location.href\u003c/code\u003e synchronously\ninside the tap handler — no \u003ccode\u003eawait\u003c/code\u003e, no \u003ccode\u003efetch\u003c/code\u003e, no \u003ccode\u003esetTimeout\u003c/code\u003e first. After\nasynchronous work the gesture context is gone, and iOS falls back to the\nwallet\u0026rsquo;s website. Never \u003ccode\u003ewindow.open\u003c/code\u003e.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eNever test by pasting into the address bar.\u003c/strong\u003e Universal links deliberately\ndo not fire there; test with a tapped link or a QR code scanned by the\ncamera.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eMind the messenger webviews.\u003c/strong\u003e Opened inside Telegram\u0026rsquo;s or Instagram\u0026rsquo;s\nin-app browser, universal links are frequently swallowed and the wallet\u0026rsquo;s\nplain website loads instead. User-agent detection is heuristic at best, so\nalso give users a visible escape hatch: \u0026ldquo;open in Safari or Chrome, then\nconnect\u0026rdquo;.\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"the-bigger-fix-on-android\"\u003eThe bigger fix on Android\u003c/h2\u003e\n\n\u003cp\u003eHand-rolled deep links are the iOS story. On Android, Solana Mobile\u0026rsquo;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 \u003ccode\u003e@solana/wallet-adapter-react\u003c/code\u003e 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.\u003c/p\u003e\n\u003ch2 id=\"verifying-on-a-device\"\u003eVerifying on a device\u003c/h2\u003e\n\n\u003col\u003e\n\u003cli\u003eReal device, wallet installed, link opened from the system browser — not\nfrom a messenger.\u003c/li\u003e\n\u003cli\u003eTap a rendered link or scan a QR code; never paste into the address bar.\u003c/li\u003e\n\u003cli\u003eConfirm the wallet opens and the dApp loads in its in-app browser tab — the\nsecond half is the part that fails.\u003c/li\u003e\n\u003cli\u003eRepeat without the wallet installed: the universal link should degrade to\nthe wallet\u0026rsquo;s website. That page appearing while the app is installed means\nthe link or the trigger is still wrong.\u003c/li\u003e\n\u003cli\u003eThen test the messenger path, and add the \u0026ldquo;open in browser\u0026rdquo; hint if it\nfails there.\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"sources\"\u003eSources\u003c/h2\u003e\n\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://docs.phantom.com/phantom-deeplinks/deeplinks-ios-and-android\"\u003ePhantom: deep links on iOS and Android\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://docs.solflare.com/solflare/technical/deeplinks/other-methods/browse\"\u003eSolflare: the Browse deep link\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://docs.backpack.app/deeplinks/other-methods/browse\"\u003eBackpack: the Browse deep link\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://docs.solanamobile.com/mobile-wallet-adapter/mobile-apps\"\u003eSolana Mobile: Mobile Wallet Adapter\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n","date_published":"2026-08-10T00:00:00Z","date_modified":"2026-09-13T01:45:50+02:00","language":"en"}]}