API & tooling reference

Every HTTP surface this app exposes — 78 endpoints — with who may call it and how. Generated from the codebase and checked against it by a test, so nothing here describes an endpoint that does not exist, and nothing that exists is missing.

Reading this as an AI agent?

Fetch soleos.app/docs.md instead. Same catalogue, one plaintext file, no HTML to parse. There is also /llms.txt for what the product is, rather than what it exposes.

Support API

Everything a shipped product talks to. Public by design: no cookies, no SDK, no auth library. The ingest key identifies an app and grants exactly two capabilities — open a conversation, and append to one whose end_user_ref the caller already knows.

/api/support/v1/messagesGET · POST · OPTIONSapp key

The whole public support surface. POST opens a conversation for an (app, end_user_ref) pair or appends to the open one; GET polls for new messages. CORS-open, no cookies, no SDK required.

Auth: Public ingest key (`app_key`) + the caller's own `end_user_ref`

curl -X POST https://soleos.app/api/support/v1/messages \
  -H 'Content-Type: application/json' \
  -d '{
    "app_key": "pk_...",
    "end_user_ref": "hy2Nq8Rf3vKp1sLd7wZx4tCb",
    "message": "The game crashes on level 3",
    "client_message_id": "aX9...",
    "metadata": { "app_version": "1.4.2", "os_version": "Android 15",
                  "device_model": "SM-A155F", "locale": "tr-TR" }
  }'

# → 201 {"conversation_id":"...","message_id":42,"status":"open","duplicate":false}
#   200 with "duplicate":true when this client_message_id already landed

curl "https://soleos.app/api/support/v1/messages?app_key=pk_...&end_user_ref=hy2N...&since=42"
# → {"status":"open","messages":[...],"cursor":43,"poll_after_ms":15000,"app":{...}}
ErrorStatusRetry?Meaning
invalid_request400 / 413neverMalformed JSON, or a body over 32KB. A client bug.
invalid_app_key403neverUnrecognised key. Check what you shipped.
app_key_revoked403neverThe key was turned off in SoleOS. Hide the feature rather than retrying.
invalid_end_user_ref400neverNot 16-200 chars of [A-Za-z0-9._:-]. Fix the generator; do not retry.
empty_message400neverNothing to send.
message_too_long413neverOver 4000 characters. Cap the input client-side so this cannot happen.
rate_limited429after Retry-AfterToo fast. `Retry-After` carries the wait in seconds.
server_error5xxwith backoffKeep the message queued and try again with backoff.

end_user_ref is a bearer secret: 16-200 chars of [A-Za-z0-9._:-], stored only as SHA-256. Send it as X-End-User-Ref where your client can set headers. Rate limited per ref, per IP and per app; ETag + since make a steady-state poll cost no rows and no body.

/api/widgetPOST · OPTIONSapp key

The v0 widget protocol (actions open/send/poll), kept as a translation layer over the v1 core so script tags already embedded on live sites keep working.

Auth: Public ingest key (`app_key`) + the caller's own `end_user_ref`

curl -X POST https://soleos.app/api/widget \
  -H 'Content-Type: application/json' \
  -d '{"action":"open","key":"pk_...","meta":{"page":"/pricing","lang":"en"}}'
# → {"token":"...","greeting":"...","accent":"#F2A93B","messages":[]}

Frozen on purpose. `token` here IS a v1 `end_user_ref` — same value, same hash — so a browser holding one keeps its thread across the migration. New integrations should use v1.

/widget.jsGETpublic

The v0 embeddable chat widget, served as dependency-free JS in a Shadow DOM.

Auth: None — public

<script src="https://soleos.app/widget.js" data-soleos="pk_..."></script>
/widget/v1.jsGETpublic

The same widget speaking v1, with a persisted outbox that retries rather than dropping messages.

Auth: None — public

<script src="https://soleos.app/widget/v1.js" data-soleos="pk_..."></script>

Migrating from /widget.js is a one-line src change: this script adopts the old script's stored token so returning visitors keep their history.

/api/supportPOSTsession

SoleOS's own in-dashboard feedback box — a signed-in SoleOS user messaging the founder. Stores to `soleos_feedback` and pings admins by email and Telegram.

