Simon Mayes software engineer · founder, Untether · climber

Article
vibe-coding pkce oauth static-sites ai Updated 9 Jun 2026

Vibe-coding shareable, infinitely scalable personal apps

PKCE for the backends and APIs you already trust: add OpenRouter to make them smart — all for free, or next to nothing.

I’ve built two personal apps with no backend at allmarshal.msyea.com (GitLab roadmaps, my way) and affinity.msyea.com (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 pixie, so that’s what I call them: Pixie apps. Here’s the pattern.

flowchart LR
    you([You])
    subgraph host["Static host: GitLab / GitHub Pages"]
        app["Pixie app (React + Vite SPA)"]
    end
    provider["Provider API + your storage"]
    ai["OpenRouter (AI models)"]
    you --> app
    app -->|OAuth 2.0 PKCE| provider
    app -->|OAuth 2.0 PKCE| ai

No server in the middle — the static page in your browser talks straight to each provider, authenticated as you.

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 local-first, self-hosted, or unhosted apps is a time-tested answer — and the architecture below makes it safe and quick.

Personal apps aren’t production apps

I write about software quality 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.

How to get the data? #

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 OAuth 2.0 Authorization Code flow with PKCE (Proof Key for Code Exchange), as a public client. 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).

What type of app? #

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.

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.

Where to host? #

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 (community discussion). If you’re not intending to use GitHub as a backend (see below), GitHub Pages is still a great place to host.

How to build? #

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.

Security #

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.

XSS

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.

CSP

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 connect-src to just your providers is the control that contains a leaked token.

Secrets

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.

Access Control

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.

Storage #

The obvious place to store data for a backendless static site is to use localStorage, 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.

Take Marshal, 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 Affinity I’d extend the same idea to Google Drive.)

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):

ProviderAuthNAuthZClient TypeBFF1StorageComments
GitLabOIDCOAuth 2.0 + PKCEPublicNoSnippet or Repo FileGit Forge
GoogleOIDCOAuth 2.0 + PKCEPublicNoApp Data FolderGoogle Drive · drive.appdata
MicrosoftOIDCOAuth 2.0 + PKCEPublicNoApp FolderOneDrive · Files.ReadWrite.AppFolder
AppleOIDCApple-proprietary2MixedYesCloudKit Private DBRequires $99/year developer program.
GitHubPseudo-auth via /userOAuth 2.0 + PKCE (secret required)ConfidentialYesGist or Repo FileGit Forge

1 Backend for Frontend (BFF) — you can unlock these providers with a minimal Cloudflare Worker (see below) if they’d be killer for your app.
2 Apple’s various resource APIs are distinct from their “Sign in with Apple” API.

Apple (CloudKit storage) #

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.

AI #

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.

ProviderAuthNAuthZClient TypeBFFStorageComments
OpenRouterN/AOAuth 2.0 + PKCEPublicNoNoUser pays-as-you-go per model; chat + embeddings, no persistence

A word of warning

That OpenRouter key spends real money and it lives in the browser, so treat any XSS as a financial compromise, not just a data leak. Bound the blast radius: on the consent screen set a spend cap on the key, give it a short expiry so a leaked key dies quickly, request least-privilege scopes, and prefer keeping it in memory (re-auth on reload) over persisting it to localStorage. OpenRouter sanctions this public-client flow — but you should still cap the downside.

APIs I’ve looked at #

ProviderAuthNAuthZClient TypeBFFStorageComments
SpotifyPseudo-auth via /meOAuth 2.0 + PKCEPublicNoNoWeb Playback SDK + limited Web API3
Last.fmPseudo-auth via user.getInfoProprietaryConfidentialYesNoRich music data4
SoundCloudPseudo-auth via /meOAuth 2.1 + PKCE (secret required)ConfidentialYesNoRich APIs; read/write playlists5

3 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.
4 Scrobble history and tags. No ISRC — matches on name / MusicBrainz MBID.
5 Read/write playlists and upload audio. Partial ISRC (on uploads only).

Backend for Frontend (BFF) — the escape hatch! #

Health warning

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.

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.

CORS

Cross-Origin Resource Sharing — a browser-enforced policy that blocks your page from calling a different origin unless that server returns Access-Control-Allow-Origin 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.

Confidential clients dance #

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).

API proxy #

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.

Apple (proprietary auth) #

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.

Watch out: providers that gate by user count #

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: since their February 2026 Development Mode changes 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.

A prompt to start from #

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.

Build "Affinity", a purely client-side static web app that analyses my
Spotify taste with AI. NO backend, NO server, NO database, NO serverless
functions — everything runs in the browser and deploys as static files to
GitLab Pages. Stack: React + Vite, no router.
THREE FEATURES ONLY — do not add anything else:
1. Spotify login via the OAuth 2.0 Authorization Code flow with PKCE as a
PUBLIC client (no client secret). I paste my Spotify Client ID on a
login screen; persist it in localStorage, never hardcode it. Redirect
URI = window.location.origin (no sub-path). Request the minimum scopes
needed to read my profile and top tracks/artists.
2. Fetch my Spotify data: profile (/me) plus my top tracks and top
artists.
3. AI analysis: connect to OpenRouter via its PKCE flow (public client,
no secret) to mint a per-user API key, then POST my taste data to the
OpenAI-compatible chat-completions endpoint (raw fetch, model
google/gemini-2.5-flash) and render the written analysis.
SECURITY IS THE PRIMARY REQUIREMENT. This app holds live OAuth tokens and
a money-spending AI key in the browser. Apply ALL of the following, and
flag anything you cannot satisfy:
- No client secret anywhere, ever. Both providers use public-client PKCE
only. If a flow appears to need a secret, STOP and tell me — never
hardcode one.
- PKCE done correctly: a cryptographically-random code_verifier; an S256
code_challenge; store the verifier in sessionStorage; generate and
VALIDATE a state parameter (CSRF); exchange the authorization code
immediately; clear the verifier after exchange.
- Inject a strict Content-Security-Policy at build time (Vite plugin).
Lock connect-src to ONLY Spotify's and OpenRouter's API/auth hosts —
nothing else. No unsafe-inline or unsafe-eval for scripts. Set the
referrer policy to no-referrer.
- Keep the OpenRouter API key IN MEMORY only (never localStorage) — it
spends real money, so a leak must die quickly. You can't set a spend
cap or expiry through the PKCE flow, so show the user an interstitial
telling them to set a spend cap (and short expiry) on their key on
OpenRouter's consent screen. The Spotify token may go in localStorage.
NEVER log a token or the key, and NEVER send either anywhere except the
provider it belongs to. Strip the auth code and state from the URL
after exchange; prefer re-auth over a persisted refresh token.
- No eval. No innerHTML or dangerouslySetInnerHTML with untrusted data.
No inline event handlers. Rely on React's default escaping for any text
rendered from a Spotify or AI response.
- Never commit secrets or personal data; the Client ID is entered at
runtime.
Keep it minimal and readable. Show clear connect/disconnect state for
both Spotify and OpenRouter. That is the entire MVP.

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.

That’s a Pixie app: a static page, a bit of magic, and a backend that was never yours to run.

Built with this pattern #

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.

ProjectWhat it isStackLiveSource
AffinitySpotify taste analysis with AISpotify · OpenRouteraffinity.msyea.commsyea/affinity
MarshalGitLab roadmaps, my wayGitLab · Snippet storagemarshal.msyea.commsyea-sa/marshal

Hand written by me. Copy-editted by Claude. I used em-dashes before the slop, they’re my style and here to stay.

← All articles