Optare v1.0 is now available. Get started โ†’
Learn
Marketplace Integration

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.one

Step 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 database

2. 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 ID

2. 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 in

3. 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.ts to 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! ๐Ÿš€