Adding OAuth Providers
This guide explains how to extend the shared OAuth connector system with a new provider (e.g. GitHub, Slack, Notion) or a new capability for an existing provider.
What are OAuth connectors?
OAuth connectors allow users to connect external accounts once and reference those connections anywhere across Workflows, Agents, Battles, and Lenses using the stable expression syntax:
[[:connector:google.gmail.primary]]This is distinct from platform API connectors (connectors.connectors), which are external systems calling LenserFight via SHA-256 tokens. OAuth connectors are the inverse: LenserFight calling external services on behalf of users.
Architecture overview
| Layer | File | Role |
|---|---|---|
| DB schema | supabase/migrations/20280101000000_user_oauth_connections.sql | public.user_oauth_connections table |
| DB RPCs | supabase/migrations/20280101000001_user_oauth_connections_rpcs.sql | List, upsert, revoke, resolve, refresh RPCs |
| Domain types | libs/domain/oauth-connections | Provider/capability contracts, ref parser |
| Resolver | libs/infra/execution/src/lib/oauth-connection-resolver.ts | Server-side token resolution + inline token refresh |
| Composite | libs/infra/execution/src/lib/composite-connector-resolver.ts | Routes OAuth refs vs. platform slugs |
| Connect hook | libs/features/settings/src/lib/hooks/useOAuthConnections.ts | Calls supabase.auth.signInWithOAuth (standard flow) |
| Callback page | libs/features/settings/src/lib/pages/GoogleConnectionCallbackPage.tsx | Extracts provider_token from Supabase session, stores via fn_oauth_connect_google RPC |
| Settings UI | libs/features/settings/src/lib/components/OAuthConnectionsSection.tsx | Connect/revoke UI |
Adding a new capability to an existing provider
Example: adding gmail.compose as a narrower alternative to gmail.
1. Update google.provider.ts
In libs/domain/oauth-connections/src/lib/providers/google.provider.ts, add to GOOGLE_CAPABILITIES:
{
capability: 'gmail_compose', // new capability key
provider: 'google',
requiredScopes: ['https://www.googleapis.com/auth/gmail.compose'],
displayName: 'Gmail Compose',
description: 'Compose and send Gmail messages (without read access)',
supportedOperations: ['send'],
},2. Extend the OAuthCapability type
In libs/domain/oauth-connections/src/lib/oauth-connection.types.ts:
export type GoogleCapability = 'gmail' | 'drive' | 'sheets' | 'docs' | 'calendar' | 'gmail_compose'3. Update the parser allowlist
In libs/domain/oauth-connections/src/lib/connector-ref.parser.ts, add to VALID_CAPABILITIES:
const VALID_CAPABILITIES = new Set<OAuthCapability>([
'gmail', 'drive', 'sheets', 'docs', 'calendar', 'gmail_compose',
])4. Update the DB CHECK constraint
Add a migration that extends the uoc_capability_check constraint:
ALTER TABLE public.user_oauth_connections
DROP CONSTRAINT uoc_capability_check,
ADD CONSTRAINT uoc_capability_check CHECK (
capability IN ('gmail', 'drive', 'sheets', 'docs', 'calendar', 'gmail_compose')
);Adding a new provider
Example: adding Slack.
1. Create a provider definition
Create libs/domain/oauth-connections/src/lib/providers/slack.provider.ts:
import type { OAuthProviderDefinition } from '../oauth-connection.types'
export const SLACK_CAPABILITIES = [
{
capability: 'slack_send' as const,
provider: 'slack' as const,
requiredScopes: ['chat:write'],
displayName: 'Slack Send',
description: 'Post messages to Slack channels',
supportedOperations: ['send'],
},
]
export const slackProvider: OAuthProviderDefinition = {
provider: 'slack',
displayName: 'Slack',
capabilities: SLACK_CAPABILITIES,
buildAuthUrl(capability, redirectUri, state) {
// Build Slack OAuth 2.0 URL
const params = new URLSearchParams({
client_id: process.env['SLACK_CLIENT_ID'] ?? '',
scope: 'chat:write',
redirect_uri: redirectUri,
state,
})
return `https://slack.com/oauth/v2/authorize?${params}`
},
}2. Register the provider
In libs/domain/oauth-connections/src/lib/provider.registry.ts:
import { slackProvider } from './providers/slack.provider'
registerOAuthProvider(slackProvider)3. Extend domain types
// oauth-connection.types.ts
export type OAuthProvider = 'google' | 'slack'
export type SlackCapability = 'slack_send'
export type OAuthCapability = GoogleCapability | SlackCapability4. Update the parser allowlists
const VALID_PROVIDERS = new Set<OAuthProvider>(['google', 'slack'])
const VALID_CAPABILITIES = new Set<OAuthCapability>([..., 'slack_send'])5. Update DB constraints
Add a migration extending uoc_provider_check and uoc_capability_check.
6. Add a callback page
Copy libs/features/settings/src/lib/pages/GoogleConnectionCallbackPage.tsx as a template. The page extracts provider_token and provider_refresh_token from the Supabase session after signInWithOAuth redirects back, then stores them via a SECURITY DEFINER RPC (equivalent to fn_oauth_connect_google).
For token refresh, extend triggerTokenRefresh() in libs/infra/execution/src/lib/oauth-connection-resolver.ts to handle the new provider's token endpoint alongside the existing Google case.
7. Add a connector ref field to relevant workflow nodes
In the relevant runner's descriptor file (e.g. cq-integration.descriptors.ts), add:
{
key: 'connectorRef',
label: 'Slack Account',
type: 'connector_ref',
required: true,
connectorProvider: 'slack',
connectorCapability: 'slack_send',
}8. Wire the runner
Follow the pattern in integration.runner.ts — read connectorRef from nodeConfig, call ctx.resolveConnector(connectorRef, requiredScopes), and include the token in the worker request envelope.
pgTAP checklist for new providers
When adding a new provider, add assertions to supabase/tests/96_user_oauth_connections.sql:
- Provider name accepted by
uoc_provider_check - Capability name accepted by
uoc_capability_check - New callback page and RPC wrapper exist for the provider
Token security requirements
- Access and refresh tokens are stored exclusively in Supabase Vault (
vault.secrets). Never store them in table columns. fn_oauth_upsert_connectionisservice_role-only — callable from the execution resolver (token refresh) and fromfn_oauth_connect_google(initial connect).fn_oauth_resolve_connectionisservice_role-only — only callable from execution workers.nullOAuthConnectionResolvermust be used in browser/dry-run context (always returns null).- Tokens must never appear in execution
output_data, logs, or frontend state.