Hadrian is experimental alpha software. Do not use in production.
Hadrian
Features

Multi-Tenancy

Flexible hierarchy for organizations, teams, projects, and users

Hadrian supports a flexible multi-tenancy hierarchy that scales from single-user deployments to large enterprises with complex organizational structures.

Hierarchy Structure

Entities

EntityDescriptionIdentifier
OrganizationsTop-level containers for all resourcesorg_slug
TeamOptional grouping within an organizationteam_slug (unique within org)
ProjectWorkspace boundary for resourcesproject_slug (unique within org)
UserIndividual identity (from SSO or local)external_id or user_id
Service AccountMachine identity for automated systems with role-based accesssa_slug (unique within org)

All entities use slug-based URLs for user-friendly API paths:

/admin/v1/organizations/{org_slug}
/admin/v1/organizations/{org_slug}/teams/{team_slug}
/admin/v1/organizations/{org_slug}/projects/{project_slug}

Resource Ownership

Resources in Hadrian follow a consistent ownership pattern using owner_type and owner_id fields. This enables flexible scoping at any level of the hierarchy.

Scoped Resources

ResourceOwnership LevelsDescription
API KeysOrg, Team, Project, User, Service AccountAuthentication credentials with budget limits
Dynamic ProvidersOrg, Team, Project, UserCustom LLM provider configurations
CollectionsOrg, Team, Project, UserVector stores for RAG
FilesOrg, Team, Project, UserUploaded documents and images
ConversationsProject, UserChat history
PromptsOrg, Team, Project, UserReusable system prompt templates
Model PricingOrg, Team, Project, User, GlobalCost calculation overrides

Ownership Examples

Organization-level API key (shared across all teams/projects):

POST /admin/v1/api-keys
{
  "name": "Shared Production Key",
  "owner": {
    "type": "organization",
    "organization_id": "550e8400-e29b-41d4-a716-446655440000"
  },
  "budget_limit_cents": 100000,
  "budget_period": "monthly"
}

Project-level API key (isolated to specific project):

POST /admin/v1/api-keys
{
  "name": "Dev Project Key",
  "owner": {
    "type": "project",
    "project_id": "660e8400-e29b-41d4-a716-446655440001"
  },
  "budget_limit_cents": 10000,
  "budget_period": "daily"
}

User-level API key (personal key):

POST /admin/v1/api-keys
{
  "name": "My Personal Key",
  "owner": {
    "type": "user",
    "user_id": "770e8400-e29b-41d4-a716-446655440002"
  }
}

Service account API key (machine identity with roles):

POST /admin/v1/api-keys
{
  "name": "CI/CD Pipeline Key",
  "owner": {
    "type": "service_account",
    "service_account_id": "880e8400-e29b-41d4-a716-446655440004"
  },
  "budget_limit_cents": 50000,
  "budget_period": "monthly"
}

When an API key is owned by a service account, the service account's roles are used for RBAC evaluation. This enables machine identities to participate in role-based authorization just like users.

Dynamic Providers

Bring your own API keys at any scope. This allows teams or projects to use their own provider credentials while sharing the gateway infrastructure.

Configuration

Dynamic providers can be created via the Admin API:

POST /admin/v1/dynamic-providers
{
  "name": "Team OpenAI",
  "provider_type": "openai",
  "owner": {
    "type": "team",
    "team_id": "880e8400-e29b-41d4-a716-446655440003"
  },
  "base_url": "https://api.openai.com/v1",
  "api_key": "sk-..."
}

Provider Resolution

When a request arrives, Hadrian resolves providers in this order:

  1. User-level provider (if exists)
  2. Project-level provider (if exists)
  3. Team-level provider (if exists)
  4. Organization-level provider (if exists)
  5. Global provider (from hadrian.toml)

This cascading resolution allows:

  • Individual users to test with personal API keys
  • Projects to have dedicated provider accounts
  • Organizations to set defaults while allowing overrides

Dynamic Routing

