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.