Unclassified

Descope as Universal Connection Hub Architecture

Status: SUPERSEDED — both this file and the refined Descope snapshot are
historical; use docs/architecture/IMPLEMENTATION_CONTRACT_v1.1.md.
Date: July 17, 2026
Version: v1.0 (Foundation)
Canonical Path: docs/architecture/CONNECTION_ARCHITECTURE.md


Overview

Descope becomes the single source of truth for all inbound and outbound connections:

  • Inbound: Descope OIDC (customer login) ✅ Already implemented
  • Outbound: Descope outbound apps (Salesforce, GitHub, Google Sheets, HubSpot, etc.)
  • No token materialization in IntegrateWise: only authorizationRef stored
  • Canonical law: IW stores reference → Descope owns credentials → MCP executes

Architecture

Current State (Inbound - Already Done)

User Browser
    ↓ (login)
Descope OIDC
    ↓ (JWT)
Gateway (mapDescopePrincipal)
    ↓ (context envelope: tenant_id, user_id, roles, permissions)
AppShell (WorkspaceShell)

Desired State (All Connections Through Descope)

Inbound Connections (Auth)

User
    ↓ (login email/password or OAuth: Google, GitHub, Microsoft)
Descope OIDC / Descope Outbound App
    ↓ (JWT + session)
Gateway (mapDescopePrincipal)
    ↓ (Principal: tenant_id, user_id, roles, permissions)
AppShell (WorkspaceShell + context)

Outbound Connections (Integrations)

User clicks "Connect Salesforce" (in Integrations Hub)
    ↓
Gateway initiates OAuth to Descope Outbound App
    ↓
User authorizes in Descope (or already connected)
    ↓
Descope returns authorization → stores in Descope backend
    ↓
Frontend receives authorizationRef (e.g., `descope:salesforce:iw`)
    ↓ (stored in Spine as IntegrationConnection.authorizationRef)
IW stores reference only (no credentials)
    ↓
Later, when executing capability requiring Salesforce:
  - Capability Fabric selects: execute(authorizationRef: "descope:salesforce:iw")
  - MCP Descope Adapter: resolve(authorizationRef) → Descope API
  - Descope returns connection + fresh credentials (refreshed if needed)
  - Adapter executes with credentials, returns result
  - Credentials immediately discarded (no persistence in IW)

Components

1. Frontend: OAuth Callback Handler (Updated)

File: apps/web/src/components/connectivity/connectors/oauth-callback.tsx

Current flow:

?code=X&state=Y → connector.callback(provider, code, state)
              → Gateway POST /api/v1/connector/callback
              → IW stores token in D1

New flow:

?code=X&state=Y → connector.callback(provider, code, state)
              → Gateway POST /api/v1/descope/callback
              → Descope stores connection + credentials
              → Gateway returns authorizationRef (e.g., "descope:salesforce:iw")
              → Frontend stores authorizationRef in Spine as IntegrationConnection
              → NO tokens stored in IW ever

2. Gateway: Descope Callback Handler (New)

File: services/gateway/src/descope-callback.ts (to create)

export async function descopeCallback(req: Request, context: ContextEnvelope) {
  const { provider, code, state } = await req.json();

  // Validate state (CSRF protection)
  const storedState = await context.kv.get(`oauth_state_${state}`);
  if (!storedState) throw new Error("Invalid state");

  // Exchange code with Descope Outbound App for authorization
  const authResult = await descopeClient.authorizeOutboundApp({
    provider, // e.g., "salesforce"
    code, // OAuth code from provider
    state, // CSRF state
    projectId: context.env.DESCOPE_PROJECT_ID,
  });

  // Descope stores credentials and returns authorizationRef
  const authorizationRef = authResult.authorizationRef; // e.g., "descope:salesforce:iw"

  return json({
    success: true,
    authorizationRef,
    provider,
    connected_at: new Date().toISOString(),
  });
}

3. Frontend: Integration Manager (Updated)

File: apps/web/src/components/connectivity/integrations-hub.tsx

Change: When OAuth callback succeeds, store authorizationRef in Spine:

const IntegrationConnection = {
  id: crypto.randomUUID(),
  provider: "salesforce",
  authorizationRef: "descope:salesforce:iw", // ← from Descope
  status: "connected",
  lastSyncAt: new Date(),
  createdAt: new Date(),
  // Never store tokens here
};

await spine.integrations.create(IntegrationConnection);

4. Capability Execution (Already Wired)

File: services/mcp-connector/src/providers/descope/descope-connection-resolver.ts

When executing a capability:

// Capability Fabric selects route:
const route = capabilityFabric.select({
  capability: "export_to_salesforce",
  authorizationRef: "descope:salesforce:iw",
});

// Continuity Bridge routes to MCP:
await mcp.invoke({
  tool: "salesforce_export",
  authorizationRef: "descope:salesforce:iw",
  inputs: { leads: [...] },
});

// MCP Descope Adapter resolves:
const resolver = new DescopeConnectionResolver(config);
const connection = await resolver.resolve({
  authorizationRef: "descope:salesforce:iw",
  tenant_id: context.tenant_id,
  user_id: context.user_id,
});
// connection now has: provider, credentials, connection metadata
// Credentials are fresh (Descope handles token refresh)

// Execute with credentials:
const result = await salesforceProvider.execute({
  operation: "export",
  credentials: connection.credentials,
  data: { leads: [...] },
});

// Credentials are discarded after execution
return result;

Integration Points

Descope Outbound Apps (To Create)

In Descope console, create outbound connections for:

  • Salesforce (OAuth 2.0)
  • GitHub (OAuth 2.0)
  • Google Workspace (OAuth 2.0)
  • HubSpot (OAuth 2.0)
  • Slack (OAuth 2.0)
  • Stripe (API key)
  • Linear (API key)
  • Airtable (API key)
  • Notion (OAuth 2.0)
  • Zapier (OAuth 2.0)

Each outbound app maps to a provider in the authorizationRef:

  • descope:salesforce:iw
  • descope:github:iw
  • descope:google:iw
  • descope:hubspot:iw
  • descope:slack:iw
  • descope:stripe:iw
  • descope:linear:iw
  • descope:airtable:iw
  • descope:notion:iw
  • descope:zapier:iw

Spine Schema (To Update)

File: apps/web/src/data/knowledge.ts

Add IntegrationConnection model:

export interface IntegrationConnection {
  id: string;
  tenant_id: string;
  user_id: string;
  provider: string; // "salesforce", "github", etc.
  authorizationRef: string; // "descope:salesforce:iw"
  status: "connected" | "disconnected" | "error" | "refreshing";
  lastSyncAt: Date;
  lastError?: string;
  metadata?: Record<string, any>; // provider-specific metadata
  createdAt: Date;
  updatedAt: Date;
}

MCP Provider Integration

File: services/mcp-connector/src/providers/salesforce/index.ts (example)

Each provider receives credentials from Descope Adapter:

export async function execute(tool: SalesforceToolRequest, context: ExecutionContext) {
  const connection = await descopeResolver.resolve({
    authorizationRef: context.authorizationRef,
  });

  // connection.credentials now has:
  // - access_token (fresh, refreshed by Descope if needed)
  // - refresh_token
  // - token_type
  // - expires_at
  // - instance_url (Salesforce-specific)

  const client = new SalesforceClient({
    instanceUrl: connection.credentials.instance_url,
    accessToken: connection.credentials.access_token,
  });

  const result = await client[tool.operation](tool.inputs);

  // Credentials are NOT stored; they're discarded after execution
  return result;
}

Security Model

Token Storage Hierarchy

❌ NOT stored in IntegrateWise state (D1, Spine, cache, localStorage)
✅ Stored only in Descope backend (encrypted, managed)
✅ Retrieved on-demand when executing capability
✅ Credentials never materialized in MCP payload (Descope manages refresh)

Credential Lifecycle

1. User authorizes in Descope (OAuth consent screen)
2. Descope stores credentials + refresh tokens (encrypted)
3. IW stores only authorizationRef ("descope:salesforce:iw")
4. When executing:
   - MCP Adapter calls Descope: "Give me credentials for descope:salesforce:iw"
   - Descope returns fresh credentials (refreshes if expired)
   - Adapter uses credentials for execution
   - Credentials discarded immediately
