Skip to content

Latest commit

 

History

History
370 lines (274 loc) · 29 KB

File metadata and controls

370 lines (274 loc) · 29 KB

Flaim Architecture

Doc routing: see docs/INDEX.md.

Flaim is an MCP (Model Context Protocol) service that connects ESPN, Yahoo, and Sleeper fantasy leagues to Flaim Fantasy in ChatGPT and Claude and, as an advanced option, to compatible AI platforms through a manual custom connector. It handles authentication, credential management, and real-time data fetching. The web app also includes a homepage live demo.

Quick Start

git clone https://github.com/jdguggs10/flaim
cd flaim
corepack pnpm install
cp web/.env.example web/.env.local   # add keys
corepack pnpm run dev

Prerequisites: Node 24+ with Corepack. Wrangler is installed through workspace dependencies and can be run with corepack pnpm exec wrangler.

Root, web, and workers use pnpm via Corepack. The Chrome extension is intentionally npm-isolated under extension/ with its own lockfile and Chrome Web Store release flow.

Core Pieces

  • Chrome Extension (/extension): Captures ESPN cookies (SWID, espn_s2) and syncs them to Flaim using Clerk Sync Host (no pairing codes).
  • Next.js web app (/web): Site pages (discovery-first landing page, setup + management hub at /leagues, privacy policy), OAuth consent screens, and homepage live demo.
  • Auth worker (/workers/auth-worker): Supabase credential + league storage, JWT verification, OAuth token management, extension APIs, durable ESPN history workflows, and the Svix-verified Clerk user.deleted webhook that drives account deletion. Uses Hono for routing.
  • Unified Gateway (/workers/fantasy-mcp): Single MCP endpoint exposing unified tools for all platforms and sports. Routes to platform-specific workers via service bindings. For most tools the gateway is a pure conduit; where a tool's provider envelopes diverge enough to confuse consumers, the gateway may layer a canonical, additive normalization on the routed result (per-tool normalizer modules applied at the route seam — get_free_agents is the template). Canonical gateway fields use camelCase; the older provider-side ownership_scope fields emitted inside get_players predate this pattern and are a legacy variant to converge in a future reviewed version. Legacy provider fields are never removed or renamed by normalization — published clients pin old schemas.
  • ESPN Client (/workers/espn-client): Internal worker handling all ESPN API calls for all sports (football, baseball, basketball, hockey). Called by fantasy-mcp gateway.
  • Yahoo Client (/workers/yahoo-client): Internal worker handling all Yahoo Fantasy API calls for all sports (football, baseball, basketball, hockey). Called by fantasy-mcp gateway.
  • Sleeper Client (/workers/sleeper-client): Internal worker handling all Sleeper API calls for NFL and NBA (public API, no auth required). Called by fantasy-mcp gateway.
  • Shared package (/workers/shared): Common utilities (CORS middleware, auth-fetch helper, types) used by all workers.
  • Supabase Postgres: espn_credentials, espn_leagues, espn_history_jobs, yahoo_credentials, yahoo_leagues, sleeper_connections, sleeper_leagues, archived_leagues (manual league archive), user_preferences (defaults), oauth_tokens, oauth_codes, and account_deletions (permanent deletion tombstones). See docs/DATABASE.md for the full model.

Runtime Choices (Next.js)

  • API routes run on the Node.js runtime (default). We removed Edge runtime flags because these routes are simple proxies/handlers and don't need Edge-specific features.
  • This avoids Edge limitations (no ISR, tighter API compatibility) and keeps behavior predictable for Node APIs like Buffer.

Directory Structure

web/                        # Next.js app (see web/README.md)
workers/                    # Cloudflare Workers (see workers/README.md)
  auth-worker/              # Auth, OAuth, credentials, leagues
  fantasy-mcp/              # Unified MCP gateway (routes to platform workers)
  espn-client/              # ESPN API client (called by fantasy-mcp)
  yahoo-client/             # Yahoo API client (called by fantasy-mcp)
  sleeper-client/           # Sleeper API client (called by fantasy-mcp; public API)
  shared/                   # @flaim/worker-shared package
extension/                  # Chrome extension; npm-isolated (see extension/README.md)
docs/                       # Documentation

What Flaim Is

Flaim is an authentication and data service, not a chatbot:

  • MCP Server: Exposes fantasy league data to ChatGPT, Claude, and optional manual MCP clients via Model Context Protocol
  • OAuth Provider: Handles secure authentication between AI clients and ESPN data
  • Credential Manager: Securely stores ESPN session cookies captured by the Chrome extension

