Skip to content

Provider Auth And Setup

Use this page when you need to understand how connector authorization is wired and where provider setup actually lives.

Connector auth is not workforce SSO. Workforce SSO signs a person into the platform. Connector auth authorizes access to an external provider through one of three strategy families: OAuth for the signed-in user’s provider account, a user-managed access token entered by that signed-in web user, or a deployment-scoped service/bot credential configured by a customer operator.

  • The platform still relies on Better Auth for the signed-in user identity.
  • Linked provider accounts are associated with that signed-in user.
  • Connector OAuth provider ids are link-only identities. Auth rejects public Better Auth OAuth sign-in initiation for connector provider ids on /api/auth/sign-in/oauth2 and /api/auth/sign-in/social; connector credentials do not enable public app sign-in or signup.
  • Connector runtime then uses provider-specific account identity and scopes to access Gmail, Drive, Calendar, Contacts, SharePoint, OneNote, Slack, Jira, Odoo, or another provider.

Current connector OAuth provider id examples:

  • Google-backed connectors use google.
  • Microsoft-backed connectors use microsoft.
  • SharePoint and OneNote use the same connector OAuth provider id as OneDrive, Outlook, Microsoft Calendar, and Microsoft Contacts: microsoft.
  • Slack uses slack.
  • Jira and Confluence use atlassian for Cloud OAuth paths.
  • GitHub uses github.
  • Odoo does not use a Better Auth provider id; it can use the signed-in user’s own API key through user-api-key or the shared deployment API-key credential profile.
  • Jira can use Better Auth provider id atlassian for OAuth, a signed-in user’s own Jira Data Center PAT through user-api-key, or a customer CMS-managed Jira Data Center service/bot PAT profile for company-managed access.
  • Confluence can use Better Auth provider id atlassian for Cloud OAuth, a signed-in user’s own Confluence Data Center PAT through user-api-key, or a customer CMS-managed Confluence Data Center service/bot PAT profile for company-managed access.
  • These ids are connector-facing runtime identifiers, not workforce sign-in ids.

Connector-linked accounts remain user-owned. The same signed-in app user can hold both:

  • a workforce SSO account such as google-sso or microsoft-sso
  • a connector-linked OAuth account such as google or microsoft

The same signed-in app user may also hold multiple connector-linked accounts for the same provider (for example two Google accounts or two Slack users) and must be allowed to choose which one a connector uses.

In the web UI, provider-linked accounts are first-class groups on /connectors. A user can add another connector under an existing OAuth account, link another account for the same provider, or open the same shared create modal from chat without leaving the current workflow. Generic connector creation starts with a searchable provider chooser, then shows account-plus-connector rows for the selected provider so the account binding is visible before a connector is saved. User-managed API-key connectors skip linked-account selection in the web app, but the signed-in user must enter their own provider credential during connect. API validates the credential with the provider, derives the account identity from provider validation when available, encrypts the raw value in API-owned user credential storage, and scopes retrieval to that user’s connector record. Deployment API-key connectors skip linked-account selection in the web app. A customer operator configures and validates the deployment credential in cms-customer; when that profile is valid, web users can add the connector without entering an API key or choosing a Better Auth linked account. The company-managed path must be labeled clearly because the provider sees the configured service or bot account, not the signed-in web user. The /connectors summary area also acts as a compact filter summary: provider logo buttons filter by provider, account chips filter by the selected linked account without also changing the provider filter, and an always-first All accounts chip clears only the account filter. Account chips stay alphabetized on larger screens. The More/Less expansion is only a small-screen affordance when the filter chips would otherwise crowd the connector list; on small screens only, the active account is visually prioritized immediately after All accounts so the current filter remains visible before expansion. When a linked account has no addable connector choices left, account-level add actions are hidden and provider-level addable summaries are labeled as requiring another account. Users can also delete a connector-linked OAuth account from its account row. The UI confirms this as a destructive action because the linked account is removed and every connector under that account is scheduled through the normal async disconnect cleanup lifecycle. This account-management action is only for connector-linked OAuth accounts; workforce SSO accounts and email/password credentials are managed by the auth/sign-in surfaces, not by /connectors. Better Auth requires a fresh session for account unlinking, so stale sessions are blocked before any new connector cleanup is scheduled. When that happens, the web app asks the user to verify with the same web sign-in method used by the current session: email/password sessions verify in place with the same full-width password input and show/hide password control used by the main sign-in form, and workforce SSO sessions redirect through the same SSO provider and return to /connectors. The app verifies the same Better Auth user id after the fresh sign-in and then reopens the destructive confirmation instead of deleting automatically. Older active sessions that predate sign-in method metadata may recover without forcing a manual sign-out. If a credential account is present, the app can show email/password verification because the password check and same-user check prove the user before deletion continues. If the Better Auth account lookup is temporarily unavailable and email/password is allowed, the app can still show email/password verification for the current user’s email; unsuccessful password verification leaves deletion blocked. If no credential account exists, exactly one configured workforce SSO account can be used. Multiple workforce SSO candidates without a credential account, or connector-only account sets, fail closed.

Those are separate provider namespaces on purpose. Connector linking must fail closed across different app users for the same external provider account.

Better Auth’s ownership key for a linked connector/social account is providerId + accountId. The same external connector/social account cannot be linked by two different Better Auth web users at the same time, regardless of which configured connector provider id owns the account. If a product flow ever needs one external account shared across app users, that must be modeled as app-owned sharing above Better Auth linked accounts, not as a relaxation of connector OAuth ownership.