Auth: Signed-in session (Supabase auth cookie), workspace-scoped

Not the customer support inbox. End users of your products come in through /api/support/v1/messages.

Support inbox (agent)

The signed-in half of the support inbox, behind /app/support. Reads go through the caller's RLS-scoped client so a teammate limited to one project cannot read another product's threads; writes go through the service role only after an RLS read has proved the caller may act.

/api/support/agentGET · PATCHsession

Inbox bootstrap: conversations, apps, canned replies, projects and settings in one request. PATCH updates the workspace's support settings (notification thresholds, agent locale, default reply mode).

Auth: Signed-in session (Supabase auth cookie), workspace-scoped

/api/support/agent/threadGET · POSTsession

One conversation. GET returns the full history and marks it read. POST takes an `action`: reply, status, read, translate, preview, draft, approve_draft, discard_draft, relabel_language.

Auth: Signed-in session (Supabase auth cookie), workspace-scoped

curl -X POST https://soleos.app/api/support/agent/thread \
  -H 'Content-Type: application/json' \
  -d '{"conversationId":"...","action":"reply","body":"On it.","translate":true}'

`translate: true` sends in the user's language and stores both texts — body is what went over the wire, body_en is what you typed.

/api/support/agent/appsGET · POST · PATCHsession

Register a product for support, mint or rotate its public ingest key, revoke it, and set its reply mode (human / auto_ack / ai_draft / ai_auto).

Auth: Signed-in session (Supabase auth cookie), workspace-scoped

/api/support/agent/cannedGET · POST · PATCH · DELETEsession

Canned replies: list, create, edit, delete. Ordered by real use.

Auth: Signed-in session (Supabase auth cookie), workspace-scoped

/api/support/agent/ticketsGET · POST · PATCHsession

Tickets, filed per project from a conversation. POST with `auto: true` asks Claude to summarise the thread in English; without it, pass your own title and body.

Auth: Signed-in session (Supabase auth cookie), workspace-scoped

Keys are per project and assigned by a database trigger under an advisory lock, e.g. PUFFZE-7.

/api/support/agent/countGETsession

Unread badge count for the sidebar. One indexed count, no joins.

Auth: Signed-in session (Supabase auth cookie), workspace-scoped

Signal ingest & webhooks

How events reach SoleOS from your own backends and from providers. Every one of these is signature- or secret-verified, and each refuses unsigned input rather than degrading into trusting it.

/api/connections/webhookPOSTsession

Mint or rotate a project's webhook secret and hand back the ready-to-paste Supabase trigger SQL or Firebase function snippet.

Auth: Signed-in session (Supabase auth cookie), workspace-scoped

The secret is shown once and stored as a SHA-256 hash.

/api/webhooks/viral-loopPOSTsigned

Viral Loop lifecycle webhook: `post.published` and `run.failed`.

Auth: Provider signature / shared secret header

X-ViralLoop-Signature: <hmac>   # verified against VIRAL_LOOP_WEBHOOK_SECRET

Refuses everything with 503 when the secret is unset rather than degrading into trusting unsigned input.

/api/webhooks/resendPOSTsigned

Resend bounce and complaint webhook. Hard bounces and spam complaints are auto-suppressed so a dead list cannot poison the sending domain shared by every product.

Auth: Provider signature / shared secret header

Svix-Id / Svix-Timestamp / Svix-Signature   # verified against RESEND_WEBHOOK_SECRET
/api/billing/webhookPOSTsigned

Stripe → Supabase sync, keeping each workspace's plan and subscription status current so the paywall can trust the database.

Auth: Provider signature / shared secret header

Stripe-Signature: <sig>   # verified against STRIPE_WEBHOOK_SECRET
/api/telegramPOSTsigned

Telegram bot webhook. Handles /start <code> (links a chat to a workspace) and /stop.

Auth: Provider signature / shared secret header

X-Telegram-Bot-Api-Secret-Token: <secret>   # set when the webhook was registered

Public site

/api/newsletterPOSTpublic

Newsletter capture. Writes through the service role because the table has no anon policy. Rate limited per IP.

Auth: None — public

