Connectors: bring your own OAuth app

Operator·8 minutes to read

F-HOME-2: a home deployment brings its own OAuth application for every connector it uses, rather than sharing one Botseon operates. This page is the setup half: the OAuth framework that uses what you enter here — the authorisation flow, the token exchange and the refresh — ships in @botseon/connectors, and a module names the provider and scopes it needs in its manifest's oauth field (see docs/reference/module-manifest.md). Per-connector pages are the console steps for one provider at a time — every plugin PLUGIN_CONNECTORS (packages/core/src/connector-catalog.ts) declares has one, below.

Per-connector setup pages

  • gmail.md
  • google-calendar.md
  • google-drive.md
  • google-sheets.md — its own plugin beside Drive: reads and edits cells.
  • microsoft-365.md — one page for the three connectors the plugin bundles (outlook-mail, outlook-calendar, onedrive); see "Finding your redirect URL" below for why.
  • microsoft-teams.md — its own plugin and redirect URL, apart from Microsoft 365; reading first, sending only when the member asks for it.
  • sharepoint.md — its own plugin and redirect URL too; reading first, saving files only when the member asks for it, and an optional admin list of allowed sites.
  • slack.md
  • notion.md
  • github.md
  • beehiiv-api.md — beehiiv's API, beside the catalogue's tile for beehiiv's own MCP server; the OAuth client is registered by asking beehiiv Support.
  • bitbucket.md — read-only; the permissions live on the workspace's OAuth consumer.
  • azure-devops.md — read-only; signs in with Microsoft Entra ID, work or school accounts only.
  • linear.md
  • x.md
  • zendesk.md
  • clickup.md
  • todoist.md
  • sentry.md
  • airtable.md
  • onepassword.md
  • stripe.md
  • granola.md
  • enable-banking.md — the EU open-banking aggregator: a hosted bank consent, not a bring-your-own OAuth app and not a pasted key; the deployment's application credential is two environment variables.
  • shopify.md — the member's shop: an ordinary bring-your-own OAuth app, except that Shopify's authorize and token endpoints live on the shop's own domain, so that page says which URL each column carries.
  • payments.md — Botseon's own, and the one page here with no vendor console step
  • figma.md
  • asana.md
  • zoom.md
  • google-meet.md
  • hubspot.md
  • intercom.md
  • monday.md
  • jira.md
  • salesforce.md
  • plain.md — key-only and read-only, installs dark
  • quickbooks.md — QuickBooks Online: the company id arrives on Intuit's redirect, so there is nothing to type; development keys reach only a sandbox company.
  • posthog.md — key-only (region and project checked at paste), on for every member
  • gong.md — key-only and read-only; on for every member; never run against a live account
  • etoro.md — eToro's own OAuth sign-in with PKCE; read-only for now, on the real or demo account the member names
  • clay-api.md — Clay's public API, key-only, beside the catalogue's tile for Clay's own MCP server; a function run waits on the member's approval

A connector that lives in its own folder (extensions/connectors/<slug>/) keeps its setup page there, published at the same address; its entry in the list above is rendered from its manifest, not written here.

Why bring your own

Two reasons, and either alone would be enough.

Nobody's client secret is shared. A managed (SaaS) deployment can run one OAuth app on behalf of every tenant, because Botseon operates the servers those tokens are used from. A self-hosted deployment has no equivalent trust boundary: a shared client secret baked into this repository would be a secret every home operator who ever cloned it could read, which is not a secret at all.

A self-hosted deployment is not a verified publisher. Google (and most providers) show an "unverified app" warning — or refuse restricted scopes outright without a completed app-review process — for an OAuth app that has not gone through their own verification, which is a per-application process a shared app could complete once and a thousand home installs could not inherit. Your own OAuth app, created under your own Google/Microsoft/etc. account, sidesteps that entirely for scopes at the level a single operator actually needs: it warns whoever consents (you) that the app is unverified, and that is the whole cost.

One app for every organisation (--deployment)

Every sign-up gets a personal organisation of its own, and an oauth_apps row belongs to one organisation. So on a deployment where people sign up, an app set with --org alone reaches only that organisation: every newcomer would need an operator command run for theirs before they could connect anything.

