Vibe-Coding Shareable, Infinitely Scalable Personal Apps
PKCE (pronounced “pixie”) for the backends and APIs you already trust, add OpenRouter to make them smart — all for free, or next to nothing.
Most apps have a missing feature or a nit that’s hugely frustrating, or you have a workflow you cannot quite achieve with the apps you already use. Building personal local-first, self-hosted, or unhosted apps is a time-tested answer. I’ve recently been tackling this with https://marshal.msyea.com (GitLab roadmaps my way) and https://affinity.msyea.com (trying to understand my music taste with Spotify and OpenRouter). Along the way I’ve developed an architecture for a safe and easy way to vibe-code personal apps, and I wanted to share it.
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, 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 us in the direction of a web app. We could consider a localhost app or an 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 us 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 one-shot it. 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 UGC (user-generated content), 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.
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 one. Luckily you have a huge choice. I have another requirement — the user brings their own account and uses their own storage (free or otherwise). I don’t want any running costs if my app goes viral. I have therefore discounted Firebase/Supabase.
| Provider | AuthN | AuthZ | Client Type | BFF1 | Storage | Comments |
|---|---|---|---|---|---|---|
| GitLab | OIDC | OAuth 2.0 + PKCE | Public | No | Snippet or Repo File | Git Forge |
| OIDC | OAuth 2.0 + PKCE | Public | No | App Data Folder | Google Drive · drive.appdata | |
| Microsoft | OIDC | OAuth 2.0 + PKCE | Public | No | App Folder | OneDrive · Files.ReadWrite.AppFolder |
| Apple | OIDC | Apple-proprietary2 | Mixed | Yes | CloudKit Private DB | Requires $99/year developer program. |
| GitHub | Pseudo-auth via /user | OAuth 2.0 + PKCE (secret required) | Confidential | Yes | Gist or Repo File | Git Forge |
1 BFF — you can unlock these providers with a minimal Cloudflare Worker running a Backend-for-Frontend (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.
In my Marshal app (GitLab Roadmap) I chose to store my data as a JSON blob in a personal snippet. I was already authenticated to the GitLab API and a simple read/write to the Snippet API persisted the data across devices. The Affinity app (Spotify + OpenRouter) I would probably extend to store my data in Google Drive.
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.
| Provider | AuthN | AuthZ | Client Type | BFF | Storage | Comments |
|---|---|---|---|---|---|---|
| OpenRouter | N/A | OAuth 2.0 + PKCE | Public | No | No | User 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 #
| Provider | AuthN | AuthZ | Client Type | BFF | Storage | Comments |
|---|---|---|---|---|---|---|
| Spotify | Pseudo-auth via /me | OAuth 2.0 + PKCE | Public | No | No | Web Playback SDK + limited Web API3 |
| Last.fm | Pseudo-auth via user.getInfo | Proprietary | Confidential | Yes | No | Rich music data4 |
| SoundCloud | Pseudo-auth via /me | OAuth 2.1 + PKCE (secret required) | Confidential | Yes | No | Rich 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).
BFF (Backend-for-Frontend) — 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.
One final gotcha #
Some API providers impose serious limitations that undercut my promise of infinitely scalable personal apps — 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.
The one shot #
Build "Affinity", a purely client-side static web app that analyses my Spotify taste withAI. NO backend, NO server, NO database, NO serverless functions — everything runs in thebrowser 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-spendingAI 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 andOpenRouter. 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.
PKCE is pronounced “pixie”, so that’s what I’m calling them: Pixie apps. 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. The source is below, so feel free to contribute.
| Project | What it is | Stack | Live | Source |
|---|---|---|---|---|
| Affinity | Spotify taste analysis with AI | Spotify · OpenRouter | affinity.msyea.com | msyea/affinity |
| Marshal | GitLab roadmaps, my way | GitLab · Snippet storage | marshal.msyea.com | msyea-sa/marshal |