Actions you (Tom) need to take outside this repo. Everything that can be automated by Claude Code is being automated — this doc covers the things that require your credentials, accounts, or browser approval.
Legend:
- ⚡ Now — needed for current dev loop
- 🔜 Soon — needed for Phase 18 (hosted backend)
- 🧰 Nice-to-have — dev ergonomics, do when convenient
Why this exists (June 2026): local dev used to point at the prod Supabase DB and the prod GitHub OAuth app. That cross-talk silently revoked prod's GitHub token: every reconnect (dev or prod) minted a new token against the same OAuth app until GitHub's 10-tokens-per-user/app/scope cap revoked the oldest, and any backend still holding the revoked token in memory would 401 and delete the shared
integrationsrow. Dev and prod must never share a DB, an OAuth app, or aTALYN_TOKEN_KEY.
Local dev runs the full stack on your machine via the Supabase CLI (Docker required):
npm run dev:db # supabase start (db + auth + api; heavyweight services excluded)
npm run dev # backend (migrations auto-apply on boot) + desktop
npm run dev:db:stop # when you're doneLocal endpoints (stable across restarts):
- API / auth:
http://127.0.0.1:54321 - Postgres:
postgresql://postgres:postgres@127.0.0.1:54322/postgres - Studio:
http://127.0.0.1:54323 - Keys:
npx supabase statusprints the local anon + service_role keys
Two dev-only classic GitHub OAuth apps back this (create once in the browser — GitHub has no API for it):
Talyn Login (Local Dev)— desktop login via local Supabase auth.- Homepage:
http://127.0.0.1:54321 - Callback:
http://127.0.0.1:54321/auth/v1/callback - Client id + secret go in
supabase/.env(gitignored) asSUPABASE_AUTH_EXTERNAL_GITHUB_CLIENT_ID/…_SECRET; the provider is wired up insupabase/config.toml. The dev desktop deep linkfastowl-dev://auth-callbackis already inadditional_redirect_urls(dev builds use thefastowl-dev://scheme so the OAuth callback reopens your dev build instead of an installed production FastOwl.app).
- Homepage:
Talyn (Local Dev)— the workspace GitHub integration (PR monitoring etc.).- Homepage:
http://localhost:4747 - Callback:
http://localhost:4747/api/v1/github/callback - Client id + secret go in
packages/backend/.envasGITHUB_CLIENT_ID/GITHUB_CLIENT_SECRET.
- Homepage:
packages/backend/.env and apps/desktop/.env must point at the local stack —
never at Railway/prod values (those live only in Railway service variables).
Used by packages/backend/src/services/ai.ts for auto-generating task titles/descriptions from prompts.
- Go to https://console.anthropic.com/settings/keys
- Create a key
- Export it when running the backend:
Or add it to a
export ANTHROPIC_API_KEY=sk-ant-....envfile at the repo root (add to.gitignorefirst — don't commit).
Without this, task metadata falls back to first-60-chars heuristic, which is functional but noticeably worse.
Talyn spawns claude (interactive mode) via node-pty on the chosen environment. The binary must be in the PATH of whichever shell gets spawned.
- Local:
npm install -g @anthropic-ai/claude-cli(or whatever the current install command is), log in viaclaude login - VMs: same, on the remote user's shell
Verify by running claude --version as the shell user Talyn will use.
One-time MCP trust approval (only if you run autonomous tasks in strict mode on this environment — i.e., the env's "Allow unattended Claude runs to bypass permission prompts" toggle is OFF):
Talyn's repo root ships a .mcp.json registering the Supabase MCP server. On first encounter, Claude Code prompts you to trust it. Autonomous runs can't answer that prompt, so do it once interactively:
cd ~/path/to/fastowl # or wherever the clone lives on this env
claude # opens the TUI
# → prompted: "New MCP server found in .mcp.json: supabase — use this?"
# → pick "Use this and all future MCP servers in this project"
# → Ctrl-D to exitThe approval lands in your user-level Claude config and sticks. You don't need to do this on daemon envs that have "bypass permissions" enabled — they skip all prompts by design.
Used by the GitHub integration (connect GitHub → PR monitoring, PR actions, repo listing).
Use a classic OAuth App, NOT a GitHub App. The connect flow in
services/github.tsis the classic OAuth web flow (scope=repo read:user read:org, exchange code → long-lived user token). A GitHub App grants access by installation, so it can't read a repo you personally have access to (e.g.posthog/posthog) without being installed on that org — the wrong model here. A classic OAuth App's token acts as you, so it reads any public repo and any private repo your account can reach (private org repos may still need the org to approve the app under its third-party-access policy). You can tell the two apart by the client ID: classic OAuth = bare hex /Ov23…; a GitHub App isIv1.…/Iv23…— if you see that prefix, you made the wrong app type.
- https://github.com/settings/developers → OAuth Apps tab → New OAuth App (do not use the "GitHub Apps" tab)
- Application name:
Talyn (Dev)(make a separate prod one later) - Homepage URL:
http://localhost:4747 - Authorization callback URL:
http://localhost:4747/api/v1/github/callback(must matchGITHUB_REDIRECT_URIexactly) - Create, then Generate a new client secret
- Export before running the backend:
export GITHUB_CLIENT_ID=Ov23xxxxx # classic OAuth App id (NOT Iv1./Iv23 — that's a GitHub App) export GITHUB_CLIENT_SECRET=xxxxx export GITHUB_REDIRECT_URI=http://localhost:4747/api/v1/github/callback
No scopes are configured on the app itself — the backend requests
repo read:user read:org at authorize time.
Without these, the "Connect GitHub" button in Settings will fail loudly.
The App is what lets us replace polling with webhooks and scale across replicas. It uses hybrid auth: an installation token does all repo/PR/checks reads + receives webhooks, while a user-to-server token (requested during install) resolves the viewer's login + authored/review-requested buckets. The classic OAuth App above still works; the App lights up per workspace as each one re-connects via the install flow.
Make TWO Apps — "Talyn App Dev" (slug
talyn-app-dev) and "Talyn App" (slugtalyn-app). A GitHub App has exactly one webhook URL, so it can't serve localhost and Railway at once. Dev's creds go inpackages/backend/.env; prod's go in Railway variables. The backend just reads whateverGITHUB_APP_*/GITHUB_WEBHOOK_SECRETenv it finds — no code difference. Use a separate private key + secret per App.
- https://github.com/settings/apps → New GitHub App.
- Webhook URL:
https://<your-backend>/api/v1/webhooks/github.- Prod:
https://prod.talyn.dev/api/v1/webhooks/github. - Local: tunnel it —
npx smee-client --url https://smee.io/<channel> --target http://localhost:4747/api/v1/webhooks/github, and use thesmee.io/<channel>URL here. Webhook secret: generate one →GITHUB_WEBHOOK_SECRET.
- Prod:
- Permissions (read-only unless noted): Pull requests R/W (merge/auto-merge write paths), Checks R, Commit statuses R, Contents R, Metadata R, Members R (team-based review-request resolution).
- Subscribe to events:
pull_request,pull_request_review,pull_request_review_comment,issue_comment,check_run,check_suite,status,push,installation,installation_repositories. (pushis what lets us catch a PR becoming conflicting because its base branch advanced — GitHub sends no per-PR event for that; on a push we refresh every open PR based on the pushed branch.) - Enable "Request user authorization (OAuth) during installation", and set the
Callback URL to
http://localhost:4747/api/v1/github/app/callback(prod: the Railway host equivalent — an App can list multiple callback URLs). - Leave "Expire user authorization tokens" OFF. With it on, the user token dies after 8h and we have no refresh-token rotation yet — the workspace would silently break (and a 401 on the user token can tear down the integration). The installation token is separate and always short-lived; we mint/refresh it ourselves regardless. (Turning expiry on is a follow-up, paired with rotation.)
- Generate a private key (.pem), base64 it, and set the env vars (see the
.envblock below):GITHUB_APP_ID,GITHUB_APP_SLUG,GITHUB_APP_CLIENT_ID,GITHUB_APP_CLIENT_SECRET,GITHUB_APP_PRIVATE_KEY,GITHUB_WEBHOOK_SECRET. - Set
REDIS_URL(webhooks are enqueued onto a Redis Stream; without it the receiver acks-and-drops and the reconcile sweep keeps PRs fresh).- Optional:
WEBHOOK_STREAM_MAXLENcaps the ingest stream length (default50000, approximate trim). A backlog only forms if the worker can't keep up; raise it for more runway before the oldest deliveries are dropped, but size it to your Redis memory — each entry holds a full webhook payload (~10-30KB), so 50k ≈ up to ~1GB worst case.
- Optional:
Connecting (two steps):
- Install the App on each account/org whose repos you want to track — GitHub → the App's page → Install (or Configure to add repos). One-time per account; a user can install it on their personal account and several orgs.
- In the desktop, Settings → Integrations → GitHub → Connect GitHub. This
runs the OAuth user-authorization flow (
/login/oauth/authorize), which always redirects back to/github/app/callbackwith a signedstate— whether or not the App is already installed. (We deliberately do NOT use/installations/new, which dead-ends on the "configure" page for an existing install.) The callback exchanges the code for the user token, discovers all the user's installations viaGET /user/installations, records them, and bulk-refreshes.
Data-plane reads resolve the installation per repo owner, so one workspace can span repos across multiple installed accounts/orgs. A repo only delivers webhooks once the App is installed on its owner with that repo selected. The reconcile sweep (~5 min) + manual refresh are the safety net for missed deliveries.
Do these when we're ready to stand up the hosted infrastructure — not urgent yet, but creating the accounts early is free and removes friction when we get there.
For Postgres + auth when Phase 18.1/18.2 lands.
- Create account at https://supabase.com
- New project (free tier is fine for dev) — pick closest region
- From the project dashboard, grab:
SUPABASE_URL(Project settings → API → Project URL)SUPABASE_ANON_KEY(same page → anon public)SUPABASE_SERVICE_ROLE_KEY(same page → service_role) — never expose to the desktop app, backend onlyDATABASE_URL(Project settings → Database → Connection string → URI, with pooling for runtime)
- Enable GitHub OAuth in Supabase Auth (Authentication → Providers → GitHub). You'll need to create a separate GitHub OAuth app for Supabase auth (the one from #3 is for workspace-level GitHub integration):
- https://github.com/settings/developers → New OAuth App
- Homepage URL: your Supabase project URL
- Authorization callback URL:
https://<project-ref>.supabase.co/auth/v1/callback - Paste the client ID/secret into Supabase Authentication → Providers → GitHub
- Add
fastowl://auth-callbackto Redirect URLs in Supabase (Authentication → URL Configuration → Redirect URLs). Without this, the desktop deep-link flow fails silently with an "invalid redirect URL" error.
Talyn doesn't ship an invite flow yet — anyone with a GitHub account can sign in to an instance. To lock a self-hosted instance to just you, set on the backend:
TALYN_ALLOWED_EMAILS=you@example.com
Multiple emails are comma-separated. Unauthorised callers get a 403 on first request. Once invite flows land (TODO in ROADMAP Phase 19) this can go away.
Hosted backend lives at https://prod.talyn.dev
(project Talyn, service fastowl-backend, env production).
Auto-deploy on push to main via .github/workflows/deploy-backend.yml —
needs a RAILWAY_TOKEN GitHub secret:
- Railway dashboard → Project Settings → Tokens → create a project token (account-scoped works too) → copy.
- GitHub repo → Settings → Secrets and variables → Actions → New
repository secret → name
RAILWAY_TOKEN, paste value.
Supabase connection: Railway can't route IPv6, so the backend uses
the transaction pooler (aws-1-eu-west-2.pooler.supabase.com:6543)
not the direct connection. If you rotate the DB password, update
DATABASE_URL on Railway too (railway variables --set 'DATABASE_URL=...').
GitHub OAuth callback: the workspace-integration GitHub OAuth app
(the one from section #3 above, used for PR monitoring) has its
authorization callback URL pointing at localhost. Once you actually
start using GitHub integration against the hosted backend, update the
OAuth app's callback to
https://prod.talyn.dev/api/v1/github/callback.
The Supabase-auth GitHub OAuth app (for user sign-in) is separate and
already points at Supabase's domain, not ours.
Single source of truth for analytics + error tracking + logs (Phase 18.8).
- Create project at https://posthog.com (or self-host)
- From Project settings grab:
POSTHOG_PROJECT_API_KEY(write key, used in server + desktop)POSTHOG_PERSONAL_API_KEY(read key, used by CI / MCP server / dashboards)POSTHOG_HOST(https://us.i.posthog.comorhttps://eu.i.posthog.comor self-hosted URL)
- In the project, enable Error tracking and Session replay features
- Create a feature flag called
fastowl_debug(off by default) that we can flip for verbose logging per user
Where the write key goes (both use the same project key):
- Desktop —
TALYN_POSTHOG_KEY/TALYN_POSTHOG_HOST, baked in at webpack build time (CI secret). The renderer also bakesTALYN_APP_VERSIONfromrelease/app/package.jsonautomatically so every event carriesapp_version. - Backend (Railway env) —
TALYN_POSTHOG_KEY/TALYN_POSTHOG_HOSTenable server-side task-lifecycle events (task_dispatched/task_completed/task_failed), attributed to the workspace owner. Unset ⇒ server analytics is a no-op (seepackages/backend/src/services/analytics.ts).
Wiring these up lets Claude Code answer questions about GitHub state, DB schema, PostHog events without manual copy-pasting. Add them to ~/.claude/mcp_servers.json or via claude mcp add.
claude mcp add github -- npx -y @modelcontextprotocol/server-githubNeeds a GITHUB_PERSONAL_ACCESS_TOKEN env var. Create one at https://github.com/settings/tokens with repo + read:org scopes.
Follow https://github.com/supabase-community/supabase-mcp for the install command. Needs SUPABASE_ACCESS_TOKEN (Supabase account level) and the project ref.
Not yet officially released but community versions exist — search posthog mcp on GitHub. Will use POSTHOG_PERSONAL_API_KEY + POSTHOG_HOST.
Official Railway MCP server — see https://docs.railway.com for the current install command. Uses a Railway account token (same one you minted in #5). Lets Claude Code create projects, deploy services, read logs, and manage variables without leaving the editor.
Exposes Talyn's own task + backlog operations as Claude tools. Useful for letting a Claude Code session (or a child agent running inside a Talyn task) create tasks, sync backlog sources, and kick the Continuous Build scheduler without dropping to a shell.
# build first
npm run build -w @talyn/shared -w @talyn/mcp-server
# register
claude mcp add fastowl -- node "$(pwd)/packages/mcp-server/dist/index.js"Or add to ~/.claude/mcp_servers.json manually:
No external account needed — it talks to your local Talyn backend. For agents Talyn spawns, parent-injected env vars (TALYN_WORKSPACE_ID, TALYN_TASK_ID) mean the tools work argument-free.
After adding any MCP server, restart Claude Code. Verify with /mcp in the prompt.
Once the accounts above exist, add these to Repo Settings → Secrets and variables → Actions so CI can use them:
| Secret | Purpose |
|---|---|
ANTHROPIC_API_KEY |
Future: CI-run tests that hit the API |
GITHUB_TOKEN |
Already provided by Actions |
RAILWAY_TOKEN |
Deploy the backend on merges to main |
DATABASE_URL |
drizzle-kit migrate step |
SUPABASE_SERVICE_ROLE_KEY |
Server-side admin operations in migrations |
POSTHOG_PROJECT_API_KEY |
Ship error events + build metrics |
APPLE_ID / APPLE_TEAM_ID / APPLE_ID_PASS |
macOS notarization (already in publish.yml) |
CSC_LINK / CSC_KEY_PASSWORD |
Code signing (same) |
Backend reads env vars on startup. To avoid exporting them every terminal, create packages/backend/.env:
ANTHROPIC_API_KEY=sk-ant-...
# Workspace-level GitHub integration (for PR monitoring)
GITHUB_CLIENT_ID=Iv1.xxx
GITHUB_CLIENT_SECRET=xxx
GITHUB_REDIRECT_URI=http://localhost:4747/api/v1/github/callback
# Database + auth (Phase 18)
DATABASE_URL=postgres://...supabase.co:6543/postgres?pgbouncer=true
SUPABASE_URL=https://<ref>.supabase.co
SUPABASE_SERVICE_ROLE_KEY=eyJ... # service role, bypasses RLS
# TALYN_ALLOWED_EMAILS=you@example.com # optional single-user lock
# Redis — cross-replica WebSocket fan-out + the GitHub webhook ingest queue.
# Optional: leave unset to run single-process; the app degrades to local-only
# delivery and webhooks are not enqueued (the reconcile sweep keeps PRs fresh).
# Start the container with `npm run dev:redis` (see docker-compose.yml).
# REDIS_URL=redis://localhost:6379
# GitHub App (webhooks + installation auth). Optional until you create the App;
# when unset, the app stays on the OAuth connect flow. See the GitHub App
# section below. GITHUB_APP_PRIVATE_KEY is the .pem, base64-encoded.
# GITHUB_APP_ID=123456
# GITHUB_APP_SLUG=talyn-app
# GITHUB_APP_CLIENT_ID=Iv1.xxxxxxxx
# GITHUB_APP_CLIENT_SECRET=xxxxxxxx
# GITHUB_APP_PRIVATE_KEY=<base64 of the downloaded .pem>
# GITHUB_WEBHOOK_SECRET=<the secret you set on the App's webhook>
POSTHOG_PROJECT_API_KEY=phc_xxx # optional, for when 18.8 lands
POSTHOG_HOST=https://us.i.posthog.com
# Polar billing (free 3-active-task limit / $15/mo Unlimited). Optional as a
# group — when ALL are unset, plan limits are simply not enforced (loud boot
# warning); a PARTIAL group is a boot error. Use the sandbox
# (sandbox.polar.sh) org + products for anything that isn't prod. Register
# the webhook at <backend>/api/v1/webhooks/polar for subscription.* events.
# POLAR_ACCESS_TOKEN=polar_oat_...
# POLAR_WEBHOOK_SECRET=<the standard-webhooks secret from the webhook config>
# POLAR_ENVIRONMENT=sandbox # or production
# POLAR_PRODUCT_ID_MONTHLY=<uuid of the $15/mo product>
# POLAR_PRODUCT_ID_ANNUAL=<uuid of the $150/yr product>
# POLAR_SUCCESS_URL=https://www.talyn.dev/checkout-success # optional
# --- Browser app (app.talyn.dev) -------------------------------------------
# All three are unset for a desktop-only deployment, which is the default.
#
# ALLOWED_ORIGINS: comma-separated CORS/WS allowlist, EXACT string match (no
# globs — a pattern rule is how "https://app.talyn.dev.evil.com" gets in).
# Loopback and a missing Origin are always allowed, so the desktop app, the
# CLI, and the MCP server never need an entry. Set this the moment a browser
# client exists, or every one of its requests is blocked.
# ALLOWED_ORIGINS=https://app.talyn.dev
#
# WEB_APP_URL: where the browser app lives. Must be https (localhost is
# allowed for dev); a malformed or http:// value is a BOOT error, because it
# becomes the GitHub App callback's redirect target and a bad one strands the
# user on the API origin. Only ever read from env, never from a request.
# WEB_APP_URL=https://app.talyn.dev
#
# TALYN_ALLOW_NULL_ORIGIN_WS: kill switch, defaults ON. The packaged desktop
# renderer loads from file://, whose WS handshake Chromium reports as the
# opaque `null`, so the upgrade accepts it. `null` is forgeable by ANY page
# (sandboxed iframe, data: URL), which is harmless while WS auth is a Bearer
# JWT in the first frame — there are no ambient credentials to hijack. Set to
# 0 the same day anything moves to cookie auth, or it becomes a live
# cross-site WebSocket hijack.
# TALYN_ALLOW_NULL_ORIGIN_WS=0
Client-side OAuth means Supabase — not the backend — owns the allowed
redirects. In the dashboard (Authentication → URL Configuration) add
https://app.talyn.dev/auth/callback to Redirect URLs and keep
fastowl://auth-callback there for the desktop. Locally the same list lives
in supabase/config.toml (additional_redirect_urls).
Vercel preview deployments get random hostnames, so they can't be allowlisted without a glob — point previews at a staging backend or accept that sign-in only works on the production hostname. Don't add a wildcard.
To comp an account onto Unlimited without paying (e.g. your own):
UPDATE users SET plan_override = 'unlimited' WHERE email = 'you@example.com';Desktop app env (set in the shell before npm run build or npm start):
TALYN_SUPABASE_URL=https://<ref>.supabase.co
TALYN_SUPABASE_ANON_KEY=eyJ... # anon key, safe to bundle
TALYN_API_URL=http://localhost:4747 # default if unset
CLI uses ~/.fastowl/token (populated via fastowl token set — copy the current token from desktop → Settings → Account → Copy CLI token). MCP server expects TALYN_AUTH_TOKEN in its spawn env.
And ensure .gitignore has packages/backend/.env. (If the backend doesn't yet load .env automatically, we'll add a dotenv import in Phase 18 cleanup — not critical yet.)
Everything in this doc that requires an account, a browser approval, or a credential you own. Specifically:
- Create Anthropic / GitHub / Supabase / Railway / PostHog accounts
- Approve OAuth apps
- Generate API keys
- Add GitHub repo secrets
- Install
claudeCLI binaries on remote VMs (we can automate this via the Phase 18.3 remote install flow once that ships, but not yet)
Everything else — schema, migrations, deploy configs, Dockerfiles, CI YAML — Claude Code can scaffold. Just share the credentials above when each phase starts.
{ "mcpServers": { "fastowl": { "command": "node", "args": ["/absolute/path/to/fastowl/packages/mcp-server/dist/index.js"], "env": { "TALYN_API_URL": "http://localhost:4747" } } } }