The app

Bytestream is one Expo SDK 57 universal app (managed workflow) with Expo Router for file-based navigation, targeting web, iOS, and Android from a single TypeScript codebase.

Storage keys never rename

Every persisted browser-storage key still carries the product’s original atlas. prefix — atlas.settings, atlas.saved, atlas.watch, atlas.theater, atlas.stills.v1, atlas.catalog.v*, atlas.epg.v2, and the atlas IndexedDB database name itself (src/state/storage.ts).
These are not configuration — they are where a viewer’s data already lives, in their browser, right now. Renaming a key does not migrate that data; it orphans it, and the app comes up as if the viewer had never used it (settings reset, saved channels gone). A rename is only safe behind a read-old/write-new migration, and none exists.

Auth: Clerk + Supabase

Clerk handles sign-in; Supabase holds the data, gated by row-level security keyed on clerk_sub() — a Postgres function that reads the Clerk subject out of the request, so a signed-in viewer can only read or write their own rows (see the RLS policies in supabase/migrations/20260920090000_recommendations.sql, e.g. (select public.clerk_sub()) = user_id). Sign-in is fully optional. Without EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY set to a real pk_ key, no ClerkProvider mounts at all, and the app renders exactly as it always has — anonymous watch, no auth context.
Clerk hooks (useAuth, etc.) may only be called behind useClerkReady() (src/auth/AuthRoot.tsx), never behind clerkConfigured() alone. A key being configured and a ClerkProvider actually being mounted above a given component are different questions — a component that mounts before the client-only provider does (2026-09-12: AccountControl in the sidebar) throws “useAuth can only be used within the <ClerkProvider />” and blanks the app if it skips this check.

The stream relay

Roughly 868 channels in the catalog serve every one of their streams from jmp2.uk (Pluto TV’s edge), which answers with a CORS header that allows only http://pluto.tv — a browser refuses to fetch it, even though the native app and VLC play it fine. worker/ is a small Cloudflare Worker that fetches such a stream server-side and returns it with a header a browser will accept. It runs on Cloudflare’s free plan by design — no KV, R2, D1, Durable Objects, Queues, cron, smart placement, or custom domain — and is deployed at atlas-stream-relay.brainpowerux.workers.dev.
It is not an open proxy: it relays only hosts on an explicit allowlist and refuses everything else with a 403. It is not a way around geo-blocking — Cloudflare answers from a data centre near the viewer, so a geo-blocked upstream still refuses that viewer. Nothing of the viewer’s goes upstream: no cookies, no Authorization, no client User-Agent, no Referer.
The allowlist (worker/wrangler.toml, [vars] ALLOWED_HOSTS) is five hosts: jmp2.uk, pluto.tv, plutotv.net (Pluto’s redirect chain), plus channel-assembly.mediatailor.us-east-1.amazonaws.com and nasaplus.akamaized.net (NASA+‘s manifest and segment hosts). HLS is recursive — a master playlist points at media playlists, which point at segments and encryption keys — so every URL inside a playlist is rewritten to route back through the relay, resolved against wherever the playlist was finally served from after redirects. The relay is enabled only when EXPO_PUBLIC_ATLAS_RELAY_URL is set at Expo export time, and only for web — native never routes through it. See Quickstart for the variable and the --clear caveat when changing it.

Deploying

The web build deploys to Vercel as a static site (vercel.json: framework: null, buildCommand: pnpm build:web, outputDirectory: dist). The Vercel project is connected to the GitHub repository (sachio222/atlas): every push to main builds and promotes production, and every other branch gets a preview URL. Nothing in the repo deploys itself; the EPG workflow publishes data to Supabase Storage and the docs workflow deploys the separate bytestream-docs project. Two project settings keep deployments small. VERCEL_FORCE_NO_BUILD_CACHE=1 is set on Production and Preview: Vercel stores a build-cache snapshot (about 216 MB here) with every deployment, and at eight deployments a day that reached 10 GB in eleven days; a cold pnpm install costs about a minute per build instead. .vercelignore also drops api/*.test.ts, which Vercel would otherwise build into functions of their own. A manual deploy still works when you need one:
The GitHub connection depends on the Vercel GitHub App having access to the repository. If a push does not produce a deployment, check the app’s repository list under GitHub → Settings → Applications → Vercel, then vercel git connect from the repo.
Vercel refuses any deployment whose git commit author email is not verified on the account, and the CLI then waits on a build-event stream that never opens — a blocked deploy looks like a hang, not an error. Commit as the verified address (git config user.email brainpowerux@gmail.com) and check vercel inspect <deployment-url> if a deploy sits at UNKNOWN for more than a minute or two.