Architecture¶
System Overview¶
The Sentinel Auth is a centralized authentication and authorization platform that sits between your applications and external identity providers. It handles user lifecycle, workspace tenancy, group management, and a Zanzibar-style permission system -- all exposed through a REST API and a companion Python SDK.
graph TB
subgraph Clients
Browser[Browser / Frontend]
App[Consuming Application]
end
subgraph Identity Service
API[FastAPI API Server<br/>Port 9003]
Admin[Admin Panel<br/>Port 9004]
end
subgraph Identity Providers
Google[Google OIDC]
GitHub[GitHub OAuth2]
Entra[Microsoft Entra ID]
end
subgraph Data Stores
PG[(PostgreSQL 16)]
Redis[(Redis 7)]
end
Browser -->|OAuth login| API
Browser -->|Admin UI| Admin
Admin -->|Admin API calls| API
App -->|SDK / REST<br/>X-Service-Key + JWT| API
API -->|OAuth2 / OIDC| Google
API -->|OAuth2| GitHub
API -->|OIDC| Entra
API -->|Async queries| PG
API -->|Token denylist<br/>Refresh families<br/>Auth codes| Redis
Design Decisions¶
D1: Authlib over raw OAuth2¶
We use Authlib as the OAuth2/OIDC client library rather than building OAuth flows from scratch. Authlib handles PKCE challenge generation, OIDC discovery, token exchange, and ID token validation out of the box. This lets us focus on business logic (user matching, token issuance, workspace context) rather than protocol plumbing.
D2: PostgreSQL for relational data¶
PostgreSQL 16 is the single source of truth for users, workspaces, groups, and permissions. Its strong consistency guarantees, UUID support, JSONB columns (for provider metadata), and mature async driver (asyncpg) make it the natural choice for an identity store where correctness matters more than write throughput.
D3: Soft isolation via workspace_id¶
Rather than provisioning a separate database per tenant, we use a workspace_id foreign key on all tenant-scoped tables. Every query filters by workspace_id -- enforced at the service layer and validated against the JWT. This approach keeps the operational footprint simple (one database, one connection pool) while still providing logical tenant isolation.
D4: Stateless JWT with Redis-backed revocation¶
Access tokens are stateless RS256 JWTs containing the full user context (user ID, workspace, role, groups). This means consuming applications can validate tokens locally using the public key without calling the Identity Service on every request. For the minority of cases where a token must be revoked before expiry (logout, password change), we maintain a lightweight jti denylist in Redis with automatic TTL expiration.
D5: Custom build, not Keycloak¶
We chose to build a purpose-built identity service rather than deploying Keycloak, Ory, or another off-the-shelf solution. The reasons:
- Full control over the permission model (Zanzibar-style entity ACLs are not a standard Keycloak feature)
- Minimal footprint -- a single Python process rather than a JVM-based application server
- Deep integration with our workspace and group model, which maps directly to our multi-tenant product architecture
- SDK-first design -- the Python SDK is a first-class citizen, not an afterthought
Tech Stack¶
| Component | Technology | Purpose |
|---|---|---|
| Web framework | FastAPI | Async HTTP server with OpenAPI docs |
| OAuth2/OIDC | Authlib | Provider integration, PKCE, token exchange |
| JWT signing | PyJWT | RS256 token creation and validation |
| ORM | SQLAlchemy 2.0 (async) | Database models and queries |
| DB driver | asyncpg | High-performance async PostgreSQL driver |
| Migrations | Alembic | Schema versioning and migration |
| Validation | Pydantic v2 | Request/response schemas |
| Cache/state | Redis 7 | Token denylist, refresh token families |
| Logging | structlog | Structured JSON logging |
| Rate limiting | slowapi | Per-endpoint rate limits |
Entity-Relationship Diagram¶
erDiagram
users {
uuid id PK
text email UK
text name
text avatar_url
bool is_active
bool is_admin
timestamptz created_at
timestamptz updated_at
}
social_accounts {
uuid id PK
uuid user_id FK
text provider
text provider_user_id
jsonb provider_data
}
workspaces {
uuid id PK
text slug UK
text name
text description
uuid created_by FK
timestamptz created_at
}
workspace_memberships {
uuid id PK
uuid workspace_id FK
uuid user_id FK
text role
timestamptz joined_at
}
groups {
uuid id PK
uuid workspace_id FK
text name
text description
uuid created_by FK
timestamptz created_at
}
group_memberships {
uuid id PK
uuid group_id FK
uuid user_id FK
timestamptz added_at
}
resource_permissions {
uuid id PK
text service_name
text resource_type
uuid resource_id
uuid workspace_id FK
uuid owner_id FK
text visibility
timestamptz created_at
}
resource_shares {
uuid id PK
uuid resource_permission_id FK
text grantee_type
uuid grantee_id
text permission
uuid granted_by FK
timestamptz granted_at
}
service_actions {
uuid id PK
text service_name
text action
text description
timestamptz created_at
}
roles {
uuid id PK
uuid workspace_id FK
text name
text description
uuid created_by FK
timestamptz created_at
}
role_actions {
uuid id PK
uuid role_id FK
uuid service_action_id FK
}
user_roles {
uuid id PK
uuid user_id FK
uuid role_id FK
uuid assigned_by FK
timestamptz assigned_at
}
client_apps {
uuid id PK
text name
text[] redirect_uris
bool is_active
uuid created_by FK
timestamptz created_at
timestamptz updated_at
}
service_apps {
uuid id PK
text name
text service_name
text key_hash
text key_prefix
bool is_active
timestamptz last_used_at
uuid created_by FK
timestamptz created_at
timestamptz updated_at
}
activity_logs {
uuid id PK
text action
uuid actor_id FK
text target_type
uuid target_id
uuid workspace_id FK
jsonb detail
timestamptz created_at
}
users ||--o{ client_apps : "created"
users ||--o{ service_apps : "created"
users ||--o{ activity_logs : "performed"
users ||--o{ social_accounts : "has"
users ||--o{ workspace_memberships : "belongs to"
users ||--o{ group_memberships : "belongs to"
users ||--o{ user_roles : "assigned"
workspaces ||--o{ workspace_memberships : "has members"
workspaces ||--o{ groups : "contains"
workspaces ||--o{ roles : "defines"
workspaces ||--o{ resource_permissions : "scopes"
groups ||--o{ group_memberships : "has members"
roles ||--o{ role_actions : "grants"
roles ||--o{ user_roles : "assigned to"
service_actions ||--o{ role_actions : "used in"
resource_permissions ||--o{ resource_shares : "grants"
users ||--o{ resource_permissions : "owns"
Key constraints¶
social_accountshas a unique constraint on(provider, provider_user_id)-- a provider account maps to exactly one user.workspace_membershipshas a unique constraint on(workspace_id, user_id)and a check constraint limitingroletoowner,admin,editor, orviewer.groupshas a unique constraint on(workspace_id, name)-- group names are unique within a workspace.resource_permissionshas a unique constraint on(service_name, resource_type, resource_id)-- each resource is registered exactly once.resource_shareshas a unique constraint on(resource_permission_id, grantee_type, grantee_id)-- a grantee gets at most one share per resource.service_actionshas a unique constraint on(service_name, action)-- each action is registered exactly once per service.roleshas a unique constraint on(workspace_id, name)-- role names are unique within a workspace.role_actionshas a unique constraint on(role_id, service_action_id)-- an action can only be added to a role once.user_roleshas a unique constraint on(user_id, role_id)-- a user can only be assigned to a role once.