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
- A provider describes how to authorize and the single HTTPS
apiBaseevery 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:
- An
Authorization: Bearer sc_…header is verified by the Better AuthapiKeyplugin, whosereferenceIdis the organization. - Otherwise the session cookie must carry an
activeOrganizationIdand 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:
/api/auth/*→ Better Auth/health/oauth/start,/oauth/callback→ browser routes, authorized by thestaterow and a browser cookie- The
/api/v1prefix is stripped. Anything else at the root other than/proxy/*answers404, so the SPA owns the remaining paths /connect/session*→ authorized by the connect session token/operator/*→ authorized by an operator session- Organization resolution happens here. Everything after it is organization-scoped.
- Providers, integrations, connect sessions, connections, actions, invocations
/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:
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
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:
- dedupes concurrent callers in-process with an
inFlightmap - takes a 30-second lease with a conditional
UPDATE … WHERE refresh_until < now AND access_token = ?, so only one isolate refreshes - exchanges the refresh token and writes back under the same lease
- 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/<receive id>, and one
encrypted inbound secret. A provider POST is verified against that secret by
the webhook adapter of the provider's template, src/catalogs/<template>/webhook.ts,
which is carried through the catalog port. Its receive is a pure function
over the raw body and headers that either answers a handshake itself, rejects
the request, or identifies the event. The provider is answered at that point.
Then, in waitUntil, the adapter's recipients sees every connection of the
integration with its metadata, filled either from a provider-declared
token-response allowlist, such as Slack's team.id and authed_user.id, or
by the customer over the API, as GitHub's repository and organization IDs
are, and returns the connections to forward to. The log row and one outbox
row per recipient are written in one batch, so a slow customer endpoint
cannot cost the provider its acknowledgment. No recipients, or an adapter
that throws, forwards nothing; there is no forward-to-everyone fallback
(ADR 0001).
Every received request is logged in inbound_webhooks, verified or not. The
raw body is kept for seven days and then nulled by the cron sweep; the rows
stay.
Proxy and actions
One request path is the only way to reach a provider.
It resolves the target against apiBase, rejects anything that leaves the
origin or path prefix, refreshes if needed, injects the bearer token, and
never follows redirects.
The proxy forwards the caller's request through it with a fixed header
allowlist. Actions call it through a small adapter that validates input with the action's Zod schema, runs exec
under a 30-second wall clock and a 50-subrequest cap, validates the output,
and returns a discriminated ActionResult. actionResponse maps each result
code to an HTTP status.
Catalogs load lazily per template through dynamic import(), so a Worker
that never touches Slack never parses Slack's actions.
SDKs
@super-connect/client wraps every management route. On a backend it sends
the organization key; in a browser without a key it sends the session cookie.
The admin panel uses this same package, so the shipped client is the one the
product exercises.
@super-connect/connect is the browser side of the connect flow:
openConnectUI for the modal, createSessionClient for a custom picker
against the two session routes.
Both packages are dependency-free and resolved from source in this repository
through tsconfig paths and a Vite alias.
What is deliberately not here
- Provider-specific auth beyond OAuth 2 authorization code with bearer tokens. Unknown provider options are rejected rather than ignored.
- Revocation at the provider on disconnect.
- Crash-safe token rotation across the provider and database writes.
- Inbound webhooks for providers other than Slack and GitHub, and managed subscriptions such as Google Calendar watch channels, which belong with the sync work that needs them.