Provider Auth And Setup
Use this page when you need to understand how connector authorization is wired and where provider setup actually lives.
The Short Version
Section titled “The Short Version”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.
How Better Auth Fits In
Section titled “How Better Auth Fits In”- 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/oauth2and/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.
Provider Ids In Connector Auth
Section titled “Provider Ids In Connector Auth”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
atlassianfor 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-keyor the shared deployment API-key credential profile. - Jira can use Better Auth provider id
atlassianfor OAuth, a signed-in user’s own Jira Data Center PAT throughuser-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
atlassianfor Cloud OAuth, a signed-in user’s own Confluence Data Center PAT throughuser-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-ssoormicrosoft-sso - a connector-linked OAuth account such as
googleormicrosoft
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.
Callback URI Patterns
Section titled “Callback URI Patterns”Register callback URIs from the auth service origin that the provider will actually redirect to.
- Connector OAuth callbacks use the OAuth origin:
AUTH_BASE_URLby defaultAUTH_PUBLIC_BASE_URLonly for provider ids listed inAUTH_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
- local auth origin such as
Generic patterns
Section titled “Generic patterns”| 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 |
Practical examples
Section titled “Practical examples”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/googlehttp://localhost:3001/api/auth/oauth2/callback/google
- Microsoft connector OAuth:
https://your-tunnel.example/api/auth/oauth2/callback/microsofthttp://localhost:3001/api/auth/oauth2/callback/microsoft
- Jira connector OAuth:
https://your-tunnel.example/api/auth/oauth2/callback/atlassianhttp://localhost:3001/api/auth/oauth2/callback/atlassian
- GitHub connector OAuth:
https://your-tunnel.example/api/auth/oauth2/callback/githubhttp://localhost:3001/api/auth/oauth2/callback/github
- Workforce Microsoft SSO:
https://your-tunnel.example/api/auth/sso/callback/microsoft-ssohttp://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/callbackhttp://localhost:8080/api/v1/data/microsoft/callbackhttp://localhost:8080/api/v1/data/slack/callbackhttp://localhost:8080/api/v1/data/atlassian/callbackhttp://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.
Provider Onboarding Or Rotation Checklist
Section titled “Provider Onboarding Or Rotation Checklist”Use this checklist when adding or rotating built-in connector OAuth credentials:
- Create or update the provider app in the upstream provider console.
- Register the callback URI for the connector OAuth origin and generic OAuth path shown above.
- Confirm the required scopes in
packages/domain/src/connectors/connectorRegistry.tsand the provider page for the connector. - 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.
- If connector registry defaults changed, rerun the customer connector seed through the repo-owned seed path for that environment.
- Reconnect a test account so Better Auth stores a fresh linked connector OAuth account with the current scopes.
- 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
Section titled “User-Managed API-Key Connectors”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
userIdfrom 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
Section titled “Deployment API-Key Connectors”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.comand*.atlassian.netbefore 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.
Why Connector Auth Uses Generic OAuth
Section titled “Why Connector Auth Uses Generic OAuth”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, andgithub
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.
Planned Re-audit
Section titled “Planned Re-audit”- 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.
How SSO And Connector OAuth Work Together
Section titled “How SSO And Connector OAuth Work Together”- A person signs into the platform through workforce SSO or email/password.
- Better Auth creates or resumes the app user session.
- Later, that signed-in app user links a provider account for a connector.
- 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
googleconnector auth account as Gmail, Google Drive, and Google Contacts, withhttps://www.googleapis.com/auth/calendar.calendarlist.readonlyandhttps://www.googleapis.com/auth/calendar.events.readonlyadded 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
googleconnector auth account as Gmail and Google Drive, withhttps://www.googleapis.com/auth/contacts.readonlyadded 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
microsoftconnector auth account as Outlook, OneDrive, and Microsoft Contacts, with delegatedCalendars.Readadded to the linked account grant when needed. It is separate from the Outlook mail connector and does not requestMail.ReadorCalendars.Read.Sharedby itself. - Microsoft Contacts uses the same
microsoftconnector auth account as Outlook and OneDrive, with delegatedPeople.ReadandContacts.Readadded to the linked account grant when needed. It is separate from the Outlook connector and does not requireMail.Readby itself. - GitHub Repositories, GitHub Issues, and GitHub Pull Requests use the same Better Auth generic OAuth provider id
github, withrepo,read:user, anduser:emailrequested 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, wherescopeKeydefaults to empty and Jira usessiteId. - Catalog/config contracts expose registry-derived
connectionScopeso 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-derivedscopeKeyis 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-ssoandmicrosoft-ssomust 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
allowUnlinkingAllis 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.
Future Provider-Wide Scope Preparation
Section titled “Future Provider-Wide Scope Preparation”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 / Atlassian Setup Notes
Section titled “Jira / Atlassian Setup Notes”- 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
siteIdper 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’sscopeKey. - 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
siteIdper 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-pagedomain. 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 Setup Notes
Section titled “Odoo Setup Notes”- Odoo uses connector key
odoo, live domainbusiness-record, and canonical tool idbusiness-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.comdatabase fields are normally left blank so Odoo can route by URL/Host, and hosted values such asmycompany.odoo.comare normalized to the JSON-2 database tokenmycompanywhen 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 Setup Notes
Section titled “GitHub Setup Notes”- 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_IDandGITHUB_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, anduser:email. - The GitHub OAuth App
reposcope 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 throughwork-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.
Real-Provider Test Fixture Ownership
Section titled “Real-Provider Test Fixture Ownership”The real-provider connector smoke harness is testing app-user ownership, not just provider reachability.
REAL_PROVIDER_E2E_*_ACCOUNT_IDis the linked provider account id from Better Authaccount.accountId.- The harness resolves the owning Better Auth app user from that linked account id. Do not provide display emails or Better Auth
account.idrow ids. - Jira real-provider smoke also requires
REAL_PROVIDER_E2E_JIRA_SITE_ID.REAL_PROVIDER_E2E_JIRA_PROJECT_IDSis 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
accountIdplus a ConfluencesiteId; 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_URLandREAL_PROVIDER_E2E_ODOO_API_KEY.REAL_PROVIDER_E2E_ODOO_DATABASEis 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:doctorto preflight every configured fixture account before the heavier smoke suite. - Run
bun run test:api:connectors:real-provider:doctor gmailto inspect one provider. - If an account id is stale, the doctor prints safe candidate
accountIdvalues 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:allfor 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>, andREAL_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>, andREAL_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.
Where Provider Setup Lives
Section titled “Where Provider Setup Lives”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.exampleapps/api/.env.example
Connector runtime truth
Section titled “Connector runtime truth”Connector keys, OAuth bindings, default scopes, supported modes, resource defaults, and scoped-resource policy live in:
packages/domain/src/connectors/connectorRegistry.ts
Current DRY shape
Section titled “Current DRY shape”- 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, andconnectionScope; 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.
CMS-managed connector configuration
Section titled “CMS-managed connector configuration”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.
What To Check When A Provider Is Failing
Section titled “What To Check When A Provider Is Failing”- Is the user signed in correctly through Better Auth?
- Is the correct
connectorKeyin play? - Does the linked provider account exist for that user?
- Are the expected scopes present?
- Does the connector registry/runtime extension say the requested mode is actually implemented?
Provider Pages
Section titled “Provider Pages”Atlassian
Section titled “Atlassian”GitHub
Section titled “GitHub”Microsoft
Section titled “Microsoft”Read Next In Repo
Section titled “Read Next In Repo”| 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 |
Canonical Sources
Section titled “Canonical Sources”apps/auth/src/auth.tsapps/auth/src/genericOAuthProviders.tsapps/auth/src/oauthProviders.tsapps/web/app/utils/providerAccountBinding.tspackages/domain/src/connectors/connectorRegistry.tsapps/api/src/connectors/githubApiClient.tsapps/api/src/connectors/connectorRuntimeExtensions.tsdocs/features/connectors/spec.md
Official Docs
Section titled “Official Docs”- Better Auth generic OAuth plugin
- Better Auth social providers
- Better Auth GitHub provider
- Odoo JSON-2 external API
- Kubernetes Secrets
- Kubernetes Secrets good practices
- GitHub OAuth App scopes
- GitHub App installation authentication
- GitHub REST API versioning
- Google People API contacts guide
- Google Calendar API authorization
- Google Calendar API Events.list
- Microsoft Graph calendar overview
- Microsoft Graph calendarView
- Microsoft Graph Search events
- Microsoft Graph Search person concept
- Microsoft Graph list contacts
- Atlassian OAuth 2.0 (3LO) apps
- Atlassian Data Center personal access tokens
- Confluence Cloud Basic auth for REST APIs