Multi-Tenancy Architecture
Jet Admin is built from the ground up as a multi-tenant platform, enabling organizations to manage multiple isolated workspaces (tenants) with complete data separation, independent configurations, and granular access control.
π Table of Contentsβ
- What is Multi-Tenancy?
- Tenant Architecture
- Data Isolation
- User Management
- Role-Based Access Control
- API Key Authentication
- Security Model
- Best Practices
What is Multi-Tenancy?β
Multi-tenancy is an architecture where a single instance of Jet Admin serves multiple independent organizations (tenants). Each tenant has:
- β Complete data isolation - Cannot access other tenants' data
- β Independent configuration - Custom datasources, workflows, dashboards
- β Separate user management - Own users, roles, and permissions
- β Dedicated resources - Isolated execution environments
Use Casesβ
| Scenario | Description |
|---|---|
| SaaS Platform | Serve multiple customer organizations from one installation |
| Enterprise | Separate departments (HR, Finance, Engineering) with isolated data |
| Agency | Manage multiple clients with strict data separation |
| Development | Isolate dev/staging/production environments |
Tenant Architectureβ
Database Schemaβ
// Core tenant model
model tblTenants {
tenantID String @id @default(gen_random_uuid())
tenantTitle String
tenantDBURL String? // Optional: dedicated DB per tenant
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime?
// Tenant-scoped resources
datasources tblDatasources[]
workflows tblWorkflows[]
dashboards tblDashboards[]
widgets tblWidgets[]
queries tblDataQueries[]
users tblUsersTenantsRelationship[]
apiKeys tblAPIKeys[]
roles tblRoles[]
}
Tenant Hierarchyβ
βββββββββββββββββββββββββββββββββββββββββββ
β Jet Admin Platform β
β β
β ββββββββββββ ββββββββββββ βββββββββββ
β β Tenant A β β Tenant B β βTenant Cββ
β β ββββββββββ β ββββββββββ βββββββββββ
β β β’ Users β β β’ Users β β β’ Usersββ
β β β’ Data β β β’ Data β β β’ Data ββ
β β β’ Config β β β’ Config β β β’ Configββ
β ββββββββββββ ββββββββββββ βββββββββββ
βββββββββββββββββββββββββββββββββββββββββββ
Data Isolationβ
Isolation Strategiesβ
Jet Admin implements logical isolation through tenant scoping:
1. Query-Level Isolationβ
All database queries automatically include tenant filtering:
// Every query includes tenantID filter
const datasources = await prisma.tblDatasources.findMany({
where: {
tenantID: req.tenantID, // β Automatic tenant scoping
deletedAt: null
}
});
2. Middleware Enforcementβ
Tenant context is established via middleware:
// middleware/tenant.middleware.js
async function tenantMiddleware(req, res, next) {
const tenantId = req.headers['x-tenant-id'] || req.params.tenantID;
// Verify tenant exists
const tenant = await prisma.tblTenants.findUnique({
where: { tenantID: tenantId, deletedAt: null }
});
if (!tenant) {
throw new AppError('Invalid tenant', 'INVALID_TENANT', 400);
}
req.tenant = tenant;
req.tenantID = tenantId;
next();
}
3. Foreign Key Constraintsβ
Database-level enforcement through foreign keys:
model tblDatasources {
id String @id @default(gen_random_uuid())
tenantID String // β Foreign key to tenants
tenant tblTenants @relation(fields: [tenantID], references: [tenantID])
// ... other fields
}
Isolation Guaranteesβ
| Level | Guarantee |
|---|---|
| API | All endpoints require tenant context |
| Database | All queries filtered by tenantID |
| Workflow | Workflow execution tenant-scoped |
| Real-time | Socket rooms tenant-isolated |
| Cache | Cache keys include tenant prefix |
User Managementβ
User-Tenant Relationshipβ
Users can belong to multiple tenants with different roles:
model tblUsers {
userID String @id @default(gen_random_uuid())
email String @unique
firebaseID String @unique
// Many-to-many with tenants
tenants tblUsersTenantsRelationship[]
}
model tblUsersTenantsRelationship {
id String @id @default(gen_random_uuid())
userID String
tenantID String
roleID String
user tblUsers @relation(fields: [userID], references: [userID])
tenant tblTenants @relation(fields: [tenantID], references: [tenantID])
role tblRoles @relation(fields: [roleID], references: [roleID])
@@unique([userID, tenantID]) // One role per tenant per user
}
User Journeyβ
Switching Tenantsβ
Users can switch between tenants:
// Frontend: Tenant switcher component
function TenantSwitcher() {
const { tenants, currentTenant, selectTenant } = useTenant();
return (
<select
value={currentTenant?.tenantID}
onChange={(e) => {
const tenant = tenants.find(t => t.tenantID === e.target.value);
selectTenant(tenant);
}}
>
{tenants.map(tenant => (
<option key={tenant.tenantID} value={tenant.tenantID}>
{tenant.tenantTitle}
</option>
))}
</select>
);
}
Role-Based Access Control (RBAC)β
RBAC Modelβ
model tblRoles {
roleID String @id @default(gen_random_uuid())
tenantID String
roleName String // e.g., "Admin", "Editor", "Viewer"
permissions tblPermissions[]
users tblUsersTenantsRelationship[]
}
model tblPermissions {
permissionID String @id @default(gen_random_uuid())
roleID String
permissionName String // e.g., "datasource:create"
resource String // e.g., "datasource"
action String // e.g., "create"
role tblRoles @relation(fields: [roleID], references: [roleID])
}
Default Rolesβ
| Role | Permissions | Use Case |
|---|---|---|
| Admin | All permissions | Platform administrators |
| Editor | Create, read, update | Content creators |
| Viewer | Read-only | Stakeholders, auditors |
Permission Systemβ
Permissions follow a resource:action pattern:
const PERMISSIONS = {
// Datasources
'datasource:list': 'View datasource list',
'datasource:read': 'View datasource details',
'datasource:create': 'Create new datasources',
'datasource:update': 'Edit existing datasources',
'datasource:delete': 'Remove datasources',
// Workflows
'workflow:list': 'View workflow list',
'workflow:read': 'View workflow details',
'workflow:create': 'Create new workflows',
'workflow:update': 'Edit existing workflows',
'workflow:execute': 'Execute workflows',
'workflow:delete': 'Delete workflows',
// Dashboards
'dashboard:list': 'View dashboards',
'dashboard:read': 'View dashboard',
'dashboard:create': 'Create dashboards',
'dashboard:update': 'Edit dashboards',
'dashboard:delete': 'Delete dashboards',
// Users & Roles
'user:list': 'View users',
'user:create': 'Invite users',
'user:update': 'Update user roles',
'user:delete': 'Remove users',
'role:manage': 'Manage roles and permissions',
};
Permission Middlewareβ
// middleware/permission.middleware.js
function requirePermission(permission) {
return async (req, res, next) => {
const { user, tenantID } = req;
// Get user's roles in tenant
const relationships = await prisma.tblUsersTenantsRelationship.findMany({
where: { userID: user.uid, tenantID },
include: { role: { include: { permissions: true } } }
});
// Check if any role has required permission
const hasPermission = relationships.some(rel =>
rel.role.permissions.some(p => p.permissionName === permission)
);
if (!hasPermission) {
return res.status(403).json({
error: 'Permission denied',
code: 'PERMISSION_DENIED',
required: permission
});
}
next();
};
}
// Usage in routes
router.post(
'/',
authMiddleware,
tenantMiddleware,
requirePermission('datasource:create'),
datasourceController.createDatasource
);
API Key Authenticationβ
For machine-to-machine communication, Jet Admin supports API key authentication:
API Key Modelβ
model tblAPIKeys {
keyID String @id @default(gen_random_uuid())
tenantID String
keyName String
keyHash String // Hashed key value
createdAt DateTime @default(now())
expiresAt DateTime?
tenant tblTenants @relation(fields: [tenantID], references: [tenantID])
roleMappings tblAPIKeyRoleMappings[]
}
Creating API Keysβ
// API: Generate API key
async function createAPIKey(tenantID, name, permissions) {
// Generate random key
const rawKey = `jet_${crypto.randomBytes(32).toString('hex')}`;
// Hash for storage
const keyHash = await bcrypt.hash(rawKey, 10);
// Store in database
const apiKey = await prisma.tblAPIKeys.create({
data: {
tenantID,
keyName: name,
keyHash,
roleMappings: {
create: permissions.map(permission => ({
permissionName: permission
}))
}
}
});
// Return raw key once (never stored)
return {
keyID: apiKey.keyID,
key: rawKey, // Only shown once
expiresAt: apiKey.expiresAt
};
}
Using API Keysβ
# API request with API key
curl -X GET https://api.jetadmin.io/api/v1/tenants/:tenantID/datasources \
-H "Authorization: Bearer jet_abc123..." \
-H "x-tenant-id: tenant-uuid"
API Key Middlewareβ
async function apiKeyMiddleware(req, res, next) {
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith('Bearer ')) {
return next(); // Try Firebase auth instead
}
const key = authHeader.split(' ')[1];
// Find API key
const apiKey = await prisma.tblAPIKeys.findFirst({
where: { tenantID: req.tenantID },
include: { roleMappings: true }
});
if (!apiKey || !await bcrypt.compare(key, apiKey.keyHash)) {
return res.status(401).json({ error: 'Invalid API key' });
}
// Attach permissions to request
req.apiKey = apiKey;
req.permissions = apiKey.roleMappings.map(m => m.permissionName);
next();
}
Security Modelβ
Defense in Depthβ
Security Layersβ
| Layer | Protection |
|---|---|
| Transport | HTTPS/TLS encryption |
| Authentication | Firebase Auth or API Keys |
| Authorization | RBAC permissions |
| Tenant Isolation | Query-level filtering |
| Input Validation | Request sanitization |
| Audit Logging | All actions logged |
| Encryption | Credentials encrypted at rest |
Audit Loggingβ
All tenant-scoped actions are logged:
model tblAuditLogs {
logID String @id @default(gen_random_uuid())
tenantID String
userID String
action String
resource String
timestamp DateTime @default(now())
ipAddress String
userAgent String
metadata Json
}
Logged Events:
- User login/logout
- Datasource CRUD operations
- Workflow execution
- Dashboard modifications
- Role changes
- API key usage
Best Practicesβ
Tenant Organizationβ
β Use Clear Naming
Good: "Acme Corp - Production"
Bad: "Tenant 1"
β Separate Environments
Tenant: "MyApp - Development"
Tenant: "MyApp - Staging"
Tenant: "MyApp - Production"
β Document Purpose
{
tenantTitle: 'Engineering Team',
description: 'Engineering department tools and dashboards',
metadata: {
department: 'engineering',
costCenter: 'ENG-001'
}
}
Access Controlβ
β Principle of Least Privilege
- Start with minimal permissions
- Grant additional access as needed
- Review permissions regularly
β Use Roles, Not Individual Permissions
// Good: Assign predefined role
await assignRoleToUser(userId, 'editor');
// Avoid: Assign individual permissions
await assignPermissions(userId, [
'datasource:read',
'workflow:read',
// ... 20 more
]);
β Regular Audits
- Review user access quarterly
- Remove inactive users
- Rotate API keys every 90 days
Securityβ
β Enable MFA
- Require multi-factor authentication
- Especially for admin accounts
β Monitor Suspicious Activity
// Alert on unusual patterns
if (loginAttempts > 5 ||
ipChange ||
unusualHour) {
alertSecurityTeam();
}
β Encrypt Sensitive Data
- Use field-level encryption for PII
- Encrypt datasource credentials
- Secure key management
Performanceβ
β Index Tenant Queries
model tblDatasources {
@@index([tenantID, deletedAt])
@@index([tenantID, createdAt])
}
β Partition Large Tables
-- Partition audit logs by tenant
CREATE TABLE audit_logs (
logID uuid,
tenantID uuid,
...
) PARTITION BY LIST (tenantID);
β Cache Tenant Context
// Cache tenant data
const tenant = await cache.get(`tenant:${tenantID}`);
if (!tenant) {
const tenant = await db.tblTenants.findUnique(...);
await cache.set(`tenant:${tenantID}`, tenant, 300);
}
Next Stepsβ
- Authentication - Firebase auth integration
- Security - Security best practices
- RBAC Implementation - Role management
- API Reference - Auth endpoints