Use dynamic routing to specify the scope in the model string:

# Route through organization's provider
curl -X POST http://localhost:8080/v1/chat/completions \
  -H "Authorization: Bearer $API_KEY" \
  -d '{
    "model": ":org/acme-corp/openai/gpt-4o",
    "messages": [{"role": "user", "content": "Hello"}]
  }'

# Route through project's provider
curl -X POST http://localhost:8080/v1/chat/completions \
  -H "Authorization: Bearer $API_KEY" \
  -d '{
    "model": ":org/acme-corp/:project/ml-research/anthropic/claude-sonnet-4-20250514",
    "messages": [{"role": "user", "content": "Hello"}]
  }'

# Route through user's provider
curl -X POST http://localhost:8080/v1/chat/completions \
  -H "Authorization: Bearer $API_KEY" \
  -d '{
    "model": ":org/acme-corp/:user/alice/openai/gpt-4o",
    "messages": [{"role": "user", "content": "Hello"}]
  }'

Cascading Settings

Settings cascade from specific to general, allowing overrides at any level.

Model Pricing Cascade

Pricing lookups follow this order (first match wins):

User pricing     →  Most specific
Project pricing  →  ↓
Team pricing     →  ↓
Org pricing      →  ↓
Global pricing   →  Fallback (from config or provider API)

Example: Set custom pricing for a specific project:

POST /admin/v1/model-pricing
{
  "provider": "openai",
  "model": "gpt-4o",
  "input_cost_per_million": 2500,
  "output_cost_per_million": 10000,
  "owner": {
    "type": "project",
    "project_id": "660e8400-e29b-41d4-a716-446655440001"
  }
}

Requests from this project use the custom pricing; other projects fall back to org or global pricing.

Pricing Sources

SourceDescription
manualUser-configured, never overwritten
provider_apiFetched from provider (e.g., OpenRouter)
defaultSystem defaults

Manual pricing always takes precedence over automatic updates.

Membership Management

Users belong to organizations, teams, and projects through membership records.

Membership Levels

MembershipDescription
org_membershipsUsers → Organizations (required for access)
team_membershipsUsers → Teams (optional grouping)
project_membershipsUsers → Projects (workspace access)

Roles

Each membership includes a role for authorization:

POST /admin/v1/organizations/{org_slug}/members
{
  "user_id": "770e8400-e29b-41d4-a716-446655440002",
  "role": "admin"
}

Common roles:

  • owner - Full control, can delete the resource
  • admin - Manage members and settings
  • member - Standard access
  • viewer - Read-only access

Roles are enforced through CEL-based authorization policies. See Authorization for policy configuration.

Service Accounts

Service accounts are machine identities for automated systems, CI/CD pipelines, and background jobs. Unlike user accounts, service accounts carry roles directly, enabling role-based access control for API key authentication.

Why Service Accounts?

Traditional API key authentication lacks role information:

API Key → Owner (org/team/project/user) → No roles!

This breaks RBAC policies that check subject.roles. Service accounts solve this by acting as a bridge:

API Key → Service Account → Roles → RBAC works!

Creating a Service Account

curl -X POST http://localhost:8080/admin/v1/organizations/acme-corp/service-accounts \
  -H "Authorization: Bearer $ADMIN_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "slug": "ci-cd-bot",
    "name": "CI/CD Bot",
    "description": "Automated deployment service account",
    "roles": ["deployer", "viewer"]
  }'

Service Account Fields

FieldDescription
slugURL-friendly identifier (unique within organization)
nameDisplay name
descriptionOptional description
rolesList of roles for RBAC evaluation

Service accounts are the foundation for the Unified Principal Model. API keys can be owned by service accounts, enabling machine identities to participate in role-based authorization. When an API key owned by a service account is used, the service account's roles are evaluated in RBAC policies.

Role Mapping

Service account roles are processed through the same role_mapping configuration as user roles from identity providers. This allows consistent role normalization:

