<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Simon Mayes — Type II Fun</title><description>Writing by Simon Mayes on software quality, developer experience, and the slow work of building Untether.</description><link>https://type2fun.net</link><language>en-gb</language><item><title>Software Quality Is Not Cyber Security</title><link>https://type2fun.net/software-quality-is-not-cyber-security</link><guid isPermaLink="true">https://type2fun.net/software-quality-is-not-cyber-security</guid><description>AI gets you functional code. Quality-conscious prompting gets you further. But that&apos;s only 30% of the security controls — the other 70% aren&apos;t in your codebase.</description><pubDate>Mon, 27 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; AI is great at writing functional code. Thoughtful prompting pushes it further — reliability, maintainability, security. But even nailing all nine ISO 25010 quality characteristics addresses only 30% of security controls. The other 70% aren’t in the codebase at all.&lt;/p&gt;
&lt;h2&gt;Building a Functional App&lt;/h2&gt;
&lt;p&gt;More people than ever are creating software. If you know your product domain, you can team up with GenAI and start writing code. Whether you’re vibe-coding all the way and never read a line, or whether you meticulously review every change and understand it all — chances are that you’ll create a highly functional app That Works on My Your Machine™️.&lt;/p&gt;
&lt;p&gt;Next you want to share or monetise the app. You might consider — is my app of sufficient quality to put on the public internet?&lt;/p&gt;
&lt;h2&gt;Software Quality&lt;/h2&gt;
&lt;p&gt;If you’ve read my blog before you’ll know I care about software quality. You might think the next step is to take ISO 25010:2023 as a prompt and get GenAI to fix all of your issues. Give it a go — ask it to interview you and help discover all your Non-Functional Requirements and come up with a plan for addressing them.&lt;/p&gt;
&lt;h2&gt;Infrastructure&lt;/h2&gt;
&lt;p&gt;As you go through the NFRs you realise they feel out of your control. You’ve written the application code but some of the software quality characteristics are not application code — particularly reliability and security.&lt;/p&gt;
&lt;p&gt;Who monitors the app 24/7, runs the database, ensures the servers are healthy and can handle the scale? Who secures the firewall, patches the software and manages access control?&lt;/p&gt;
&lt;h2&gt;The Realisation&lt;/h2&gt;
&lt;p&gt;You start to realise that writing application code is actually a relatively small part of operating high-quality software, that infrastructure is equally important and that security is a broad subject.&lt;/p&gt;
&lt;p&gt;If you look into Cyber (or Information) Security you will see that there are many competing frameworks/standards: Cyber Essentials, ISO 27001, SOC 2, etc. and that they appear to explode in scope to include Physical Environment, People and Organisational Controls.&lt;/p&gt;
&lt;p&gt;You will realise that most of these controls aren’t about code at all. They are about firewalls, policies, training, supplier contracts. Things that you cannot fix with a better prompt.&lt;/p&gt;
&lt;p&gt;I did some analysis of the standards. I mapped 77 canonical security controls across eight industry frameworks — Cyber Essentials, ISO 27001, NIST CSF, NIST SSDF, CIS Controls, SOC 2, NCSC CAF and ISO 42001. Then I classified each control by where it lives: application, infrastructure or organisational.&lt;/p&gt;
&lt;p&gt;The results may surprise you!&lt;/p&gt;
&lt;h2&gt;30%&lt;/h2&gt;
&lt;pre class=&quot;mermaid&quot;&gt;pie title 77 Security Controls by Layer
    &quot;Application: 23 (30%)&quot; : 23
    &quot;Infrastructure: 19 (25%)&quot; : 19
    &quot;Organisational: 35 (45%)&quot; : 35&lt;/pre&gt;
