Data Isolation and Tenant Security
How the SCORM API ensures complete data isolation between tenants.
Table of Contents
- Overview
- Tenant Isolation Mechanisms
- Row Level Security (RLS)
- API-Level Isolation
- Storage Isolation
- Best Practices
Overview
The SCORM API implements strict multi-tenant data isolation to ensure:
- Complete data separation between tenants
- No cross-tenant data access
- Automatic tenant filtering
- Security at multiple layers
Tenant Isolation Mechanisms
Layer 1: Authentication
API Key Authentication:
- Each API key is associated with exactly one tenant
- Tenant is resolved from API key record
- All requests automatically scoped to tenant
Clerk Authentication:
- User's tenant resolved from
user_tenantstable - Tenant cannot be overridden in requests
- Automatic filtering by user's tenant
Layer 2: Application Logic
Automatic Filtering:
// All queries automatically filter by tenant
const { data: packages } = await supabaseAdmin
.from(TABLES.PACKAGES)
.select('*')
.eq('tenant_id', tenantId); // Always included
Request Validation:
// Tenant ID in request body is ignored (security)
// Tenant always comes from authentication
const { tenantId } = await requireCustomerAuth();
// Request body tenant_id is ignored
Layer 3: Database (RLS)
Row Level Security Policies:
- Database-level enforcement
- Prevents direct database access
- Works even if application logic fails
Row Level Security (RLS)
How RLS Works
RLS policies automatically filter database queries:
-- Example RLS policy
CREATE POLICY "tenant_isolation" ON scorm_packages
FOR ALL
USING (tenant_id = current_setting('app.tenant_id')::uuid);
RLS Policies
Packages Table:
- Users can only see packages from their tenant
- INSERT requires matching tenant_id
- UPDATE/DELETE only on own tenant's packages
Sessions Table:
- Sessions filtered by tenant
- Cross-tenant session access prevented
- Automatic tenant assignment on creation
Events Table:
- Events filtered by tenant
- No cross-tenant event visibility
- Tenant set automatically
Testing RLS
-- Set tenant context
SET LOCAL app.tenant_id = '550e8400-e29b-41d4-a716-446655440000';
-- Query - only returns tenant's data
SELECT * FROM scorm_packages;
-- Returns only packages for tenant 550e8400...
-- Try to access other tenant's data
SELECT * FROM scorm_packages WHERE tenant_id = 'other-tenant-id';
-- Returns empty (RLS blocks it)
API-Level Isolation
Customer Routes
Automatic Tenant Resolution:
export async function GET(request: NextRequest) {
// Tenant resolved from authenticated user
const { tenantId } = await requireCustomerAuth();
// All queries filtered by tenant
const packages = await getPackages(tenantId);
// Request body tenant_id is IGNORED (security)
return NextResponse.json({ packages });
}
Security Features:
- Tenant ID from authentication only
- Request body tenant_id ignored
- No way to override tenant
- Automatic filtering
API Key Routes
Tenant from API Key:
export async function GET(request: NextRequest) {
// Tenant resolved from API key
const { tenantId } = await requireTenantAuth();
// All queries filtered by tenant
const packages = await getPackages(tenantId);
return NextResponse.json({ packages });
}
Security Features:
- Tenant from API key record
- Cannot access other tenants
- Automatic filtering
- Scope-based permissions
Admin Routes
Optional Tenant Filtering:
export async function GET(request: NextRequest) {
// System admin can view all tenants
await requireSystemAdmin();
// Optional tenant filter
const tenantId = request.nextUrl.searchParams.get('tenant_id');
const packages = tenantId
? await getPackages(tenantId)
: await getAllPackages();
return NextResponse.json({ packages });
}
Security Features:
- System admin only
- Explicit tenant filtering
- Audit logging
- Access control
Storage Isolation
Storage Paths
Tenant-Specific Paths:
tenant-550e8400/packages/pkg_abc123/
tenant-550e8400/sessions/session_xyz789/
Benefits:
- Logical separation
- Easy to identify tenant
- Simplified backup/restore
- Clear access patterns
Access Control
Storage Permissions:
- Tenant-specific access
- No cross-tenant file access
- Signed URLs for temporary access
- Expiration on URLs
Example:
// Generate tenant-scoped signed URL
const url = await generateSignedUrl({
path: `tenant-${tenantId}/packages/${packageId}`,
expiresIn: 3600 // 1 hour
});
Best Practices
1. Never Trust Client-Supplied Tenant ID
❌ Wrong:
const tenantId = request.body.tenant_id; // NEVER DO THIS
✅ Correct:
const { tenantId } = await requireCustomerAuth(); // From authentication
2. Always Filter by Tenant
❌ Wrong:
const packages = await db.packages.findMany(); // No tenant filter
✅ Correct:
const packages = await db.packages.findMany({
where: { tenant_id: tenantId } // Always filter
});
3. Validate Tenant on Updates
❌ Wrong:
await db.packages.update({
where: { id: packageId },
data: updates
}); // No tenant check
✅ Correct:
await db.packages.update({
where: {
id: packageId,
tenant_id: tenantId // Verify tenant matches
},
data: updates
});
4. Use RLS Policies
Enable RLS:
ALTER TABLE scorm_packages ENABLE ROW LEVEL SECURITY;
CREATE POLICY "tenant_isolation" ON scorm_packages
FOR ALL
USING (tenant_id = current_setting('app.tenant_id')::uuid);
5. Test Isolation
Test Cross-Tenant Access:
// Should fail - cannot access other tenant's data
const otherTenantPackage = await getPackage(
otherTenantPackageId,
myTenantId
);
// Should return null or throw error
Security Checklist
- RLS policies enabled on all tables
- Tenant ID always from authentication
- Request body tenant_id ignored
- All queries filter by tenant
- Storage paths are tenant-specific
- Cross-tenant access tests pass
- Admin routes properly secured
- Audit logging enabled
Related Documentation
- Security Overview - Complete security guide
- API Key Security - API key security
- Authentication Guide - Auth details
Last Updated: 2025-01-15