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:iwdescope:github:iwdescope:google:iwdescope:hubspot:iwdescope:slack:iwdescope:stripe:iwdescope:linear:iwdescope:airtable:iwdescope:notion:iwdescope: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