[auth.rbac]
role_mapping = { "deployer" = "deploy_admin", "viewer" = "read_only" }

With this configuration, a service account with roles ["deployer", "viewer"] will have mapped roles ["deploy_admin", "read_only"] in RBAC policy evaluation.

Priority with Identity Auth

When both API key (service account) and identity authentication are present (e.g., API key used on behalf of a logged-in user), identity roles take precedence over service account roles. This ensures user-level policies always apply when a user identity is established.

Principal Model

The Principal abstraction represents "who is making the request" regardless of the credential type used for authentication. This unified model simplifies authorization and audit logging by providing a consistent view of the authenticated actor.

Principal Types

TypeDescriptionExamples
UserHuman identity from OIDC/SAML/proxy or user API keySSO users, user-owned API keys
ServiceAccountMachine identity with explicit rolesCI/CD bots, deployment automation
MachineShared/organizational credential (no roles)Org/team/project-owned API keys

Principal Derivation

The principal is derived from the authentication context:

API Key only:
  - Service account owner → ServiceAccount principal (with SA roles)
  - User owner → User principal (user_id, no identity roles)
  - Org/Team/Project owner → Machine principal (scope only)

Identity only (OIDC/SAML/proxy):
  - Always User principal (with identity roles and claims)

Both (API key + Identity):
  - Service account owner → ServiceAccount principal (SA takes precedence)
  - Otherwise → User principal (identity provides fields)

Using Principal in Code

Access the principal from any authenticated request:

use crate::auth::{AuthenticatedRequest, Principal};

fn handle_request(auth: &AuthenticatedRequest) {
    let principal = auth.principal();

    match principal {
        Principal::User { email, roles, .. } => {
            // Handle human user
        }
        Principal::ServiceAccount { id, roles, .. } => {
            // Handle service account
        }
        Principal::Machine { kind } => {
            // Handle shared credential
        }
    }
}

Principal for RBAC

Convert a principal to a Subject for policy evaluation:

let subject = principal.to_subject();
// subject now has all fields needed for CEL policy evaluation

Principal for Audit Logging

Get the appropriate audit actor type:

let actor_type = principal.actor_type();
// Returns: User, ServiceAccount, ApiKey, or System

Usage Tracking

Usage is tracked with full attribution context, enabling analytics at every level of the hierarchy. Each request records the authenticated user, organization, project, team, and service account — regardless of whether the request was made with an API key or a browser session.

Attribution

SourceAttribution
API key (org-owned)org_id from key owner
API key (project)org_id and project_id from key owner
API key (team)org_id and team_id from key owner
API key (user)org_id and user_id from key owner
API key (SA)org_id and service_account_id from service account
Session (SSO/proxy)user_id and org_id from session; project_id via X-Hadrian-Project

Session-based users can set the X-Hadrian-Project header (or use the project picker in the chat UI) to attribute their usage to a specific project.

Usage Analytics API

Usage data is available through the Admin API at each scope:

ScopeEndpoints
OrganizationGET /admin/v1/organizations/{org}/usage, by-date, by-model
TeamGET /admin/v1/organizations/{org}/teams/{team}/usage, by-date, by-model, by-provider, forecast
ProjectGET /admin/v1/organizations/{org}/projects/{project}/usage, by-date, by-model
UserGET /admin/v1/users/{id}/usage, by-date, by-model
API KeyGET /admin/v1/api-keys/{id}/usage, by-date, by-model
Self-serviceGET /admin/v1/me/usage, by-date, by-model (no admin role required)

See Budget Enforcement for details on the usage dashboards in the admin UI.

Budget Enforcement

Budgets are enforced at the API key level, with keys scoped to any ownership level.

Budget Configuration

{
  "name": "Team Budget Key",
  "owner": {
    "type": "team",
    "team_id": "880e8400-e29b-41d4-a716-446655440003"
  },
  "budget_limit_cents": 50000,
  "budget_period": "monthly"
}

Budget Periods

