Skip to content

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

LayerFileRole
DB schemasupabase/migrations/20280101000000_user_oauth_connections.sqlpublic.user_oauth_connections table
DB RPCssupabase/migrations/20280101000001_user_oauth_connections_rpcs.sqlList, upsert, revoke, resolve, refresh RPCs
Domain typeslibs/domain/oauth-connectionsProvider/capability contracts, ref parser
Resolverlibs/infra/execution/src/lib/oauth-connection-resolver.tsServer-side token resolution + inline token refresh
Compositelibs/infra/execution/src/lib/composite-connector-resolver.tsRoutes OAuth refs vs. platform slugs
Connect hooklibs/features/settings/src/lib/hooks/useOAuthConnections.tsCalls supabase.auth.signInWithOAuth (standard flow)
Callback pagelibs/features/settings/src/lib/pages/GoogleConnectionCallbackPage.tsxExtracts provider_token from Supabase session, stores via fn_oauth_connect_google RPC
Settings UIlibs/features/settings/src/lib/components/OAuthConnectionsSection.tsxConnect/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:

typescript
{
  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:

typescript
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:

typescript
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:

sql
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:

typescript
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:

typescript
import { slackProvider } from './providers/slack.provider'
registerOAuthProvider(slackProvider)

3. Extend domain types

typescript
// oauth-connection.types.ts
export type OAuthProvider = 'google' | 'slack'
export type SlackCapability = 'slack_send'
export type OAuthCapability = GoogleCapability | SlackCapability

4. Update the parser allowlists

typescript
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:

typescript
{
  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_connection is service_role-only — callable from the execution resolver (token refresh) and from fn_oauth_connect_google (initial connect).
  • fn_oauth_resolve_connection is service_role-only — only callable from execution workers.
  • nullOAuthConnectionResolver must be used in browser/dry-run context (always returns null).
  • Tokens must never appear in execution output_data, logs, or frontend state.