Building Multi-Tenant SaaS Applications with PersonQL


Building a secure, scalable multi-tenant SaaS application is complex. You need to handle organizations, user roles, permissions, data isolation, and organization switching - all while maintaining security and performance. PersonQL provides all of this out of the box.

The Challenge of Multi-Tenancy

Traditional authentication systems force you to build multi-tenant architecture from scratch:

  • Organization Management: Creating and managing organizations (tenants)
  • Role-Based Access Control: Defining roles and permissions per organization
  • Data Isolation: Ensuring users only access their organization’s data
  • Organization Switching: Allowing users to switch between multiple organizations
  • Permission Checks: Validating permissions before every operation
  • Audit Logging: Tracking who did what in which organization

Building this infrastructure takes weeks or months, and getting it wrong creates security vulnerabilities.

PersonQL’s Multi-Tenant Architecture

PersonQL includes comprehensive multi-tenant functionality across all SDKs:

Organization Management

Create and manage organizations with a simple API:

// Create a new organization
const org = await personql.organizations.create({
  name: "Acme Corp",
  slug: "acme-corp",
  metadata: {
    industry: "Technology",
    size: "50-100"
  }
});

// Add members to the organization
await personql.organizations.addMember(org.id, {
  userId: user.id,
  role: "admin"
});

Role-Based Access Control (RBAC)

Define custom roles and permissions for your application:

// Create custom roles
await personql.roles.create({
  organizationId: org.id,
  name: "Project Manager",
  permissions: [
    "projects.read",
    "projects.create",
    "projects.update",
    "tasks.manage"
  ]
});

// Assign roles to users
await personql.organizations.assignRole(org.id, user.id, "Project Manager");

// Check permissions before operations
const canEdit = await personql.permissions.check(
  user.id,
  org.id,
  "projects.update"
);

SDK Integration Examples

React SDK

The React SDK provides hooks for seamless multi-tenant integration:

import { useOrganization, useAuth } from '@personql/react';

function Dashboard() {
  const { user } = useAuth();
  const { organization, switchOrganization, userRole } = useOrganization();

  if (!user) return <SignIn />;

  return (
    <div>
      <h1>Welcome to {organization?.name}</h1>
      <p>Your role: {userRole}</p>

      <OrganizationSwitcher
        onSwitch={switchOrganization}
        organizations={user.organizations}
      />
    </div>
  );
}

Next.js Middleware

The Next.js SDK provides server-side organization context:

import { withOrganization } from '@personql/nextjs';

export default withOrganization((req) => {
  // Extracts organization from:
  // - Request headers (X-Organization-Id)
  // - Cookies (organization_id)
  // - Query params (?org=acme-corp)
  return req.organization;
});

// In your API routes
export async function GET(req: Request) {
  const orgContext = await getServerOrganizationContext(req);

  if (!orgContext.organization) {
    return Response.json({ error: "No organization" }, { status: 400 });
  }

  // Fetch data scoped to this organization
  const data = await db.query(
    `SELECT * FROM projects WHERE organization_id = ?`,
    [orgContext.organization.id]
  );

  return Response.json(data);
}

React Native SDK

Mobile apps get organization management with native storage:

import { useOrganization } from '@personql/react-native';
import AsyncStorage from '@react-native-async-storage/async-storage';

function OrganizationSelector() {
  const { organization, organizations, switchOrganization } = useOrganization();

  const handleSwitch = async (orgId: string) => {
    await switchOrganization(orgId);
    // Organization context persisted to AsyncStorage automatically
  };

  return (
    <ScrollView>
      {organizations.map((org) => (
        <TouchableOpacity
          key={org.id}
          onPress={() => handleSwitch(org.id)}
        >
          <Text>{org.name}</Text>
          {organization?.id === org.id && <Text>✓ Active</Text>}
        </TouchableOpacity>
      ))}
    </ScrollView>
  );
}

Data Isolation Patterns

Database-Level Isolation

Always scope queries to the current organization:

// ❌ Bad - No organization filter
const projects = await db.projects.findMany();