Register callback URIs from the auth service origin that the provider will actually redirect to.

  • Connector OAuth callbacks use the OAuth origin:
    • AUTH_BASE_URL by default
    • AUTH_PUBLIC_BASE_URL only for provider ids listed in AUTH_CONNECTOR_OAUTH_PUBLIC_BASE_URL_PROVIDER_IDS
  • Workforce SSO callbacks should be registered on every auth origin you actually use for sign-in:
    • local auth origin such as http://localhost:3001
    • public tunnel or production auth origin such as https://auth.example.com
Flow Pattern Current repo examples
Generic OAuth provider callback <oauth-origin>/api/auth/oauth2/callback/<providerId> Google connector auth: /api/auth/oauth2/callback/google Microsoft connector auth: /api/auth/oauth2/callback/microsoft Jira connector auth: /api/auth/oauth2/callback/atlassian GitHub connector auth: /api/auth/oauth2/callback/github
Better Auth social provider callback <oauth-origin>/api/auth/callback/<providerId> Reserved for built-in social-provider sign-in/link flows, not the shared connector OAuth link flow.
Workforce SSO callback <auth-origin>/api/auth/sso/callback/<workforce-provider-id> Google SSO: /api/auth/sso/callback/google-sso Microsoft SSO: /api/auth/sso/callback/microsoft-sso

If local auth runs on http://localhost:3001 and public OAuth flows run through https://your-tunnel.example:

  • Google connector OAuth:
    • https://your-tunnel.example/api/auth/oauth2/callback/google
    • http://localhost:3001/api/auth/oauth2/callback/google
  • Microsoft connector OAuth:
    • https://your-tunnel.example/api/auth/oauth2/callback/microsoft
    • http://localhost:3001/api/auth/oauth2/callback/microsoft
  • Jira connector OAuth:
    • https://your-tunnel.example/api/auth/oauth2/callback/atlassian
    • http://localhost:3001/api/auth/oauth2/callback/atlassian
  • GitHub connector OAuth:
    • https://your-tunnel.example/api/auth/oauth2/callback/github
    • http://localhost:3001/api/auth/oauth2/callback/github
  • Workforce Microsoft SSO:
    • https://your-tunnel.example/api/auth/sso/callback/microsoft-sso
    • http://localhost:3001/api/auth/sso/callback/microsoft-sso

Register only the origins you actually use, but do not guess the path shape. The callback path is provider-type-specific. For local development with both localhost-capable providers and providers that require HTTPS, keep AUTH_BASE_URL=http://localhost:3001, set AUTH_PUBLIC_BASE_URL to the HTTPS tunnel, and list only the tunnel-required connector OAuth provider ids in AUTH_CONNECTOR_OAUTH_PUBLIC_BASE_URL_PROVIDER_IDS. For example, slack can use the ngrok callback while github remains on http://localhost:3001/api/auth/oauth2/callback/github. If AUTH_PUBLIC_BASE_URL was previously copied into apps/auth/.env, clear it in root .env.common, run bun run env:refresh, and restart the dev stack; otherwise the app-local auth env can keep using the stale tunnel even when root .env.common is blank.

Linked worktrees run Auth on assigned ports such as http://localhost:5001, so a live provider OAuth browser check from a worktree completes only when the provider app accepts that exact Auth callback URI or an explicit public tunnel callback. If the provider app is registered only for the main checkout callback, use the main checkout for real remove/re-add OAuth testing, or register the worktree callback before testing the consent return path. Worktree checks can still verify the Connectors page and chat add-connector modal up to the app-started provider redirect.

Planned future local tooling may add a worktree-only connector OAuth sidecar proxy that binds the default local Auth port and forwards Auth requests to the active worktree Auth port. That would let dev OAuth apps keep one stable localhost callback origin while the worktree Auth process still handles the canonical callback. That helper does not exist today. If added, it should remain development-only, localhost-only, connector-provider allowlisted, separate from the temporary Open WebUI bridge below, and invisible to production OAuth, workforce SSO, and customer deployment configuration.

Temporary Local Callback Bridge For Existing Dev OAuth Apps

Section titled “Temporary Local Callback Bridge For Existing Dev OAuth Apps”

Some local connector-development OAuth apps may already be registered to old Open WebUI callback URLs:

  • http://localhost:8080/api/v1/data/google/callback
  • http://localhost:8080/api/v1/data/microsoft/callback
  • http://localhost:8080/api/v1/data/slack/callback
  • http://localhost:8080/api/v1/data/atlassian/callback
  • http://localhost:8080/api/v1/data/github/callback

Use the temporary local callback bridge only for those existing dev OAuth apps. The bridge is hard-disabled unless the auth process runs with NODE_ENV=development. Set AUTH_OPEN_WEBUI_OAUTH_CALLBACK_BRIDGE_PROVIDER_IDS to the comma-separated provider ids that need the old callback path, such as github or google,microsoft. For local root dev, this key is present in .env.common.example, so it can be set in root .env.common, copied into apps/auth/.env with bun run env:refresh, and picked up after the dev stack is restarted. When a provider id is listed, Better Auth requests that provider’s old callback URL and the auth app forwards the callback into /api/auth/oauth2/callback/<providerId>. For local HTTP bridge origins such as http://localhost:8080, the auth service starts a lightweight secondary listener for the old callback paths so the old Open WebUI stack does not need to run.

The bridge is disabled by default, provider-specific, and not permanent connector configuration. Enabling the bridge for Google does not change GitHub, Microsoft, Slack, or Jira redirect URIs unless those provider ids are also listed. If a provider app is already registered with the canonical generic OAuth callback path, leave that provider id out of the allowlist so the normal /api/auth/oauth2/callback/<providerId> callback remains in use. Remove the bridge env values when those old dev OAuth app registrations are retired. Clearing AUTH_OPEN_WEBUI_OAUTH_CALLBACK_BRIDGE_PROVIDER_IDS in root .env.common and then running bun run env:refresh also clears the copied app-local value so stale bridge provider ids do not survive in apps/auth/.env. The bridge base URL follows the same refresh rule, so moving AUTH_OPEN_WEBUI_OAUTH_CALLBACK_BRIDGE_BASE_URL back to the default replaces any stale app-local custom origin. It defaults to http://localhost:8080.