Add --deployment to botseon oauth-app set and the app becomes the deployment's: every organisation without an app of its own for that connector uses it (migration 0520, deployment_oauth_apps). --org is still the organisation the secret is sealed under, and the secret is not copied anywhere. An organisation that brings its own app keeps using its own.

printf %s "$CLIENT_SECRET" | botseon oauth-app set \
  --org <your operator organisation id> --slug todoist \
  --client-id <client id> \
  --auth-url https://todoist.com/oauth/authorize \
  --token-url https://todoist.com/oauth/access_token \
  --configured-by <your user id> --deployment

With it set, a member picks the connector in their bot's @ menu and signs in to the vendor in the window that opens. That is the whole path, with no settings screen.

Finding your redirect URL

Every provider's OAuth console asks for a redirect URL to send the browser back to after consent. It has the same shape for every connector:

<BOTSEON_PUBLIC_URL>/api/oauth/<plugin>/callback

<plugin> is the plugin's stable, lower-case slug — gmail, for the first one. The plugin's, not a connector's: one install is one OAuth app, one consent and one redirect URL, whatever number of connectors it bundles. microsoft-365 is where the two differ — it is one plugin over outlook-mail, outlook-calendar and onedrive, so its redirect URL says microsoft-365 and there is no URL for any of the three by name. With the quickstart's default, unconfigured BOTSEON_PUBLIC_URL (http://127.0.0.1:3000), Gmail's redirect URL is:

http://127.0.0.1:3000/api/oauth/gmail/callback

Set BOTSEON_PUBLIC_URL first if you are serving the LAN — the redirect URL has to be the address your browser can actually reach, the same way the sign-in link does. @botseon/connectors's redirectUrlFor(publicUrl, connectorSlug) is what builds this string in code (packages/connectors/src/oauth.ts) — the same function the authorisation request itself uses, so the two cannot disagree about a single character, which is the single most common OAuth setup failure and the one a provider's error message never explains. You never have to run it yourself: botseon oauth-app set and botseon oauth-app show both print it (below).

Where the client secret goes, and where it never goes

The client id and client secret your provider's console gives you go in through one command, per organisation and per connector:

printf %s "$CLIENT_SECRET" | botseon oauth-app set \
  --org <organisation id> --slug gmail \
  --client-id <the client id from the console> \
  --auth-url https://accounts.google.com/o/oauth2/v2/auth \
  --token-url https://oauth2.googleapis.com/token \
  --configured-by <your user id>

The secret is read from standard input and there is no flag that takes it. A flag's value is in ps output, in your shell's history file and in the ledger of whatever ran the command; an environment variable is in /proc/<pid>/environ and in every child process. Both URLs must be https:, and the command refuses them otherwise rather than letting a member find out at their consent screen. It stores the secret encrypted in oauth_apps — the same envelope encryption every other secret in this deployment is under — and prints the redirect URL to paste into the provider console, and nothing else.

botseon oauth-app show --org <id> --slug <plugin> says whether an app is configured, under which client id, and what the redirect URL is. Both subcommands refuse a --slug that is not a known plugin, so a typo is caught at the command rather than at a member's consent screen. There is no subcommand and no flag that prints the secret back: once it is in, the only thing that ever reads it is the token exchange.

The secret never goes anywhere a model reads as ordinary text: not into a bot's description, not into a memory, not into a routine's configuration. A model that can read a secret can also repeat it, in a reply, in a tool call's arguments, in whatever gets logged along the way — the same reasoning that keeps BOTSEON_MASTER_KEY and every other generated credential out of application code entirely, and out of anything a bot's own context window would ever hold.

The plugin sheet in the web app shows the same three things the show subcommand does — the redirect URL with a copy control, and whether an app is configured for this connector — and no more.

Writing a connector

The two pages above are the operator's half — setting up an OAuth app for a connector that already exists. The developer's half is next door:

  • adding-a-connector.md — the walk-through, from deciding the tools to clicking the finished plugin through.
  • conformance.md — the thirteen checks every connector has to pass, one section per identifier the gate prints.
  • examples/uptime-connector — the smallest complete one, with a row in its README for each section of the walk-through.
  • docs/superpowers/specs/2026-09-17-connector-kit.md — the connector kit's own spec: the fourteen-vendor wave's rules, the live smoke (D511) and the dark-ship rule (D512) in full.

Last verified against build c0f77aa.