/api/unsubscribeGET · POSTpublic

One-click unsubscribe (RFC 8058). Deliberately unauthenticated — an opt-out that first asks you to sign in is not an opt-out.

Auth: None — public

GET  /api/unsubscribe?token=<signed>   # human-facing confirmation page
POST /api/unsubscribe                  # List-Unsubscribe-Post from the mail client

The token is HMAC-signed and scoped to one address and mail kind.

/docs.mdGETpublic

This reference, as one plaintext file — every endpoint, its auth and runnable examples in a single fetch. What an AI agent should be pointed at instead of crawling /docs.

Auth: None — public

curl https://soleos.app/docs.md

Rendered from the same lib/api-catalog.ts as the web page, so the two cannot say different things.

/llms.txtGETpublic

Product summary for AI assistants, generated from content/product-facts.json at build time so it cannot drift.

Auth: None — public

/feed.xmlGETpublic

RSS 2.0 for the blog and answers series, so readers, aggregators and AI crawlers get every post the moment the engine publishes it.

Auth: None — public

/auth/callbackGEToauth return

PKCE code exchange — the landing point for magic links.

Auth: Signed state / cookie from the matching `start` leg

Portfolio & projects

The dashboard's own API. All session-authenticated and workspace-scoped; a teammate's project allowlist is enforced by RLS, not by the route.

/api/projectsPOSTsession

Create a project. Enriches it with the site's declared theme colour, favicon, or an exact-name App Store artwork match — misses stay null rather than being invented.

Auth: Signed-in session (Supabase auth cookie), workspace-scoped

/api/projects/[id]PATCH · DELETEsession

Update a project (name, platforms, website, status) or delete it.

Auth: Signed-in session (Supabase auth cookie), workspace-scoped

/api/projects/[id]/refreshGET · POSTsession

Re-pull one project's connected sources on demand instead of waiting for the daily cron.

Auth: Signed-in session (Supabase auth cookie), workspace-scoped

/api/projects/[id]/shareGET · PUTsession

Public-share settings for one project. Writes mint an unguessable share token.

Auth: Signed-in session (Supabase auth cookie), workspace-scoped

/api/projects/[id]/site-auditGET · POSTsession

Audit the project's website for SEO and AI-answer readiness.

Auth: Signed-in session (Supabase auth cookie), workspace-scoped

/api/projects/[id]/contentGETsession

Top content posts for one project — which hook actually earned the reach.

Auth: Signed-in session (Supabase auth cookie), workspace-scoped

/api/connectionsGETsession

Every connection in the workspace with its provider, health and last pull. Drives the sidebar Sources widget.

Auth: Signed-in session (Supabase auth cookie), workspace-scoped

/api/connections/disconnectPOSTsession

Remove a project's connection(s) for a provider. Snapshots already captured are left in place.

Auth: Signed-in session (Supabase auth cookie), workspace-scoped

/api/insightsGET · POST · PATCH · DELETEsession

AI insights: GET the latest stored brief, POST to generate one, PATCH to mark an item done, DELETE to clear.

Auth: Signed-in session (Supabase auth cookie), workspace-scoped

Requires ANTHROPIC_API_KEY and is dormant without it. Respects the per-workspace AI opt-out.

/api/goalsGET · POST · DELETEsession

Goals CRUD. The arrival-date maths lives in lib/goals.ts and runs client-side against the same series the chart draws.

Auth: Signed-in session (Supabase auth cookie), workspace-scoped

/api/inboxGET · POST · PUTsession

PUT provisions a project's widget key (used by the project page). GET and POST answer 410 — they moved to /api/support/agent/*.

Auth: Signed-in session (Supabase auth cookie), workspace-scoped

Connecting a data source

Two shapes: OAuth flows (start → consent → callback → pick → link) and key-paste connectors. Credentials go to Supabase Vault, never to a column.

/api/connect/stripe/startGETsession

Stripe Connect OAuth, leg 1. Read-only; state is HMAC-signed.

Auth: Signed-in session (Supabase auth cookie), workspace-scoped

/api/connect/stripe/callbackGEToauth return

Stripe Connect OAuth, leg 2. Verifies state, swaps the code, attaches the account.