The public live showcase lives on the homepage, with /chat retained as a redirect to /#live-demo. The live demo is backed by a dedicated demo account and server-owned auth. The interactive dev console has been extracted to a separate flaim-chat repo (chat.flaim.app).

Primary User Flow

Extension path (automatic on sync):

  1. Sign in — Create an account at flaim.app
  2. Connect ESPN — Install extension → sync credentials
  3. Auto-discover leagues + past seasons: Current leagues save immediately; past seasons continue in the background
  4. Set defaults — Manage at /leagues (extension v1.4.0 no longer handles defaults)

Connect AI:

  • Open Flaim Fantasy in ChatGPT or Claude, or copy the MCP URL from /leagues and add it as an optional custom connector in a compatible AI platform.

Season Year Defaults

Season year defaults are deterministic and use America/New_York time. The canonical form always stores the start year of the season.

Sport Rollover Date Rationale
Baseball Feb 1 ~10 weeks before Opening Day (late March)
Football Jun 1 Longer pre-draft window before NFL kickoff (early September)
Basketball Aug 1 ~10 weeks before NBA opening night (late October)
Hockey Aug 1 ~10 weeks before NHL opening night (early October)

ESPN normalization: ESPN uses the END year for NBA/NHL seasons (e.g., 2025 for the 2024-25 season). Flaim normalizes this to the start year internally via the shared toCanonicalYear()/toPlatformYear() helpers in @flaim/worker-shared (workers/shared/src/season.ts).

User Defaults

Defaults are stored centrally in user_preferences:

  • default_sport - User's preferred sport (football, baseball, etc.)
  • default_football - Default football league: { platform, leagueId, seasonYear }
  • default_baseball - Default baseball league
  • default_basketball - Default basketball league
  • default_hockey - Default hockey league

Each per-sport column is nullable JSONB. Cross-platform exclusivity is automatic (one column per sport = one value).

Chrome Extension

The extension simplifies ESPN credential capture. See extension/README.md for full documentation.

Extension Popup → Clerk Sync Host → POST /api/extension/sync → Auth Worker → Supabase
     ↓
ESPN Cookies → POST /api/extension/sync → Auth Worker → Supabase

Sync Host flow:

  1. User signs in at flaim.app (Clerk session)
  2. Extension popup detects the session via Sync Host
  3. Extension reads ESPN cookies and syncs to Flaim with Clerk JWT

Extension APIs (via Next.js proxy → auth-worker):

Endpoint Auth Purpose
POST /extension/sync Clerk JWT Sync ESPN credentials
POST /extension/discover Clerk JWT Save current leagues and start historical discovery
GET /extension/history Clerk JWT Read the latest historical discovery status
GET /extension/status Clerk JWT Check connection status
GET /extension/connection Clerk Web UI status check

Security:

  • Clerk JWT verification in auth-worker
  • Extension never stores long-lived custom tokens

AI Client OAuth 2.1

ChatGPT, Claude, and optional manual MCP clients connect to Flaim's MCP servers:

  • MCP URL: https://api.flaim.app/mcp (unified gateway - handles all sports; /fantasy/mcp also works as legacy alias)
  • Opt-in authenticated discovery: The exact path /mcp?auth=required requires OAuth during connector discovery in production and preview. The canonical resource remains /mcp, public widget resources remain available, and the default unauthenticated /mcp discovery handshake is unchanged.
  • OAuth Flow: Full OAuth 2.1 with PKCE, Dynamic Client Registration (RFC 7591), Protected Resource Metadata (RFC 9728)
  • Endpoints: /auth/register (DCR), /auth/authorize, /auth/token, /auth/revoke
  • Metadata: /.well-known/oauth-authorization-server, /.well-known/oauth-protected-resource
  • Token lifetime: MCP access tokens are short-lived (1 hour). Refresh tokens rotate on each successful refresh and use a 1-year inactivity window by default (OAUTH_REFRESH_TOKEN_TTL_SECONDS, default 31536000, clamped to 1 hour minimum and 1 year maximum).

The exact observed Grok OAuth callback, https://grok.com/connectors-oauth-exchange-code/, is accepted for registration and authorization. With the opt-in authenticated-discovery path, Grok completes the Flaim authorization flow and can use the authenticated get_user_session tool.

