# Super Connect documentation Super Connect lets your users connect their accounts on services such as GitHub, Slack or Google to your product, and lets your backend call those services on their behalf without handling OAuth tokens yourself. You configure providers in the admin panel at https://super-connect.dogar.biz, drop a modal into your frontend, and call the API from your backend with one key. ![The Super Connect admin panel overview](assets/overview.png) ## Start here | I want to… | Read | | ---------------------------------------------------------- | ----------------------------------------------- | | Sign up, set up my organization and connect a first user | [Getting started](tutorials/getting-started.md) | | Understand what a provider, integration and connection are | [How it works](explanation/architecture.md) | ## How-to guides - [Add a provider](how-to/add-a-provider.md) - [Manage your organization](how-to/manage-your-organization.md) - [Handle connection errors](how-to/handle-connection-errors.md) - [Receive webhooks](how-to/receive-webhooks.md): `auth` events about connection health - [Receive provider events](how-to/receive-provider-events.md): Slack and GitHub webhooks forwarded to you - [Coding agent setup](how-to/coding-agent-setup.md): a skill and llms.txt for Claude Code, Cursor and others ## Reference - [SDKs](reference/sdks.md): `@super-connect/client` and `@super-connect/connect` - [HTTP API](reference/http-api.md) - [Settings](reference/settings.md): provider, integration and session fields --- # How it works This document explains how Super Connect is put together and why. Read it to get a mental model before integrating. ## The four nouns ```mermaid flowchart LR P[Provider
OAuth protocol or API key header + API host] --> I[Integration
your OAuth client + scopes] I --> C[Connection
one user's grant] C --> V[Invocation
one proxy call or action] ``` - A **provider** describes how to authorize and the single HTTPS `apiBase` every request must stay under. For OAuth 2: authorization and token endpoints, PKCE, how client credentials are sent, where the token lives in the response. For API keys: the header the key travels in and the field the connect sheet asks for. Providers carry no secrets. - An **integration** binds a provider to one OAuth client: encrypted client ID and secret, the scope string, an optional webhook destination, and the receive URL and inbound secret that provider webhooks arrive with. Several integrations can share a provider with different clients or scopes. API key providers need no client. - A **connection** is one end user's grant against one integration: encrypted access and refresh tokens, expiry, granted scopes and refresh lifecycle facts, or one encrypted API key that is never refreshed. Its ID is stable across refresh and reauthorization. - An **invocation** is a log row for one proxy request or action run. Configuration is split from credentials on purpose. Endpoint mistakes can be corrected without rotating secrets, and secrets can rotate without touching protocol details. ## Organizations are the tenancy boundary Every table above carries `organization_id`. An organization is a customer company from the Better Auth `organization` plugin. It owns its providers, integrations, connections, connect sessions, OAuth states, webhook events, invocations and API keys. The organization is derived once per request: 1. An `Authorization: Bearer sc_…` header is verified by the Better Auth `apiKey` plugin, whose `referenceId` is the organization. 2. Otherwise the session cookie must carry an `activeOrganizationId` and a membership row. No URL contains an organization. The credential implies it, so a key from one organization cannot name another organization's resources even by guessing IDs; the query simply finds nothing and the route answers `404`. End users are a separate axis. Your backend creates each one through `POST /api/v1/users` with its own id; Super Connect keeps them as Better Auth users with an `end_user` membership that no dashboard or API route accepts. `X-User-Id` names one by that id on connection routes. It is required with an organization key because a key alone would otherwise let any connection ID reach a provider. Operators are platform users with `user.role = 'admin'` from the Better Auth `admin` plugin. They enter an organization by impersonating a member, so tenancy resolution never has a special case for them. ## Request dispatch order Requests are matched in a fixed order. The order is the security model: 1. `/api/auth/*` → Better Auth 2. `/health` 3. `/oauth/start`, `/oauth/callback` → browser routes, authorized by the `state` row and a browser cookie 4. The `/api/v1` prefix is stripped. Anything else at the root other than `/proxy/*` answers `404`, so the SPA owns the remaining paths 5. `/connect/session*` → authorized by the connect session token 6. `/operator/*` → authorized by an operator session 7. **Organization resolution** happens here. Everything after it is organization-scoped. 8. Providers, integrations, connect sessions, connections, actions, invocations 9. `/proxy/:id/*` as the fallthrough Anything that does not match answers `404`. Thrown `TenantError` and `ConfigError` map to their status; anything else is `500`. ## Presets are read-only until used Super Connect ships a few hundred built-in providers. `GET /api/v1/providers` lists only the organization's own rows. A preset becomes a row only when the organization saves it or an integration references it. Deleting a never-saved preset is `404` because there is nothing to delete. This keeps the catalog free to update without migrating every organization, while a saved copy protects an organization from upstream changes. ## Connect sessions and the hosted page The browser never sees an organization key. Instead: ```mermaid sequenceDiagram participant B as Backend participant F as Frontend participant U as Connect UI (iframe) participant W as Super Connect participant P as Provider B->>W: POST /api/v1/connect/sessions W-->>B: token, connectLink B-->>F: token F->>U: openConnectUI, setSessionToken U->>W: GET /api/v1/connect/session U->>W: POST /api/v1/connect/session/authorize W-->>U: handoff URL U->>P: popup: /oauth/start then consent P->>W: /oauth/callback?code W-->>U: callback page posts result U-->>F: connect { connectionId } ``` A session is scoped to one end user, an optional integration allowlist and a lifetime. Only its hash is stored, and it is consumed on a successful callback. A session minted with `connectionId` runs in reauthorize mode and inherits the connection's owner and integration, so it cannot be widened. Consent opens in a popup because most providers refuse to be framed. The callback renders HTML rather than JSON so it can post to its opener and still read sensibly when a connect link is opened standalone. ## OAuth state and browser binding `/api/v1/connect/:integrationId` and `authorize` create an `oauth_states` row with a random `state`, the redirect URI, an optional PKCE verifier and a ten-minute expiry. `/oauth/start` claims the row for one browser by storing a hashed cookie value; a second browser gets "already used". `/oauth/callback` checks the cookie, deletes the row atomically, and only then exchanges the code. ## Token lifecycle ```mermaid stateDiagram-v2 [*] --> healthy: consent completed healthy --> failing: refresh fails failing --> healthy: refresh succeeds (recovery) failing --> needs_reauthorization: fourth failure day needs_reauthorization --> healthy: user reauthorizes (override) ``` `refreshing` is an overlay, true whenever a refresh lease is active. Two things trigger a refresh: the ten-minute cron sweep, which picks connections whose `next_refresh_at` has passed (15 minutes before expiry, or 24 hours after the last refresh, whichever is sooner), and a proxy request that finds the token within 15 minutes of expiry. Both go through the same refresh routine, which: 1. dedupes concurrent callers in-process with an `inFlight` map 2. takes a 30-second lease with a conditional `UPDATE … WHERE refresh_until < now AND access_token = ?`, so only one isolate refreshes 3. exchanges the refresh token and writes back under the same lease 4. on failure records the error, schedules the next attempt for tomorrow, and counts one attempt per UTC day; the fourth day sets `refresh_exhausted` A proxy caller that meets a live lease waits up to 12 seconds, then answers `409 connection_refresh_backoff`. Health is never stored. It is derived from `refresh_until`, `refresh_exhausted` and `last_refresh_failure`, so it cannot drift from the facts. ## Webhook outbox Refresh failures, recoveries and reauthorizations insert a `webhook_events` row in the same D1 batch as the connection update, guarded by the value that update wrote. The exact body is persisted before delivery, so retries send identical bytes. Delivery is attempted immediately via `waitUntil` and again by the cron sweep. Events for one connection go out in `sequence` order; a stuck event blocks only its own connection. ## Inbound webhooks Each integration owns a public receive URL, `/webhook/`, and one encrypted inbound secret. A provider POST is verified against that secret by the webhook adapter of the provider's template, `src/catalogs/