Auth: Signed state / cookie from the matching `start` leg

/api/connect/revenuecat/startGETsession

RevenueCat OAuth, leg 1: send the user to RevenueCat's approval screen with read-only scopes. State is HMAC-signed so the callback can trust which workspace it belongs to.

Auth: Signed-in session (Supabase auth cookie), workspace-scoped

/api/connect/revenuecat/callbackGEToauth return

RevenueCat OAuth, leg 2. Stashes the token bundle in Vault.

Auth: Signed state / cookie from the matching `start` leg

/api/connect/posthog/startGETsession

PostHog OAuth, leg 1. PKCE S256 public client (CIMD — the client_id is a URL we host).

Auth: Signed-in session (Supabase auth cookie), workspace-scoped

/api/connect/posthog/callbackGEToauth return

PostHog OAuth, leg 2: verify the PKCE verifier, exchange the code, and store the grant before redirecting back with an immediate first pull.

Auth: Signed state / cookie from the matching `start` leg

/api/connect/ga4/startGETsession

Google Analytics 4 OAuth, leg 1. offline + consent so a refresh token is issued.

Auth: Signed-in session (Supabase auth cookie), workspace-scoped

/api/connect/ga4/callbackGEToauth return

GA4 OAuth, leg 2: verify the cookie, exchange the code, stash the token in Vault on the project's ga4 connection, then return the user to pick a property.

Auth: Signed state / cookie from the matching `start` leg

/api/connect/search-console/startGETsession

Search Console OAuth, leg 1: Google consent for read-only Search Console, with offline + consent so a refresh token is issued.

Auth: Signed-in session (Supabase auth cookie), workspace-scoped

/api/connect/search-console/callbackGEToauth return

Search Console OAuth, leg 2: verify the cookie, exchange the code, stash the token in Vault, then return the user to pick a verified site.

Auth: Signed state / cookie from the matching `start` leg

/api/connect/supabase/startGETsession

Supabase Management OAuth, leg 1. PKCE plus a signed context cookie.

Auth: Signed-in session (Supabase auth cookie), workspace-scoped

/api/connect/supabase/callbackGEToauth return

Supabase OAuth, leg 2: verify the signed context cookie, exchange the code for a management token, and stash it in Vault on the project's supabase connection.

Auth: Signed state / cookie from the matching `start` leg

/api/connections/revenuecatPOSTsession

Workspace-level RevenueCat import from a v2 secret key; the key goes to Vault.

Auth: Signed-in session (Supabase auth cookie), workspace-scoped

/api/connections/revenuecat/projectsGETsession

List the RevenueCat projects the connected grant can see, so the user picks which one maps to this SoleOS project.

Auth: Signed-in session (Supabase auth cookie), workspace-scoped

/api/connections/revenuecat/pickPOSTsession

Link a chosen RevenueCat project to a SoleOS project.

Auth: Signed-in session (Supabase auth cookie), workspace-scoped

/api/connections/revenuecat/linkPOSTsession

Per-project RevenueCat link using whichever account grant applies.

Auth: Signed-in session (Supabase auth cookie), workspace-scoped

/api/connections/posthogPOSTsession

Validate a PostHog personal API key (query:read) and attach a traffic connection.

Auth: Signed-in session (Supabase auth cookie), workspace-scoped

/api/connections/ga4/propertiesGETsession

List GA4 properties the account can read, flattened across accounts.

Auth: Signed-in session (Supabase auth cookie), workspace-scoped

/api/connections/ga4/linkPOSTsession

Record which GA4 property this project tracks. The puller reads config.propertyId from here on every run.

Auth: Signed-in session (Supabase auth cookie), workspace-scoped

/api/connections/search-console/sitesGETsession

List the verified Search Console sites the account owns, so the user picks which property maps to this project.

Auth: Signed-in session (Supabase auth cookie), workspace-scoped

/api/connections/search-console/linkPOSTsession

Record which verified Search Console site this project tracks. The puller reads it from the connection config on every run.

Auth: Signed-in session (Supabase auth cookie), workspace-scoped

/api/connections/supabase/projectsGETsession