Gemini Spark registers six callbacks together: /r/ and /a/ user-bound paths on each of its production, test, and sandbox Google redirect hosts. Flaim accepts only those exact hosts and path forms with Gemini's numeric identifier and Flaim production-host suffix; sibling hosts and structural URI variations remain rejected.

Cursor's docs publish two fixed OAuth callbacks: http://localhost:8787/callback for the desktop app, already covered by the generic loopback rule (/callback is an allowed loopback path), and https://www.cursor.com/agents/mcp/oauth/callback for web and Cloud/Background Agents, accepted as an exact match. Separately, Flaim also matches Cursor's older cursor://anysphere.cursor-*/oauth/{id}/callback custom-scheme redirect structurally, for continuity with earlier desktop-IDE versions.

User flow: Open Flaim Fantasy in ChatGPT or Claude, or add the MCP URL as an optional custom connector in a compatible AI platform → 401 triggers OAuth → user consents at flaim.app/oauth/consent → token exchange → tools available.

MCP Tools

The unified gateway exposes tools with explicit parameters (platform, sport, league_id, season_year). See the Unified Gateway Architecture section below for the full tool list. Legacy per-sport workers are still functional but deprecated.

Unified Gateway Architecture

The unified gateway (fantasy-mcp) provides a single MCP endpoint for all platforms and sports, replacing the per-sport workers.

ChatGPT / Claude / manual MCP clients → fantasy-mcp (gateway) → espn-client → ESPN API
                                                           → yahoo-client   → Yahoo API
                                                           → sleeper-client → Sleeper API (public)
                                                           → auth-worker    → Supabase

Key benefits:

  • Single MCP URL for all sports: https://api.flaim.app/mcp
  • Explicit tool parameters: platform, sport, league_id, season_year
  • Easier multi-platform support (ESPN + Yahoo + Sleeper)
  • Service bindings for worker-to-worker communication (no 522 timeouts)

Unified tools:

  • get_user_session — Current-season leagues only with structuredContent for ChatGPT widget rendering
  • refresh_leagues — Re-discover connected leagues and update Flaim's league records (mcp:write; non-destructive)
  • get_ancient_history — Past seasons and historical leagues (everything not in the current season)
  • get_league_info — Baseline league context: settings, roster config, teams/owners (requires platform, sport, league_id, season_year)
  • get_draft: Confirmed draft results and provider-grounded draft-pick ownership (optional round, team_id, and Sleeper draft_id)
  • get_standings — League standings
  • get_matchups — Current/specified week matchups
  • get_roster — Team roster with player details
  • get_free_agents — Players available in the selected league, with a canonical gateway-normalized envelope (capabilities, ordering, ownership scope) layered over each platform's legacy fields
  • get_players — Player lookup across roster statuses with market/global ownership context (market_percent_owned, ownership_scope) and league ownership when available
  • get_transactions — Recent transactions (adds, drops, waivers, trades)
    • ESPN treats the public week selector as a matchup period for every sport and returns source/window/limitation metadata; the structured mTransactions2 view is the primary source (failed bids, trade lifecycle, FAAB, directional trade sides) with an activity-feed fallback where structured-only filters fail explicitly.
    • Week semantics are platform-specific: ESPN accepts matchup periods starting at 0 (preseason); Sleeper accepts positive matchup weeks starting at 1; Yahoo uses a recent 14-day timestamp window and ignores explicit week.
    • Yahoo type=waiver and type=pending_trade return pending items for the authenticated user's own team; other supported types use Yahoo's recent league transaction feed.

Status:

  • Unified gateway is the sole MCP endpoint (Jan 2026)

