Multi-Tenant RBAC with Header-Based Authentication
Building a multi-tenant platform is not just about isolating data—it is also about ensuring that users can only perform actions appropriate to their role within a specific tenant.
Codily addresses this challenge with a Role-Based Access Control (RBAC) model that associates every request with both a tenant and a role. During development, this information can be supplied through simple HTTP headers such as X-Tenant-ID and X-Tenant-Role. In production, the same concepts are carried inside authenticated OAuth sessions protected by signed cookies.
This design keeps local development friction low while providing stronger authentication guarantees in production.
Understanding Multi-Tenant Authorization
Authentication answers:
Who is the user?
Authorization answers:
What is that user allowed to do?
In a multi-tenant application, authorization must also answer:
Within which tenant or project may they perform those actions?
A simplified request lifecycle looks like:
Incoming Request
│
▼
Authenticate User
│
▼
Resolve Tenant
│
▼
Resolve Role
│
▼
Authorize Action
│
▼
Execute Request
Every step is necessary to prevent unauthorized access across organizational boundaries.
Header-Based Authentication in Development
During local development, repeatedly configuring OAuth providers can slow iteration.
Codily therefore supports a development mode where tenant context is supplied explicitly through request headers.
Example:
GET /jobs
X-Tenant-ID: demo
X-Tenant-Role: operator
The backend trusts these values only when running in development mode, making it easy to simulate different users and permission levels without external identity providers.
Why Headers Are Useful During Development
Header-based authentication enables developers to:
- test multiple tenants quickly,
- simulate different permission levels,
- debug authorization logic,
- automate integration tests,
- and switch contexts without logging in repeatedly.
For example:
Developer
│
▼
curl Request
│
▼
X-Tenant-ID: staging
X-Tenant-Role: admin
This simplicity accelerates local iteration and CI testing.
The RBAC Permission Model
Codily defines four primary roles.
Viewer
The most restrictive role.
Capabilities:
- View jobs
- Read metrics
- Inspect job details
Cannot:
- Submit jobs
- Cancel jobs
- Requeue jobs
Ideal for dashboards and read-only access.
Operator
Operators manage day-to-day execution.
They can:
- Submit new jobs
- Cancel jobs
- Requeue failed jobs
- View queues and metrics
Operators cannot perform organization-wide administrative actions.
Admin
Administrators inherit operational capabilities while gaining broader management permissions.
Typical responsibilities include:
- Managing projects
- Viewing tenant-wide resources
- Inviting collaborators
- Performing cross-project operational tasks
They coordinate infrastructure without necessarily owning the organization itself.
Owner
Owners possess the highest privilege level.
They can:
- Configure organizations
- Manage administrators
- Control projects
- Perform operational actions
- Access tenant-wide administrative capabilities
Ownership is generally reserved for the primary account responsible for a tenant.
Visualizing Permissions
Read Write Manage
Viewer ✔ ✖ ✖
Operator ✔ ✔ ✖
Admin ✔ ✔ ✔
Owner ✔ ✔ ✔
Each role builds upon the permissions of the previous one while introducing additional administrative capabilities.
Authorization in Practice
Every request carries tenant and role context into the service layer.
Conceptually:
type TenantInfo struct {
ID string
Role string
}
Business logic then verifies permissions before executing sensitive operations.
For example:
if !CanWrite(role) {
return ErrForbidden
}
Keeping authorization checks centralized reduces duplication and minimizes the chance of inconsistent enforcement.
Tenant Isolation
Role checks alone are insufficient.
Even administrators should only interact with resources belonging to authorized tenants.
Database queries therefore combine RBAC with tenant filtering:
SELECT *
FROM jobs
WHERE tenant_id = $1;
The tenant identifier comes from authenticated context rather than arbitrary client input.
This prevents accidental or malicious cross-tenant access.
Transitioning to OAuth
Header-based authentication is convenient but unsuitable for production.
Clients could otherwise forge arbitrary values:
X-Tenant-Role: owner
To eliminate this risk, production deployments authenticate users through OAuth providers such as GitHub and Google.
The flow becomes:
User
│
▼
GitHub / Google Login
│
▼
OAuth Callback
│
▼
Build Session
│
▼
Issue Signed Cookie
Subsequent requests derive tenant and role information from the authenticated session rather than trusting incoming headers.
Signed Session Cookies
After successful authentication, the server creates a session containing information such as:
- user identity,
- active tenant,
- assigned role,
- and expiration metadata.
This payload is cryptographically signed before being stored in an HTTP-only cookie.
Session Data
│
▼
HMAC Signature
│
▼
Signed Cookie
When requests arrive, the server verifies the signature before trusting the contents.
Tampering invalidates the session.
Switching Active Tenants
Users may belong to multiple organizations or projects.
Rather than maintaining separate accounts, the active tenant can be switched while preserving identity.
Conceptually:
User
│
▼
Available Projects
│
▼
Select Tenant
│
▼
Issue Updated Session
This keeps authorization decisions tied to both identity and current context.
Development vs Production
The two authentication modes serve different purposes.
Development Mode
Advantages:
- Fast local setup
- No OAuth configuration
- Easy testing
- Simple API experimentation
Tradeoffs:
- Trusts client-supplied headers
- Not appropriate for public deployment
- Must remain disabled outside trusted environments
Production Mode
Advantages:
- Verified user identity
- Cryptographically protected sessions
- Secure role assignment
- Reduced spoofing risk
Tradeoffs:
- More configuration
- OAuth provider dependencies
- Additional login flow complexity
The convenience cost is justified by the significantly stronger security guarantees.
Why This Evolution Matters
Many systems begin with simple authentication during early development.
As products mature, requirements change:
- multiple users,
- organizational ownership,
- project membership,
- external identities,
- auditability,
- and secure session management.
By evolving from trusted development headers to OAuth-backed signed sessions while preserving the same underlying RBAC model, Codily maintains a clean separation between authentication and authorization without rewriting business logic.
Best Practices for Multi-Tenant RBAC
Several principles emerge from this design:
- Never trust client-supplied roles in production.
- Resolve authorization from authenticated identity.
- Scope every data access by tenant.
- Keep permission checks centralized.
- Use least-privilege defaults.
- Separate authentication from authorization logic.
- Make development ergonomics easy without weakening production security.
These guidelines reduce both accidental mistakes and security vulnerabilities.
Final Thoughts
Codily's RBAC model demonstrates how simple concepts can scale into a robust authorization system. Development environments use X-Tenant-ID and X-Tenant-Role headers for speed and convenience, enabling engineers to simulate different tenants and permission levels with minimal setup. Production deployments replace those trusted headers with OAuth-based identity providers and signed session cookies, ensuring that tenant context and roles originate from verified authentication rather than client input.
Combined with tenant-scoped database queries and clearly defined viewer, operator, admin, and owner roles, this approach provides a practical foundation for secure multi-tenant applications while balancing developer productivity with operational security.