PeriodResetUse Case
dailyMidnight UTCDevelopment, testing
monthly1st of month UTCProduction budgets

Enforcement Flow

1. Request arrives with API key
2. Check current spend for key's budget period
3. Reserve estimated cost (atomic operation)
4. Forward to LLM provider
5. Adjust reservation with actual cost
6. Track in usage_records table

See Budget Enforcement for detailed configuration.

Authorization

Hadrian uses CEL (Common Expression Language) policies for fine-grained access control.

Authorization Context

Every request includes context for policy evaluation:

FieldDescription
subject.user_idAuthenticated user ID
subject.org_idUser's organization
subject.rolesUser's roles (from identity or service account)
subject.service_account_idService account ID (if authenticated via SA API key)
resource.typeResource being accessed
resource.owner_typeOwner level of resource
resource.owner_idOwner ID of resource

Example Policies

[auth.rbac]

# Organization admins can manage all resources in their org
[[auth.rbac.policies]]
expression = "subject.org_id == resource.org_id && 'admin' in subject.roles"
permissions = ["*"]

# Users can only access their own resources
[[auth.rbac.policies]]
expression = "resource.owner_type == 'user' && resource.owner_id == subject.user_id"
permissions = ["read", "write", "delete"]

# Project members can read project resources
[[auth.rbac.policies]]
expression = "resource.owner_type == 'project' && resource.owner_id in subject.project_ids"
permissions = ["read"]

# Service accounts with 'deployer' role can call chat/completions
[[auth.rbac.policies]]
name = "service_account_deployer"
expression = "subject.service_account_id != '' && 'deployer' in subject.roles"
resources = ["chat_completions", "embeddings"]
permissions = ["write"]

Cross-Org Isolation

By default, users cannot access resources outside their organization:

[[auth.rbac.policies]]
expression = "subject.org_id == resource.org_id"
permissions = ["read"]

This ensures complete isolation between organizations.

Audit Logging

All administrative operations are logged with full context.

Audit Log Fields

FieldDescription
timestampWhen the action occurred
actor_typeuser, api_key, service_account, or system
actor_idID of the actor
actionOperation (e.g., team.create, api_key.revoke)
resource_typeType of affected resource
resource_idID of affected resource
org_idOrganization context
project_idProject context (if applicable)
detailsJSON with request details
ip_addressClient IP
user_agentClient user agent

Querying Audit Logs

# Get audit logs for an organization
curl http://localhost:8080/admin/v1/organizations/{org_slug}/audit-logs \
  -H "Authorization: Bearer $ADMIN_KEY"

# Filter by action
curl "http://localhost:8080/admin/v1/organizations/{org_slug}/audit-logs?action=api_key.create" \
  -H "Authorization: Bearer $ADMIN_KEY"

Session Management

Hadrian provides administrative control over user browser sessions, enabling the critical enterprise use case: force logout terminated employees immediately.

Configuration

Enable enhanced session tracking in your configuration:

[auth.admin.session]
cookie_name = "__gw_session"
duration_secs = 604800  # 7 days

[auth.admin.session.enhanced]
enabled = true                    # Enable session tracking
track_devices = true              # Store user agent and device info
max_concurrent_sessions = 3       # Limit sessions per user (0 = unlimited)
inactivity_timeout_secs = 28800   # Auto-logout after 8 hours idle

Without enhanced session management enabled, the admin UI shows a notice explaining that session tracking is not available. Sessions will still work, but listing and revocation are not possible.

Session limit is eventually consistent. The max_concurrent_sessions limit is checked after session creation, not before. During login, users may briefly exceed the limit by one session (e.g., max + 1 sessions) before the oldest session is evicted. This is a deliberate design choice for performance: it avoids holding locks during the OAuth callback flow. Eviction is oldest-first and best-effort.

Listing User Sessions

View all active sessions for a user:

curl http://localhost:8080/admin/v1/users/{user_id}/sessions \
  -H "Authorization: Bearer $ADMIN_KEY"