Security

  • JWKS-based Clerk JWT verification in auth-worker (5m cache). Prod rejects spoofed headers.
  • MCP workers forward Authorization; auth-worker alone validates tokens.
  • Per-user isolation via verified sub; credentials never sent back to client after setup.
  • Self-service account deletion (FLA-311): Clerk's native delete-account flow fires a dedicated Svix-verified user.deleted webhook on auth-worker, which calls an atomic Postgres purge (permanent account_deletions tombstone + per-user advisory lock, all connected-platform credentials and league data removed in one transaction). Guard triggers on every user-keyed table reject later writes for a deleted account. Usage telemetry is retained per the privacy policy.
  • Rate limiting: Cloudflare Workers native rate_limits bindings — 10 req/60s per IP on token endpoint, 15 req/60s per user on credentials endpoint, and 60 req/60s per user on OAuth/Clerk-authenticated MCP requests (internal eval and demo API keys are exempt).
  • Public demo cache: the homepage reads precomputed answers from demo_answer_cache. The live-turn public-chat path has been removed.
  • Public demo refresh pipeline: an external private runner uses static MCP bearer auth to populate demo_answer_cache out of band on a scheduled cadence. The website only reads cached answers and never triggers refresh runs. The legacy ESPN demo contract is public-demo-answer:{presetId}:{sport}:v7:v2. Platform-aware targets use public-demo-answer:{presetId}:{platform}:{sport}:v8:v3, with matching platform, sport, prompt_version, and context_version columns. The platform-aware reader requires the target to be enabled and fully warmed through /api/public-chat/capabilities; during the current transition, only ESPN baseball may fall back from a missing v8/v3 target row to its legacy v7/v2 row. Version tags and homepage preset metadata live in web/lib/public-chat.ts, while LLM system prompts and per-preset generation instructions remain outside this public repository.
  • Public demo phone client: the homepage phone reads /api/public-chat/capabilities on mount and derives its selectable platforms, sports, and presets from that response. When at least one target is advertised it sends platform, sport, and presetId on every cache read, shows only the selected target's advertised presets, and switches sport automatically when the chosen platform has no demo for the current one. An empty or failed capabilities response keeps the legacy ESPN baseball behavior and omits platform, which is what keeps the one-release fallback lane reachable. Prepared prompts stay inert until that response lands, so manual and deep-link runs both wait and the client never issues a read whose identity it is about to change. Activating a target discards any run that predates the response instead of adopting it. Because that request gates the prompts, it carries an 8s client deadline, and a stalled response falls back to legacy mode rather than leaving the demo unusable. The client state model lives in web/lib/public-demo-client.ts: capability parsing, target selection, request construction, and the run-token reducer that discards stale responses.
  • Public demo prompt lifecycle: add or remove homepage demo prompts in both codebases. Update web/lib/public-chat.ts here for site metadata and keep the external runner aligned on preset IDs, prompt versions, and context versions. If a prompt is removed from the site, remove it from the runner too so the Pi stops refreshing an unreachable cache key.
  • OAuth tokens stored in Supabase with expiration tracking.
  • ESPN credentials: AES-256 encrypted at rest (Supabase default).

See workers/README.md for worker-to-worker communication requirements.

Data Flow

  1. User syncs ESPN credentials via the Chrome extension → stored in Supabase via auth-worker.
  2. User confirms and manages discovered leagues at /leagues (per-season rows) → stored in Supabase.
  3. User connects through ChatGPT, Claude, or an optional manual MCP client → OAuth flow → token stored in Supabase.
  4. ChatGPT, Claude, or the manual MCP client calls an MCP tool after connection → MCP worker fetches creds from auth-worker → calls ESPN → returns data.

For allowlisted web and extension users, current ESPN leagues are discovered inside the request and historical seasons can be processed by a Cloudflare Workflow. This durable path is rollout-gated and disabled by default in every environment; it requires both ESPN_DURABLE_HISTORY_ENABLED=true and an exact Clerk user ID in ESPN_DURABLE_HISTORY_USERS. When enabled, the request and workflow share an exact ESPN sync lease, and every league write is checked against its current owner. Changing or removing ESPN credentials, or deleting or replacing saved ESPN leagues, takes over that lease before changing rows, so an in-flight refresh cannot restore stale data. The workflow checkpoints each historical league-season through a fenced Supabase RPC and can resume after worker retries. A versioned full-repair marker makes the first scan exhaustive; later scans skip existing historical rows. get_ancient_history remains a fast read-only index over the rows already committed by that workflow. MCP-triggered refresh remains synchronous until the separately gated MCP behavior change ships.

Legacy ESPN repair uses a separate, default-off backend migration lane. Every five minutes, auth-worker may atomically claim one account from a fixed pre-deployment cohort, seed the durable job from the newest saved row for each league root, and transfer that account's exact ESPN lease to the same Workflow. Database constraints allow only one active scheduled job globally. Candidate selection excludes completed full repairs, hidden roots, active leases, recent failed attempts, exhausted credential snapshots, and accounts with no saved league roots. The interactive rollout gate does not enable or select these scheduled jobs.

Usage Analytics