// ✅ Good - Scoped to organization
const projects = await db.projects.findMany({
  where: {
    organizationId: currentOrg.id
  }
});

API-Level Isolation

Use middleware to automatically inject organization context:

// Express.js example with Node SDK
import { organizationMiddleware } from '@personql/node';

app.use('/api', organizationMiddleware({
  // Extracts organization from header, cookie, or query
  headerName: 'X-Organization-Id',
  cookieName: 'organization_id',
}));

app.get('/api/projects', async (req, res) => {
  // req.organization is automatically populated
  const projects = await getProjects(req.organization.id);
  res.json(projects);
});

Permission Enforcement

UI-Level Permissions

Hide or disable features based on permissions:

import { usePermissions } from '@personql/react';

function ProjectActions({ project }) {
  const { hasPermission } = usePermissions();

  const canEdit = hasPermission('projects.update');
  const canDelete = hasPermission('projects.delete');

  return (
    <div>
      {canEdit && (
        <button onClick={() => editProject(project)}>
          Edit
        </button>
      )}
      {canDelete && (
        <button onClick={() => deleteProject(project)}>
          Delete
        </button>
      )}
    </div>
  );
}

API-Level Permissions

Enforce permissions before operations:

app.delete('/api/projects/:id', async (req, res) => {
  const canDelete = await personql.permissions.check(
    req.user.id,
    req.organization.id,
    'projects.delete'
  );

  if (!canDelete) {
    return res.status(403).json({ error: 'Forbidden' });
  }

  await deleteProject(req.params.id, req.organization.id);
  res.json({ success: true });
});

Organization Switching

PersonQL makes it simple for users to switch between organizations:

// Frontend - React
const { organizations, switchOrganization } = useOrganization();

// Switch to different organization
await switchOrganization('org_abc123');

// The SDK handles:
// - Updating local state
// - Setting cookies/headers for API calls
// - Persisting selection (AsyncStorage on mobile)
// - Refetching data with new context

Audit Logging

Track all organization activities automatically:

// PersonQL automatically logs:
// - User authentication events
// - Organization creation/updates
// - Role assignments
// - Permission checks
// - Member additions/removals

// Query audit logs
const logs = await personql.audit.query({
  organizationId: org.id,
  startDate: '2025-10-01',
  endDate: '2025-10-08',
  actions: ['organization.member.added', 'role.assigned']
});

logs.forEach(log => {
  console.log(`${log.actor.email} performed ${log.action} at ${log.timestamp}`);
});

Best Practices

1. Always Validate Organization Context

// Before every operation
if (!req.organization) {
  throw new Error('No organization context');
}

2. Use Consistent Organization Identifiers

// Use the same organization ID throughout your app
const orgId = req.organization.id; // Not slug or name

3. Implement Permission Checks at Multiple Layers

  • UI layer: Hide unavailable actions
  • API layer: Enforce permissions before operations
  • Database layer: Use row-level security when possible

4. Test Multi-Tenant Isolation

// Test that users can't access other organizations' data
test('users cannot access other organizations', async () => {
  const org1 = await createOrg('Org 1');
  const org2 = await createOrg('Org 2');

  const project = await createProject(org1.id, 'Secret Project');

  // Try to access org1's project from org2 context
  await expect(
    getProject(project.id, org2.id)
  ).rejects.toThrow('Not found');
});

Pricing Considerations

Multi-tenant features are available on all PersonQL plans:

  • Starter (Free): Up to 3 organizations per user
  • Professional: Up to 10 organizations per user
  • Enterprise: Unlimited organizations with advanced features

Get Started

Add multi-tenant support to your SaaS app in minutes:

# Install your preferred SDK
npm install @personql/react      # For React apps
npm install @personql/nextjs     # For Next.js apps
npm install @personql/node       # For backend APIs
npm install @personql/react-native  # For mobile apps

# Initialize PersonQL
import { PersonQLProvider } from '@personql/react';

function App() {
  return (
    <PersonQLProvider apiKey="your-api-key">
      <YourApp />
    </PersonQLProvider>
  );
}

Organizations, roles, and permissions are automatically available through our hooks and middleware.

View complete documentation →