{"slug":"developers/DESCOPE_CONNECTION_HUB","title":"Descope as Universal Connection Hub Architecture","section":"developers","sectionLabel":"Developers","status":"unclassified","type":"document","path":"developers/DESCOPE_CONNECTION_HUB.mdx","bytes":12653,"description":null,"updated":null,"source":"integratewise/Integratewise-docs-wiki","content":"\n**Status**: SUPERSEDED — both this file and the refined Descope snapshot are\nhistorical; use `docs/architecture/IMPLEMENTATION_CONTRACT_v1.1.md`.\n**Date**: July 17, 2026  \n**Version**: v1.0 (Foundation)  \n**Canonical Path**: docs/architecture/CONNECTION_ARCHITECTURE.md\n\n---\n\n## Overview\n\nDescope becomes the **single source of truth** for all inbound and outbound connections:\n\n- **Inbound**: Descope OIDC (customer login) ✅ Already implemented\n- **Outbound**: Descope outbound apps (Salesforce, GitHub, Google Sheets, HubSpot, etc.)\n- **No token materialization** in IntegrateWise: only authorizationRef stored\n- **Canonical law**: IW stores reference → Descope owns credentials → MCP executes\n\n---\n\n## Architecture\n\n### Current State (Inbound - Already Done)\n\n```\nUser Browser\n    ↓ (login)\nDescope OIDC\n    ↓ (JWT)\nGateway (mapDescopePrincipal)\n    ↓ (context envelope: tenant_id, user_id, roles, permissions)\nAppShell (WorkspaceShell)\n```\n\n### Desired State (All Connections Through Descope)\n\n#### Inbound Connections (Auth)\n\n```\nUser\n    ↓ (login email/password or OAuth: Google, GitHub, Microsoft)\nDescope OIDC / Descope Outbound App\n    ↓ (JWT + session)\nGateway (mapDescopePrincipal)\n    ↓ (Principal: tenant_id, user_id, roles, permissions)\nAppShell (WorkspaceShell + context)\n```\n\n#### Outbound Connections (Integrations)\n\n```\nUser clicks \"Connect Salesforce\" (in Integrations Hub)\n    ↓\nGateway initiates OAuth to Descope Outbound App\n    ↓\nUser authorizes in Descope (or already connected)\n    ↓\nDescope returns authorization → stores in Descope backend\n    ↓\nFrontend receives authorizationRef (e.g., `descope:salesforce:iw`)\n    ↓ (stored in Spine as IntegrationConnection.authorizationRef)\nIW stores reference only (no credentials)\n    ↓\nLater, when executing capability requiring Salesforce:\n  - Capability Fabric selects: execute(authorizationRef: \"descope:salesforce:iw\")\n  - MCP Descope Adapter: resolve(authorizationRef) → Descope API\n  - Descope returns connection + fresh credentials (refreshed if needed)\n  - Adapter executes with credentials, returns result\n  - Credentials immediately discarded (no persistence in IW)\n```\n\n---\n\n## Components\n\n### 1. Frontend: OAuth Callback Handler (Updated)\n\n**File**: `apps/web/src/components/connectivity/connectors/oauth-callback.tsx`\n\n**Current flow**:\n\n```\n?code=X&state=Y → connector.callback(provider, code, state)\n              → Gateway POST /api/v1/connector/callback\n              → IW stores token in D1\n```\n\n**New flow**:\n\n```\n?code=X&state=Y → connector.callback(provider, code, state)\n              → Gateway POST /api/v1/descope/callback\n              → Descope stores connection + credentials\n              → Gateway returns authorizationRef (e.g., \"descope:salesforce:iw\")\n              → Frontend stores authorizationRef in Spine as IntegrationConnection\n              → NO tokens stored in IW ever\n```\n\n### 2. Gateway: Descope Callback Handler (New)\n\n**File**: `services/gateway/src/descope-callback.ts` (to create)\n\n```typescript\nexport async function descopeCallback(req: Request, context: ContextEnvelope) {\n  const { provider, code, state } = await req.json();\n\n  // Validate state (CSRF protection)\n  const storedState = await context.kv.get(`oauth_state_${state}`);\n  if (!storedState) throw new Error(\"Invalid state\");\n\n  // Exchange code with Descope Outbound App for authorization\n  const authResult = await descopeClient.authorizeOutboundApp({\n    provider, // e.g., \"salesforce\"\n    code, // OAuth code from provider\n    state, // CSRF state\n    projectId: context.env.DESCOPE_PROJECT_ID,\n  });\n\n  // Descope stores credentials and returns authorizationRef\n  const authorizationRef = authResult.authorizationRef; // e.g., \"descope:salesforce:iw\"\n\n  return json({\n    success: true,\n    authorizationRef,\n    provider,\n    connected_at: new Date().toISOString(),\n  });\n}\n```\n\n### 3. Frontend: Integration Manager (Updated)\n\n**File**: `apps/web/src/components/connectivity/integrations-hub.tsx`\n\n**Change**: When OAuth callback succeeds, store authorizationRef in Spine:\n\n```typescript\nconst IntegrationConnection = {\n  id: crypto.randomUUID(),\n  provider: \"salesforce\",\n  authorizationRef: \"descope:salesforce:iw\", // ← from Descope\n  status: \"connected\",\n  lastSyncAt: new Date(),\n  createdAt: new Date(),\n  // Never store tokens here\n};\n\nawait spine.integrations.create(IntegrationConnection);\n```\n\n### 4. Capability Execution (Already Wired)\n\n**File**: `services/mcp-connector/src/providers/descope/descope-connection-resolver.ts`\n\nWhen executing a capability:\n\n```typescript\n// Capability Fabric selects route:\nconst route = capabilityFabric.select({\n  capability: \"export_to_salesforce\",\n  authorizationRef: \"descope:salesforce:iw\",\n});\n\n// Continuity Bridge routes to MCP:\nawait mcp.invoke({\n  tool: \"salesforce_export\",\n  authorizationRef: \"descope:salesforce:iw\",\n  inputs: { leads: [...] },\n});\n\n// MCP Descope Adapter resolves:\nconst resolver = new DescopeConnectionResolver(config);\nconst connection = await resolver.resolve({\n  authorizationRef: \"descope:salesforce:iw\",\n  tenant_id: context.tenant_id,\n  user_id: context.user_id,\n});\n// connection now has: provider, credentials, connection metadata\n// Credentials are fresh (Descope handles token refresh)\n\n// Execute with credentials:\nconst result = await salesforceProvider.execute({\n  operation: \"export\",\n  credentials: connection.credentials,\n  data: { leads: [...] },\n});\n\n// Credentials are discarded after execution\nreturn result;\n```\n\n---\n\n## Integration Points\n\n### Descope Outbound Apps (To Create)\n\nIn Descope console, create outbound connections for:\n\n- Salesforce (OAuth 2.0)\n- GitHub (OAuth 2.0)\n- Google Workspace (OAuth 2.0)\n- HubSpot (OAuth 2.0)\n- Slack (OAuth 2.0)\n- Stripe (API key)\n- Linear (API key)\n- Airtable (API key)\n- Notion (OAuth 2.0)\n- Zapier (OAuth 2.0)\n\nEach outbound app maps to a provider in the authorizationRef:\n\n- `descope:salesforce:iw`\n- `descope:github:iw`\n- `descope:google:iw`\n- `descope:hubspot:iw`\n- `descope:slack:iw`\n- `descope:stripe:iw`\n- `descope:linear:iw`\n- `descope:airtable:iw`\n- `descope:notion:iw`\n- `descope:zapier:iw`\n\n### Spine Schema (To Update)\n\n**File**: `apps/web/src/data/knowledge.ts`\n\nAdd IntegrationConnection model:\n\n```typescript\nexport interface IntegrationConnection {\n  id: string;\n  tenant_id: string;\n  user_id: string;\n  provider: string; // \"salesforce\", \"github\", etc.\n  authorizationRef: string; // \"descope:salesforce:iw\"\n  status: \"connected\" | \"disconnected\" | \"error\" | \"refreshing\";\n  lastSyncAt: Date;\n  lastError?: string;\n  metadata?: Record<string, any>; // provider-specific metadata\n  createdAt: Date;\n  updatedAt: Date;\n}\n```\n\n### MCP Provider Integration\n\n**File**: `services/mcp-connector/src/providers/salesforce/index.ts` (example)\n\nEach provider receives credentials from Descope Adapter:\n\n```typescript\nexport async function execute(tool: SalesforceToolRequest, context: ExecutionContext) {\n  const connection = await descopeResolver.resolve({\n    authorizationRef: context.authorizationRef,\n  });\n\n  // connection.credentials now has:\n  // - access_token (fresh, refreshed by Descope if needed)\n  // - refresh_token\n  // - token_type\n  // - expires_at\n  // - instance_url (Salesforce-specific)\n\n  const client = new SalesforceClient({\n    instanceUrl: connection.credentials.instance_url,\n    accessToken: connection.credentials.access_token,\n  });\n\n  const result = await client[tool.operation](tool.inputs);\n\n  // Credentials are NOT stored; they're discarded after execution\n  return result;\n}\n```\n\n---\n\n## Security Model\n\n### Token Storage Hierarchy\n\n```\n❌ NOT stored in IntegrateWise state (D1, Spine, cache, localStorage)\n✅ Stored only in Descope backend (encrypted, managed)\n✅ Retrieved on-demand when executing capability\n✅ Credentials never materialized in MCP payload (Descope manages refresh)\n```\n\n### Credential Lifecycle\n\n```\n1. User authorizes in Descope (OAuth consent screen)\n2. Descope stores credentials + refresh tokens (encrypted)\n3. IW stores only authorizationRef (\"descope:salesforce:iw\")\n4. When executing:\n   - MCP Adapter calls Descope: \"Give me credentials for descope:salesforce:iw\"\n   - Descope returns fresh credentials (refreshes if expired)\n   - Adapter uses credentials for execution\n   - Credentials discarded immediately\n5. Token refresh handled entirely by Descope\n```\n\n### RBAC & Governance\n\n```\nUser clicks \"Connect Salesforce\"\n  ↓\nGovernance engine checks: does user have INTEGRATION_CONNECT authority?\n  ↓\nIf yes: redirect to Descope OAuth → user authorizes\nIf no: show \"You don't have permission to connect integrations\"\n  ↓\nAfter authorization: Governance checks INTEGRATION_USE authority\n  ↓\nOnly users with INTEGRATION_USE can trigger capabilities using this connection\n```\n\n---\n\n## Implementation Checklist\n\n### Phase 1: Foundation (Week 1)\n\n- [ ] Create Descope Outbound Apps (Salesforce, GitHub, Google, HubSpot)\n- [ ] Add DESCOPE_OUTBOUND_APP_ID env variable to services/gateway\n- [ ] Implement descopeCallback handler in services/gateway/src/descope-callback.ts\n- [ ] Update services/gateway/wrangler.toml with outbound app secret\n\n### Phase 2: Frontend Integration (Week 1)\n\n- [ ] Update oauth-callback.tsx to call /api/v1/descope/callback (not connector)\n- [ ] Remove token storage from OAuthCallbackPage\n- [ ] Update IntegrationsHub to store authorizationRef in Spine\n- [ ] Add IntegrationConnection model to knowledge.ts\n\n### Phase 3: Spine Schema (Week 1)\n\n- [ ] Create IntegrationConnection table in D1\n- [ ] Add migrations for existing integrations (migrate to authorizationRef)\n- [ ] Update Spine client to fetch IntegrationConnections\n\n### Phase 4: MCP Execution (Week 2)\n\n- [ ] Update DescopeConnectionResolver to handle all provider types\n- [ ] Add provider-specific handlers (Salesforce, GitHub, Google, etc.)\n- [ ] Update MCP tools to receive credentials from Descope Adapter\n- [ ] Remove credential storage from provider code\n\n### Phase 5: Testing & Monitoring (Week 2)\n\n- [ ] Add integration tests (connect → execute → disconnect)\n- [ ] Monitor credential refresh rates + expiry handling\n- [ ] Alert on credential resolution failures\n- [ ] Document troubleshooting guide\n\n---\n\n## Testing Scenarios\n\n### Test 1: OAuth Connect Flow\n\n```\n1. User clicks \"Connect Salesforce\"\n2. Redirected to Descope OAuth consent\n3. User authorizes\n4. Callback to /api/v1/descope/callback\n5. authorizationRef returned: \"descope:salesforce:iw\"\n6. Spine records IntegrationConnection.authorizationRef\n7. Verify: NO tokens stored in D1, localStorage, or cache\n```\n\n### Test 2: Capability Execution\n\n```\n1. User triggers \"Export leads to Salesforce\"\n2. Capability Fabric selects route with authorizationRef\n3. MCP Descope Adapter resolves authorizationRef → credentials\n4. Provider executes with Salesforce client\n5. Result returned to user\n6. Verify: No credentials in logs, Spine records execution\n```\n\n### Test 3: Token Refresh\n\n```\n1. Execute capability with expired token\n2. Descope automatically refreshes token\n3. Execution succeeds\n4. Verify: No manual refresh logic needed in IW\n```\n\n### Test 4: Credential Revocation\n\n```\n1. User disconnects Salesforce in Descope\n2. MCP Adapter tries to resolve authorizationRef\n3. Descope returns error: \"Connection revoked\"\n4. IW updates IntegrationConnection.status = \"disconnected\"\n5. User sees \"Reconnect Salesforce\" button\n```\n\n---\n\n## Benefits\n\n✅ **Zero credential leakage**: No tokens in IW state ever  \n✅ **Centralized token management**: Descope handles refresh, expiry, rotation  \n✅ **Audit trail**: All connections managed in Descope console  \n✅ **GDPR compliant**: No PII credentials in application logs  \n✅ **Scalable**: Add new providers by creating Descope Outbound App  \n✅ **Governance enforced**: INTEGRATION_CONNECT and INTEGRATION_USE authorities  \n✅ **Just-in-time execution**: Credentials fetched only when needed  \n✅ **Automatic token refresh**: No background job needed\n\n---\n\n## Timeline\n\n- **Week 1**: Foundation + Frontend + Spine = ✅ Ready to test\n- **Week 2**: MCP Execution + Testing = ✅ Ready for production\n- **Total**: 2 weeks to full implementation\n\n---\n\n## References\n\n- Descope Outbound Connections: https://docs.descope.com/build/outbound-connections/\n- MCP Descope Adapter Tests: `services/mcp-connector/src/providers/descope/descope-adapter.test.ts`\n- Integration Manager: `apps/web/src/components/connectivity/integrations-hub.tsx`\n- Current OAuth Callback: `apps/web/src/components/connectivity/connectors/oauth-callback.tsx`\n"}