Optare v1.0 is now available. Get started →
Architecture
B2B SaaS

B2B SaaS Architecture

This guide explains how to design a B2B SaaS application using Optare for identity and access management.

Overview

A typical B2B SaaS application has three user types:

┌─────────────────────────────────────────────────────────────┐
│                     Your Application                         │
├─────────────────────────────────────────────────────────────┤
│                                                              │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐       │
│  │   Platform   │  │   Tenant     │  │   End User   │       │
│  │   Admin      │  │   Admin      │  │              │       │
│  ├──────────────┤  ├──────────────┤  ├──────────────┤       │
│  │ Manage all   │  │ Manage org   │  │ Use product  │       │
│  │ tenants      │  │ settings     │  │              │       │
│  └──────────────┘  └──────────────┘  └──────────────┘       │
│                                                              │
└─────────────────────────────────────────────────────────────┘

Architecture Layers

1. Authentication Layer (Optare)

Optare handles all authentication:

  • User login/signup
  • Session management
  • Token issuance
  • MFA/2FA
  • SSO integration

2. Authorization Layer (Your App + Optare RBAC)

Combine Optare's RBAC with your business logic:

// Optare provides role from token
const { role, organizationId } = req.user;
 
// Your app enforces business rules
if (role !== 'admin' && action === 'delete_project') {
  throw new UnauthorizedError();
}

3. Data Layer (Multi-Tenant)

Always scope data by organization:

// Every query includes organizationId
const projects = await db.projects.findMany({
  where: { organizationId: req.user.organizationId }
});

The Token-Based Architecture

┌─────────┐     ┌────────────┐     ┌─────────────┐
│ Browser │────►│  Optare ID │────►│   Tokens    │
└─────────┘     └────────────┘     └──────┬──────┘


┌─────────────────────────────────────────────────────────────┐
│                      Your API                                │
├─────────────────────────────────────────────────────────────┤
│  1. Validate Access Token                                    │
│  2. Extract user ID, org ID, role                           │
│  3. Scope queries by org                                    │
│  4. Check permissions                                        │
│  5. Return data                                              │
└─────────────────────────────────────────────────────────────┘

Database Schema Pattern

Organization Ownership

Every table should reference the organization:

CREATE TABLE projects (
  id UUID PRIMARY KEY,
  organization_id UUID NOT NULL REFERENCES organizations(id),
  name TEXT NOT NULL,
  created_at TIMESTAMP DEFAULT NOW()
);
 
-- Index for efficient org-scoped queries
CREATE INDEX idx_projects_org ON projects(organization_id);

The "Belongs To Org" Pattern

// Base query helper
function orgScoped(orgId: string) {
  return { organizationId: orgId };
}
 
// Usage
const projects = await db.projects.findMany({
  where: {
    ...orgScoped(req.user.organizationId),
    status: 'active'
  }
});

Authentication Flow

1. User Login

User → Your App → Optare ID → Authenticate → Tokens → Your App

2. API Request

Frontend → API Request + Access Token → Validate → Process → Response

3. Token Refresh

Access Token Expired → SDK uses Refresh Token → New Tokens → Continue

Common Patterns

Pattern 1: Single Tenant Per Session

User operates in one organization at a time:

// Token contains current org
{
  "sub": "user_123",
  "org_id": "org_456",
  "role": "admin"
}
 
// Switching orgs requires new token
await client.switchOrganization('org_789');

Pattern 2: Org-Aware Routes

Structure your API around organizations:

/api/orgs/:orgId/projects
/api/orgs/:orgId/members
/api/orgs/:orgId/settings
app.get('/api/orgs/:orgId/projects', async (req, res) => {
  // Verify user can access this org
  if (req.user.organizationId !== req.params.orgId) {
    return res.status(403).json({ error: 'Access denied' });
  }
  // ...
});

Pattern 3: Feature Flags per Org

Different orgs have different features:

const features = await client.license.getEntitlements();
 
// In component
{features.find(f => f.feature === 'sso')?.isEnabled && (
  <SSOSettings />
)}

Scaling Considerations

Multi-Region

                     ┌─────────────┐
          ┌────────►│  US Region  │
          │         └─────────────┘
┌─────────┴─┐
│  Optare   │       ┌─────────────┐
│  ID       │──────►│  EU Region  │
└─────────┬─┘       └─────────────┘
          │         ┌─────────────┐
          └────────►│ APAC Region │
                    └─────────────┘

Data Isolation Options

LevelHowUse Case
Row-Levelorganization_id columnMost SaaS apps
Schema-LevelSeparate Postgres schemasRegulated industries
Database-LevelSeparate databasesEnterprise contracts

Security Checklist

  • All database queries scoped by organizationId
  • Access tokens validated on every request
  • iss and aud claims verified
  • Token expiration checked
  • RBAC enforced for sensitive operations
  • Audit logs for compliance
  • Rate limiting per organization

Next Steps