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.
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 keyThe 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":{...}}| Error | Status | Retry? | Meaning |
|---|---|---|---|
| invalid_request | 400 / 413 | never | Malformed JSON, or a body over 32KB. A client bug. |
| invalid_app_key | 403 | never | Unrecognised key. Check what you shipped. |
| app_key_revoked | 403 | never | The key was turned off in SoleOS. Hide the feature rather than retrying. |
| invalid_end_user_ref | 400 | never | Not 16-200 chars of [A-Za-z0-9._:-]. Fix the generator; do not retry. |
| empty_message | 400 | never | Nothing to send. |
| message_too_long | 413 | never | Over 4000 characters. Cap the input client-side so this cannot happen. |
| rate_limited | 429 | after Retry-After | Too fast. `Retry-After` carries the wait in seconds. |
| server_error | 5xx | with backoff | Keep 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 keyThe 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.jsGETpublicThe 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.jsGETpublicThe 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/supportPOSTsessionSoleOS'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 · PATCHsessionInbox 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 · POSTsessionOne 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 · PATCHsessionRegister 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 · DELETEsessionCanned replies: list, create, edit, delete. Ordered by real use.
Auth: Signed-in session (Supabase auth cookie), workspace-scoped
/api/support/agent/ticketsGET · POST · PATCHsessionTickets, 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/countGETsessionUnread 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/webhookPOSTsessionMint 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-loopPOSTsignedViral Loop lifecycle webhook: `post.published` and `run.failed`.
Auth: Provider signature / shared secret header
X-ViralLoop-Signature: <hmac> # verified against VIRAL_LOOP_WEBHOOK_SECRETRefuses everything with 503 when the secret is unset rather than degrading into trusting unsigned input.
/api/webhooks/resendPOSTsignedResend 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/webhookPOSTsignedStripe → 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/telegramPOSTsignedTelegram 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 registeredPublic site
/api/newsletterPOSTpublicNewsletter capture. Writes through the service role because the table has no anon policy. Rate limited per IP.
Auth: None — public
/api/unsubscribeGET · POSTpublicOne-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 clientThe token is HMAC-signed and scoped to one address and mail kind.
/docs.mdGETpublicThis 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.mdRendered from the same lib/api-catalog.ts as the web page, so the two cannot say different things.
/llms.txtGETpublicProduct summary for AI assistants, generated from content/product-facts.json at build time so it cannot drift.
Auth: None — public
/feed.xmlGETpublicRSS 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 returnPKCE 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/projectsPOSTsessionCreate 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 · DELETEsessionUpdate a project (name, platforms, website, status) or delete it.
Auth: Signed-in session (Supabase auth cookie), workspace-scoped
/api/projects/[id]/refreshGET · POSTsessionRe-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 · PUTsessionPublic-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 · POSTsessionAudit the project's website for SEO and AI-answer readiness.
Auth: Signed-in session (Supabase auth cookie), workspace-scoped
/api/projects/[id]/contentGETsessionTop content posts for one project — which hook actually earned the reach.
Auth: Signed-in session (Supabase auth cookie), workspace-scoped
/api/connectionsGETsessionEvery 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/disconnectPOSTsessionRemove 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 · DELETEsessionAI 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 · DELETEsessionGoals 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 · PUTsessionPUT 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/startGETsessionStripe Connect OAuth, leg 1. Read-only; state is HMAC-signed.
Auth: Signed-in session (Supabase auth cookie), workspace-scoped
/api/connect/stripe/callbackGEToauth returnStripe Connect OAuth, leg 2. Verifies state, swaps the code, attaches the account.
Auth: Signed state / cookie from the matching `start` leg
/api/connect/revenuecat/startGETsessionRevenueCat 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 returnRevenueCat OAuth, leg 2. Stashes the token bundle in Vault.
Auth: Signed state / cookie from the matching `start` leg
/api/connect/posthog/startGETsessionPostHog 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 returnPostHog 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/startGETsessionGoogle 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 returnGA4 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/startGETsessionSearch 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 returnSearch 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/startGETsessionSupabase Management OAuth, leg 1. PKCE plus a signed context cookie.
Auth: Signed-in session (Supabase auth cookie), workspace-scoped
/api/connect/supabase/callbackGEToauth returnSupabase 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/revenuecatPOSTsessionWorkspace-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/projectsGETsessionList 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/pickPOSTsessionLink a chosen RevenueCat project to a SoleOS project.
Auth: Signed-in session (Supabase auth cookie), workspace-scoped
/api/connections/revenuecat/linkPOSTsessionPer-project RevenueCat link using whichever account grant applies.
Auth: Signed-in session (Supabase auth cookie), workspace-scoped
/api/connections/posthogPOSTsessionValidate a PostHog personal API key (query:read) and attach a traffic connection.
Auth: Signed-in session (Supabase auth cookie), workspace-scoped
/api/connections/ga4/propertiesGETsessionList GA4 properties the account can read, flattened across accounts.
Auth: Signed-in session (Supabase auth cookie), workspace-scoped
/api/connections/ga4/linkPOSTsessionRecord 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/sitesGETsessionList 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/linkPOSTsessionRecord 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/projectsGETsessionList Supabase projects the connected account can see.
Auth: Signed-in session (Supabase auth cookie), workspace-scoped
/api/connections/supabase/installPOSTsessionInstall the signup trigger through the Management API — no copy-paste.
Auth: Signed-in session (Supabase auth cookie), workspace-scoped
/api/connections/app-storePOSTsessionApp Store Connect via a pasted API key (issuer id, key id, .p8).
Auth: Signed-in session (Supabase auth cookie), workspace-scoped
/api/connections/playPOSTsessionGoogle Play Console. Install counts come from the Play-managed GCS bucket.
Auth: Signed-in session (Supabase auth cookie), workspace-scoped
/api/connections/firebasePOSTsessionFirebase, 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/bingPOSTsessionBing 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/appsGETsessionList 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/linkPOSTsessionConfirm 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/checkoutPOSTsessionCreate 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/portalPOSTsessionOpen the Stripe billing portal. Returns a URL to redirect to.
Auth: Signed-in session (Supabase auth cookie), workspace-scoped
/api/billing/statusGETsessionCurrent workspace plan and subscription status.
Auth: Signed-in session (Supabase auth cookie), workspace-scoped
/api/teamGET · POST · PATCH · DELETEownerList 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 · PATCHsessionWorkspace-level notification preferences — currently whether integration health changes are worth telling you about.
Auth: Signed-in session (Supabase auth cookie), workspace-scoped
/api/workspace/aiPOSTsessionThe 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 · DELETEsessionTelegram 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 · POSTadminPlatform 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 · POSTsecretThe 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 · POSTsecretWeekly 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 · POSTsecretRe-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 · POSTsecretSubmit 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 · POSTsecretAnswers-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 · POSTsecretWeekly 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-digestGETsecretThe 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/backtestGETsecretWalk-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.
| Platform | File | Dependencies |
|---|---|---|
| GameMaker (GML) | packages/support-v1/gamemaker/soleos_support.gml | none — http_request + the Async HTTP event |
| Unity (C#) | packages/support-v1/unity/SoleOSSupport.cs | none — UnityWebRequest + JsonUtility |
| iOS / macOS (Swift) | packages/support-v1/swift/SupportClient.swift | none — Foundation + Security |
| Android / JVM (Kotlin) | packages/support-v1/kotlin/SupportClient.kt | none beyond what an Android app ships |
| JavaScript / React Native | packages/support-v1/javascript/support-client.js | none |
| Web, as a script tag | served at /widget/v1.js | none |
Repo toolkit
What to run, and what each command is actually for.
npm run devNext.js dev server (Turbopack by default in 16).
npm run buildProduction build. The real gate — it typechecks and compiles every route.
npm run lintESLint, including the React hooks rules this codebase actually enforces.
npx tsc --noEmitTypecheck without building.
npm run pullTrigger the metric pullers against your local server.
node --experimental-strip-types --test lib/**/*.test.tsThe 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.sqlThe 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.
supabase startLocal Postgres + Auth + Storage + Realtime stack (needs Docker).
supabase db resetRebuild the local database from every migration, then seed.
supabase db push --dry-runShow which migrations would be applied to the linked remote project.
supabase db pushApply 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.
npx tsx scripts/sentry-coverage.tsWhich products report to Sentry, which are silent, and which are misnamed.
npx tsx scripts/stripe-setup.tsCreate the Stripe products and prices the pricing page promises.
npx tsx scripts/appstore-backfill.tsxBackfill App Store Connect history for a project.
node scripts/backfill-icons.mjsFill in missing project icons from favicons and App Store artwork.
npx tsx scripts/email-preview.tsxRender the transactional emails to HTML without sending them.
npx tsx scripts/gen-logos.tsRegenerate the provider logo sprite.
psql "$DB_URL" -f scripts/seed-live.sqlSeed 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.
| Variable | Required | What it does |
|---|---|---|
| NEXT_PUBLIC_SUPABASE_URL | required | Supabase project URL. Public. |
| NEXT_PUBLIC_SUPABASE_ANON_KEY | required | Supabase anon key. Public, and assumed hostile — RLS and table grants are what protect data, not this key. |
| SUPABASE_SECRET_KEY | required | Service-role key. Server only. Bypasses RLS, so every route using it must do its own scoping. |
| PULL_SECRET | required | Bearer secret for the cron and pull endpoints. `requireSecret` throws in production if it is missing, so a misconfiguration is loud. |
| CRON_SECRET | optional | Alternative bearer secret, sent by Vercel Cron when set. |
| SOLEOS_INGEST_SECRET | optional | Bearer secret for the signal ingest gateway. |
| ANTHROPIC_API_KEY | optional | Claude, for AI insights, support drafts, translation and ticket summaries. Every one of those is dormant without it rather than broken. |
| RESEND_API_KEY | optional | Transactional email. Sending is skipped rather than failed when unset. |
| RESEND_WEBHOOK_SECRET | optional | Svix secret for the bounce/complaint webhook. Unset means the webhook refuses input rather than trusting it. |
| STRIPE_SECRET_KEY | optional | Stripe API key for billing. |
| STRIPE_WEBHOOK_SECRET | optional | Verifies Stripe webhook signatures. |
| STRIPE_CONNECT_CLIENT_ID | optional | Stripe Connect OAuth client for connecting a founder's own Stripe account. |
| TELEGRAM_BOT_TOKEN | optional | Telegram bot for signal notifications. |
| TELEGRAM_WEBHOOK_SECRET | optional | Echoed by Telegram in X-Telegram-Bot-Api-Secret-Token; the only thing authenticating that webhook. |
| VIRAL_LOOP_API_KEY | optional | Viral Loop content-engine connector. |
| VIRAL_LOOP_WEBHOOK_SECRET | optional | HMAC secret for Viral Loop lifecycle webhooks. |
| GOOGLE_OAUTH_CLIENT_ID / _SECRET | optional | Shared Google client for GA4 and Search Console. |
| SUPABASE_OAUTH_CLIENT_ID / _SECRET | optional | Supabase Management OAuth, for the native Supabase connector. |
| SOLEOS_ADMIN_EMAILS | optional | Comma-separated platform admins. Defaults to the owner. |
| NOTIFY_EMAIL | optional | Fallback recipient for signal and support mail. |
| SENTRY_ORG / SENTRY_PROJECT / SENTRY_AUTH_TOKEN | optional | Source-map upload. Only runs when the token is present, so local builds stay clean. |
| NEXT_PUBLIC_DEMO | optional | Forces 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.