&lt;p&gt;An AI-assisted, quality-focused codebase covers less than a third of the security picture. The three layers:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Application:&lt;/strong&gt; your code, your dependencies, your tests — secure coding, input validation, code review, dependency management, threat modelling&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Infrastructure:&lt;/strong&gt; platform, network, configuration, endpoints — firewalls, patching, antimalware, encryption, backups&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Organisational:&lt;/strong&gt; people, governance, legal, operations — policies, training, incident response, supplier management, background checks&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;What’s NOT in the Prompt&lt;/h2&gt;
&lt;p&gt;This is the part that stopped me cold. No prompt fixes these. No code covers them:&lt;/p&gt;

























































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;What’s missing&lt;/th&gt;&lt;th&gt;Why it matters&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Security awareness training&lt;/td&gt;&lt;td&gt;Human factor&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Incident response plans&lt;/td&gt;&lt;td&gt;When things go wrong&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Supplier due diligence&lt;/td&gt;&lt;td&gt;Third-party risk&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Background checks&lt;/td&gt;&lt;td&gt;Insider threat&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Physical security&lt;/td&gt;&lt;td&gt;Device theft&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Business continuity planning&lt;/td&gt;&lt;td&gt;Keeping the lights on&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Risk assessment&lt;/td&gt;&lt;td&gt;Knowing what to protect&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Termination procedures&lt;/td&gt;&lt;td&gt;Access revocation&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Privacy notice&lt;/td&gt;&lt;td&gt;Saying what you collect&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Consent and lawful basis&lt;/td&gt;&lt;td&gt;Permission to process&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Data subject rights&lt;/td&gt;&lt;td&gt;Access, correction, deletion&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Breach notification&lt;/td&gt;&lt;td&gt;A 72-hour clock&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;You can tell Claude to write secure code all day long. It won’t help when someone clicks a phishing link. It won’t help when a contractor keeps access after leaving. It won’t help when the S3 bucket is public because someone misconfigured it in the console. And it won’t help when the droplet you deployed to eight months ago is three versions behind on security patches.&lt;/p&gt;
&lt;p&gt;Even with gold-standard code, compromises come from phishing, credential theft, misconfiguration, insider threats and software you didn’t write. The application layer is necessary — but it’s not where most attacks land.&lt;/p&gt;
&lt;div&gt;
&lt;p&gt;The Reality of Breaches&lt;/p&gt;
&lt;p&gt;The most common way in is an unpatched vulnerability — and it’s almost never in code you wrote. It’s the OS packages, the container base image, the database you installed and never touched again. Spin up a droplet, ship the app, forget the box exists. Add stolen credentials, phishing and misconfiguration on top, and the shape is clear. Code can be flawless and you still get owned. (&lt;a href=&quot;https://www.verizon.com/business/resources/reports/dbir/&quot;&gt;Verizon 2026 Data Breach Investigations Report&lt;/a&gt;)&lt;/p&gt;
&lt;/div&gt;
&lt;h2&gt;Risk Acceptance&lt;/h2&gt;
&lt;p&gt;Unless you’re an enterprise with a GRC team, you’re not going to do all 77 controls. That’s not the point.&lt;/p&gt;
&lt;p&gt;The point is knowing they exist — and consciously deciding which ones you’re accepting risk on. There’s a difference between “we’ve looked at background checks and accepted the risk” and “wait, background checks are a security control?”&lt;/p&gt;
&lt;p&gt;If the app never leaves &lt;code&gt;localhost:3000&lt;/code&gt;, there’s not much to argue about. No deployment, no infrastructure, no attack surface. Nothing at stake, nothing to protect.&lt;/p&gt;
&lt;p&gt;The moment it’s shared, the stakes change:&lt;/p&gt;

























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Stage&lt;/th&gt;&lt;th&gt;What’s at stake&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Personal&lt;/strong&gt; (localhost)&lt;/td&gt;&lt;td&gt;Nothing&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Shared&lt;/strong&gt; (friends, colleagues)&lt;/td&gt;&lt;td&gt;Reputation&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Public&lt;/strong&gt; (users on the internet)&lt;/td&gt;&lt;td&gt;Trust, personal data, legal duties&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Commercial&lt;/strong&gt; (customer data, payments)&lt;/td&gt;&lt;td&gt;Money, contractual liability, career&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Note where the legal duties start. UK GDPR attaches the moment you hold real people’s personal data — not the moment you start charging for it. A lawful basis for processing, a privacy notice, subject access requests, a 72-hour breach notification clock: all of that lands on the free side of the line. Privacy is the second-largest group of controls in this whole set, and not one of them is code.&lt;/p&gt;
&lt;p&gt;Higher stakes don’t mean you need all 77 controls. They mean you need to &lt;em&gt;consciously&lt;/em&gt; decide which risks you’re accepting — rather than accidentally skipping controls you didn’t know existed.&lt;/p&gt;
&lt;p&gt;The jump from localhost to deployment is where the infrastructure layer kicks in. That can be manual — click-ops in a cloud console, hoping the security groups are locked down — or automated with infrastructure as code (Terraform, Helm, CI/CD). Either way, it’s a different discipline. The AI wrote the app. It didn’t write the deployment.&lt;/p&gt;
&lt;p&gt;And even with IaC for everything, you’re now a platform engineer as well as a product developer. Different skills, different concerns, different controls.&lt;/p&gt;
&lt;p&gt;Building secure platforms is hard and can’t be an afterthought once you’ve built your application. If you don’t have a dedicated platform team, buy rather than build — a PaaS like Vercel or Render transfers a huge chunk of your infrastructure controls to a mature, full-time provider. That’s not avoiding security; it’s transferring risk to someone better equipped to handle it.&lt;/p&gt;
&lt;h2&gt;The Framework Landscape&lt;/h2&gt;
&lt;p&gt;Maybe this is just my selection of frameworks? Maybe I picked ones that happen to be infrastructure-heavy?&lt;/p&gt;
&lt;p&gt;So I broke it down. Different frameworks emphasise different layers — some are code-focused, some are governance-focused, some are infrastructure-focused. Here’s how each of the eight frameworks breaks down:&lt;/p&gt;













































































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Framework&lt;/th&gt;&lt;th&gt;Controls&lt;/th&gt;&lt;th&gt;App&lt;/th&gt;&lt;th&gt;Infra&lt;/th&gt;&lt;th&gt;Org&lt;/th&gt;&lt;th&gt;Focus&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;NIST SSDF&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;16&lt;/td&gt;&lt;td&gt;88%&lt;/td&gt;&lt;td&gt;6%&lt;/td&gt;&lt;td&gt;6%&lt;/td&gt;&lt;td&gt;Secure software development&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;CIS Controls&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;36&lt;/td&gt;&lt;td&gt;47%&lt;/td&gt;&lt;td&gt;39%&lt;/td&gt;&lt;td&gt;14%&lt;/td&gt;&lt;td&gt;Technical hygiene&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;NCSC CAF&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;30&lt;/td&gt;&lt;td&gt;43%&lt;/td&gt;&lt;td&gt;33%&lt;/td&gt;&lt;td&gt;23%&lt;/td&gt;&lt;td&gt;Critical infrastructure&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;ISO 42001&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;13&lt;/td&gt;&lt;td&gt;38%&lt;/td&gt;&lt;td&gt;8%&lt;/td&gt;&lt;td&gt;54%&lt;/td&gt;&lt;td&gt;AI systems governance&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;ISO 27001&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;65&lt;/td&gt;&lt;td&gt;31%&lt;/td&gt;&lt;td&gt;29%&lt;/td&gt;&lt;td&gt;40%&lt;/td&gt;&lt;td&gt;Comprehensive ISMS&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;NIST CSF&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;41&lt;/td&gt;&lt;td&gt;22%&lt;/td&gt;&lt;td&gt;46%&lt;/td&gt;&lt;td&gt;32%&lt;/td&gt;&lt;td&gt;Risk management framework&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;SOC 2&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;50&lt;/td&gt;&lt;td&gt;20%&lt;/td&gt;&lt;td&gt;22%&lt;/td&gt;&lt;td&gt;58%&lt;/td&gt;&lt;td&gt;Trust services criteria&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Cyber Essentials&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;12&lt;/td&gt;&lt;td&gt;0%&lt;/td&gt;&lt;td&gt;100%&lt;/td&gt;&lt;td&gt;0%&lt;/td&gt;&lt;td&gt;UK baseline certification&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;&lt;em&gt;Percentages are rounded to the nearest whole number, so rows may not sum to exactly 100.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Look at the spread. NIST SSDF — the Secure Software Development Framework — is 88% application-focused. If I’m building software and want code-level guidance, that’s where it lives. It’s literally in the name.&lt;/p&gt;
&lt;p&gt;But NIST SSDF is the outlier. Most frameworks aren’t about code.&lt;/p&gt;
&lt;p&gt;Cyber Essentials is 100% infrastructure — firewalls, patching, secure configuration, endpoint protection. The UK government’s baseline certification for organisations doesn’t require writing a single line of code. You could pass Cyber Essentials with an off-the-shelf WordPress site, as long as your infrastructure is locked down.&lt;/p&gt;
&lt;p&gt;SOC 2 is 58% organisational — policies, governance and operational controls for service providers. It cares about how you run your business, not how you write your code.&lt;/p&gt;
&lt;p&gt;ISO 27001 — the comprehensive one, the gold standard for information security management — reaches more of these controls than any other framework here, 65 of the 77, and still puts under a third of them in the application layer.&lt;/p&gt;
&lt;p&gt;The point isn’t that one framework is right. It’s that the broadest three here — ISO 27001, SOC 2 and NIST CSF, the ones trying to cover a whole organisation — put 31%, 20% and 22% into the application layer. The frameworks that score higher are the narrow ones: SSDF is a secure-development standard. Breadth and code-focus pull against each other.&lt;/p&gt;
&lt;p&gt;One caveat on the maths: counting controls measures breadth, not effort or risk. “Secure coding practices” is one control. So is “background checks”. They aren’t equivalent in cost, complexity or how much they reduce your exposure. The 30% is a map of where the controls live — not a budget for where to spend your time.&lt;/p&gt;
&lt;p&gt;One specific worth naming. Cyber Essentials’ user access control theme is counted as infrastructure rather than application, because it certifies how an organisation administers accounts rather than how developers write auth code — that single decision is what produces the 0%.&lt;/p&gt;
&lt;h2&gt;The Path Forward&lt;/h2&gt;
&lt;pre class=&quot;mermaid&quot;&gt;flowchart TB
    app[&quot;&amp;lt;b&amp;gt;Layer 1: Application — 30%&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;Secure coding · Dependency management&amp;lt;br/&amp;gt;Security testing · Code review&quot;]
    infra[&quot;&amp;lt;b&amp;gt;Layer 2: Infrastructure — 25%&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;Firewalls &amp;amp; network · Patch management&amp;lt;br/&amp;gt;Secure configuration · Endpoint protection&quot;]
    org[&quot;&amp;lt;b&amp;gt;Layer 3: Organisational — 45%&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;Policies &amp;amp; governance · People &amp;amp; training&amp;lt;br/&amp;gt;Supplier management · Incident response&quot;]
    app --&amp;gt; infra --&amp;gt; org&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Layer 1 — Application:&lt;/strong&gt; Quality code is essential. This is where AI-assisted development lives. Secure coding practices, dependency management, testing, code review. Necessary — but layer one.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Layer 2 — Infrastructure:&lt;/strong&gt; Firewalls, patching, secure configuration, antimalware. Without this, code quality is almost irrelevant.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Layer 3 — Organisational:&lt;/strong&gt; Policies, training, incident response, supplier management, governance. The human and process layer that wraps everything else.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;The Landing&lt;/h2&gt;
&lt;p&gt;AI gets you functional.&lt;/p&gt;
&lt;p&gt;Quality-conscious prompting gets you further.&lt;/p&gt;
&lt;p&gt;Neither gets you secure.&lt;/p&gt;
&lt;p&gt;High-quality software is necessary. It’s not sufficient. The codebase is less than a third of the security picture — and for baseline security, it might not even be the first third to focus on.&lt;/p&gt;
&lt;p&gt;When someone asks you to “review the code and confirm it’s secure,” the code was never the whole question.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;em&gt;Two interactive tools sit behind this analysis. &lt;a href=&quot;https://type2fun.net/software-quality-meets-cyber-security&quot;&gt;Software Quality Meets Cyber Security&lt;/a&gt; maps 77 canonical controls across eight frameworks to ISO 25010 quality characteristics. &lt;a href=&quot;https://type2fun.net/security-controls-by-layer&quot;&gt;Security Controls by Layer&lt;/a&gt; compares how each framework splits across application, infrastructure and organisational controls.&lt;/em&gt;&lt;/p&gt;</content:encoded><category>software-quality</category><category>security</category><category>ai</category></item><item><title>Keep the screen in your pocket</title><link>https://type2fun.net/keep-the-screen-in-your-pocket</link><guid isPermaLink="true">https://type2fun.net/keep-the-screen-in-your-pocket</guid><description>Don&apos;t ditch the smartphone — reconfigure your relationship with it. Use a constrained smartwatch, shift essential interaction to it, and keep your phone out of sight.</description><pubDate>Fri, 19 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; Don’t ditch the smartphone; reconfigure your interaction model with it. Use a constrained smartwatch (one that doesn’t pretend to be a smartphone on your wrist). Shift essential functionality to a &lt;em&gt;gateway device&lt;/em&gt;; put the phone down and keep it out of sight.&lt;/p&gt;
&lt;h2&gt;The Thesis&lt;/h2&gt;
&lt;p&gt;A constrained smartwatch is currently the best way to rebalance your relationship with your smartphone. Intentionally use technology and keep the attention-seeking smartphone out of sight. Thoughtfully designed software can enable you to use a smartwatch as a respectful &lt;em&gt;gateway device&lt;/em&gt;, stay appropriately connected, achieve utility and reduce reflexive smartphone usage.&lt;/p&gt;
&lt;h3&gt;Why the constrained smartwatch&lt;/h3&gt;
&lt;p&gt;Watches are the most unintrusive technology available today, originally they just passively displayed the time. Aesthetically they’re timeless, completely unremarkable and by default the screen is not in your eyeline. You have to move your wrist into an effective stress position to interact with them. They can run software, accept input (buttons, touch, mic) and return an output (screen, haptic, sound). You could create attention-seeking apps for them, but due to the limited screen, ergonomics, and physical discomfort of extended use, you’d get arm ache and no dopamine hit. The friction is a feature, not a bug.&lt;/p&gt;
&lt;p&gt;But if the functionality is genuinely valuable — responding to an urgent message, looking at a schedule, pulling a note out of memory, asking AI a closed question — you’ll do it. Happily. Then you’ll drop your arm down and get on with your life. That is the sweet spot almost nobody is building for. Every other screen in your life is optimised to keep you on it. Infinite scroll, autoplay, pull-to-refresh, notification badges. Your watch is the opposite. It’s physically optimised to get you off it.&lt;/p&gt;
&lt;h3&gt;Why keep the smartphone&lt;/h3&gt;
&lt;p&gt;Smartphones are amazing and extremely useful, and no other devices can replace the functionality that they bestow. There are dozens of critical apps that you use weekly: messaging (WhatsApp), banking, password managers that you cannot really live without unless you seek extreme digital minimalism and discomfort. They’re excellent at letting you consume media, information, and… social media. This is the challenge: it takes discipline to separate utility and consumption, but if you have a way to keep it out of your hand, consumption can become deliberate.&lt;/p&gt;
&lt;p&gt;I’m a software engineer. I’m not a Luddite. I like my phone — for watching videos, reading articles, browsing maps, taking photos. The phone is great at those things.&lt;/p&gt;
&lt;p&gt;What I don’t like is reaching for it to check the weather and surfacing 40 minutes later wondering what just happened. The phone isn’t the problem. The compulsion is the problem. We use our phones reflexively — and once it’s in your hand, you’ve already lost.&lt;/p&gt;
&lt;h2&gt;How we got here&lt;/h2&gt;
&lt;h3&gt;Passive feature phones and gadget era&lt;/h3&gt;
&lt;p&gt;Before the emergence of proper smartphones we had limited connectivity and loads of gadgets. I think at one point in the late 90s/early 00s my Every Day Carry (EDC) included at various times a pager, walkman (tape/CD), noname MP3 player, dictaphone, compact camera (film and then digital), calculator/torch watches etc. It started with a Nokia nk402 (5110) and I worked my way through a 3310 (which came with an amazing MP3 player btw the HDR-1), a Razr and probably a few others. WAP appeared but was barely functional and none of this technology imposed on my life. I recall long conversations with girlfriends, friends and family and the functional exchange of SMSs. (MMSs weren’t bundled in the UK and were expensive.) All these gadgets were passive and not attention seeking.&lt;/p&gt;
&lt;p&gt;A step change came when I bought a Sony Ericsson K800i (original firmware, my favourite phone of all time). It had email, basic web, a decent camera (for the time), expandable memory (M2), tethering and music/FM radio. This changed my EDC to a phone and an A-Z pocket map (navigation apps weren’t a thing yet). Armed with a laptop (via tethering) I definitely felt more connected with more frequent email, consuming news via basic web and even have a mental image of myself on a public bus browsing the internet on my laptop (why?!). Texting had overtaken calls and conversations.&lt;/p&gt;
&lt;h3&gt;Smartphone era (no gadgets)&lt;/h3&gt;
&lt;p&gt;Then came the iPhone. The iPhone crept up on me slowly. I recall touch and non-touch smartphones of the era; they were cool and powerful but the UX sucked. Why didn’t Apple just stick a SIM card in the iPod? Well, they did, but also removed the stylus, introduced multitouch and revolutionised the UX. I let the 1st gen iPhone pass me by but bought the 2nd gen iPhone 3G. My EDC changed again… now it was just the iPhone. Smartphone apps replaced all the gadgets and life evolved to staring and stroking the slab. In the early years with basic apps and limited notifications I didn’t notice much of a change until I broke my iPhone.&lt;/p&gt;
&lt;h3&gt;Loss of control&lt;/h3&gt;
&lt;p&gt;It was only when my smartphone died that I realised how much it had crept into my life, become an extension of me and use became a reflex. This is not a unique insight but it was the first time that I paused, and considered my smartphone relationship.&lt;/p&gt;
&lt;p&gt;Most apps are free — which means you are the product. Most apps’ incentives are to maximise use/consumption. This is so they can profile you and collect data to better target ads and “improve” their products, which is recursive. Social networks evolved into social media. Short form content became the norm, allowing companies to better A/B test and improve their algorithm. This made tech companies fabulously wealthy but has had significant negative effects on us — every pause in life was replaced with a moment of consumption. The incentives are not aligned — the smartphone was serving the app companies. Always-on communication, through the messengers (WhatsApp, Facebook messenger etc.), raised everyone’s expectation of a near-instant response, and the social pressure to engage.&lt;/p&gt;
&lt;p&gt;Screen time became a noun. Most people know they use their smartphones too much but doomscrolling short-form content is dopamine rewarding and we unconsciously fill every gap of the day with a hit. The notification itself takes two seconds. The phone keeps you for fifteen minutes.&lt;/p&gt;
&lt;h3&gt;The Pushback&lt;/h3&gt;
&lt;p&gt;I had a decision. Replace the iPhone or adopt a dumbphone. Luckily I already had a feature phone available as I’m a climber and those early smartphones were too fragile to take up a mountain. Out came a proper dumbphone, my Nokia 105. About as simple as you can get: SMS and calls, nothing else, doesn’t even import vCard contacts, so I had to manually copy my contacts. I think I lasted 1 week. WhatsApp in my circle was a must, I used it professionally and with friends. No internet is extreme: I couldn’t even tether it to my laptop for emergency use. It may have had FM radio and basic mp3 player but I’m a Spotify guy so that was also tough. Initially I kept on picking up my phone in moments of pause but rapidly put it down when there was nothing to do, over the week this reflex faded. Adopting a dumbphone is extreme and not practical. Was there a compromise available?&lt;/p&gt;
&lt;p&gt;I then doubled down and committed, so ironically I bought a smarter feature phone, a new Nokia 800 Tough. Surely KaiOS would save the day: it had WhatsApp (unfortunately no longer supported), tethering, email, contacts sync and an SD card slot. This time I loaded it up with some recently bought music. It worked and I tried really hard to persevere but it was buggy as hell. Email and contacts crashed daily and it was simply unworkable. It was better, and if it was workable I probably would have lasted longer but the tech wasn’t there yet. I returned it and bought an iPhone SE3.&lt;/p&gt;
&lt;p&gt;I then looked into software app blockers and deduced that they’re either wrapping Apple’s native Content &amp;amp; Privacy Restrictions or a VPN loopback (I wasn’t aware of hardware app blockers like Brick or Bloom at the time). I already had a VPN and didn’t want to configure a random app to MITM all my communications, so I bypassed the wrappers and used the native platform controls. I also did the classic changes to make the phone less attractive: switch to greyscale/monochrome, reduced motion, optimised notifications, deleted apps, optimised the home screen etc. Initially this worked but over time I found the native controls too naive and coarse and found an increasing willingness to toggle them off.&lt;/p&gt;
&lt;p&gt;Another compromise is the minimalist phone (Light Phone, Mudita Kompakt, Punkt, etc.). These are intentionally stripped-back Android phones with lo-fi screens, and some swap touch for buttons. A subset let you sideload third-party apps, with difficulty. This sounds like a great escape hatch: a lo-fi phone running your “essential” apps. But in practice it doesn’t work. They’re de-Googled, built on the Android Open Source Project (AOSP) without Google Play services, and the critical apps you need (banking, password managers) are exactly the ones whose vendors don’t support, or actively block, unsupported platforms. This was true in my case, so I haven’t explored them much further.&lt;/p&gt;
&lt;p&gt;The most effective behaviour that worked for me was to buy a nice wireless charging dock, stick it by my front door, and not have a smartphone on my person at home. I would just suck up smartphone use when out and about. Could a behaviour or environmental change rather than willpower alone reduce phone usage?&lt;/p&gt;
&lt;h3&gt;The moment&lt;/h3&gt;
&lt;p&gt;The moment came when I upgraded my Suunto Ambit to a Garmin Fenix 5 Sapphire and carelessly left “Smart Notifications” enabled. To be clear I never wanted notifications on my wrist and didn’t even realise that this feature existed but it changed my relationship with my phone forever.&lt;/p&gt;
&lt;p&gt;You would think that WhatsApp and other notifications on your wrist would be toxic and worse but it’s the opposite. You receive a trash notification from some WhatsApp group you love to hate — your wrist vibrates, you skim the notification (sometimes just the contact/group name), and you can immediately ignore it in less than a second without changing context. You can triage it immediately without cost. It gets noisy, so you just set Do Not Disturb (DND) on the watch for a bit. All without reaching for your phone. The alternative: your phone vibrates in your pocket. You have no idea of the source or priority, so you take it out, unlock it, and open the app. It was indeed junk, but the phone is now in your hand, so you have a quick look across WhatsApp, maybe another notification source. Slip into social media and you’ve lost. You’ve switched context, lost 20 minutes, and the personal cost is huge for zero reward.&lt;/p&gt;
&lt;p&gt;Most notifications don’t need a reply. They need a glance and a dismiss. When your phone buzzes in your pocket, you pull it out, unlock it, read it, and now you’re holding your phone with the screen on. You’re already context-switching. With the watch, you glance for less than a second, see it’s just your mates sharing photos in a group chat, and you’re back to what you were doing. No unlock, no phone in hand, no distraction spiral. The interaction is so short it doesn’t break your focus.&lt;/p&gt;
&lt;p&gt;Could a smartwatch be a gateway device to keep the smartphone out of my hand?&lt;/p&gt;
&lt;h3&gt;Gadgets - they’re now called wearables&lt;/h3&gt;
&lt;p&gt;I wanted to lean into the smartwatch and find the opportunity to leave my phone behind more. So I upgraded to the Garmin Fenix 7x. This gave me contactless payments (utility) and Spotify (screenless consumption). I could now regularly leave my phone behind when exercising and going out for short solo outings. It also had a torch (which is wildly useful) and the go-go gadget watch was back!&lt;/p&gt;
&lt;p&gt;After discovering the enabler of a smartwatch I wanted to look into the vast world of gadgets and wearables and see whether, perversely, they could help me adopt digital minimalism through reduced smartphone usage. My assessment criteria were: are they remarkable, behaviourally attractive/triggering, and goal (gateway device) enabling?&lt;/p&gt;
&lt;h4&gt;Capabilities&lt;/h4&gt;
&lt;p&gt;Wearables have three types of capability: input, output and compute. Inputs: camera, microphone, buttons, biometric, location and environmental data. Outputs: display, LED indicators, haptics, headphones and speakers. Compute: cloud, on-board, or via a companion (smartphone) app — giving you rules-based deterministic or AI-based probabilistic processing. Blending these capabilities gives you either a respectful and useful device or an attention-, anxiety- and privacy-disrespecting device. Often the capabilities have a sliding scale: displays run from MIP (memory-in-pixel)/e-ink (64 colours) — unremarkable, not attention seeking — to always-on OLED, AMOLED and heads-up displays (HUD), which are attention seeking and invasive. Cameras and microphones can be selective or always-on and either invasive or privacy respecting.&lt;/p&gt;
&lt;h4&gt;Glasses&lt;/h4&gt;
&lt;p&gt;Glasses (Meta, Solos) come with various capabilities: open-ear headphones, camera, Heads Up Display (HUD), voice, and AI (normally offloaded to companion app). Can any of these help reduce smartphone usage? I believe AI and voice is a great enabler (see below) but I feel a HUD and camera would result in overstimulation and not have a positive effect. The privacy issues and hostility from others ensure that you’re always aware of them, and do not help reduce technology use. They’re remarkable and conversation inducing. AR glasses (Apple Vision, XREAL) are not considered as they’re not wearables.&lt;/p&gt;
&lt;h4&gt;Pendants and pins&lt;/h4&gt;
&lt;p&gt;Pendants and pins (Humane, Plaud) can come with: mic, camera, haptics, voice and AI. In some ways they’re very similar to glasses. They’re mainly data-collection devices and lack outputs, which greatly reduces their capability to be a gateway device.&lt;/p&gt;
&lt;h4&gt;Fitness trackers and bands&lt;/h4&gt;
&lt;p&gt;Screenless fitness bands (Whoop, Amazfit, Fitbit) are purely dedicated to biometric, location and environmental data collection and therefore do not help with the goal. They are, however, unremarkable and more discreet than the above.&lt;/p&gt;
&lt;h4&gt;Smartrings&lt;/h4&gt;
&lt;p&gt;There are two types of smart ring: biometric data-collection sources (Oura, Ultrahuman) and functional devices (NEO, CNICK, Pebble Index 01). The data-collection rings are similar to the above, but the functional ones (mic, payments, NFC) are interesting. The latter could complement a dumbphone or perform some useful action that would keep your phone out of your hand.&lt;/p&gt;
&lt;h4&gt;Smartwatches&lt;/h4&gt;
&lt;p&gt;There are two types of smartwatch: ones attempting to be a smartphone on your wrist (Apple Watch, WearOS, and arguably Pebble) and more purposeful ones with genuine smartwatch capability (Garmin). Smartwatches generally have all the capabilities apart from a camera.&lt;/p&gt;
&lt;p&gt;A smartwatch that apes the smartphone — always-on, rich colour, feature-rich — is just a smaller attention machine on your wrist. Not what I’m after. Pebble is an interesting crossover: it’s attempting to be a smartphone on your wrist, but the e-ink display, buttons and battery physically hold it in the purposeful camp. The constraints are the feature.&lt;/p&gt;
&lt;p&gt;I’m drawn towards low-colour, non-backlit MIP/e-ink watches from Garmin and Pebble that can be utilised in a respectful way. They’re unremarkable, discreet and can be non-attention seeking.&lt;/p&gt;
&lt;h4&gt;Headphones (AI and voice)&lt;/h4&gt;
&lt;p&gt;LLMs have opened a huge capability to talk to our devices with natural language and respond in kind. Using voice alone is hugely aligned to my goals. However it does have problems. You cannot always wear headphones, closed-ear headphones are immersive/isolating, voice input/output is non-deterministic, not always efficient and talking to yourself is remarkable. I would prefer to supplement voice with a basic display and buttons to enable rapid interaction. Visual feedback is significantly faster than voice.&lt;/p&gt;
&lt;p&gt;An interesting aside: it appears OpenAI are working on open-ear AI headphones (see rumours on “Dime” and “Sweet Pea”). I doubt they’d be goal aligned but are one to watch (privacy/connectivity issues acknowledged).&lt;/p&gt;
&lt;h3&gt;Requirements&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;MUST&lt;/strong&gt; be unremarkable, have some form of input and output and largely out of sight&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SHOULD&lt;/strong&gt; have basic display for instantaneous information consumption, buttons and haptics for discreet and silent operation&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;COULD&lt;/strong&gt; have microphone for voice AI&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;WON’T&lt;/strong&gt; have a rich display, camera.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;There is only one wearable that fulfils these requirements.&lt;/p&gt;
&lt;h3&gt;Achieving a balance - the constrained smartwatch&lt;/h3&gt;
&lt;p&gt;It’s a balance between benefiting from modern technology and keeping your smartphone out of your hand.&lt;/p&gt;
&lt;h4&gt;The quadrant&lt;/h4&gt;
&lt;pre class=&quot;mermaid&quot;&gt;quadrantChart
    x-axis Loses modern tech --&amp;gt; Keeps modern tech
    y-axis Phone stays in hand --&amp;gt; Phone out of hand
    quadrant-1 Modern and hands-free
    quadrant-2 Hands-free but cut off
    quadrant-3 Neither
    quadrant-4 Modern but in-hand
    Dumbphone: [0.12, 0.80]
    Feature phone: [0.30, 0.78]
    Minimalist phone: [0.42, 0.73]
    Native platform controls: [0.52, 0.22]
    Software app blockers: [0.58, 0.30]
    Hardware app blockers: [0.64, 0.38]
    Mainstream smartwatch: [0.78, 0.13]
    Phone dock by the door: [0.80, 0.55]
    Constrained smartwatch:::gateway: [0.80, 0.84]
    classDef gateway color: #b85a2a, radius: 8&lt;/pre&gt;
&lt;p&gt;App blockers demand willpower while the phone stays in your hand. Dumbphones actually work, but you lose WhatsApp, maps, banking, boarding passes — everything modern life requires. The quadrant that’s both modern and hands-free is basically empty.&lt;/p&gt;
&lt;p&gt;I would personally recommend the Garmin Fenix 8 Solar, any older (or secondhand) Garmin MIP or a Pebble watch. A decent smartwatch (which specifically doesn’t attempt to be a smartphone on your wrist) enables you to keep your phone out of sight for the majority of the time, to be intentionally connected, and to perform quick actions, but notably keeps the phone out of your hand and significantly reduces screen time.&lt;/p&gt;
&lt;p&gt;Garmin and Pebble are uniquely placed to enable this space. The Garmin Fenix 8 Solar is the ideal device. It has a low-colour display, an onboard mic, contactless payments and music. My current EDC is a Fenix 7x, Pixel 10 Pro, and I hope to unlock voice with a Core Devices Index 01 (smart ring with mic) when it ships. I have other AMOLED and mic Garmin watches for app development, but my technology choice is intentional and considered. I do not want a rich-colour, backlit, attention-seeking display on my wrist.&lt;/p&gt;
&lt;p&gt;When your watch can handle messages, notes, and quick actions, reaching for your phone becomes a choice instead of a reflex. &lt;strong&gt;You pick it up when you want to — not because you have to. The phone goes back to being a consumption device, used deliberately for what it’s genuinely good at. Your watch handles the rest.&lt;/strong&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;em&gt;Disclosure: I build &lt;a href=&quot;https://untether.watch&quot;&gt;Untether&lt;/a&gt; — apps for Garmin watches so I can leave you can leave your phone out of sight.&lt;/em&gt;&lt;/p&gt;</content:encoded><category>digital-minimalism</category><category>wearables</category></item><item><title>Stop generating, start rendering: personal apps backed by your System of Record</title><link>https://type2fun.net/personal-apps-backed-by-your-system-of-record</link><guid isPermaLink="true">https://type2fun.net/personal-apps-backed-by-your-system-of-record</guid><description>AI spews static artefacts that rot the moment you edit. Stop duplicating — vibe-code disposable apps that render live views from your System of Record.</description><pubDate>Sun, 07 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;How fighting AI content-bloat pushed me to fix my System of Record — and got me a one-shot, dependency-aware roadmap that beats GitLab’s native date-based one.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;The trap&lt;/h2&gt;
&lt;p&gt;AI is amazing at rapidly generating huge volumes of content and mockups. This is a blessing and a curse which I immediately fell into. This all started with me trying to do a planning session. GitLab didn’t give me the roadmap view I wanted, so I never bothered to maintain all the epic metadata and relationships — I kept them in my head. I initially shared this extra hidden knowledge with AI and got it to cross-reference it with my KB and GitLab. It created a very pretty, accurate static HTML roadmap with the extra info included. But it hit me in the face that this data now only resides in this artefact and would immediately rot. Ideally it should live in GitLab (my SoR) or if it doesn’t fit there in my Knowledge Base.&lt;/p&gt;
&lt;p&gt;What’s the rule? If it can fit into your System of Record and is a canonical record, maintain it there. Do not duplicate.&lt;/p&gt;
&lt;div&gt;
&lt;p&gt;System of Record (SoR)&lt;/p&gt;
&lt;p&gt;The authoritative source of truth for a given set of data — the one place that holds the canonical copy, that everything else syncs from. For work items that’s your tracker (GitLab, Jira); for code, your Git forge. It’s about authority over data, not documentation — if two systems disagree, the SoR wins.&lt;/p&gt;
&lt;/div&gt;
&lt;h2&gt;The rot&lt;/h2&gt;
&lt;p&gt;Managing context and knowledge is hard and all AI wants to do is verbosely generate more. I’ve been managing mine in markdown and Obsidian for years — it’s not bad but it goes stale quickly, only grows, is not very well cross-referenced and frankly was a mess. I also maintained a project management system but it suffered from some of the same flaws and doesn’t quite work in the way I want it to.&lt;/p&gt;
&lt;div&gt;
&lt;p&gt;PKM &amp;amp; KB&lt;/p&gt;
&lt;p&gt;A &lt;strong&gt;Personal Knowledge Management&lt;/strong&gt; system (PKM) is where you keep your own notes and thinking — Obsidian, a pile of markdown, your “second brain”. A &lt;strong&gt;Knowledge Base&lt;/strong&gt; (KB) is the shared, team-facing version. Either way it’s human-readable knowledge — how things work, what you decided — not structured records.&lt;/p&gt;
&lt;/div&gt;
&lt;p&gt;AI needs high-quality context and access to your current (not stale) knowledge. AI also works best with strict spec-driven development with fixed defined scope and goals (that sounds a lot like epics and tickets…)&lt;/p&gt;
&lt;p&gt;Both you and AI need a common System of Record. AI can hit up the APIs to fetch the work items and you can fight the UI to maintain them.&lt;/p&gt;
&lt;h2&gt;The two UXes&lt;/h2&gt;
&lt;p&gt;The human UX on most tools is generally bad; bloated, slow and too many clicks (looking at you, Jira) or too lean and minimal (GitHub). I personally favour GitLab for completeness but I struggle to view the work items at a suitable resolution (roadmaps and strategy). The robot UX is either REST or GraphQL API, and AI is expert at both. GraphQL is optimal because the AI can pick and choose the exact data it needs and minimise token burn.&lt;/p&gt;
&lt;h2&gt;The payoff&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;The payoff: a live roadmap better than GitLab’s own, backed entirely by canonical SoR data — and building it made me improve the records themselves.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;So I vibe-coded a personal app. I knew GitLab Pages could do this quickly and securely — it has a decent API with public-client PKCE. And boom. It converted my roadmap into an app backed by canonical SoR data. Better still, building it made me improve the records: populating metadata, standardising templates, building relationships. &lt;strong&gt;This is something I would never have done via the GitLab UI — it’s too slow and it doesn’t always surface properly in my mental model.&lt;/strong&gt; But shouting at Claude with Wispr Flow is one of my favourite pastimes. Collaborating with AI is much more fun than doing it yourself.&lt;/p&gt;
&lt;p&gt;So I now had the visual aids that I needed, which were live, and AI had the rich data in the SoR. It was now less likely to rot, because the live view always reflects the SoR.&lt;/p&gt;
&lt;div&gt;
&lt;p&gt;Security&lt;/p&gt;
&lt;p&gt;Use OAuth and let your app consumers authenticate securely with their own credentials to access the app. This ensures that data stays secure and only the right people can access it. Do not save a copy of sensitive SoR data and share it outside your existing controls. Do not bake sensitive tokens or secrets into the frontend app.&lt;/p&gt;
&lt;p&gt;And mind the token &lt;em&gt;after&lt;/em&gt; login: PKCE protects the code exchange, not what you do with the token afterwards. Keep the access token in memory, never &lt;code&gt;localStorage&lt;/code&gt;. Request the narrowest scope that works (&lt;code&gt;read_api&lt;/code&gt;, only adding write where you genuinely need it — like updating a snippet). Your frontend is only as safe as your XSS and CSP posture, so treat both as existential.&lt;/p&gt;
&lt;/div&gt;
&lt;h2&gt;What else could you build?&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Decks/Presentations&lt;/strong&gt; vibe-code a private gitlab pages site and for each slide populate the slides with Title: &lt;code&gt;&amp;amp;epic .title&lt;/code&gt;, &lt;code&gt;&amp;amp;epic .description #Summary&lt;/code&gt;. Or &lt;code&gt;#ticket .note&lt;/code&gt;. Cross-link back to the canonical source so collaborators want to see the canonical source for more info.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Collaborative microsites&lt;/strong&gt; vibe-code a private gitlab pages site where each page is a ticket &lt;code&gt;#ticket .title&lt;/code&gt;, &lt;code&gt;.description&lt;/code&gt;, &lt;em&gt;label&lt;/em&gt; it as &lt;code&gt;page::/the-big-idea&lt;/code&gt;. Encourage collaborators to comment/edit on the tickets (auto capture the history/changes) and render them inline. Enable page interaction with (GitLab) ticket emoji reactions and project stars.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Workshop collaboration space&lt;/strong&gt; vibe-code a front-end for your project wiki (all wikis are ugly). Edit inline and push the changes back.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;em&gt;(&lt;code&gt;&amp;amp;epic&lt;/code&gt; and &lt;code&gt;#ticket&lt;/code&gt; are GitLab notation. &lt;code&gt;.attribute #title&lt;/code&gt; are made-up selectors for the record details etc.)&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Need printed material?&lt;/strong&gt; Print to PDF in the browser and make extensive use of &lt;code&gt;@media print&lt;/code&gt; CSS. Or need a different format, go bananas with &lt;code&gt;URL.createObjectURL(blob)&lt;/code&gt; or &lt;code&gt;window.showSaveFilePicker()&lt;/code&gt; and allow people to download dynamically generated artefacts where the canonical source is the SoR.&lt;/p&gt;
&lt;p&gt;It doesn’t matter that these personal apps might be short-lived. Code is now cheap — which is exactly why well-structured data and knowledge in your SoR is priceless.&lt;/p&gt;
&lt;h2&gt;Takeaways&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Identify your SoR (you likely already have one without fully appreciating it).&lt;/li&gt;
&lt;li&gt;Define what lives where and try to use the fewest SoR/data repositories.&lt;/li&gt;
&lt;li&gt;Define their boundaries, relationships and workflows. Vibe-code some apps backed by your SoR (you can hook up APIs to multiple) and ensure that data stays in one place.&lt;/li&gt;
&lt;li&gt;Do not create more static content.&lt;/li&gt;
&lt;li&gt;Be security conscious.&lt;/li&gt;
&lt;li&gt;Use frontend OAuth flow so data remains gated behind your existing auth.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;You, your team, and your AI will benefit.&lt;/strong&gt;&lt;/p&gt;
&lt;div&gt;
&lt;p&gt;Pixie apps&lt;/p&gt;
&lt;p&gt;After the initial draft of this post I wrote up the pattern separately at &lt;a href=&quot;https://type2fun.net/infinitely-scalable-personal-apps&quot;&gt;Vibe-Coding Shareable, Infinitely Scalable Personal Apps&lt;/a&gt;.&lt;/p&gt;
&lt;/div&gt;
&lt;h2&gt;The Build&lt;/h2&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;Claude build me a React SPA, use the GitLab API (implementing public PKCE - no secrets) and render all my epics as cards. Display labels and appropriate metadata. Match the epic and label colours.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Wow. That was easy. Now to sort them.&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;Claude I can&apos;t afford GitLab Ultimate (I only have Premium), so epics don&apos;t have parents. Instead I label parent epics as X and child epics as Y and link them as related items. I want to display parent epics as swimlanes. Child epics are linked as blocked or blocking. Fetch all epics and display them top left to bottom right sequenced in blocking order.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Boom. In 30 mins I have a roadmap based on dependencies that is significantly better than GitLab’s native date-based Roadmap.&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;Claude when I click on a child epic card, highlight the critical path and filter out swimlanes that are irrelevant.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Now I have a clear view of my critical path and a clear visual aid of where I am at.&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;Claude when I click on a card, bring in a ⅓ side panel on the right that has all the epic details.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;Claude find my epic templates and create a linter to highlight poorly written epics. Parse the description sections/components against the template, highlight missing items and add a warning glyph to the epic card.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;Claude fetch all my labels from GitLab and interview me to understand my workflow. Update the linter to show missing or noncompliant labels.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;I could go on ad nauseam. But hopefully you get the point. I now had the clearest view of my roadmap ever.&lt;/p&gt;
&lt;h3&gt;The Live View&lt;/h3&gt;
&lt;p&gt;I then went back to the static roadmap AI had built only a few hours earlier. It was already wrong — I’d added relationships and made edits since — while the live view was never out of date. Both my AI and I now work from the same current picture.&lt;/p&gt;
&lt;p&gt;I also stress this is only a &lt;strong&gt;view&lt;/strong&gt;. I am not trying to replace GitLab and build a competing UI. That is madness. I dictate with Wispr Flow to Claude for mass updates and refresh. I still use the GitLab UI for most tasks.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;I cannot overstate the value of this milestone. I now have my SoR up to date. I’m now motivated and have the tools to keep it up to date — and AI will consume exactly the same information.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;It’s now key to document the workflow so both you and AI are on the same page.&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;Claude update my user CLAUDE.md with the workflow in which I manage my System of Record (GitLab). Rehearse with me how you fetch and link work items and document API quirks.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;&lt;em&gt;* GitLab’s APIs are spread across REST and GraphQL, with deeply nested GraphQL structures, and it can be a challenge to fetch the right thing. Ordering work items on epics and boards takes a bit of experimentation.&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;Bonus: Shared State and Storage&lt;/h3&gt;
&lt;p&gt;I then continued to refine my backlog but it was difficult for my AI to understand what I was looking at and discussing.&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;Claude add a copy button to each work item that copies the GitLab work item reference to the clipboard.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Even this got a bit tedious after a while. So I wondered how I could give AI access to the page…&lt;/p&gt;
&lt;p&gt;I could bastardise a work item and labels in the SoR but I figured I’d eventually need some settings and state — so a GitLab snippet (same as a GitHub gist) would be just the thing.&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;Claude create a private user GitLab snippet and update it whenever I click on a work item. Whenever I talk to you about a work item, query the snippet for context.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Claude was running as a limited service account, so couldn’t access my private snippet. So I had to create a private project snippet instead.&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;Claude add a &quot;Share&quot; button, which creates a private user configuration project and an empty snippet. Add an option to add a service account as a Reporter member to the configuration project. When I click on a work item, update the snippet with the current item in focus. Whenever I talk to you about a work item, query the snippet for context.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Now you can store simple state and as a bonus share it with peers that access via the API.&lt;/p&gt;
&lt;p&gt;Hand written by me. Copy-editted by Claude. I used em-dashes before the slop, they’re my style and here to stay.&lt;/p&gt;</content:encoded><category>system-of-record</category><category>ai</category></item><item><title>Vibe-coding shareable, infinitely scalable personal apps</title><link>https://type2fun.net/infinitely-scalable-personal-apps</link><guid isPermaLink="true">https://type2fun.net/infinitely-scalable-personal-apps</guid><description>Build genuinely useful, AI-powered personal apps with no backend at all — a static site, OAuth PKCE to the providers you already use, and OpenRouter for the smarts.</description><pubDate>Sat, 06 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;PKCE for the backends and APIs you already trust: add OpenRouter to make them smart — all for free, or next to nothing.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I’ve built two personal apps with &lt;strong&gt;no backend at all&lt;/strong&gt; — &lt;a href=&quot;https://marshal.msyea.com&quot;&gt;marshal.msyea.com&lt;/a&gt; (GitLab roadmaps, my way) and &lt;a href=&quot;https://affinity.msyea.com&quot;&gt;affinity.msyea.com&lt;/a&gt; (understanding my music taste with Spotify and AI). No server, no database, no running costs: each user brings their own storage and their own AI spend, so my costs never grow — however many people use them. PKCE is pronounced &lt;em&gt;pixie&lt;/em&gt;, so that’s what I call them: &lt;strong&gt;Pixie apps&lt;/strong&gt;. Here’s the pattern.&lt;/p&gt;
&lt;pre class=&quot;mermaid&quot;&gt;flowchart LR
    you([You])
    subgraph host[&quot;Static host: GitLab / GitHub Pages&quot;]
        app[&quot;Pixie app (React + Vite SPA)&quot;]
    end
    provider[&quot;Provider API + your storage&quot;]
    ai[&quot;OpenRouter (AI models)&quot;]
    you --&amp;gt; app
    app --&amp;gt;|OAuth 2.0 PKCE| provider
    app --&amp;gt;|OAuth 2.0 PKCE| ai&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;No server in the middle — the static page in your browser talks straight to each provider, authenticated as you.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Most apps have a missing feature or a nit that’s hugely frustrating, or there’s a workflow you can’t quite achieve with what you already use. Building personal &lt;a href=&quot;https://www.inkandswitch.com/essay/local-first/&quot;&gt;local-first&lt;/a&gt;, self-hosted, or &lt;a href=&quot;https://unhosted.org/&quot;&gt;unhosted&lt;/a&gt; apps is a time-tested answer — and the architecture below makes it safe and quick.&lt;/p&gt;
&lt;div&gt;
&lt;p&gt;Personal apps aren’t production apps&lt;/p&gt;
&lt;p&gt;I write about &lt;a href=&quot;https://type2fun.net/software-quality-as-a-discipline&quot;&gt;software quality&lt;/a&gt; and take it
seriously — so to be clear, this is deliberately not that. Matching your engineering
rigour to the stakes is part of the discipline, and a personal app’s stakes are low:
with a careful architecture you can smash these out and happily skip the production
apparatus — tests, CI gates, observability, scale. The one thing you don’t get to skip
is security. These apps hold live tokens and a money-spending key, so that bar stays high.&lt;/p&gt;
&lt;/div&gt;
&lt;h2&gt;How to get the data?&lt;/h2&gt;
&lt;p&gt;Most APIs require a token. Most tokens are secrets. Managing and distributing them is difficult and requires proper infrastructure. Luckily there is another way to establish trust. Introducing the &lt;strong&gt;OAuth 2.0 Authorization Code flow with &lt;a href=&quot;https://datatracker.ietf.org/doc/html/rfc7636&quot;&gt;PKCE&lt;/a&gt; (Proof Key for Code Exchange), as a public client&lt;/strong&gt;. PKCE (without a secret — yes, I’m looking at you, GitHub) is the pixie dust that lets web apps fetch API tokens in the browser and hit up APIs directly. Static API tokens and other OAuth flows that require a client secret (a confidential client) are immediately dismissed. The Device Authorization Grant (device code) is considered, but the public PKCE flow is better UX (no copy/paste).&lt;/p&gt;
&lt;h2&gt;What type of app?&lt;/h2&gt;
&lt;p&gt;The public PKCE flow really points to a web app. I could consider a localhost or Electron-style app, but I want to be able to share the app easily and access it from multiple devices.&lt;/p&gt;
&lt;p&gt;My other constraints: I want zero-maintenance, minimal security concerns, and for it to be basically free — and I also want AI. This points towards a static site hosted somewhere, and the key AI unlock — OpenRouter supports the public-client PKCE flow.&lt;/p&gt;
&lt;h2&gt;Where to host?&lt;/h2&gt;
&lt;p&gt;The obvious vibe coder’s destination of choice would be GitHub, but i) I’m a GitLab guy, and ii) GitHub does not support the public-client PKCE flow (&lt;a href=&quot;https://github.com/orgs/community/discussions/15752&quot;&gt;community discussion&lt;/a&gt;). If you’re not intending to use GitHub as a backend (see below), GitHub Pages is still a great place to host.&lt;/p&gt;
&lt;h2&gt;How to build?&lt;/h2&gt;
&lt;p&gt;I would advocate using React+Vite because there is so much training data there and Claude (or your AI of choice) will just nail the scaffolding. If you’re an expert in another static site/frontend framework, or feeling fruity, use whatever you like.&lt;/p&gt;
&lt;h2&gt;Security&lt;/h2&gt;
&lt;p&gt;You’re holding sensitive API keys in the browser, so XSS is the risk that matters — if hostile JavaScript runs in your page, it can read anything the page can, tokens included. Be careful rendering third-party, unverified user-generated content (UGC), consider the blast radius of what you’re exposing, lock down a CSP, and escape/sanitise any HTML you consume from an API.&lt;/p&gt;
&lt;div&gt;
&lt;p&gt;XSS&lt;/p&gt;
&lt;p&gt;Cross-site scripting — an attacker getting their JavaScript to execute in your page, where it can read tokens from localStorage and call your APIs as you. It’s the main threat for a browser-only app.&lt;/p&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;p&gt;CSP&lt;/p&gt;
&lt;p&gt;Content Security Policy — a browser policy (set via a header or meta tag) that restricts what your page can load and, crucially, where it can send data. Locking &lt;code&gt;connect-src&lt;/code&gt; to just your providers is the control that contains a leaked token.&lt;/p&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;p&gt;Secrets&lt;/p&gt;
&lt;p&gt;Use OAuth with public-client PKCE and short-lived tokens. Do not hardcode sensitive tokens or secrets into your frontend app. Consider carefully what OAuth tokens you persist to localStorage.&lt;/p&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;p&gt;Access Control&lt;/p&gt;
&lt;p&gt;Your app consumers will authenticate with their own credentials. The API will resolve access controls automatically. Use minimal scopes where possible. Consider only fetching elevated scopes just-in-time before privileged actions.&lt;/p&gt;
&lt;/div&gt;
&lt;h2&gt;Storage&lt;/h2&gt;
&lt;p&gt;The obvious place to store data for a backendless static site is to use &lt;code&gt;localStorage&lt;/code&gt;, but I want this app to work across devices — so I do need some form of backend. Luckily there’s a huge choice. I have another requirement — your users bring their own account and use their own storage (free or otherwise). I don’t want any running costs if my app goes viral. I have therefore discounted Firebase/Supabase.&lt;/p&gt;
&lt;p&gt;Take &lt;strong&gt;Marshal&lt;/strong&gt;, my GitLab roadmap app. Its entire data layer is: log into GitLab with PKCE, then read and write a JSON blob to a snippet through the Snippet API — that persists state across all my devices. The app itself deploys as static files to GitLab Pages. No database, no server. (For &lt;strong&gt;Affinity&lt;/strong&gt; I’d extend the same idea to Google Drive.)&lt;/p&gt;
&lt;p&gt;Here are the providers I weighed against the same checklist — how you authenticate, where your data lives, and whether it needs a BFF (see below):&lt;/p&gt;



























































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Provider&lt;/th&gt;&lt;th&gt;AuthN&lt;/th&gt;&lt;th&gt;AuthZ&lt;/th&gt;&lt;th&gt;Client Type&lt;/th&gt;&lt;th&gt;BFF&lt;sup&gt;1&lt;/sup&gt;&lt;/th&gt;&lt;th&gt;Storage&lt;/th&gt;&lt;th&gt;Comments&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;GitLab&lt;/td&gt;&lt;td&gt;OIDC&lt;/td&gt;&lt;td&gt;OAuth 2.0 + PKCE&lt;/td&gt;&lt;td&gt;Public&lt;/td&gt;&lt;td&gt;No&lt;/td&gt;&lt;td&gt;Snippet or Repo File&lt;/td&gt;&lt;td&gt;Git Forge&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Google&lt;/td&gt;&lt;td&gt;OIDC&lt;/td&gt;&lt;td&gt;OAuth 2.0 + PKCE&lt;/td&gt;&lt;td&gt;Public&lt;/td&gt;&lt;td&gt;No&lt;/td&gt;&lt;td&gt;App Data Folder&lt;/td&gt;&lt;td&gt;Google Drive · &lt;code&gt;drive.appdata&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Microsoft&lt;/td&gt;&lt;td&gt;OIDC&lt;/td&gt;&lt;td&gt;OAuth 2.0 + PKCE&lt;/td&gt;&lt;td&gt;Public&lt;/td&gt;&lt;td&gt;No&lt;/td&gt;&lt;td&gt;App Folder&lt;/td&gt;&lt;td&gt;OneDrive · &lt;code&gt;Files.ReadWrite.AppFolder&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Apple&lt;/td&gt;&lt;td&gt;OIDC&lt;/td&gt;&lt;td&gt;Apple-proprietary&lt;sup&gt;2&lt;/sup&gt;&lt;/td&gt;&lt;td&gt;Mixed&lt;/td&gt;&lt;td&gt;Yes&lt;/td&gt;&lt;td&gt;CloudKit Private DB&lt;/td&gt;&lt;td&gt;Requires $99/year developer program.&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;GitHub&lt;/td&gt;&lt;td&gt;Pseudo-auth via &lt;code&gt;/user&lt;/code&gt;&lt;/td&gt;&lt;td&gt;OAuth 2.0 + PKCE (secret required)&lt;/td&gt;&lt;td&gt;Confidential&lt;/td&gt;&lt;td&gt;Yes&lt;/td&gt;&lt;td&gt;Gist or Repo File&lt;/td&gt;&lt;td&gt;Git Forge&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;&lt;sup&gt;1&lt;/sup&gt; &lt;em&gt;Backend for Frontend (BFF) — you can unlock these providers with a minimal Cloudflare Worker (see below) if they’d be killer for your app.&lt;/em&gt;&lt;br /&gt;
&lt;sup&gt;2&lt;/sup&gt; &lt;em&gt;Apple’s various resource APIs are distinct from their “Sign in with Apple” API.&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;Apple (CloudKit storage)&lt;/h3&gt;
&lt;p&gt;CloudKit looks genuinely awesome — public, private, and shared databases. If I were prepared to build a BFF, weren’t a GitLab guy, and had an iPhone, I’d seriously consider it.&lt;/p&gt;
&lt;h2&gt;AI&lt;/h2&gt;
&lt;p&gt;Thank you, OpenRouter! None of the frontier model providers make it easy to use your API credits in third-party apps — but OpenRouter does. It implements a clientless (you don’t even need a client ID) PKCE flow to mint an API token. You can use any of their models through a single OpenAI-compatible API — and even implement tool use directly in the browser with a capable model.&lt;/p&gt;























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Provider&lt;/th&gt;&lt;th&gt;AuthN&lt;/th&gt;&lt;th&gt;AuthZ&lt;/th&gt;&lt;th&gt;Client Type&lt;/th&gt;&lt;th&gt;BFF&lt;/th&gt;&lt;th&gt;Storage&lt;/th&gt;&lt;th&gt;Comments&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;OpenRouter&lt;/td&gt;&lt;td&gt;N/A&lt;/td&gt;&lt;td&gt;OAuth 2.0 + PKCE&lt;/td&gt;&lt;td&gt;Public&lt;/td&gt;&lt;td&gt;No&lt;/td&gt;&lt;td&gt;No&lt;/td&gt;&lt;td&gt;User pays-as-you-go per model; chat + embeddings, no persistence&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;div&gt;
&lt;p&gt;A word of warning&lt;/p&gt;
&lt;p&gt;That OpenRouter key spends real money and it lives in the browser, so treat any XSS as a &lt;em&gt;financial&lt;/em&gt; compromise, not just a data leak. Bound the blast radius: on the consent screen set a &lt;strong&gt;spend cap&lt;/strong&gt; on the key, give it a &lt;strong&gt;short expiry&lt;/strong&gt; so a leaked key dies quickly, request least-privilege scopes, and prefer keeping it &lt;strong&gt;in memory&lt;/strong&gt; (re-auth on reload) over persisting it to localStorage. OpenRouter sanctions this public-client flow — but you should still cap the downside.&lt;/p&gt;
&lt;/div&gt;
&lt;h2&gt;APIs I’ve looked at&lt;/h2&gt;









































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Provider&lt;/th&gt;&lt;th&gt;AuthN&lt;/th&gt;&lt;th&gt;AuthZ&lt;/th&gt;&lt;th&gt;Client Type&lt;/th&gt;&lt;th&gt;BFF&lt;/th&gt;&lt;th&gt;Storage&lt;/th&gt;&lt;th&gt;Comments&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Spotify&lt;/td&gt;&lt;td&gt;Pseudo-auth via &lt;code&gt;/me&lt;/code&gt;&lt;/td&gt;&lt;td&gt;OAuth 2.0 + PKCE&lt;/td&gt;&lt;td&gt;Public&lt;/td&gt;&lt;td&gt;No&lt;/td&gt;&lt;td&gt;No&lt;/td&gt;&lt;td&gt;Web Playback SDK + limited Web API&lt;sup&gt;3&lt;/sup&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Last.fm&lt;/td&gt;&lt;td&gt;Pseudo-auth via &lt;code&gt;user.getInfo&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Proprietary&lt;/td&gt;&lt;td&gt;Confidential&lt;/td&gt;&lt;td&gt;Yes&lt;/td&gt;&lt;td&gt;No&lt;/td&gt;&lt;td&gt;Rich music data&lt;sup&gt;4&lt;/sup&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;SoundCloud&lt;/td&gt;&lt;td&gt;Pseudo-auth via &lt;code&gt;/me&lt;/code&gt;&lt;/td&gt;&lt;td&gt;OAuth 2.1 + PKCE (secret required)&lt;/td&gt;&lt;td&gt;Confidential&lt;/td&gt;&lt;td&gt;Yes&lt;/td&gt;&lt;td&gt;No&lt;/td&gt;&lt;td&gt;Rich APIs; read/write playlists&lt;sup&gt;5&lt;/sup&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;&lt;sup&gt;3&lt;/sup&gt; &lt;em&gt;Spotify has two surfaces: the Web Playback SDK plays audio in the browser tab, while the Web API (data + playback control) is heavily limited since the Nov 2024 endpoint deprecations — you can fetch limited data and queue tracks. The Web API does expose ISRC (a unique cross-service recording ID) for matching tracks against other providers.&lt;/em&gt;&lt;br /&gt;
&lt;sup&gt;4&lt;/sup&gt; &lt;em&gt;Scrobble history and tags. No ISRC — matches on name / MusicBrainz MBID.&lt;/em&gt;&lt;br /&gt;
&lt;sup&gt;5&lt;/sup&gt; &lt;em&gt;Read/write playlists and upload audio. Partial ISRC (on uploads only).&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;Backend for Frontend (BFF) — the escape hatch!&lt;/h2&gt;
&lt;div&gt;
&lt;p&gt;Health warning&lt;/p&gt;
&lt;p&gt;Do not do this unless you genuinely know what you’re doing. Authentication or authorisation is hard, and vibe coders may cock this up and create real security issues.&lt;/p&gt;
&lt;/div&gt;
&lt;p&gt;Some great APIs don’t support OAuth 2.0 + PKCE as a public client. A BFF is the escape hatch. You can cheaply build one in Cloudflare Workers; it can do the extra API authentication or bypass CORS, enabling you to consume other great APIs.&lt;/p&gt;
&lt;div&gt;
&lt;p&gt;CORS&lt;/p&gt;
&lt;p&gt;Cross-Origin Resource Sharing — a browser-enforced policy that blocks your page from calling a different origin unless that server returns &lt;code&gt;Access-Control-Allow-Origin&lt;/code&gt; headers. A Worker runs server-side, where CORS doesn’t apply, so it can call the API and relay the response back to your page.&lt;/p&gt;
&lt;/div&gt;
&lt;h3&gt;Confidential clients dance&lt;/h3&gt;
&lt;p&gt;Store your client ID and client secret in the Worker (as Worker secrets), handle the redirect, and either proxy API calls on behalf of the browser or — if acceptable — pass the API token down to the browser (if CORS allows).&lt;/p&gt;
&lt;h3&gt;API proxy&lt;/h3&gt;
&lt;p&gt;Some APIs have CORS restrictions and won’t work from your domain. You can proxy those calls through a Worker to get around it. Some API calls also require signing — you must not share the secret key with the browser, so do the signing in the proxy.&lt;/p&gt;
&lt;h3&gt;Apple (proprietary auth)&lt;/h3&gt;
&lt;p&gt;Apple has some amazingly useful APIs, but they use proprietary authentication mechanisms involving certificates, identifiers, and profiles — all of which must be done server-side.&lt;/p&gt;
&lt;h2&gt;Watch out: providers that gate by user count&lt;/h2&gt;
&lt;p&gt;Some API providers impose serious limitations that undercut the “costs never grow” promise — access is restricted to small test groups or individual users. I hit this with the Spotify app: &lt;a href=&quot;https://developer.spotify.com/blog/2026-02-06-update-on-developer-access-and-platform-security&quot;&gt;since their February 2026 Development Mode changes&lt;/a&gt; you have to invite individual users and are limited to 5 (down from 25), with a Premium account required. If an API has this limitation, don’t hardcode a client ID — tell the user to get their own and store it in the best place you have (localStorage in the browser). Or, if it’s a secondary API call, store the client ID in your third-party storage system — the client ID isn’t a secret, so in this instance it’s acceptable.&lt;/p&gt;
&lt;h2&gt;A prompt to start from&lt;/h2&gt;
&lt;p&gt;I won’t pretend I one-shot Affinity from a single prompt — real apps take iteration. But this is the shape of spec I’d hand an agent: tight scope, and the security constraints I wouldn’t let it skip. Adapt it.&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;Build &quot;Affinity&quot;, a purely client-side static web app that analyses my&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;Spotify taste with AI. NO backend, NO server, NO database, NO serverless&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;functions — everything runs in the browser and deploys as static files to&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;GitLab Pages. Stack: React + Vite, no router.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;THREE FEATURES ONLY — do not add anything else:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;1. Spotify login via the OAuth 2.0 Authorization Code flow with PKCE as a&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;   &lt;/span&gt;&lt;/span&gt;&lt;span&gt;PUBLIC client (no client secret). I paste my Spotify Client ID on a&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;   &lt;/span&gt;&lt;/span&gt;&lt;span&gt;login screen; persist it in localStorage, never hardcode it. Redirect&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;   &lt;/span&gt;&lt;/span&gt;&lt;span&gt;URI = window.location.origin (no sub-path). Request the minimum scopes&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;   &lt;/span&gt;&lt;/span&gt;&lt;span&gt;needed to read my profile and top tracks/artists.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;2. Fetch my Spotify data: profile (/me) plus my top tracks and top&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;   &lt;/span&gt;&lt;/span&gt;&lt;span&gt;artists.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;3. AI analysis: connect to OpenRouter via its PKCE flow (public client,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;   &lt;/span&gt;&lt;/span&gt;&lt;span&gt;no secret) to mint a per-user API key, then POST my taste data to the&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;   &lt;/span&gt;&lt;/span&gt;&lt;span&gt;OpenAI-compatible chat-completions endpoint (raw fetch, model&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;   &lt;/span&gt;&lt;/span&gt;&lt;span&gt;google/gemini-2.5-flash) and render the written analysis.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;SECURITY IS THE PRIMARY REQUIREMENT. This app holds live OAuth tokens and&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;a money-spending AI key in the browser. Apply ALL of the following, and&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;flag anything you cannot satisfy:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;- No client secret anywhere, ever. Both providers use public-client PKCE&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;only. If a flow appears to need a secret, STOP and tell me — never&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;hardcode one.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;- PKCE done correctly: a cryptographically-random code_verifier; an S256&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;code_challenge; store the verifier in sessionStorage; generate and&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;VALIDATE a state parameter (CSRF); exchange the authorization code&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;immediately; clear the verifier after exchange.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;- Inject a strict Content-Security-Policy at build time (Vite plugin).&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;Lock connect-src to ONLY Spotify&apos;s and OpenRouter&apos;s API/auth hosts —&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;nothing else. No unsafe-inline or unsafe-eval for scripts. Set the&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;referrer policy to no-referrer.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;- Keep the OpenRouter API key IN MEMORY only (never localStorage) — it&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;spends real money, so a leak must die quickly. You can&apos;t set a spend&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;cap or expiry through the PKCE flow, so show the user an interstitial&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;telling them to set a spend cap (and short expiry) on their key on&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;OpenRouter&apos;s consent screen. The Spotify token may go in localStorage.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;NEVER log a token or the key, and NEVER send either anywhere except the&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;provider it belongs to. Strip the auth code and state from the URL&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;after exchange; prefer re-auth over a persisted refresh token.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;- No eval. No innerHTML or dangerouslySetInnerHTML with untrusted data.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;No inline event handlers. Rely on React&apos;s default escaping for any text&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;rendered from a Spotify or AI response.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;- Never commit secrets or personal data; the Client ID is entered at&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;runtime.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;Keep it minimal and readable. Show clear connect/disconnect state for&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;both Spotify and OpenRouter. That is the entire MVP.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;hr /&gt;
&lt;p&gt;That’s the whole pattern: vibe-code a static frontend personal app, wire it to the free storage you already trust with PKCE, and let OpenRouter make it smart — no server to run, no bill that grows with your users.&lt;/p&gt;
&lt;p&gt;That’s a &lt;strong&gt;Pixie app&lt;/strong&gt;: a static page, a bit of magic, and a backend that was never yours to run.&lt;/p&gt;
&lt;h2&gt;Built with this pattern&lt;/h2&gt;
&lt;p&gt;These apps are raw and not optimised for mobile — they’re personal apps, built for me, but they might be useful to you too. Grab the source below and contribute.&lt;/p&gt;


























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Project&lt;/th&gt;&lt;th&gt;What it is&lt;/th&gt;&lt;th&gt;Stack&lt;/th&gt;&lt;th&gt;Live&lt;/th&gt;&lt;th&gt;Source&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Affinity&lt;/td&gt;&lt;td&gt;Spotify taste analysis with AI&lt;/td&gt;&lt;td&gt;Spotify · OpenRouter&lt;/td&gt;&lt;td&gt;&lt;a href=&quot;https://affinity.msyea.com&quot;&gt;affinity.msyea.com&lt;/a&gt;&lt;/td&gt;&lt;td&gt;&lt;a href=&quot;https://gitlab.com/msyea/affinity&quot;&gt;msyea/affinity&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Marshal&lt;/td&gt;&lt;td&gt;GitLab roadmaps, my way&lt;/td&gt;&lt;td&gt;GitLab · Snippet storage&lt;/td&gt;&lt;td&gt;&lt;a href=&quot;https://marshal.msyea.com&quot;&gt;marshal.msyea.com&lt;/a&gt;&lt;/td&gt;&lt;td&gt;&lt;a href=&quot;https://gitlab.com/msyea-sa/marshal&quot;&gt;msyea-sa/marshal&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Hand written by me. Copy-editted by Claude. I used em-dashes before the slop, they’re my style and here to stay.&lt;/p&gt;</content:encoded><category>vibe-coding</category><category>pkce</category><category>oauth</category><category>static-sites</category><category>ai</category></item><item><title>Software Quality: Adopting Common Industry Language</title><link>https://type2fun.net/software-quality-as-a-discipline</link><guid isPermaLink="true">https://type2fun.net/software-quality-as-a-discipline</guid><description>Engineers don&apos;t talk about software quality — they get stuck arguing about test types. A case for the discipline, in plain language.</description><pubDate>Thu, 14 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;It’s been my observation that engineers don’t talk about &lt;em&gt;Software Quality&lt;/em&gt;, they get stuck in word-spaghetti soup talking about the numerous types of automated tests covering various characteristics; security, availability, accessibility etc. Each set of characteristics has different stakeholders; InfoSec team, compliance team, UX team etc. and the importance of each tends to be political within the organisation. I want to focus on Software Quality in the holistic sense and suggest some clear language to describe the discipline.&lt;/p&gt;
&lt;p&gt;The first issue is; what are we talking about? We are talking about &lt;em&gt;Software Quality&lt;/em&gt; a simple, self titled phrase that encapsulates everything. There is no need to rebrand this term for your specific organisation.&lt;/p&gt;
&lt;pre class=&quot;mermaid&quot;&gt;mindmap
  root((Software Quality))
    )Engineering Excellence(
    )Service Maturity(
    )Immunisation(
    )Software Standards(
    )Engineering Playbook(
    )Compliance(
    )Checks(
    )Software Health(
    )Service Readiness(&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;Various organisational terms that might be synonyms for Software Quality.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;All this word-spaghetti just complicates things. It then further complicated by external stakeholders. You often find that experienced engineers organically introduce tools over time but once your organisation hits a certain point you spin up InfoSec teams, Compliance teams, Risk teams, UX teams and start caring about Info and Data security, compliance, risk, performance and accessibility as separate disciplines. The list can then be endless. Those teams then speak to vendors, senior leaders and purchase tools, which may not be suitable but are subsequently mandated for use. This introduces politics and distances the concern of software quality from the engineers to the external stakeholders and sometimes results the engineers developing the behaviour of — “it’s a tick box I must fulfil before production”.&lt;/p&gt;
&lt;p&gt;The cherry on the cake is the when people start talking about tools — which are brand names and frequently have no meaning by themselves. “Make sure your &lt;code&gt;[Aqua|Wiz|Sonarqube|k6|Lighthouse]&lt;/code&gt; scans are passing.” All of this increases the cognitive load on engineers and is mumbo-jumbo and foreign to new engineers. How are they meant to know all the different tools you use and critically, what is the value and importance of each?&lt;/p&gt;
&lt;p&gt;Software Quality as a term is important as it is something every engineer should care deeply about. It transcends all technologies, tools and levels of experience. All these organisational behaviours complicate things and have the effect to change engineers’ behaviours to turn software quality (not that you call it that) into a separate tick-box exercise. In some cases as engineers don’t care about some of the checks they think, “how can we bypass this” or “can we get an exception for that”.&lt;/p&gt;
&lt;p&gt;So what is the solution? The first step is to adopt precise, universal and relatively plain language based on industry standards.&lt;/p&gt;
&lt;div&gt;
&lt;p&gt;Software Quality (SQ)&lt;/p&gt;
&lt;p&gt;“The degree to which a system or software product enables specified users to achieve specified goals with effectiveness, efficiency, satisfaction, and freedom from risk in specified contexts of use, evaluated through nine quality characteristics, including functional suitability, performance efficiency, and security.”
— &lt;em&gt;ISO/IEC 25010:2023 — Systems and Software Quality Requirements and Evaluation — SQuaRE&lt;/em&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;p&gt;You then need to define what are these stated and implied needs? A established way to describe this is through requirements — separated out into &lt;em&gt;Functional and Non-functional requirements&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;Functional requirements are easy — they are typically &lt;strong&gt;what&lt;/strong&gt; are defined in the user story/ticket: “An actor performs an interaction and then software then responds in a defined way.” Non-functional requirements are frequently implied and specify &lt;strong&gt;how&lt;/strong&gt; the product will perform: “The response will be within 200ms”.&lt;/p&gt;
&lt;div&gt;
&lt;p&gt;Functional Requirements (FRs)&lt;/p&gt;
&lt;p&gt;“Functional requirements specify the specific functions, behaviors, or capabilities that a software system must provide to meet user or stakeholder needs, describing what the system must do in terms of inputs, outputs, and interactions under defined conditions.”
— &lt;em&gt;IEEE Std 830-1998&lt;/em&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;p&gt;Non-functional Requirements (NFRs)&lt;/p&gt;
&lt;p&gt;“Non-functional requirements specify the quality attributes, constraints, or performance criteria of a software system, describing how the system should perform rather than what it does, including aspects like performance, reliability, usability, security, and scalability.”
— &lt;em&gt;IEEE Std 830-1998&lt;/em&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;p&gt;Info&lt;/p&gt;
&lt;p&gt;ISO/IEC 25010:2023 is principally concerned about Software Quality and doesn’t explicitly define functional and non-functional requirements. I’ve pulled a definition from IEEE Std 830-1998 instead.&lt;/p&gt;
&lt;/div&gt;
&lt;p&gt;Now to define what software quality characteristics to talk about. I recommend that you follow ISO/IEC 25010:2023 — which specifies the following characteristics:&lt;/p&gt;
&lt;h2&gt;Functional suitability&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;capability of a product to provide functions that meet stated and implied needs intended users when it is used under specified conditions&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Sub-characteristics:&lt;/strong&gt; functional completeness, functional correctness, and functional appropriateness.
&lt;strong&gt;Testing approaches:&lt;/strong&gt; unit tests, integration tests, acceptance tests, end-to-end tests
&lt;strong&gt;Stakeholders:&lt;/strong&gt; End-users, product owners, engineers, UX&lt;/p&gt;
&lt;h2&gt;Performance efficiency&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;capability of a product to perform its functions within specified time and throughput parameters and be efficient in the use of resources under specified conditions&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Sub-characteristics:&lt;/strong&gt; time behaviour, resource utilisation, and capacity.
&lt;strong&gt;Testing approaches:&lt;/strong&gt; load tests, stress tests, volume tests, spike tests, resource monitoring
&lt;strong&gt;Stakeholders:&lt;/strong&gt; End-users, on-call engineers, DevOps&lt;/p&gt;
&lt;h2&gt;Compatibility&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;capability of a product to exchange information with other produces, and/or to perform its required functions while sharing the same common environment and resources&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Sub-characteristics:&lt;/strong&gt; co-existence, and interoperability.
&lt;strong&gt;Testing approaches:&lt;/strong&gt; API tests, contract tests, environment tests, interoperability tests
&lt;strong&gt;Stakeholders:&lt;/strong&gt; System integrators, engineers, DevOps&lt;/p&gt;
&lt;h2&gt;Interaction capability (aka Usability)&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;capability of a product to be interacted with by specified users to exchange information between a user and a system via the user interface to complete the intended task&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Sub-characteristics:&lt;/strong&gt; appropriateness recognisability, learnability, operability, user error protection, user engagement, inclusivity, user assistance, and self-descriptiveness.
&lt;strong&gt;Testing approaches:&lt;/strong&gt; usability tests, accessibility tests, user journey tests, A/B tests
&lt;strong&gt;Stakeholders:&lt;/strong&gt; End-users, UX and QA&lt;/p&gt;
&lt;h2&gt;Reliability&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;capability of a product to perform specified functions under specified conditions for a specified period of time without interruptions and failures&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Sub-characteristics:&lt;/strong&gt; faultlessness, availability, fault tolerance, and recoverability.
&lt;strong&gt;Testing approaches:&lt;/strong&gt; chaos tests, failover tests, recovery tests, availability monitoring
&lt;strong&gt;Stakeholders:&lt;/strong&gt; End-users, DevOps, QA&lt;/p&gt;
&lt;h2&gt;Security&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;capability of a product to protect information and data so that persons or other products have the degree of data access appropriate to their types and levels of authorisation, and to defend against attack patterns by malicious actors&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Sub-characteristics:&lt;/strong&gt; confidentiality, integrity, non-repudiation, accountability, authenticity, and resistance.
&lt;strong&gt;Testing approaches:&lt;/strong&gt; penetration tests, vulnerability scans, security audits, threat modeling
&lt;strong&gt;Stakeholders:&lt;/strong&gt; End-users, security, compliance, risk, engineers&lt;/p&gt;
&lt;h2&gt;Maintainability&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;capability of a product to be modified by the intended maintainers with effectiveness and efficiency&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Sub-characteristics:&lt;/strong&gt; modularity, reusability, analysability, modifiability, and testability.
&lt;strong&gt;Testing approaches:&lt;/strong&gt; static analysis, code reviews, complexity analysis, test coverage analysis
&lt;strong&gt;Stakeholders:&lt;/strong&gt; Engineers, DevOps&lt;/p&gt;
&lt;h2&gt;Flexibility (formerly Portability)&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;capability of a product to be adapted to changes in its requirements, context of use, or system environment&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Sub-characteristics:&lt;/strong&gt; adaptability, scalability, installability, and replaceability.
&lt;strong&gt;Testing approaches:&lt;/strong&gt; scalability tests, deployment tests, migration tests, configuration tests
&lt;strong&gt;Stakeholders:&lt;/strong&gt; Engineers, DevOps&lt;/p&gt;
&lt;h2&gt;Safety (new)&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;capability of a product under defined conditions to avoid a state in which human life, health, property or the environment is endangered&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Sub-characteristics:&lt;/strong&gt; operational constraint, risk identification, fail safe, hazard warning, and safe integration.
&lt;strong&gt;Testing approaches:&lt;/strong&gt; boundary tests, fault injection tests, model verification, hazard analysis
&lt;strong&gt;Stakeholders:&lt;/strong&gt; End-users, safety engineers, compliance, risk&lt;/p&gt;
&lt;p&gt;The final steps are to take the characteristics and turn them into measurable Software Quality Attributes (SQAs). SQAs enable you to create a Software Quality framework (SQF) that you can practically use. Industry standards are excellent guidelines but they don’t tell you how to actually execute them. A SQF will factor in software lifecycle, environment, software criticality, etc.&lt;/p&gt;
&lt;div&gt;
&lt;p&gt;Software Quality Attributes (SQAs)&lt;/p&gt;
&lt;p&gt;Software Quality Attributes (SQAs) are measurable properties of a software system that define its quality, typically expressed as non-functional requirements specifying how the system performs, such as performance, reliability, usability, or security. These attributes are evaluated to ensure the system meets stakeholder expectations and may be automated and asserted during development (build-time) and operations (run-time).&lt;/p&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;p&gt;Software Quality Framework (SQF)&lt;/p&gt;
&lt;p&gt;A practical approach for evaluating software quality by measuring its performance across multiple quality attributes, such as functional suitability, performance efficiency, and security. It enables systematic assessment during development and operations, often through automated testing and monitoring. Defines ownership of attributes and maps quality attributes to tools.&lt;/p&gt;
&lt;/div&gt;
&lt;p&gt;Adopting industry standard language will bring huge benefits across the board. It will empower engineers to understand and own domain of Software Quality. These standards, though formal, provide clear, precise definitions — functional requirements, non-functional requirements and software quality attributes — will improve communication and enable engineers better scope work. They will enable engineers to use open-source resources/documentation and AI-driven tools more effectively to develop their understanding. Clarity of language will speed onboarding, aid design decisions, and reduce miscommunication. Ultimately, improved software quality will reduce incidents, enhances customer satisfaction, and improves the company bottom line.&lt;/p&gt;
&lt;p&gt;Over the next few weeks I will continue this software quality deep dive and publish a practical Software Quality Framework using open-source tools.&lt;/p&gt;</content:encoded><category>software-quality</category></item><item><title>Introducing the AWS GitLab Helper</title><link>https://type2fun.net/introducing-the-aws-gitlab-helper</link><guid isPermaLink="true">https://type2fun.net/introducing-the-aws-gitlab-helper</guid><description>A tool for GitLab Pipelines to fetch temporary credentials, secrets, and parameters from AWS using ABAC.</description><pubDate>Sat, 19 Oct 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;What and why?&lt;/h2&gt;
&lt;p&gt;The AWS GitLab Helper (AGH) is a tool that can be implemented in GitLab Pipelines to automatically fetch temporary credentials, secrets and parameters from AWS using &lt;a href=&quot;https://docs.aws.amazon.com/IAM/latest/UserGuide/introduction_attribute-based-access-control.html&quot;&gt;ABAC&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I wasn’t happy with the &lt;a href=&quot;https://docs.gitlab.com/ee/ci/cloud_services/aws/&quot;&gt;suggested way&lt;/a&gt; of authenticating with AWS. Due to limitations of AWS OIDC provider implementation, you can only assert on the &lt;a href=&quot;https://docs.gitlab.com/ee/ci/secrets/id_token_authentication.html&quot;&gt;ID Token&lt;/a&gt;’s subject claim &lt;code&gt;{ &quot;sub&quot;: &quot;project_path:my-group/my-project:ref_type:branch:ref:feature-branch-1&quot; }&lt;/code&gt;, which means you’re closely coupling security and your policies to Git References (tags or branches), which in my mind seems semantically wrong. Security should be coupled with environments and GitLab has &lt;a href=&quot;https://docs.gitlab.com/ee/ci/environments/&quot;&gt;Protected Environments&lt;/a&gt; — so why couldn’t I use that.&lt;/p&gt;
&lt;p&gt;Well it transpires that you can.&lt;/p&gt;
&lt;h2&gt;Say hello to AWS Cognito&lt;/h2&gt;
&lt;p&gt;AWS OIDC when used on its own can only use limited claims (see &lt;a href=&quot;https://docs.aws.amazon.com/en_en/IAM/latest/UserGuide/reference_policies_iam-condition-keys.html#condition-keys-wif&quot;&gt;Available keys for AWS OIDC federation&lt;/a&gt;). After pouring over the docs I found &lt;a href=&quot;https://docs.aws.amazon.com/IAM/latest/UserGuide/id_session-tags.html#id_session-tags_adding-assume-role-idp&quot;&gt;Passing session tags using AssumeRoleWithWebIdentity&lt;/a&gt;, but alas it would only work if GitLab changed their ID Token to match AWS’s schema. I eventually found &lt;a href=&quot;https://docs.aws.amazon.com/cognito/latest/developerguide/attributes-for-access-control.html&quot;&gt;Using attributes for access control&lt;/a&gt; in the AWS Cognito Identity Pools documentation and got very excited.&lt;/p&gt;
&lt;p&gt;Unfortunately you cannot hack with it using the &lt;code&gt;aws-cli&lt;/code&gt;, so I had to use the &lt;a href=&quot;https://aws.amazon.com/sdk-for-javascript/&quot;&gt;SDK for JavaScript&lt;/a&gt;. And it was incredibly hacky and ugly code but it worked.&lt;/p&gt;
&lt;p&gt;The magic is in Cognito mappings:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://type2fun.net/assets/img/cognito-mappings.png&quot; alt=&quot;Screen grab of Cognito attribute mappings&quot; /&gt;&lt;/p&gt;
&lt;p&gt;“Attributes for access control” is 50% of the magic sauce. This takes the claims from the GitLab ID Token (JWT), and maps them to the &lt;code&gt;AghCognito&lt;/code&gt; role. However they’re a bit janky to use directly. I don’t want my AWS Resources having to have tags like &lt;code&gt;NamespacePath&lt;/code&gt; and &lt;code&gt;ProjectPath&lt;/code&gt;. I describe my infrastructure using &lt;code&gt;Domain&lt;/code&gt;, &lt;code&gt;System&lt;/code&gt;, &lt;code&gt;Service&lt;/code&gt; and &lt;code&gt;Environment&lt;/code&gt; tags. I thought, if I assume a 2nd role I could rename them — assuming a 2nd role opens up the environment for abuse (changing the tags to a claim you’re not entitled to). However after too much coffee and some creative thinking I worked out a &lt;strong&gt;safe&lt;/strong&gt; Trust Policy that would achieve what I wanted (it also has some extra features too).&lt;/p&gt;
&lt;p&gt;Introducing the wild Trust Policy for &lt;code&gt;AghAbac&lt;/code&gt; — the other 50%:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;&quot;Version&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;2012-10-17&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;&quot;Statement&quot;&lt;/span&gt;&lt;span&gt;: [&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;&quot;Effect&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;Allow&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;&quot;Principal&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;&quot;AWS&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;arn:aws:iam::&amp;lt;account-id&amp;gt;:role/agh/AghCognito&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;&quot;Action&quot;&lt;/span&gt;&lt;span&gt;: [&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;&quot;sts:AssumeRole&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;&quot;sts:TagSession&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;],&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;&quot;Condition&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;&quot;StringEquals&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;          &lt;/span&gt;&lt;span&gt;&quot;aws:PrincipalTag/Environment&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;${aws:RequestTag/Environment}&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;          &lt;/span&gt;&lt;span&gt;&quot;aws:PrincipalTag/ProjectPath&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;${aws:PrincipalTag/NamespacePath}/${aws:RequestTag/Service}&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;          &lt;/span&gt;&lt;span&gt;&quot;aws:PrincipalTag/UserAccessLevel&quot;&lt;/span&gt;&lt;span&gt;: [&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;&quot;developer&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;&quot;maintainer&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;&quot;owner&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;          &lt;/span&gt;&lt;/span&gt;&lt;span&gt;],&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;          &lt;/span&gt;&lt;span&gt;&quot;aws:PrincipalTag/EnvironmentProtected&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;${aws:RequestTag/EnvironmentProtected}&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;&quot;Null&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;          &lt;/span&gt;&lt;span&gt;&quot;aws:TagKeys&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;false&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;&quot;StringLike&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;          &lt;/span&gt;&lt;span&gt;&quot;aws:PrincipalTag/NamespacePath&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;*/${aws:RequestTag/Domain}/${aws:RequestTag/System}&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;&quot;ForAllValues:StringEquals&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;          &lt;/span&gt;&lt;span&gt;&quot;aws:TagKeys&quot;&lt;/span&gt;&lt;span&gt;: [&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;&quot;Domain&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;&quot;System&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;&quot;Service&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;&quot;Environment&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;&quot;EnvironmentProtected&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;&quot;UserAccessLevel&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;          &lt;/span&gt;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Let me explain it:&lt;/p&gt;



































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Desired Tag&lt;/th&gt;&lt;th&gt;Assertion&lt;/th&gt;&lt;th&gt;Explanation&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;Domain&lt;/code&gt; and &lt;code&gt;System&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;&quot;${aws:PrincipalTag/NamespacePath}&quot; like &quot;*/${aws:RequestTag/Domain}/${aws:RequestTag/System}&quot;&lt;/code&gt;&lt;/td&gt;&lt;td&gt;The &lt;code&gt;NamespacePath&lt;/code&gt; contains &lt;code&gt;root-group/subgroup1/subgroup2&lt;/code&gt; and my GitLab group hierarchy is &lt;code&gt;root-group/domain/system&lt;/code&gt;.&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;Service&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;&quot;${aws:PrincipalTag/NamespacePath}/${aws:RequestTag/Service}&quot; == &quot;${aws:PrincipalTag/ProjectPath}&quot;&lt;/code&gt;&lt;/td&gt;&lt;td&gt;The &lt;code&gt;ProjectPath&lt;/code&gt; contains &lt;code&gt;root-group/subgroup1/subgroup2/service&lt;/code&gt; and my GitLab service hierarchy is &lt;code&gt;root-group/domain/system/service&lt;/code&gt;.&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;Environment&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;&quot;${aws:RequestTag/Environment}&quot; == &quot;${aws:PrincipalTag/Environment}&quot;&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;EnvironmentProtected&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;&quot;${aws:RequestTag/EnvironmentProtected}&quot; == &quot;${aws:PrincipalTag/EnvironmentProtected}&quot;&lt;/code&gt;&lt;/td&gt;&lt;td&gt;I’ve added this as in my other Policies I assert that &lt;code&gt;&quot;Environment&quot; = &quot;production&quot;&lt;/code&gt; also has &lt;code&gt;&quot;EnvironmentProtected&quot; = &quot;true&quot;&lt;/code&gt;.&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;UserAccessLevel&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;&quot;${aws:PrincipalTag/UserAccessLevel}&quot; in [&quot;owner&quot;, &quot;maintainer&quot;, &quot;owner&quot;]&lt;/code&gt;&lt;/td&gt;&lt;td&gt;This gives me the option to block &lt;code&gt;developers&lt;/code&gt; from ever interacting with some resources.&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;So now I have a well-configured ABAC role, what can I do? In my &lt;code&gt;.gitlab-ci.yml&lt;/code&gt; file I can add the following code:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;include&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;project&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;msyea-sa/aws-gitlab-helper&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;file&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;/templates/AwsGitLabHelper.gitlab-ci.yml&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;variables&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;AGH_CREDENTIAL_DEFAULT&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;arn:aws:iam::123456789012:role/service-role/shared-system-role&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;AGH_CREDENTIAL_PROFILE1&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;arn:aws:iam::123456789012:role/service-role/service-role&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;AGH_SECRET_SLACK_TOKEN&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;arn:aws:secretsmanager:us-east-1:123456789012:secret:slack-token-V6d7a8&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;AGH_SECRET_TEST_DB&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;arn:aws:secretsmanager:us-west-2:123456789012:secret:db-connection-string-F1r2t3&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;AGH_PARAMETER_MAX_RETRY&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;arn:aws:ssm:us-west-2:123456789012:parameter/max-retry-attempts&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;test&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;extends&lt;/span&gt;&lt;span&gt;: [&lt;/span&gt;&lt;span&gt;.aws-gitlab-helper&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;script&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;aws --profile profile1 sts get-caller-identity&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;echo &quot;Max retry&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;${MAX_RETRY}&quot;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;# do not echo secrets — they will not be masked&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The template executes in the GitLab Helper Container (&lt;code&gt;hook:pre_get_sources_script&lt;/code&gt;). This is hugely advantageous as it doesn’t interfere with job scripts (like &lt;code&gt;before_script&lt;/code&gt; might) and &lt;code&gt;${GITLAB_ENV}&lt;/code&gt; plays nicely. As it’s in the GitLab Helper Container it knows &lt;code&gt;curl&lt;/code&gt; is available and with &lt;code&gt;uname -s&lt;/code&gt; it can download and execute the correct version from the GitLab Package Registry. The app is written in JavaScript/node and statically compiled for Linux and macOS. This makes the download quick with no dependencies.&lt;/p&gt;
&lt;p&gt;The AWS GitLab Helper parses the environment variables prefixed with &lt;code&gt;AGH_&lt;/code&gt;, and, when necessary, executes &lt;code&gt;AssumeRole&lt;/code&gt;, &lt;code&gt;GetSecretValue&lt;/code&gt; and &lt;code&gt;GetParameter&lt;/code&gt; commands and saves the responses. To make them available to the job it does something like:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;Terminal window&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;export&lt;/span&gt;&lt;span&gt; AWS_SHARED_CREDENTIALS_FILE&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;${&lt;/span&gt;&lt;span&gt;RUNNER_TEMP_PROJECT_DIR&lt;/span&gt;&lt;span&gt;}/aws-credentials&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;echo&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;credentials&quot;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span&gt; ${AWS_SHARED_CREDENTIALS_FILE}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;echo&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;secrets&quot;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span&gt; ${GITLAB_ENV}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;If you haven’t used &lt;code&gt;${GITLAB_ENV}&lt;/code&gt; before see &lt;a href=&quot;https://docs.gitlab.com/ee/ci/variables/#pass-an-environment-variable-from-the-script-section-to-another-section-in-the-same-job&quot;&gt;Pass an environment variable from the &lt;code&gt;script&lt;/code&gt; section to another section in the same job&lt;/a&gt;. It’s much less janky than &lt;code&gt;export $(xargs &amp;lt; .env)&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Your AWS credentials file is populated outside the &lt;code&gt;${CI_PROJECT_DIR}&lt;/code&gt;, so that it cannot be accidentally saved as an artifact or committed. The secrets and parameters are also made available as environment variables.&lt;/p&gt;
&lt;pre class=&quot;mermaid&quot;&gt;sequenceDiagram
    participant Job as GitLab Job
    box transparent GitLab Helper Container hook
        participant AGH as AWS GitLab Helper
    end
    box transparent AWS
        participant Cognito
        participant STS
        participant SM as Secrets Manager
        participant PS as Parameter Store
    end
    Job-&amp;gt;&amp;gt;+AGH: Implements AGH Template
    AGH-&amp;gt;&amp;gt;AGH: Configures ID Token
    AGH-&amp;gt;&amp;gt;AGH: Downloads AGH Binary and executes it
    AGH-&amp;gt;&amp;gt;+Cognito: Authenticates
    Cognito-&amp;gt;&amp;gt;Cognito: Maps claims to Principal Tags
    Cognito-&amp;gt;&amp;gt;-AGH: Returns temporary credentials (AghCognito Role)
    AGH-&amp;gt;&amp;gt;+STS: Assume AghAbac Role with AGH Tags
    STS-&amp;gt;&amp;gt;-AGH: Returns temporary credentials (AghAbac Role)
    loop Fetch credentials for additional roles
        AGH-&amp;gt;&amp;gt;+STS: Assume other Role with AGH Tags
        STS-&amp;gt;&amp;gt;-AGH: Returns temporary credentials
        AGH-&amp;gt;&amp;gt;Job: Write credentials to AWS_SHARED_CREDENTIALS_FILE
    end
    loop Fetch secrets
        AGH-&amp;gt;&amp;gt;+SM: GetSecret
        SM-&amp;gt;&amp;gt;-AGH: Returns secret
        AGH-&amp;gt;&amp;gt;Job: Writes secret to GITLAB_ENV
    end
    loop Fetch parameters
        AGH-&amp;gt;&amp;gt;+PS: GetParameter
        PS-&amp;gt;&amp;gt;-AGH: Returns parameter
        AGH-&amp;gt;&amp;gt;Job: Write parameter to GITLAB_ENV
    end
    AGH-xJob: AGH Binary exits and Helper Container completes
    alt GitLab Build Container
        Job-&amp;gt;&amp;gt;Job: Runs your Job scripts
    end&lt;/pre&gt;
&lt;p&gt;And that is it. AGH is now available for you to use. I’ve been using it in production for a few months but I will improve the documentation and automated tests before I make it Generally Available.&lt;/p&gt;
&lt;p&gt;Note that the AWS GitLab Helper is available under BSL → MIT license. If you have 25 or fewer employees or less than £1 million in revenue you can use it in production for free. If you’re larger than that then you will need a Commercial License or wait 4 years.&lt;/p&gt;
&lt;h2&gt;Links&lt;/h2&gt;





















&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Component&lt;/th&gt;&lt;th&gt;Location&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Template&lt;/td&gt;&lt;td&gt;&lt;a href=&quot;https://gitlab.com/msyea-sa/aws-gitlab-helper/-/tree/main/templates&quot;&gt;https://gitlab.com/msyea-sa/aws-gitlab-helper/-/tree/main/templates&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Application&lt;/td&gt;&lt;td&gt;&lt;a href=&quot;https://gitlab.com/msyea-sa/aws-gitlab-helper/-/tree/main/app&quot;&gt;https://gitlab.com/msyea-sa/aws-gitlab-helper/-/tree/main/app&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Terraform Module&lt;/td&gt;&lt;td&gt;&lt;a href=&quot;https://gitlab.com/msyea-sa/aws-gitlab-helper/-/tree/main/terraform&quot;&gt;https://gitlab.com/msyea-sa/aws-gitlab-helper/-/tree/main/terraform&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;</content:encoded><category>ci-cd</category><category>aws</category><category>gitlab</category></item></channel></rss>