List Supabase projects the connected account can see.

Auth: Signed-in session (Supabase auth cookie), workspace-scoped

/api/connections/supabase/installPOSTsession

Install the signup trigger through the Management API — no copy-paste.

Auth: Signed-in session (Supabase auth cookie), workspace-scoped

/api/connections/app-storePOSTsession

App Store Connect via a pasted API key (issuer id, key id, .p8).

Auth: Signed-in session (Supabase auth cookie), workspace-scoped

/api/connections/playPOSTsession

Google Play Console. Install counts come from the Play-managed GCS bucket.

Auth: Signed-in session (Supabase auth cookie), workspace-scoped

/api/connections/firebasePOSTsession

Firebase, connected by pasting a service-account key — no manual Cloud Function to deploy. The key goes to Vault.

Auth: Signed-in session (Supabase auth cookie), workspace-scoped

/api/connections/bingPOSTsession

Bing Webmaster Tools, connected by pasting an API key. Adds search impressions and clicks for Bing-family engines.

Auth: Signed-in session (Supabase auth cookie), workspace-scoped

/api/connections/viral-loop/appsGETsession

List Viral Loop apps the workspace key can see, auto-matched to SoleOS projects.

Auth: Signed-in session (Supabase auth cookie), workspace-scoped

/api/connections/viral-loop/linkPOSTsession

Confirm the whole Viral Loop match set at once — the match is portfolio-wide.

Auth: Signed-in session (Supabase auth cookie), workspace-scoped

Billing, team & workspace

/api/billing/checkoutPOSTsession

Create or reuse the workspace's Stripe customer and open a Checkout Session with a 7-day trial. Returns a URL to redirect to.

Auth: Signed-in session (Supabase auth cookie), workspace-scoped

/api/billing/portalPOSTsession

Open the Stripe billing portal. Returns a URL to redirect to.

Auth: Signed-in session (Supabase auth cookie), workspace-scoped

/api/billing/statusGETsession

Current workspace plan and subscription status.

Auth: Signed-in session (Supabase auth cookie), workspace-scoped

/api/teamGET · POST · PATCH · DELETEowner

List the team, invite by email, change a role, revoke. Every mutation is owner-only and re-checks the caller's role server-side.

Auth: Signed-in session — workspace owner only

/api/workspace/prefsGET · PATCHsession

Workspace-level notification preferences — currently whether integration health changes are worth telling you about.

Auth: Signed-in session (Supabase auth cookie), workspace-scoped

/api/workspace/aiPOSTsession

The per-workspace AI opt-out promised on /ai. Turning it off makes every AI feature dormant rather than degraded.

Auth: Signed-in session (Supabase auth cookie), workspace-scoped

/api/telegram/linkGET · POST · DELETEsession

Telegram link management: GET status, POST mints a one-time code and t.me deep link, DELETE unlinks.

Auth: Signed-in session (Supabase auth cookie), workspace-scoped

/api/admin/workspacesGET · POSTadmin

Platform admin: list and approve waitlisted workspaces. Defaults to the owner; override with SOLEOS_ADMIN_EMAILS.

Auth: Signed-in session — platform admin only

Scheduled & internal

Not for external callers. Authenticated with PULL_SECRET or CRON_SECRET; Vercel Cron invokes them on the schedule in vercel.json.

/api/pullGET · POSTsecret

The metric pullers. One route, one puller per provider, with per-connection error isolation so a failing connection is marked and the run continues.

Auth: `Authorization: Bearer $PULL_SECRET` (or `$CRON_SECRET`)

curl -X POST https://soleos.app/api/pull -H "Authorization: Bearer $PULL_SECRET"

Cron: daily 06:00 UTC.

/api/digestGET · POSTsecret

Weekly portfolio digest email: MRR and week delta, top movers, nearest milestone ETAs, signal counts.

Auth: `Authorization: Bearer $PULL_SECRET` (or `$CRON_SECRET`)

Cron: Mondays 08:00 UTC.

/api/retry-pendingGET · POSTsecret

Re-check connections that could not validate at connect time (e.g. a Play grant that had not propagated).

Auth: `Authorization: Bearer $PULL_SECRET` (or `$CRON_SECRET`)