The upstream provider must accept the exact redirect URL sent in the authorize request, and that origin must route the old callback path into the auth app in the current local setup. For the local secondary listener, a port conflict means another process is already using the old callback origin; stop that process or remove the provider from the bridge allowlist. A logged-in provider browser may show a provider-side redirect warning immediately, while an unauthenticated agent-browser run may stop at the provider login page and only prove that the app generated the authorize URL. If the provider app is registered with the canonical callback path instead, leave that provider out of the bridge allowlist.

Use this checklist when adding or rotating built-in connector OAuth credentials:

  1. Create or update the provider app in the upstream provider console.
  2. Register the callback URI for the connector OAuth origin and generic OAuth path shown above.
  3. Confirm the required scopes in packages/domain/src/connectors/connectorRegistry.ts and the provider page for the connector.
  4. Put built-in provider client credentials in deployment-owned auth/API env, Kubernetes Secrets, or an approved external secret store. Do not put connector OAuth client ids or client secrets in customer CMS records.
  5. If connector registry defaults changed, rerun the customer connector seed through the repo-owned seed path for that environment.
  6. Reconnect a test account so Better Auth stores a fresh linked connector OAuth account with the current scopes.
  7. Verify the connect flow, catalog readiness, real-provider doctor/smoke lane when configured, and sync behavior for indexed providers.

For Microsoft connector additions, check that reauthorization requests the connector being added plus scopes already required by active sibling connectors on the same linked microsoft account. Do not request every Microsoft connector scope by default. SharePoint selected sites and OneNote selected notebooks are connector-level resourceSelection entries, not provider-family account scopes.

User-managed API-key connectors use the shared user-api-key auth strategy. They are for provider credentials that belong to the signed-in web user, not to a customer deployment service account.

  • The web user enters the credential in the web app during connect or rotation.
  • API derives userId from the Better Auth session and never accepts a client-supplied owner id.
  • API validates the credential directly with the provider before the connector is saved as usable.
  • Raw credential values are encrypted by API-owned storage and are never returned to the browser, CMS, chat runtime, route responses, logs, prompt packs, or generated artifacts.
  • MongoDB stores ciphertext plus safe metadata such as owner ids, connector id, algorithm/key id, validation status, and provider identity label when the provider exposes one.
  • Duplicate prevention uses provider-validated identity plus validated connector config, or an opaque keyed credential fingerprint; users do not type who the token belongs to.
  • Another app user cannot list, use, rotate, infer, or retrieve the credential even if they know connector ids, provider labels, URLs, projects, or account names.
  • Current implemented user-managed credential connectors include Odoo API keys, Jira Data Center PATs, and Confluence Data Center PATs.
  • Disconnecting a user-managed credential connector removes the encrypted credential row before deleting the connector row. If that secret cleanup fails, the connector stays in the disconnect cleanup lifecycle so the next retry still has an owner-scoped connector anchor.

Deployment API-key connectors use the shared deployment-api-key auth strategy instead of Better Auth OAuth. The reusable contract is intentionally connector-agnostic so Odoo, Jira Data Center, Confluence Data Center credential setup, and similar deployment-owned integrations can use the same profile/slot model.

  • The customer operator enters or rotates the API key in cms-customer.
  • Raw API keys are never stored in Payload/Mongo.
  • Production customer-cluster deployments store the key in Kubernetes Secret storage under fixed registry-declared slots.
  • API reads credentials from mounted Secret files through the shared resolver. Provider adapters must not read arbitrary files, patch Kubernetes Secrets, or call process.env[...] for connector API keys.
  • Local development uses the same CMS entry/rotation flow backed by gitignored files under env/cms-customer/connector-credentials/.
  • Rotation writes the inactive slot, validates it, and flips the non-secret active-slot metadata only after validation succeeds.
  • Setup and rotation save non-secret setup fields only after provider validation succeeds. A failed validation must not leave a new URL, database, allowlist, or similar setup value behind in Payload.
  • CMS operators can remove a company-managed credential profile. Removal clears all registry-declared secret slots and safe setup metadata, returns the profile to not configured, and makes existing company-managed connector rows unavailable until a new credential is validated.
  • Removal does not silently delete users’ connector records. OAuth and user-managed API-key/PAT connector rows are separate; only company-managed rows that depend on the removed profile lose runtime access.
  • Existing web users do not reconnect after rotation because the deployment credential identity is server-side.
  • Customer-cluster deployments need Secret encryption at rest and least-privilege RBAC. External Secrets or CSI can manage the same fixed Secret, but the app contract remains the mounted-file resolver.
  • Provider terms can be misleading. Atlassian Data Center calls these values personal access tokens, but for this product they should be created from a dedicated customer-controlled service/bot account when used as deployment credentials. Do not ask customers to paste an employee’s personal PAT for a deployment-scoped connector.
  • Jira and Confluence Data Center PAT profiles reject Atlassian Cloud gateway/site URLs such as api.atlassian.com and *.atlassian.net before storing or sending the PAT. Cloud API tokens use email-plus-token Basic auth and need a separate accepted profile before they can be supported here.
  • Jira Data Center service/bot PAT support stores the PAT as a secret slot, keeps Jira base URL as non-secret config, derives server-side deployment identity deployment:jira, and keeps the live work-item domain. Project access is still controlled per connected app user through the connector’s selected project scope.
  • Confluence Data Center service/bot PAT support stores the PAT as a secret slot, keeps the Confluence base URL as non-secret config, derives server-side deployment identity deployment:confluence, and keeps the live knowledge-page domain. Space access is still controlled per connected app user through the connector’s selected spaces scope.
  • User-managed PAT/API-key support is separate per-user auth. It uses API-owned encrypted user credential storage and must not reuse the CMS company-managed service/bot profile.

