Marketplace App Integration Guide ๐
How to Connect Your Apps to Optare SSO
This guide shows how to make your apps (Customer Service, Bot, CRM) work with Optare ID SSO.
๐ฏ Overview
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ OPTARE ID โ
โ โ
โ 1. Tenant installs app โ
โ โ โ
โ 2. Optare creates OAuth client โ
โ โ โ
โ 3. Sends credentials to YOUR APP via webhook โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ YOUR APP โ
โ โ
โ 4. Receives webhook, stores credentials โ
โ โ โ
โ 5. User clicks "Open App" in Optare โ
โ โ โ
โ 6. Redirected to YOUR APP /auth/callback โ
โ โ โ
โ 7. Exchange code for tokens โ
โ โ โ
โ 8. Get user info from Optare ID โ
โ โ โ
โ 9. Create session, user is logged in! โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ๐ Step-by-Step Integration
Step 1: Copy the SSO Client
Copy libs/apps/src/optare-sso-client.ts to your app.
Step 2: Set Up Environment Variables
# Your app's .env
OPTARE_ID_URL=https://id.optare.one
APP_URL=https://support.optare.oneStep 3: Create Webhook Handler
// /api/webhooks/optare.ts (or route equivalent)
import { OptareSSO } from './optare-sso-client';
import { db } from './database'; // Your app's database
const sso = new OptareSSO({
optareIdUrl: process.env.OPTARE_ID_URL!,
});
export async function POST(request: Request) {
const body = await request.json();
const { event, organizationId, clientId, clientSecret } = body;
if (event === 'app.installed') {
// Store credentials in YOUR database
await db.oauthCredentials.create({
data: {
organizationId,
clientId,
clientSecret, // Encrypt this!
createdAt: new Date(),
},
});
console.log(`โ
App installed for org: ${organizationId}`);
return Response.json({ received: true });
}
if (event === 'app.uninstalled') {
// Remove credentials
await db.oauthCredentials.delete({
where: { organizationId },
});
console.log(`๐๏ธ App uninstalled for org: ${organizationId}`);
return Response.json({ received: true });
}
return Response.json({ received: true });
}Step 4: Create OAuth Callback Handler
// /auth/callback.ts (or route equivalent)
import { OptareSSO } from './optare-sso-client';
import { db } from './database';
import { createSession } from './session'; // Your session management
const sso = new OptareSSO({
optareIdUrl: process.env.OPTARE_ID_URL!,
});
export async function GET(request: Request) {
const url = new URL(request.url);
const code = url.searchParams.get('code');
const state = url.searchParams.get('state'); // Contains orgId
const error = url.searchParams.get('error');
if (error) {
return Response.redirect('/login?error=' + error);
}
if (!code) {
return Response.redirect('/login?error=missing_code');
}
try {
// Get stored credentials for this organization
// The state parameter contains the organization ID
const orgId = decodeState(state); // Implement this
const credentials = await db.oauthCredentials.findUnique({
where: { organizationId: orgId },
});
if (!credentials) {
throw new Error('No credentials found');
}
// Exchange code for tokens
const tokens = await sso.exchangeCodeForTokens({
code,
clientId: credentials.clientId,
clientSecret: credentials.clientSecret,
redirectUri: `${process.env.APP_URL}/auth/callback`,
});
// Get user info from Optare ID
const userInfo = await sso.getUserInfo(tokens.access_token);
// Create or update user in YOUR database
const user = await db.users.upsert({
where: { optareUserId: userInfo.sub },
create: {
optareUserId: userInfo.sub,
email: userInfo.email,
name: userInfo.name,
organizationId: userInfo.organization_id,
role: userInfo.role,
},
update: {
email: userInfo.email,
name: userInfo.name,
role: userInfo.role,
},
});
// Create session in YOUR app
const session = await createSession({
userId: user.id,
accessToken: tokens.access_token,
refreshToken: tokens.refresh_token,
expiresAt: new Date(Date.now() + tokens.expires_in * 1000),
});
// Set session cookie and redirect
const response = Response.redirect('/dashboard');
response.headers.set('Set-Cookie', `session=${session.id}; HttpOnly; Secure; Path=/`);
return response;
} catch (err) {
console.error('OAuth callback error:', err);
return Response.redirect('/login?error=auth_failed');
}
}Step 5: Protect Your Routes
// middleware.ts (or equivalent)
import { getSession } from './session';
import { OptareSSO } from './optare-sso-client';
const sso = new OptareSSO({
optareIdUrl: process.env.OPTARE_ID_URL!,
});
export async function authMiddleware(request: Request) {
const session = await getSession(request);
if (!session) {
// No session - redirect to Optare ID login
// The user will be redirected back after login
return Response.redirect('/login');
}
// Check if token is expired
if (session.expiresAt < new Date()) {
// Refresh the token
try {
const credentials = await db.oauthCredentials.findUnique({
where: { organizationId: session.organizationId },
});
const newTokens = await sso.refreshToken({
refreshToken: session.refreshToken,
clientId: credentials.clientId,
clientSecret: credentials.clientSecret,
});
// Update session with new tokens
await updateSession(session.id, {
accessToken: newTokens.access_token,
refreshToken: newTokens.refresh_token,
expiresAt: new Date(Date.now() + newTokens.expires_in * 1000),
});
} catch (err) {
// Refresh failed - redirect to login
return Response.redirect('/login');
}
}
// Session is valid - continue
return null;
}๐ Database Schema for Your App
Add these tables to your app's database:
-- Store OAuth credentials from Optare ID
CREATE TABLE oauth_credentials (
id TEXT PRIMARY KEY,
organization_id TEXT UNIQUE NOT NULL,
client_id TEXT NOT NULL,
client_secret TEXT NOT NULL, -- Encrypt this!
created_at TIMESTAMP DEFAULT NOW()
);
-- Store users synced from Optare ID
CREATE TABLE users (
id TEXT PRIMARY KEY,
optare_user_id TEXT UNIQUE NOT NULL, -- User ID from Optare
email TEXT NOT NULL,
name TEXT,
organization_id TEXT NOT NULL,
role TEXT,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
-- Store sessions
CREATE TABLE sessions (
id TEXT PRIMARY KEY,
user_id TEXT REFERENCES users(id),
access_token TEXT NOT NULL,
refresh_token TEXT,
expires_at TIMESTAMP NOT NULL,
created_at TIMESTAMP DEFAULT NOW()
);๐ฏ Complete Flow Example
1. Tenant Installs Your App
Tenant in Optare ID โ Apps โ Install "Customer Service"
โ
Optare ID creates OAuth client
โ
Optare ID sends webhook to YOUR APP:
POST https://support.optare.one/api/webhooks/optare
{
"event": "app.installed",
"organizationId": "org_abc123",
"clientId": "app_xyz789",
"clientSecret": "secret_..."
}
โ
YOUR APP stores credentials in database2. User Opens Your App
User in Optare ID โ Apps โ "Open Customer Service"
โ
Optare ID generates SSO URL:
https://id.optare.one/oauth/authorize?
client_id=app_xyz789&
redirect_uri=https://support.optare.one/auth/callback&
response_type=code&
scope=openid profile email organization&
state=org_abc123
โ
User is already logged into Optare ID โ Auto-approve
โ
Redirect to YOUR APP:
https://support.optare.one/auth/callback?code=auth_code_here&state=org_abc123
โ
YOUR APP exchanges code for tokens
โ
YOUR APP gets user info from Optare ID
โ
YOUR APP creates session
โ
User is logged in! ๐๐งช Testing the Integration
1. Test Webhook Locally
# Use ngrok to expose your local app
ngrok http 3000
# Update the app's webhook URL in Optare ID to your ngrok URL
# Then install the app from Optare ID2. Test OAuth Flow
# 1. Install app in Optare ID
# 2. Check your app received the webhook
# 3. Click "Open App" in Optare ID
# 4. Verify you land in your app logged in3. Verify User Data
// In your app, check the user info received
console.log('User from Optare ID:', {
id: userInfo.sub,
email: userInfo.email,
name: userInfo.name,
organization: userInfo.organization_id,
role: userInfo.role,
});๐ง Troubleshooting
"No credentials found"
- Check webhook was received
- Verify organizationId matches
- Check database has the credentials
"Token exchange failed"
- Verify client_id and client_secret are correct
- Check redirect_uri matches exactly
- Ensure code hasn't expired (codes are single-use)
"User info failed"
- Check access_token is valid
- Verify token hasn't expired
- Check scopes include 'openid profile email'
๐ What Your App Gets from Optare ID
User Info (from /oauth/userinfo)
{
"sub": "user_abc123",
"email": "john@acme.com",
"name": "John Doe",
"picture": "https://...",
"email_verified": true,
"organization_id": "org_xyz789",
"organization_name": "Acme Corp",
"role": "admin"
}Access Token Claims
{
"sub": "user_abc123",
"aud": "app_xyz789",
"iss": "https://id.optare.one",
"exp": 1234567890,
"scope": "openid profile email organization"
}โ Checklist
For each of your apps (Customer Service, Bot, CRM):
- Copy
optare-sso-client.tsto your app - Create webhook handler at
/api/webhooks/optare - Create OAuth callback at
/auth/callback - Add database tables for credentials, users, sessions
- Protect routes with auth middleware
- Test installation webhook
- Test SSO login flow
- Test token refresh
๐ Result
Once integrated, your apps will:
- โ Receive installation notifications automatically
- โ Accept SSO logins from Optare ID
- โ Know which organization the user belongs to
- โ Know the user's role (admin, member, etc.)
- โ Automatically refresh tokens
- โ Work seamlessly with the Optare ecosystem
Your apps are now plug-and-play with Optare ID! ๐