Cron: every 6 hours.

/api/indexnowGET · POSTsecret

Submit every public URL to IndexNow so Bing-family engines pick up new pages in minutes.

Auth: `Authorization: Bearer $PULL_SECRET` (or `$CRON_SECRET`)

Cron: daily 07:30 UTC.

/api/cron/blog-generatorGET · POSTsecret

Answers-engine cron: at most one grounded post per run.

Auth: `Authorization: Bearer $PULL_SECRET` (or `$CRON_SECRET`)

Cron: 06:50 and 17:50 UTC.

/api/cron/site-auditGET · POSTsecret

Weekly Site Health sweep, so the score becomes a tracked series rather than a one-off number.

Auth: `Authorization: Bearer $PULL_SECRET` (or `$CRON_SECRET`)

Cron: Mondays 05:20 UTC.

/api/cron/support-digestGETsecret

The support notification batching tick. Sends nothing most runs; the threshold logic lives in lib/support/notify.ts.

Auth: `Authorization: Bearer $PULL_SECRET` (or `$CRON_SECRET`)

Cron: every 5 minutes.

/api/cron/backtestGETsecret

Walk-forward backtest of the projection engine against every project's real MRR history.

Auth: `Authorization: Bearer $PULL_SECRET` (or `$CRON_SECRET`)

Run manually.

Signal ingest gateway

Callable surfaces that are not Next.js routes, and so are not covered by the route checks above.

Signal ingest gateway

POST {SUPABASE_URL}/functions/v1/ingest?source=<source>&project=<slug>

A Supabase Edge Function, not a Next.js route. One endpoint receives every lifecycle signal — signups from your own backend, purchases and churn from RevenueCat — and turns them into events, notifications and emails.

Auth: `Authorization: Bearer $SOLEOS_INGEST_SECRET`, or the per-project webhook secret minted by /api/connections/webhook

curl -X POST "$SUPABASE_URL/functions/v1/ingest?source=app&project=puff-zero" \
  -H "Authorization: Bearer $SOLEOS_INGEST_SECRET" \
  -H 'Content-Type: application/json' \
  -d '{"type":"signup","payload":{"desc":"new user"}}'

Idempotent by construction: dedupe_key is the source plus the provider's event id (or a body hash), and a unique index makes a provider's retries no-ops. Source lives at supabase/functions/ingest/index.ts.

Client snippets

Copy-pasteable clients for the v1 support API. Each generates and persists a stable end_user_ref on first run and attaches app version, OS version, device model and locale automatically.

