The auth service uses a dedicated auth schema in the existing Postgres instance. All tables are created by migration 004_auth_schema.sql.
Entity Relationship Diagram
┌──────────────────┐
│ public.customers │
└────────┬─────────┘
│ 1:N
│
┌────────▼─────────┐ ┌──────────────────┐
│ signing_keys │ │ app_tokens │
│ (ES256 keys) │ │ (qt_app_*) │
└────────┬─────────┘ └────────┬──────────┘
│ │
│ ┌────────────────────┘
│ │ FK: app_token_id
│ │ FK: signing_key_id
┌────▼────▼────┐
│ bearer_tokens │
│ (qt_bearer_*) │
└───────┬───────┘
│ FK: bearer_token_id
┌───────▼───────┐
│ agent_tokens │
│ (qt_agent_*) │
└───┬───────┬───┘
│ │ FK: parent_agent_token_id
│ ┌────▼──────────┐
│ │subagent_tokens│
│ │(qt_subagent_*)│
│ └───────┬───────┘
│ │
┌────▼──────────▼────┐
│ session_tokens │
│ (qt_session_*) │
└────────────────────┘
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ webhook_keys │ │ override_tokens │ │ revocation_log │
│ (per customer) │ │ (qt_override_*) │ │ (append-only) │
└──────────────────┘ └──────────────────┘ └──────────────────┘
┌──────────────────┐
│ audit_log │
│ (auth events) │
└──────────────────┘
Tables
auth.signing_keys
ES256 key pairs per customer. Private keys are encrypted at rest with AES-256-GCM.
| Column | Type | Notes |
|---|
id | UUID | PK, auto-generated |
customer_id | UUID | FK to public.customers(id) |
key_type | TEXT | Always 'ES256' (CHECK constraint) |
public_key | TEXT | PEM-encoded EC public key |
encrypted_private_key | TEXT | Base64(nonce + AES-256-GCM ciphertext) |
is_active | BOOLEAN | Only one active key per customer |
created_at | TIMESTAMPTZ | Auto-set |
rotated_at | TIMESTAMPTZ | Set when deactivated during rotation |
auth.app_tokens
Management-plane tokens. Stored as SHA-256 hashes (not JWTs).
| Column | Type | Notes |
|---|
id | UUID | PK |
customer_id | UUID | FK to public.customers(id) |
token_hash | TEXT | SHA-256 hash, UNIQUE |
name | TEXT | Human-readable label |
scopes | JSONB | List of allowed management scopes |
is_revoked | BOOLEAN | Soft delete |
created_at | TIMESTAMPTZ | |
expires_at | TIMESTAMPTZ | NULL = no expiry |
last_used_at | TIMESTAMPTZ | Updated on each use |
auth.bearer_tokens
Data-plane root tokens. JWTs signed with customer’s ES256 key.
| Column | Type | Notes |
|---|
id | UUID | PK |
customer_id | UUID | FK to public.customers(id) |
jti | TEXT | JWT ID, UNIQUE |
app_token_id | UUID | FK to auth.app_tokens(id) |
signing_key_id | UUID | FK to auth.signing_keys(id) |
environment | TEXT | CHECK: development, staging, production |
is_revoked | BOOLEAN | |
created_at | TIMESTAMPTZ | |
expires_at | TIMESTAMPTZ | NOT NULL |
auth.agent_tokens
Per-agent identity with embedded RBAC policy.
| Column | Type | Notes |
|---|
id | UUID | PK |
customer_id | UUID | FK to public.customers(id) |
jti | TEXT | JWT ID, UNIQUE |
bearer_token_id | UUID | FK to auth.bearer_tokens(id) |
agent_id | TEXT | Caller-defined agent identifier |
agent_name | TEXT | Optional human-readable name |
rbac | JSONB | RBACPolicy structure |
is_revoked | BOOLEAN | |
created_at | TIMESTAMPTZ | |
expires_at | TIMESTAMPTZ | NOT NULL |
auth.subagent_tokens
Delegated sub-agent tokens with permission narrowing.
| Column | Type | Notes |
|---|
id | UUID | PK |
customer_id | UUID | FK to public.customers(id) |
jti | TEXT | JWT ID, UNIQUE |
parent_agent_token_id | UUID | FK to auth.agent_tokens(id) |
agent_id | TEXT | Sub-agent identifier |
agent_name | TEXT | Optional |
delegation_depth | INT | CHECK: >= 1 |
rbac | JSONB | Must be subset of parent’s RBAC |
is_revoked | BOOLEAN | |
created_at | TIMESTAMPTZ | |
expires_at | TIMESTAMPTZ | NOT NULL |
auth.session_tokens
Short-lived session binding with event counting.
| Column | Type | Notes |
|---|
id | UUID | PK |
customer_id | UUID | FK to public.customers(id) |
jti | TEXT | JWT ID, UNIQUE |
parent_token_id | UUID | Agent or subagent token UUID |
parent_token_type | TEXT | CHECK: agent or subagent |
session_id | TEXT | Caller-defined session ID |
max_events | INT | Default: 1000 |
event_count | INT | Tracked in Redis, persisted on close |
is_revoked | BOOLEAN | |
created_at | TIMESTAMPTZ | |
expires_at | TIMESTAMPTZ | NOT NULL |
auth.webhook_keys
Outbound webhook signing secrets.
| Column | Type | Notes |
|---|
id | UUID | PK |
customer_id | UUID | FK to public.customers(id) |
endpoint_url | TEXT | |
secret_hash | TEXT | SHA-256 hash of the signing secret |
is_active | BOOLEAN | |
created_at | TIMESTAMPTZ | |
auth.override_tokens
Ephemeral single-use override tokens.
| Column | Type | Notes |
|---|
id | UUID | PK |
customer_id | UUID | FK to public.customers(id) |
jti | TEXT | JWT ID, UNIQUE |
event_id | UUID | The held event being decided on |
allowed_decisions | JSONB | e.g. ["approve", "reject"] |
max_uses | INT | Default: 1 |
use_count | INT | Incremented on each decision submission |
reason | TEXT | Optional context for the hold |
is_revoked | BOOLEAN | |
created_at | TIMESTAMPTZ | |
expires_at | TIMESTAMPTZ | NOT NULL |
auth.revocation_log
Append-only log of all revoked tokens. Used to rebuild the bloom filter.
This table has no RLS — it needs cross-customer access for bloom filter rebuild.
| Column | Type | Notes |
|---|
id | BIGSERIAL | PK |
jti | TEXT | Revoked token’s JTI |
token_type | TEXT | CHECK: app, bearer, agent, subagent, session, override |
customer_id | UUID | |
reason | TEXT | Optional |
revoked_by | TEXT | Who triggered the revocation |
created_at | TIMESTAMPTZ | |
auth.audit_log
Auth-specific audit trail.
| Column | Type | Notes |
|---|
id | BIGSERIAL | PK |
customer_id | UUID | |
action | TEXT | e.g. token_created, key_rotated |
token_type | TEXT | Optional |
target_jti | TEXT | JTI of the affected token |
actor_jti | TEXT | JTI of the token used to perform the action |
ip_address | TEXT | Optional |
metadata | JSONB | Additional context |
created_at | TIMESTAMPTZ | |
Indexes
| Index | Table | Columns | Condition |
|---|
idx_auth_signing_keys_customer | signing_keys | customer_id | WHERE is_active = TRUE |
idx_auth_app_tokens_customer | app_tokens | customer_id | WHERE NOT is_revoked |
idx_auth_app_tokens_hash | app_tokens | token_hash | |
idx_auth_bearer_tokens_jti | bearer_tokens | jti | |
idx_auth_bearer_tokens_customer | bearer_tokens | customer_id | WHERE NOT is_revoked |
idx_auth_agent_tokens_jti | agent_tokens | jti | |
idx_auth_agent_tokens_customer | agent_tokens | customer_id | WHERE NOT is_revoked |
idx_auth_subagent_tokens_jti | subagent_tokens | jti | |
idx_auth_subagent_tokens_customer | subagent_tokens | customer_id | WHERE NOT is_revoked |
idx_auth_subagent_tokens_parent | subagent_tokens | parent_agent_token_id | |
idx_auth_session_tokens_jti | session_tokens | jti | |
idx_auth_session_tokens_customer | session_tokens | customer_id | WHERE NOT is_revoked |
idx_auth_webhook_keys_customer | webhook_keys | customer_id | WHERE is_active = TRUE |
idx_auth_override_tokens_jti | override_tokens | jti | |
idx_auth_override_tokens_event | override_tokens | event_id | WHERE NOT is_revoked |
idx_auth_revocation_log_jti | revocation_log | jti | |
idx_auth_audit_log_customer | audit_log | customer_id, created_at DESC | |
All partial indexes use WHERE NOT is_revoked or WHERE is_active = TRUE to skip irrelevant rows.
Row-Level Security
RLS is enabled on all tables except revocation_log (needs cross-customer access for bloom filter rebuild).
Policy pattern (same as the public schema):
CREATE POLICY customer_isolation_{table} ON auth.{table}
USING (customer_id = current_setting('app.current_customer_id')::uuid);
The session factory sets app.current_customer_id via:
SET LOCAL app.current_customer_id = '{customer_id}';
Admin sessions bypass RLS by not setting this variable (uses the quint superuser role).