The gateway emits one best-effort telemetry event per MCP tool call (FLA-156), independent of the tool-call path — it cannot slow or break a tool call.

fantasy-mcp tool call → waitUntil(POST /internal/usage-event) → auth-worker → Supabase mcp_tool_events
                                                                            ↓ nightly pg_cron rollup (05:15 UTC)
                                                              mcp_user_daily + mcp_tool_daily (permanent)
  • Fire-and-forget: emitted in ctx.waitUntil with swallowed errors — never awaited, adds no latency, and a logging failure can never break a tool call.
  • Tagged for filtering: every event carries env (prod/preview/dev) and auth_type (oauth/clerk/eval-api-key/demo-api-key). Real-user metrics filter env='prod' AND auth_type='oauth', which excludes preview traffic, the demo runner (demo-api-key), and eval runs (eval-api-key).
  • Two tiers: raw mcp_tool_events is pruned after 90 days; pg_cron rolls each UTC day into the permanent, tiny mcp_user_daily / mcp_tool_daily rollups.
  • ET history is the dashboard source: the database contract contains an owner-only mcp_user_daily_et aggregate and serialized close/backfill function. The canonical dashboard payload delegates to an owner-only implementation that combines closed ET summaries with raw days after the marker. Migrations create no close cron; local synthetic seed data initializes the marker before its first snapshot refresh, while hosted backfill and scheduling remain explicit operations.
  • Health stays exact: the history implementation keeps latency and error health on raw events, using a disclosed 30-day window for the historical health keys and the existing seven-day window for recent health. It does not combine stored percentiles.
  • Attribution retained: ET summaries also preserve nullable platform and sport from each event. Missing attribution stays unknown; it is not inferred from current connections. Existing dashboard metrics sum across these dimensions.
  • Telemetry only: tool, platform, sport, status, latency, and a hashed league id — never rosters, players, or question text.

Schema is summarized in docs/DATABASE.md; the reviewed, secret-free deployable contract lives in supabase/. Clerk cannot see this usage (it only observes website-session activity), so these metrics — not Clerk's dashboard — are the source of truth for DAU/WAU/MAU and retention.


Deployment

Quick Reference

I want to... Do this What happens
Deploy to prod Push/merge to main Workers + Frontend auto-deploy (~1-2 min)
Test in preview Open a PR Workers + Frontend deploy to preview URLs
Test locally corepack pnpm run dev Nothing deploys, runs on localhost
Check deploy status gh run list --limit 5 Shows recent GitHub Actions runs
Fix broken prod Revert commit, push to main Auto-redeploys with fix
Instant worker rollback wrangler rollback --env prod in worker dir Reverts to previous version without git commit
Update extension Manual CWS upload See extension/README.md

Automatic Deployment (CI/CD)

Everything deploys automatically on merge to main:

Component Platform Trigger Environment
Workers (auth-worker, espn-client, yahoo-client, sleeper-client, fantasy-mcp) Cloudflare Push to main --env prod
Workers (auth-worker, espn-client, yahoo-client, sleeper-client, fantasy-mcp) Cloudflare PR opened/updated --env preview
Frontend (/web) Vercel Push to main Production
Frontend (/web) Vercel PR opened/updated Preview
Extension Chrome Web Store Manual N/A