PlatformFileDependencies
GameMaker (GML)packages/support-v1/gamemaker/soleos_support.gmlnone — http_request + the Async HTTP event
Unity (C#)packages/support-v1/unity/SoleOSSupport.csnone — UnityWebRequest + JsonUtility
iOS / macOS (Swift)packages/support-v1/swift/SupportClient.swiftnone — Foundation + Security
Android / JVM (Kotlin)packages/support-v1/kotlin/SupportClient.ktnone beyond what an Android app ships
JavaScript / React Nativepackages/support-v1/javascript/support-client.jsnone
Web, as a script tagserved at /widget/v1.jsnone

Repo toolkit

What to run, and what each command is actually for.

Everyday
npm run dev

Next.js dev server (Turbopack by default in 16).

npm run build

Production build. The real gate — it typechecks and compiles every route.

npm run lint

ESLint, including the React hooks rules this codebase actually enforces.

npx tsc --noEmit

Typecheck without building.

npm run pull

Trigger the metric pullers against your local server.

Tests
node --experimental-strip-types --test lib/**/*.test.ts

The unit tests. No build step, no test framework — Node's own runner over TypeScript.

Test files import with explicit `.ts` specifiers so type-stripping works; that is why tsconfig excludes them.

psql "$DB_URL" -v ON_ERROR_STOP=1 -f supabase/tests/support_rls.sql

The RLS suite: proves anon cannot touch the support tables, that workspaces are isolated, that viewers cannot write, and that the rate limiter really runs out.

Plain SQL assertions, so running them adds no extension to the database. Verified non-vacuous: introducing a deliberate anon leak makes it fail.

Database
supabase start

Local Postgres + Auth + Storage + Realtime stack (needs Docker).

supabase db reset

Rebuild the local database from every migration, then seed.

supabase db push --dry-run

Show which migrations would be applied to the linked remote project.

supabase db push

Apply pending migrations to the remote project.

Apply the migration BEFORE deploying code that reads new tables — /api/widget is embedded on live customer sites and the reverse order breaks it for the length of a build.

Scripts
npx tsx scripts/sentry-coverage.ts

Which products report to Sentry, which are silent, and which are misnamed.

npx tsx scripts/stripe-setup.ts

Create the Stripe products and prices the pricing page promises.

npx tsx scripts/appstore-backfill.tsx

Backfill App Store Connect history for a project.

node scripts/backfill-icons.mjs

Fill in missing project icons from favicons and App Store artwork.

npx tsx scripts/email-preview.tsx

Render the transactional emails to HTML without sending them.

npx tsx scripts/gen-logos.ts

Regenerate the provider logo sprite.

psql "$DB_URL" -f scripts/seed-live.sql

Seed real projects and connections (generated from RevenueCat); see also seed-inventory.sql, seed-analytics-connections.sql, backfill-mrr.sql, backfill-traffic.sql.

Environment variables

Optional ones make a feature dormant rather than broken — that is deliberate, so a missing key never takes the app down with it.

VariableRequiredWhat it does
NEXT_PUBLIC_SUPABASE_URLrequiredSupabase project URL. Public.
NEXT_PUBLIC_SUPABASE_ANON_KEYrequiredSupabase anon key. Public, and assumed hostile — RLS and table grants are what protect data, not this key.
SUPABASE_SECRET_KEYrequiredService-role key. Server only. Bypasses RLS, so every route using it must do its own scoping.
PULL_SECRETrequiredBearer secret for the cron and pull endpoints. `requireSecret` throws in production if it is missing, so a misconfiguration is loud.
CRON_SECREToptionalAlternative bearer secret, sent by Vercel Cron when set.
SOLEOS_INGEST_SECREToptionalBearer secret for the signal ingest gateway.
ANTHROPIC_API_KEYoptionalClaude, for AI insights, support drafts, translation and ticket summaries. Every one of those is dormant without it rather than broken.
RESEND_API_KEYoptionalTransactional email. Sending is skipped rather than failed when unset.
RESEND_WEBHOOK_SECREToptionalSvix secret for the bounce/complaint webhook. Unset means the webhook refuses input rather than trusting it.
STRIPE_SECRET_KEYoptionalStripe API key for billing.
STRIPE_WEBHOOK_SECREToptionalVerifies Stripe webhook signatures.
STRIPE_CONNECT_CLIENT_IDoptionalStripe Connect OAuth client for connecting a founder's own Stripe account.
TELEGRAM_BOT_TOKENoptionalTelegram bot for signal notifications.
TELEGRAM_WEBHOOK_SECREToptionalEchoed by Telegram in X-Telegram-Bot-Api-Secret-Token; the only thing authenticating that webhook.
VIRAL_LOOP_API_KEYoptionalViral Loop content-engine connector.
VIRAL_LOOP_WEBHOOK_SECREToptionalHMAC secret for Viral Loop lifecycle webhooks.
GOOGLE_OAUTH_CLIENT_ID / _SECREToptionalShared Google client for GA4 and Search Console.
SUPABASE_OAUTH_CLIENT_ID / _SECREToptionalSupabase Management OAuth, for the native Supabase connector.
SOLEOS_ADMIN_EMAILSoptionalComma-separated platform admins. Defaults to the owner.
NOTIFY_EMAILoptionalFallback recipient for signal and support mail.
SENTRY_ORG / SENTRY_PROJECT / SENTRY_AUTH_TOKENoptionalSource-map upload. Only runs when the token is present, so local builds stay clean.
NEXT_PUBLIC_DEMOoptionalForces the public demo experience with sample data and no auth.

Source of truth: lib/api-catalog.ts, kept honest by lib/api-catalog.test.ts — it walks every route.ts and fails if this page and the codebase ever disagree.