Response includes device information and timestamps:

{
  "data": [
    {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "device": {
        "device_description": "Chrome 120 on Windows 11",
        "ip_address": "192.168.1.100"
      },
      "created_at": "2024-01-15T10:30:00Z",
      "last_activity": "2024-01-15T14:22:00Z",
      "expires_at": "2024-01-22T10:30:00Z"
    }
  ],
  "enhanced_enabled": true
}

Force Logout All Sessions

Revoke all sessions for a user (e.g., terminated employee):

curl -X DELETE http://localhost:8080/admin/v1/users/{user_id}/sessions \
  -H "Authorization: Bearer $ADMIN_KEY"

Response:

{
  "sessions_revoked": 3
}

Revoke Single Session

Revoke a specific session:

curl -X DELETE http://localhost:8080/admin/v1/users/{user_id}/sessions/{session_id} \
  -H "Authorization: Bearer $ADMIN_KEY"

Admin UI

The Sessions tab in the User Detail page provides a visual interface for session management:

  • View all active sessions with device info and timestamps
  • Revoke individual sessions with one click
  • Force logout all sessions with confirmation dialog

Access it at /admin/users/{user_id} and select the "Sessions" tab.

GDPR Compliance

Hadrian includes built-in support for GDPR data subject requests.

Data Export

Export all data for a user:

GET /admin/v1/users/{user_id}/export

Returns:

  • User profile
  • Organization/team/project memberships
  • API keys (without secrets)
  • Conversations
  • Active sessions (when enhanced session management is enabled)
  • Usage summary
  • Audit logs

Data Deletion

Hard delete a user and all associated data:

DELETE /admin/v1/users/{user_id}?hard=true

This cascades to:

  • API keys
  • Conversations
  • Dynamic providers
  • Usage records
  • Membership records

Hard deletion is irreversible. Use soft deletion (DELETE without ?hard=true) for recoverable removal.

API Reference

Organizations

MethodEndpointDescription
POST/admin/v1/organizationsCreate organization
GET/admin/v1/organizations/{slug}Get organization
PATCH/admin/v1/organizations/{slug}Update organization
DELETE/admin/v1/organizations/{slug}Delete organization
GET/admin/v1/organizations/{slug}/membersList members
POST/admin/v1/organizations/{slug}/membersAdd member

Teams

MethodEndpointDescription
POST/admin/v1/organizations/{org}/teamsCreate team
GET/admin/v1/organizations/{org}/teams/{slug}Get team
PATCH/admin/v1/organizations/{org}/teams/{slug}Update team
DELETE/admin/v1/organizations/{org}/teams/{slug}Delete team

Service Accounts

MethodEndpointDescription
POST/admin/v1/organizations/{org}/service-accountsCreate service account
GET/admin/v1/organizations/{org}/service-accountsList service accounts
GET/admin/v1/organizations/{org}/service-accounts/{slug}Get service account
PATCH/admin/v1/organizations/{org}/service-accounts/{slug}Update service account
DELETE/admin/v1/organizations/{org}/service-accounts/{slug}Delete service account
GET/admin/v1/organizations/{org}/service-accounts/{slug}/api-keysList service account API keys

Projects

MethodEndpointDescription
POST/admin/v1/organizations/{org}/projectsCreate project
GET/admin/v1/organizations/{org}/projects/{slug}Get project
PATCH/admin/v1/organizations/{org}/projects/{slug}Update project
DELETE/admin/v1/organizations/{org}/projects/{slug}Delete project

Users

MethodEndpointDescription
GET/admin/v1/usersList users
GET/admin/v1/users/{id}Get user
GET/admin/v1/users/{id}/exportExport user data
DELETE/admin/v1/users/{id}Delete user
GET/admin/v1/users/{id}/sessionsList user sessions
DELETE/admin/v1/users/{id}/sessionsRevoke all sessions
DELETE/admin/v1/users/{id}/sessions/{sid}Revoke one session

On this page