Sync runners do not choose user, account, or provider tokens directly. They call the service-authenticated POST /connectors/token route with { connectorId }; API resolves the server-side connector context and exchanges it with auth through /api/internal/oauth/token. Gmail sync must receive the returned oauthCredentials refresh payload before it can run.

If the provider access token is expired, the auth service/Better Auth is the refresh authority. Sync runners and taps should treat the token-context response as the only token input for provider calls, and move into structured reauthorize/reconnect recovery if token refresh or scope validation cannot recover.

Connector account linking uses Better Auth generic OAuth in this repo for providers that use the shared /api/auth/oauth2/link route:

  • workforce platform sign-in uses microsoft-sso
  • connector account linking uses microsoft
  • workforce platform sign-in uses google-sso
  • connector account linking uses google
  • connector account linking uses atlassian, slack, and github

In the installed Better Auth 1.6.16 source, built-in social callback account lookup is provider-scoped, and the SSO plugin rejects provider ids that collide with configured social or trusted provider ids. Workforce SSO trust uses Better Auth SSO domain verification, not SSO provider ids in accountLinking.trustedProviders. This repo still keeps connector auth on generic OAuth because the current generic path is the already-verified implementation for:

  • connector-specific custom scopes across Google, Microsoft, Slack, Atlassian, GitHub, and future provider families
  • provider account selection prompts where supported
  • offline refresh-token semantics where the provider supports them
  • tenant-scoped Microsoft connector auth
  • typed profile mapping owned by connector/provider registries
  • local callback bridge behavior
  • the existing /api/auth/oauth2/* callback and test harness coverage

Using one canonical generic OAuth path keeps connector account linking DRY and avoids mixing shared connector OAuth flows with built-in social sign-in/link routes.

Implementation rule: a provider with connector definitions using linkMethod: "oauth2" must have a matching generic OAuth config in apps/auth/src/genericOAuthProviders.ts. Env credentials alone only make credentials available; they do not mount /api/auth/oauth2/link for that provider unless a generic OAuth config is built. Public sign-in guard rule: every connector OAuth provider id from the shared connector registry must stay out of Better Auth public OAuth sign-in. Auth must reject connector ids at /api/auth/sign-in/oauth2 and /api/auth/sign-in/social, while still forwarding authenticated /api/auth/oauth2/link, /api/auth/link-social, and provider callback routes.

Local dev note: root bun run dev runs through Turborepo, so built-in connector OAuth credential env names, connector OAuth public-origin provider ids, and local callback bridge env names must be present in turbo.json under the dev task env allowlist. Root .env.common is a local template source only; after changing connector credentials, callback-origin provider ids, or bridge provider ids there, run bun run env:refresh or the matching env copy workflow so app-local runtime .env files receive the values, then restart the dev stack. If a browser link request reaches /api/auth/oauth2/link but returns PROVIDER_NOT_FOUND, check that the auth process received the provider’s *_CLIENT_ID and *_CLIENT_SECRET values, not only that they exist in the shell or .env.common.

  • Audit whether future connector OAuth should move from Better Auth generic OAuth to built-in social providers as a connector-wide strategy.
  • Do not evaluate only Google/Microsoft. Account for all connector families and require parity for custom scopes, offline refresh tokens, account chooser prompts, redirect/local bridge behavior, typed profile mapping, connector registry ownership, SSO/social namespace isolation, and current callback/test harness coverage.
  1. A person signs into the platform through workforce SSO or email/password.
  2. Better Auth creates or resumes the app user session.
  3. Later, that signed-in app user links a provider account for a connector.
  4. Connector sync/live access uses the linked connector OAuth account, not the workforce SSO account.

Current limitations and rules:

  • Workforce SSO alone does not automatically create a connector-linked account.
  • Connector linking is user-scoped and must not cross over to another app user.
  • If two different app users try to link the same external provider account, the second link must fail.
  • If you want a truly shared external account later, that has to be an app-level sharing model above Better Auth linked accounts; the current ownership invariant is intentionally strict to avoid connector-linked auth ambiguity.
  • For Google and Microsoft connector OAuth, the link flow explicitly requests provider account selection (select_account) so adding Gmail after Drive, or work after personal, does not silently reuse the previous browser session.
  • When reauthorizing or adding a connector under an already linked OAuth account, the web flow requests the union of scopes required by every compatible connector under that same account. This keeps the shared linked account grant from becoming order-dependent when providers such as Google return the latest callback scope set.
  • When a Configure setting needs broader OAuth scopes, the web flow stores the pending settings update in browser-local session storage, sends the user through provider consent, and applies the same settings automatically after the callback succeeds. Users should not need to reopen Configure and save the same access-level change again.
  • Google Calendar uses the same google connector auth account as Gmail, Google Drive, and Google Contacts, with https://www.googleapis.com/auth/calendar.calendarlist.readonly and https://www.googleapis.com/auth/calendar.events.readonly added to the linked account grant when needed. The Google Cloud project for that OAuth client must also have Google Calendar API enabled; otherwise live event list/search fails closed as provider setup incomplete even when the stored account has calendar scopes.
  • Google Contacts uses the same google connector auth account as Gmail and Google Drive, with https://www.googleapis.com/auth/contacts.readonly added to the linked account grant when needed. The Google Cloud project for that OAuth client must also have People API enabled; otherwise live contact search/list fails closed as provider setup incomplete even when the stored account has the contact scope.
  • Microsoft Calendar uses the same microsoft connector auth account as Outlook, OneDrive, and Microsoft Contacts, with delegated Calendars.Read added to the linked account grant when needed. It is separate from the Outlook mail connector and does not request Mail.Read or Calendars.Read.Shared by itself.
  • Microsoft Contacts uses the same microsoft connector auth account as Outlook and OneDrive, with delegated People.Read and Contacts.Read added to the linked account grant when needed. It is separate from the Outlook connector and does not require Mail.Read by itself.
  • GitHub Repositories, GitHub Issues, and GitHub Pull Requests use the same Better Auth generic OAuth provider id github, with repo, read:user, and user:email requested for the current OAuth App access profile. GitHub OAuth Apps do not provide a private-repository read-only scope, so the stored token is write-capable at the provider scope level while this app enforces read-only runtime behavior through connector policy, live tools, and provider adapters.
  • GitHub has one active access profile today: oauth-app-write-capable. Because it is the only implemented GitHub profile, the product does not expose a GitHub profile selector. Future GitHub App read-only installation auth is planned for least-privilege private repository access; when both profiles exist, changing profiles must run the appropriate OAuth or installation flow instead of silently flipping a saved setting.
  • Slack and Atlassian do not get the same explicit chooser prompt from this repo’s auth config today; when multiple Slack or Atlassian accounts are already linked, the shared connector configure flow is responsible for letting the user choose the account before the connector is saved.
  • Connector duplicate prevention is shared and scope-based: connector persistence dedupes on user + connectorKey + linked account + scopeKey, where scopeKey defaults to empty and Jira uses siteId.
  • Catalog/config contracts expose registry-derived connectionScope so web can hide impossible duplicate rows before submit: account-scoped connectors disappear once already added under that linked account, while configuration-scoped connectors stay available until their config-derived scopeKey is known.
  • Account-focused chat uses connector OAuth accounts only. The picker derives account choices from connector rows and connector OAuth bindings, not workforce SSO linked-account rows; provider ids such as google-sso and microsoft-sso must not appear in focus choices or pass focus validation.
  • Chat-launched connector creation still uses connector OAuth, not workforce SSO. Creating a connector from chat does not automatically mutate a locked chat focus.
  • Better Auth blocks unlinking the final auth account unless allowUnlinkingAll is enabled. This repo does not enable that option for connector account deletion; the web boundary blocks final-account deletion before connector cleanup is scheduled.
  • Better Auth also requires a fresh session for unlinking accounts. The web boundary checks session freshness before scheduling connector cleanup so a stale session cannot create a predictable partial deletion. The Connectors page handles that operational code by asking the user to verify with the same web sign-in method used by the current session, never with connector OAuth provider ids.

The current connector create flow grants scopes for the selected connector and preserves sibling connector scopes for the same linked account during reauthorization. It does not expose a blind “request all scopes for this provider” toggle.

If product later wants users to prepare one linked account for more connectors under the same provider, the preferred shape is explicit and reversible:

  • Provider-level child action, for example “Prepare this Google account for more connectors”: show the connector list, exact additional scopes, and privacy/storage implications before OAuth. This grants account scopes only; it does not create or enable connector records.
  • Provider multi-add flow: let the user select multiple connectors for the provider, review the combined scope grant, accept each connector’s defaults, complete any required configuration, and see per-connector success or failure after OAuth returns.

Do not implement this as a hidden toggle on a single connector. Extra provider scopes can authorize more private data than the immediate connector needs, and a scope grant without a connector record does not make that connector usable.

  • Jira connector auth can use Better Auth generic OAuth with connector provider id atlassian, Jira Data Center user-managed PAT access through the web app, or Jira Data Center company-managed access through the customer CMS service/bot PAT profile.
  • Atlassian OAuth 2.0 (3LO) uses the code grant flow and redirects back to the configured callback URL.
  • After linking, the connector must store one explicit Jira siteId per connector. Do not infer a site from the token alone.
  • The same linked Atlassian account may be reused for multiple Jira connectors only when each connector stores a different siteId, which becomes that connector’s scopeKey.
  • Project selection is scoped to the selected Jira site and is part of the shared connector resource-selection flow.
  • Jira V1 is live-only in this repo. Do not expect a Meltano tap or embeddings to make chat-time Jira access work.
  • Jira Data Center user-managed access is for a signed-in user’s own Data Center PAT. API validates and encrypts the token for only that user’s connector row.
  • Jira Data Center company-managed access is a separate deployment API-key profile, not a replacement for OAuth or user-managed PATs. The PAT belongs to a dedicated Jira service/bot user and is sent as bearer auth by the provider adapter.
  • When Jira OAuth, user-managed PAT, and company-managed access are available, the web app should show each in ordinary language and make the identity boundary clear. Company-managed access means the provider sees the service/bot user, not the signed-in web user.
  • Confluence connector auth can use Better Auth generic OAuth with connector provider id atlassian, Confluence Data Center user-managed PAT access through the web app, or Confluence Data Center company-managed access through the customer CMS service/bot PAT profile.
  • After Cloud OAuth linking, the connector must store one explicit Confluence siteId per connector. Data Center PAT paths store the configured base URL, including any context path.
  • Confluence requires an explicit saved spaces selection using the shared all-or-selected resource selection contract. Do not silently query all accessible spaces without a saved selection.
  • Confluence V1 is live-only on the knowledge-page domain. Do not expect a Meltano tap, embeddings, write methods, attachment-content extraction, or Cloud API-token Basic auth to make chat-time Confluence access work.
  • Confluence Data Center company-managed access means the provider sees the dedicated service/bot user, not the signed-in web user. Company-managed deployment PATs must be customer-controlled service/bot PATs, not employee personal PATs.
  • Odoo uses connector key odoo, live domain business-record, and canonical tool id business-record-live-query.
  • Odoo auth uses user-managed API keys or the shared deployment API-key profile, not Better Auth OAuth.
  • For user-managed Odoo access, the signed-in web user enters their own Odoo URL, optional database, and API key in the web app. API validates the key through Odoo JSON-2 and stores it only in encrypted user credential storage.
  • For company-managed Odoo access, the operator enters the Odoo URL and API key in cms-customer. Payload stores only non-secret metadata and configuration; it never stores the raw API key. Odoo JSON-2 requires Odoo 19 or newer and Odoo external API access on a plan that includes it; Odoo documents external API access as unavailable on One App Free and Standard plans. Hosted *.odoo.com database fields are normally left blank so Odoo can route by URL/Host, and hosted values such as mycompany.odoo.com are normalized to the JSON-2 database token mycompany when entered. Operators may enter a database name when Odoo asks them to connect to one, and the optional model allowlist is an advanced limiter that should stay blank unless an Odoo administrator wants to restrict which technical model names can be queried.
  • Production customer-cluster storage is the fixed connector credential Kubernetes Secret mounted into API as files. CMS has RBAC only to get, patch, and update that named Secret.
  • Local development uses the same CMS credential entry flow with gitignored local files under env/cms-customer/connector-credentials/.
  • Odoo must expose JSON-2 endpoints such as /json/2/<model>/<method>, and the selected user or configured service/bot user must have enough Odoo permissions for the allowed models.
  • XML-RPC, JSON-RPC, write methods, create/update/delete, webhooks, indexed sync, Meltano taps, and embeddings are not part of this connector.
  • GitHub connector auth uses Better Auth generic OAuth with connector provider id github.
  • GitHub OAuth App credentials live in deployment-owned env as GITHUB_CLIENT_ID and GITHUB_CLIENT_SECRET.
  • Register the GitHub OAuth App callback on the auth origin as /api/auth/oauth2/callback/github.
  • Current required connector scopes are repo, read:user, and user:email.
  • The GitHub OAuth App repo scope is broad and write-capable for private repositories. The runtime remains read-only by app policy: there are no GitHub write tools, write UI paths, webhooks, merge/comment/review/file-update/workflow/delete calls, or mutating REST adapters.
  • GitHub Repositories is live-only through code-repository-live-query; GitHub Issues and GitHub Pull Requests are live-only through work-item-live-query.
  • Optional repository selection is part of the shared resource-selection model. When no selection is configured, live queries use bounded searches across accessible repositories and disclose incomplete or sampled provider coverage.
  • GitHub App installation auth is planned future work for selected-repository, private-repository read-only access. That future profile will need GitHub App credentials such as app id/client id, private key, installation id, and optional client secret for user authorization; it is not implemented in this rollout.
  • GitHub Enterprise Server, Discussions, Projects, Actions/check rollups, Releases, Packages, security alerts, and indexed GitHub storage are out of scope until separately accepted.

The real-provider connector smoke harness is testing app-user ownership, not just provider reachability.

  • REAL_PROVIDER_E2E_*_ACCOUNT_ID is the linked provider account id from Better Auth account.accountId.
  • The harness resolves the owning Better Auth app user from that linked account id. Do not provide display emails or Better Auth account.id row ids.
  • Jira real-provider smoke also requires REAL_PROVIDER_E2E_JIRA_SITE_ID. REAL_PROVIDER_E2E_JIRA_PROJECT_IDS is optional; when omitted, the smoke connector uses explicit all-project scope for the selected site.
  • Confluence real-provider smoke is live-only on the knowledge-page domain. Cloud smoke requires the linked Atlassian accountId plus a Confluence siteId; optional space/page/query pins strengthen the bounded space/page/facet checks.
  • Google Calendar and Microsoft Calendar real-provider smoke are live-only. When the optional query env is omitted, calendar smoke uses bounded event list mode; when the query env is set, it uses provider-native event search mode. Optional time-window, time-zone, updated-min, event-title, and event-id pins strengthen the smoke check, but the required fixture remains the linked provider accountId.
  • Google Contacts and Microsoft Contacts real-provider smoke are live-only. When the optional query env is omitted, contact smoke uses provider-native saved-contact list mode; when the query env is set, it uses focused search mode. Optional display-name/id pins strengthen the smoke check, but the required fixture remains the linked provider accountId.
  • GitHub real-provider smoke is live-only. GitHub Repositories uses repository list/search/detail or bounded code/file/commit reads. GitHub Issues and Pull Requests use work-item search/list/detail plus comment, review, and file paths where configured. The required fixture is the linked provider accountId; optional pins strengthen the provider result check.
  • Odoo real-provider smoke is live-only and deployment-scoped. It requires REAL_PROVIDER_E2E_ODOO_BASE_URL and REAL_PROVIDER_E2E_ODOO_API_KEY. REAL_PROVIDER_E2E_ODOO_DATABASE is optional for hosted Odoo and should be set only when Odoo requires an explicit database header. Optional pins can provide model/id/name fields.

Use the commands like this:

  • Run bun run test:api:connectors:real-provider:doctor to preflight every configured fixture account before the heavier smoke suite.
  • Run bun run test:api:connectors:real-provider:doctor gmail to inspect one provider.
  • If an account id is stale, the doctor prints safe candidate accountId values for the same provider id, with owner id and scope coverage only. It never prints access tokens, refresh tokens, secrets, or emails.
  • Run bun run test:api:connectors:real-provider:e2e:all for the configured-provider E2E gate.
  • The doctor and all-provider gate derive provider membership from each provider manifest’s realProviderSmoke.requiredEnv; they should not maintain a separate hand-edited provider checklist. Do not remove a provider from the gate to work around fixture failures.

Example:

  • Google Drive fixture:
    • REAL_PROVIDER_E2E_GOOGLE_DRIVE_ACCOUNT_ID=<google-linked-account-id>
  • OneDrive fixture:
    • REAL_PROVIDER_E2E_ONEDRIVE_ACCOUNT_ID=<microsoft-linked-account-id>
  • Google Contacts fixture:
    • REAL_PROVIDER_E2E_GOOGLE_CONTACTS_ACCOUNT_ID=<google-linked-account-id>
  • Google Calendar fixture:
    • REAL_PROVIDER_E2E_GOOGLE_CALENDAR_ACCOUNT_ID=<google-linked-account-id>
    • optional REAL_PROVIDER_E2E_GOOGLE_CALENDAR_QUERY=<event-title-or-keyword>
    • optional REAL_PROVIDER_E2E_GOOGLE_CALENDAR_TIME_MIN=<iso-date-time>
    • optional REAL_PROVIDER_E2E_GOOGLE_CALENDAR_TIME_MAX=<iso-date-time>
    • optional REAL_PROVIDER_E2E_GOOGLE_CALENDAR_EXPECTED_EVENT_ID=<provider-event-id>
    • optional REAL_PROVIDER_E2E_GOOGLE_CALENDAR_EXPECTED_EVENT_TITLE_TEXT=<title-substring>
  • Microsoft Contacts fixture:
    • REAL_PROVIDER_E2E_MICROSOFT_CONTACTS_ACCOUNT_ID=<microsoft-linked-account-id>
  • Microsoft Calendar fixture:
    • REAL_PROVIDER_E2E_MICROSOFT_CALENDAR_ACCOUNT_ID=<microsoft-linked-account-id>
    • optional REAL_PROVIDER_E2E_MICROSOFT_CALENDAR_QUERY=<event-title-or-keyword>
    • optional REAL_PROVIDER_E2E_MICROSOFT_CALENDAR_TIME_MIN=<iso-date-time>
    • optional REAL_PROVIDER_E2E_MICROSOFT_CALENDAR_TIME_MAX=<iso-date-time>
    • optional REAL_PROVIDER_E2E_MICROSOFT_CALENDAR_EXPECTED_EVENT_ID=<provider-event-id>
    • optional REAL_PROVIDER_E2E_MICROSOFT_CALENDAR_EXPECTED_EVENT_TITLE_TEXT=<title-substring>
  • Jira fixture:
    • REAL_PROVIDER_E2E_JIRA_ACCOUNT_ID=<atlassian-linked-account-id>
    • REAL_PROVIDER_E2E_JIRA_SITE_ID=<jira-cloud-site-id>
    • optional REAL_PROVIDER_E2E_JIRA_PROJECT_IDS=<jira-project-key>
  • Confluence fixture:
    • REAL_PROVIDER_E2E_CONFLUENCE_ACCOUNT_ID=<atlassian-linked-account-id>
    • REAL_PROVIDER_E2E_CONFLUENCE_SITE_ID=<confluence-cloud-site-id>
    • optional REAL_PROVIDER_E2E_CONFLUENCE_SPACE_IDS=<comma-separated-space-keys>
    • optional Data Center user PAT group: REAL_PROVIDER_E2E_CONFLUENCE_DATA_CENTER_BASE_URL=<confluence-data-center-url>, REAL_PROVIDER_E2E_CONFLUENCE_DATA_CENTER_PAT=<user-pat>, and REAL_PROVIDER_E2E_CONFLUENCE_DATA_CENTER_SPACE_IDS=<comma-separated-space-keys>
    • optional Data Center deployment/service-bot PAT group: REAL_PROVIDER_E2E_CONFLUENCE_DEPLOYMENT_BASE_URL=<confluence-data-center-url>, REAL_PROVIDER_E2E_CONFLUENCE_DEPLOYMENT_PAT=<service-bot-pat>, and REAL_PROVIDER_E2E_CONFLUENCE_DEPLOYMENT_SPACE_IDS=<comma-separated-space-keys>
    • optional REAL_PROVIDER_E2E_CONFLUENCE_QUERY=<page-title-or-text>
    • optional REAL_PROVIDER_E2E_CONFLUENCE_EXPECTED_SPACE_KEY=<space-key>
    • optional REAL_PROVIDER_E2E_CONFLUENCE_EXPECTED_PAGE_ID=<provider-page-id>
    • optional REAL_PROVIDER_E2E_CONFLUENCE_EXPECTED_PAGE_TITLE_TEXT=<title-substring>
    • optional REAL_PROVIDER_E2E_CONFLUENCE_EXPECTED_LABEL_NAME=<label-name>
    • optional REAL_PROVIDER_E2E_CONFLUENCE_EXPECTED_COMMENT_TEXT=<comment-substring>
    • optional REAL_PROVIDER_E2E_CONFLUENCE_EXPECTED_ATTACHMENT_TITLE_TEXT=<attachment-title-substring>
  • GitHub fixture:
    • REAL_PROVIDER_E2E_GITHUB_ACCOUNT_ID=<github-linked-account-id>
    • optional REAL_PROVIDER_E2E_GITHUB_REPOSITORY_FULL_NAME=<owner/repo>
    • optional REAL_PROVIDER_E2E_GITHUB_QUERY=<query-text>
    • optional REAL_PROVIDER_E2E_GITHUB_EXPECTED_REPOSITORY_FULL_NAME=<owner/repo>
    • optional REAL_PROVIDER_E2E_GITHUB_EXPECTED_ISSUE_NUMBER=<number>
    • optional REAL_PROVIDER_E2E_GITHUB_EXPECTED_PULL_NUMBER=<number>
    • optional REAL_PROVIDER_E2E_GITHUB_EXPECTED_PATH=<repo-path>
  • Odoo fixture:
    • REAL_PROVIDER_E2E_ODOO_BASE_URL=<odoo-json-2-origin>
    • REAL_PROVIDER_E2E_ODOO_API_KEY=<odoo-service-user-api-key>
    • optional REAL_PROVIDER_E2E_ODOO_DATABASE=<odoo-database-name>
    • optional model/id/name pins for stable smoke assertions

This keeps local runs and CI simple while still preserving the ownership check: the selected linked account must belong to the app user resolved from Better Auth, and another app user cannot connect that same external provider account.

Platform-managed connector OAuth credentials

Section titled “Platform-managed connector OAuth credentials”

These are defined in auth and API env files, Kubernetes Secrets, or an approved external secret store for the built-in provider setup path. Customer CMS records must not store connector OAuth client IDs or client secrets.

  • apps/auth/.env.example
  • apps/api/.env.example

Connector keys, OAuth bindings, default scopes, supported modes, resource defaults, and scoped-resource policy live in:

  • packages/domain/src/connectors/connectorRegistry.ts
  • Connector auth keeps connectorKey, authProviderId, default scopes, and connector-mode truth in the shared registry.
  • Resource and duplicate-identity truth stays in the shared registry as defaultResourceTypes, resourceSelectionPolicy, and connectionScope; do not duplicate it in CMS-authored metadata or provider-specific web UI branches.
  • API-only runtime extensions keep executable behavior such as indexed tap dispatch, live adapters, provider-option dependencies, select config-field defaults, eval packs, and real-provider smoke hooks.
  • Access-level downgrades are app policy changes, not provider grant revocation. A connector set back to read-only must stop using upload/write flows, but removing a previously granted write scope requires provider-side revocation or relinking.
  • Library upload folder browsing uses the provider-backed upload adapter, not the read/focus resource-list adapter. Shared read locations such as Shared with me and Shared Drives stay available for browse/focus/search where supported, but they are not upload destinations unless a dedicated write policy and adapter explicitly allow that surface.
  • Domain-oriented live Mastra tools now route providers through one typed live-tool registry instead of provider-specific tool ids.
  • Provider-backed option sources now let shared connect/settings flows load hierarchical choices such as Jira siteId -> projects.

The customer CMS owns connector policy and curation metadata, such as enablement, featured/rank/highlight settings, and registry metadata used by runtime readiness checks. It is not the place to enter connector OAuth credentials. Connector isEnabled is the only customer-facing enablement switch. OAuth provider records are hidden seed-managed relationship metadata that bind connectors to auth provider ids; they are not shown as a separate policy surface. Provider-family lists, if shown in product UI, should be derived from child connector availability. Deployment API-key profiles are also managed in cms-customer, but only as non-secret metadata and operator actions. The customer-facing profile surface shows setup status, provider setup fields such as URL/database/allowlist where applicable, validation state, and actions to validate, rotate, or remove the credential. Removing a profile clears stored credential slots and setup metadata; existing company-managed connector rows remain visible in the web app but cannot be used until an operator validates a new credential. The CMS hides registry-owned Secret names, Secret keys, mount paths, store modes, Kubernetes details, and normalized internal endpoint fields from customer operators.

  1. Is the user signed in correctly through Better Auth?
  2. Is the correct connectorKey in play?
  3. Does the linked provider account exist for that user?
  4. Are the expected scopes present?
  5. Does the connector registry/runtime extension say the requested mode is actually implemented?
File Why you would open it
packages/domain/src/connectors/connectorRegistry.ts Connector keys, OAuth binding, scopes, supported modes, and resource defaults
apps/api/src/connectors/connectorRuntimeExtensions.ts API runtime behavior registry for indexed, live, resource-list, and smoke support
apps/auth/.env.example Connector/social OAuth credential env vars
apps/api/.env.example Connector runtime env vars and connector OAuth readiness env vars
apps/auth/src/auth.ts Better Auth provider wiring and built-in vs generic OAuth
apps/auth/src/genericOAuthProviders.ts Generic OAuth provider configuration and callback paths
apps/auth/src/oauthProviders.ts Connector OAuth provider definitions, including GitHub
apps/api/src/connectors/githubApiClient.ts GitHub REST client and read-only provider request path
apps/api/src/connectors/realProviderE2e.ts Real-provider harness env contract and per-provider checks
apps/api/scripts/runRealProviderE2e.ts Real-provider wrapper that applies shared vs override user fixture envs
packages/connector-credentials/src/index.ts Shared credential secret-store contract and local/mounted-file implementations
infra/helm/customer-cluster/ Customer-cluster Secret mount and CMS least-privilege RBAC templates
docs/features/connectors/spec.md Connector lifecycle, recovery, and product behavior
  • apps/auth/src/auth.ts
  • apps/auth/src/genericOAuthProviders.ts
  • apps/auth/src/oauthProviders.ts
  • apps/web/app/utils/providerAccountBinding.ts
  • packages/domain/src/connectors/connectorRegistry.ts
  • apps/api/src/connectors/githubApiClient.ts
  • apps/api/src/connectors/connectorRuntimeExtensions.ts
  • docs/features/connectors/spec.md