5. Token refresh handled entirely by Descope

RBAC & Governance

User clicks "Connect Salesforce"
  ↓
Governance engine checks: does user have INTEGRATION_CONNECT authority?
  ↓
If yes: redirect to Descope OAuth → user authorizes
If no: show "You don't have permission to connect integrations"
  ↓
After authorization: Governance checks INTEGRATION_USE authority
  ↓
Only users with INTEGRATION_USE can trigger capabilities using this connection

Implementation Checklist

Phase 1: Foundation (Week 1)

  • Create Descope Outbound Apps (Salesforce, GitHub, Google, HubSpot)
  • Add DESCOPE_OUTBOUND_APP_ID env variable to services/gateway
  • Implement descopeCallback handler in services/gateway/src/descope-callback.ts
  • Update services/gateway/wrangler.toml with outbound app secret

Phase 2: Frontend Integration (Week 1)

  • Update oauth-callback.tsx to call /api/v1/descope/callback (not connector)
  • Remove token storage from OAuthCallbackPage
  • Update IntegrationsHub to store authorizationRef in Spine
  • Add IntegrationConnection model to knowledge.ts

Phase 3: Spine Schema (Week 1)

  • Create IntegrationConnection table in D1
  • Add migrations for existing integrations (migrate to authorizationRef)
  • Update Spine client to fetch IntegrationConnections

Phase 4: MCP Execution (Week 2)

  • Update DescopeConnectionResolver to handle all provider types
  • Add provider-specific handlers (Salesforce, GitHub, Google, etc.)
  • Update MCP tools to receive credentials from Descope Adapter
  • Remove credential storage from provider code

Phase 5: Testing & Monitoring (Week 2)

  • Add integration tests (connect → execute → disconnect)
  • Monitor credential refresh rates + expiry handling
  • Alert on credential resolution failures
  • Document troubleshooting guide

Testing Scenarios

Test 1: OAuth Connect Flow

1. User clicks "Connect Salesforce"
2. Redirected to Descope OAuth consent
3. User authorizes
4. Callback to /api/v1/descope/callback
5. authorizationRef returned: "descope:salesforce:iw"
6. Spine records IntegrationConnection.authorizationRef
7. Verify: NO tokens stored in D1, localStorage, or cache

Test 2: Capability Execution

1. User triggers "Export leads to Salesforce"
2. Capability Fabric selects route with authorizationRef
3. MCP Descope Adapter resolves authorizationRef → credentials
4. Provider executes with Salesforce client
5. Result returned to user
6. Verify: No credentials in logs, Spine records execution

Test 3: Token Refresh

1. Execute capability with expired token
2. Descope automatically refreshes token
3. Execution succeeds
4. Verify: No manual refresh logic needed in IW

Test 4: Credential Revocation

1. User disconnects Salesforce in Descope
2. MCP Adapter tries to resolve authorizationRef
3. Descope returns error: "Connection revoked"
4. IW updates IntegrationConnection.status = "disconnected"
5. User sees "Reconnect Salesforce" button

Benefits

✅ Zero credential leakage: No tokens in IW state ever
✅ Centralized token management: Descope handles refresh, expiry, rotation
✅ Audit trail: All connections managed in Descope console
✅ GDPR compliant: No PII credentials in application logs
✅ Scalable: Add new providers by creating Descope Outbound App
✅ Governance enforced: INTEGRATION_CONNECT and INTEGRATION_USE authorities
✅ Just-in-time execution: Credentials fetched only when needed
✅ Automatic token refresh: No background job needed


Timeline

  • Week 1: Foundation + Frontend + Spine = ✅ Ready to test
  • Week 2: MCP Execution + Testing = ✅ Ready for production
  • Total: 2 weeks to full implementation

References

  • Descope Outbound Connections: https://docs.descope.com/build/outbound-connections/
  • MCP Descope Adapter Tests: services/mcp-connector/src/providers/descope/descope-adapter.test.ts
  • Integration Manager: apps/web/src/components/connectivity/integrations-hub.tsx
  • Current OAuth Callback: apps/web/src/components/connectivity/connectors/oauth-callback.tsx