GitHub Actions workflows (.github/workflows/):

  • deploy-workers.yml — Tests + deploys all 5 workers on push/PR (paths-filtered to workers/**)
  • check-web.yml — Lint + type-check for Next.js app (paths-filtered to web/**)
  • claude.yml — Claude Code bot responds to @claude mentions in issues/PRs
  • claude-code-review.yml — Auto-reviews PRs with Claude

Environments

Env ENVIRONMENT NODE_ENV Notes
dev dev development Local corepack pnpm run dev
preview preview production PR deploys (auto)
prod prod production main branch (auto)

Manual Deploy Commands

Usually not needed since CI/CD handles it, but available for debugging:

  • Workers (manual fallback, per worker):
    • corepack pnpm --dir workers/auth-worker exec wrangler deploy --env preview (or --env prod)
    • corepack pnpm --dir workers/fantasy-mcp run deploy:preview (or deploy:prod)
    • corepack pnpm --dir workers/espn-client run deploy:preview (or deploy:prod)
    • corepack pnpm --dir workers/yahoo-client run deploy:preview (or deploy:prod)
    • corepack pnpm --dir workers/sleeper-client run deploy:preview (or deploy:prod)
  • Frontend: Push to main or PR (Vercel auto-deploys)
  • Extension: See extension/README.md for Chrome Web Store update process

Secrets & Environment Variables

  • Frontend: web/.env.local (see web/README.md)
  • Workers: Via wrangler dev locally, Cloudflare Dashboard in prod (see workers/README.md)
  • GitHub Actions: CLOUDFLARE_ACCOUNT_ID, CLOUDFLARE_API_TOKEN in repo secrets

Preview Environment

Opening a PR triggers a full preview stack: Vercel preview deploy + all 5 Cloudflare Workers in --env preview. The preview chain is fully isolated from production at the worker level (preview workers bind to each other via service bindings).

Key differences from production:

Layer Production Preview
Frontend flaim.app (Vercel) flaim-git-{branch}-gerald-guggers-projects.vercel.app
Clerk Production instance (pk_live_) Development instance (pk_test_), separate user pool
Workers auth-worker, fantasy-mcp, etc. auth-worker-preview, fantasy-mcp-preview, etc.
Supabase Isolated production project Isolated preview project with synthetic data
Worker URLs Custom domains (api.flaim.app/*) .workers.dev URLs

Vercel env vars are scoped by environment. NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY, CLERK_SECRET_KEY, AUTH_WORKER_URL, NEXT_PUBLIC_AUTH_WORKER_URL, and NEXT_PUBLIC_FANTASY_MCP_URL each have separate Production and Preview values. Preview points to dev Clerk and preview worker URLs. Server-only web routes should use AUTH_WORKER_URL with the direct .workers.dev worker URL; browser-visible configuration uses the NEXT_PUBLIC_* names and may point at the public custom gateway.

Auth-worker's MCP OAuth consent redirect (/authorize) uses a static preview FRONTEND_URL (https://preview.flaim.app, the domain pinned to the staging branch below) rather than resolving dynamically per PR branch. This is deliberate: an MCP client (ChatGPT, Claude) opens the user's browser directly at /authorize with no preceding Flaim webpage in that navigation, so there is no flaim-*.vercel.app Origin/Referer header to recover the calling branch from — a dynamic lookup here would have nothing to read and fall through to production. Auth-worker does resolve the frontend dynamically for the Yahoo-connect flow specifically (stored redirect_after, captured from the Origin/X-Forwarded-Origin header on the initiating request), since that flow starts from a user actively browsing a specific PR's Vercel preview URL.

Supabase is isolated. Preview Workers use the dedicated preview database; production Workers use the production database. Preview verification uses synthetic or preview-created rows and must not copy production credentials or production user data.

Triggering preview deploys: Workers only deploy on PRs (not bare branch pushes). A PR must exist for GitHub Actions to run deploy-workers.yml with --env preview. Vercel deploys on any push.

Staging integration preview. staging is a disposable integration-preview branch: it is rebuilt from main plus selected in-flight branches whenever composition changes, so one continuously deployed Vercel preview always shows the combined upcoming site (a team preview domain may be pinned to it). Rules:

  • Nothing ever merges from staging into main. Every real change lands through its own PR against main with normal review and CI.
  • The branch may be force-pushed or rebuilt at any time; it carries no review or deployment authority, and no PR is opened for it.
  • Preview deployments read the isolated preview database, so the homepage demo shows its unavailable state on staging by design; demo answers are only proven in production.

See Notion (Platform + Infrastructure) for decision rationale.

DNS for Custom Routes

Cloudflare DNS: A record, name api, IPv4 192.0.2.1, proxied (orange).

Verify: curl https://api.flaim.app/auth/health

Verification

  • Local: curl http://localhost:8786/health (auth-worker), localhost:3000 (frontend)
  • Remote: Deployed worker URL + /health
  • Check deploy status: gh run list --limit 5

Troubleshooting

Symptom Cause Fix
Double slashes in URLs Trailing slash in env vars Remove trailing slashes
Extension "Failed to fetch" Production build loaded locally Rebuild from extension/ with npm run build:dev
Extension not signed in Clerk session not syncing Close/reopen extension popup, confirm flaim.app sign-in
MCP error 424 "Failed Dependency" AI client can't reach localhost MCP URLs Deploy workers to preview, update .env.local with preview URLs
Node.js v25 localStorage warning Known Node v25 regression Harmless; suppressed via --no-webstorage in dev script

See workers/README.md for worker-specific troubleshooting (522s, 404s, 500s).