Agent Skills Framework Extension
Multi-Tenant Architecture Skill
When to Use This Skill
Use this skill when implementing multi tenant architecture patterns in your codebase.
How to Use This Skill
- Review the patterns and examples below
- Apply the relevant patterns to your implementation
- Follow the best practices outlined in this skill
Production multi-tenancy patterns with isolation, security, and resource management.
Core Capabilities
- Isolation Strategies - Database per tenant, schema per tenant, row-level security
- Data Partitioning - Logical and physical separation
- Security Boundaries - Cross-tenant access prevention
- Resource Quotas - Compute, storage, API rate limits
- Tenant Lifecycle - Provisioning, migration, decommissioning
Row-Level Security (PostgreSQL)
-- migrations/multi_tenant_rls.sql
BEGIN;
-- Enable RLS on tables
ALTER TABLE projects ENABLE ROW LEVEL SECURITY;
ALTER TABLE tasks ENABLE ROW LEVEL SECURITY;
ALTER TABLE documents ENABLE ROW LEVEL SECURITY;
-- Policy: Users can only see data from their organization
CREATE POLICY tenant_isolation_policy ON projects
USING (organization_id = current_setting('app.current_tenant_id')::uuid);
CREATE POLICY tenant_isolation_policy ON tasks
USING (
project_id IN (
SELECT id FROM projects
WHERE organization_id = current_setting('app.current_tenant_id')::uuid
)
);
-- Policy: Admins bypass RLS
CREATE POLICY admin_access_policy ON projects
USING (current_setting('app.is_admin')::boolean = true);
COMMIT;
Tenant Context Middleware
// src/middleware/tenant_context.rs
use actix_web::{
dev::{Service, ServiceRequest, ServiceResponse, Transform},
Error, HttpMessage,
};
use futures::future::{ok, Ready};
use std::task::{Context, Poll};
use uuid::Uuid;
pub struct TenantContext;
impl<S, B> Transform<S, ServiceRequest> for TenantContext
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
S::Future: 'static,
{
type Response = ServiceResponse<B>;
type Error = Error;
type Transform = TenantContextMiddleware<S>;
type InitError = ();
type Future = Ready<Result<Self::Transform, Self::InitError>>;
fn new_transform(&self, service: S) -> Self::Future {
ok(TenantContextMiddleware { service })
}
}
pub struct TenantContextMiddleware<S> {
service: S,
}
impl<S, B> Service<ServiceRequest> for TenantContextMiddleware<S>
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
S::Future: 'static,
{
type Response = ServiceResponse<B>;
type Error = Error;
type Future = S::Future;
fn poll_ready(&self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.service.poll_ready(cx)
}
fn call(&self, req: ServiceRequest) -> Self::Future {
// Extract tenant ID from JWT claims
if let Some(tenant_id) = extract_tenant_id(&req) {
req.extensions_mut().insert(TenantId(tenant_id));
}
self.service.call(req)
}
}
#[derive(Clone, Debug)]
pub struct TenantId(pub Uuid);
fn extract_tenant_id(req: &ServiceRequest) -> Option<Uuid> {
// Extract from JWT claims
req.headers()
.get("Authorization")?
.to_str()
.ok()?
.strip_prefix("Bearer ")?
.parse()
.ok()
}
Tenant Provisioning
// src/tenants/provisioning.rs
use sqlx::PgPool;
use uuid::Uuid;
pub struct TenantProvisioner {
pool: PgPool,
}
impl TenantProvisioner {
pub async fn provision_tenant(
&self,
tenant_name: &str,
plan: &str
) -> Result<Uuid, sqlx::Error> {
let tenant_id = Uuid::new_v4();
let mut tx = self.pool.begin().await?;
// Create organization
sqlx::query!(
r#"
INSERT INTO organizations (id, name, plan, created_at)
VALUES ($1, $2, $3, NOW())
"#,
tenant_id,
tenant_name,
plan
)
.execute(&mut *tx)
.await?;
// Set resource quotas
let (max_users, max_projects, storage_gb) = match plan {
"free" => (5, 3, 10),
"pro" => (50, 50, 100),
"enterprise" => (500, 500, 1000),
_ => (5, 3, 10),
};
sqlx::query!(
r#"
INSERT INTO tenant_quotas (tenant_id, max_users, max_projects, storage_gb)
VALUES ($1, $2, $3, $4)
"#,
tenant_id,
max_users as i32,
max_projects as i32,
storage_gb as i32
)
.execute(&mut *tx)
.await?;
tx.commit().await?;
Ok(tenant_id)
}
pub async fn check_quota(
&self,
tenant_id: Uuid,
resource: &str
) -> Result<bool, sqlx::Error> {
let quota = sqlx::query!(
r#"
SELECT max_users, max_projects
FROM tenant_quotas
WHERE tenant_id = $1
"#,
tenant_id
)
.fetch_one(&self.pool)
.await?;
let current = sqlx::query!(
r#"
SELECT
(SELECT COUNT(*) FROM organization_members WHERE organization_id = $1) as users,
(SELECT COUNT(*) FROM projects WHERE organization_id = $1) as projects
"#,
tenant_id
)
.fetch_one(&self.pool)
.await?;
match resource {
"users" => Ok(current.users.unwrap_or(0) < quota.max_users.into()),
"projects" => Ok(current.projects.unwrap_or(0) < quota.max_projects.into()),
_ => Ok(false),
}
}
}
Kubernetes Namespace Isolation
# k8s/tenant-namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
name: tenant-acme-corp
labels:
tenant: acme-corp
---
apiVersion: v1
kind: ResourceQuota
metadata:
name: tenant-quota
namespace: tenant-acme-corp
spec:
hard:
requests.cpu: "10"
requests.memory: 20Gi
pods: "50"
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: deny-cross-tenant
namespace: tenant-acme-corp
spec:
podSelector: {}
policyTypes:
- Ingress
- Egress
ingress:
- from:
- namespaceSelector:
matchLabels:
tenant: acme-corp
egress:
- to:
- namespaceSelector:
matchLabels:
tenant: acme-corp
Usage Examples
RLS Setup
Apply multi-tenant-architecture skill to implement row-level security for tenant isolation
Tenant Provisioning
Apply multi-tenant-architecture skill to automate tenant provisioning with quota management
Namespace Isolation
Apply multi-tenant-architecture skill to configure Kubernetes namespace per tenant with network policies
Integration Points
- database-design-patterns - RLS and partitioning
- cloud-infrastructure-patterns - Resource isolation
- k8s-statefulset-patterns - Per-tenant deployments
Success Output
When this skill completes successfully, output:
✅ SKILL COMPLETE: multi-tenant-architecture
Completed:
- [x] Row-level security (RLS) enabled on all tenant tables
- [x] Tenant context middleware implemented
- [x] Tenant provisioning with quota management operational
- [x] Kubernetes namespace isolation configured
- [x] Network policies applied for cross-tenant protection
- [x] Resource quotas enforced per tenant
Outputs:
- migrations/multi_tenant_rls.sql (RLS policies)
- src/middleware/tenant_context.rs (Context middleware)
- src/tenants/provisioning.rs (Provisioning service)
- k8s/tenant-namespace.yaml (K8s isolation)
Validation:
- [x] Cross-tenant access blocked (security test passed)
- [x] Quota enforcement verified (resource limits active)
- [x] Namespace isolation confirmed (network policy test passed)
Completion Checklist
Before marking this skill as complete, verify:
- RLS policies created and enabled on all tenant-scoped tables
- Tenant context middleware extracting tenant_id from JWT
- Provisioning service creates tenants with appropriate quotas
- Quota checking prevents resource limit violations
- Kubernetes namespaces created per tenant with labels
- NetworkPolicy blocks cross-tenant communication
- ResourceQuota limits enforced (CPU, memory, pods)
- Security test confirms no cross-tenant data leakage
- Migration rollback tested and verified
Failure Indicators
This skill has FAILED if:
- ❌ RLS policies not applied (cross-tenant data visible)
- ❌ Tenant context not set (queries fail or return all data)
- ❌ Provisioning creates tenant without quotas (unlimited resources)
- ❌ Quota checks not enforced (tenants exceed limits)
- ❌ Kubernetes namespaces missing labels (isolation incomplete)
- ❌ NetworkPolicy allows cross-tenant traffic (security breach)
- ❌ ResourceQuota not enforced (resource exhaustion possible)
- ❌ Security test shows data leakage between tenants
When NOT to Use
Do NOT use this skill when:
- Single-tenant application - Adds unnecessary complexity for one organization
- Prototype/MVP - Multi-tenancy overhead slows initial development
- Small user base - < 10 organizations don't justify isolation infrastructure
- Simple resource sharing OK - No compliance requirements for data separation
- No SaaS model - Self-hosted deployments per customer work better
- Performance critical - RLS and isolation add query overhead
- Legacy migration - Retrofitting multi-tenancy is complex; plan from start
Use these alternatives instead:
- Single tenant: Standard database schema without RLS
- Prototype: Defer multi-tenancy to post-MVP
- Small scale: Separate databases per customer
- No compliance needs: Shared tables with organization_id filtering
- Self-hosted: Deploy separate instances per customer
- Performance needs: Optimize single-tenant first, then add isolation
Anti-Patterns (Avoid)
| Anti-Pattern | Problem | Solution |
|---|---|---|
| No RLS on sensitive tables | Data leakage, security breach | Enable RLS on ALL tenant-scoped tables |
| Hardcoded tenant_id | Context not dynamic, breaks isolation | Use current_setting() for RLS policies |
| Missing quota checks | Resource exhaustion, unfair usage | Check quotas before creation operations |
| Shared Kubernetes namespace | No isolation, resource conflicts | Create namespace per tenant with labels |
| No NetworkPolicy | Cross-tenant network access possible | Apply deny-by-default NetworkPolicy |
| Unlimited resource quotas | Tenants consume all cluster resources | Set CPU, memory, pod limits per plan tier |
| Skipping migration rollback | Can't recover from failed deployment | Always test DOWN migrations |
| Admin bypass not implemented | Support can't troubleshoot tenant issues | Add admin access policy with flag |
| Tenant ID in URL path | Enumeration attacks, security risk | Use JWT claims, not URL parameters |
| No provisioning audit trail | Can't track tenant lifecycle | Log all provisioning/deprovisioning events |
Principles
This skill embodies these CODITECT principles:
#2 First Principles Thinking
- Understand WHY multi-tenancy matters (cost efficiency, SaaS scalability, compliance)
- Question isolation needs: database, compute, network levels
- Design from tenant needs up, not database capabilities down
#3 Keep It Simple
- Start with row-level security (simplest isolation)
- Add schema-per-tenant or database-per-tenant only if needed
- Kubernetes namespaces for logical separation, not complex service meshes
#4 Separation of Concerns
- Tenant context extraction separate from business logic
- Provisioning service owns tenant lifecycle, not application code
- RLS policies enforce isolation at database level, not application
#5 Eliminate Ambiguity
- Explicit tenant_id required for all tenant-scoped operations
- Clear quota limits per plan tier (free/pro/enterprise)
- Unambiguous network policies (default deny, explicit allow)
#7 Security First
- RLS prevents data leakage at database level
- NetworkPolicy blocks unauthorized cross-tenant communication
- Audit logging tracks all tenant provisioning/access
#8 No Assumptions
- Don't assume RLS enabled; verify with security tests
- Don't assume quotas enforced; test resource limit violations
- Don't assume namespaces isolated; validate NetworkPolicy
#11 Automate Everything
- Automated tenant provisioning with quota assignment
- Automated namespace creation with labels and policies
- Automated security testing for cross-tenant isolation
Full Standard: CODITECT-STANDARD-AUTOMATION.md
Version: 1.1.0 | Updated: 2026-01-04 | Quality Standard: SKILL-QUALITY-STANDARD.md v1.0.0