Database Schema Reference¶
Complete technical reference for the SaaS LiteLLM database schema. This system uses PostgreSQL with a multi-tenant architecture supporting organizations, teams, jobs, credits, and model group management.
Overview¶
The database schema is organized into several functional areas:
- Multi-tenancy: Organizations and teams hierarchy
- Job Tracking: Jobs, LLM calls, and cost summaries
- Credit System: Credit allocation, usage tracking, and transactions
- Model Groups: Dynamic model routing with access control
- Analytics: Team usage summaries and webhook registrations
Schema Diagram¶
organizations
|
+-- team_credits (1:N)
| |
| +-- credit_transactions (1:N)
|
+-- jobs (1:N)
|
+-- llm_calls (1:N)
+-- job_cost_summaries (1:1)
+-- credit_transactions (1:N)
model_groups
|
+-- model_group_models (1:N)
+-- team_model_groups (N:M) -- teams
Core Tables¶
organizations¶
Represents top-level organizational entities in the multi-tenant hierarchy.
Columns:
| Column | Type | Constraints | Description |
|---|---|---|---|
organization_id | VARCHAR(255) | PRIMARY KEY | Unique organization identifier |
name | VARCHAR(500) | NOT NULL | Organization display name |
status | VARCHAR(50) | DEFAULT 'active' | Status: active, suspended, deleted |
metadata | JSONB | DEFAULT '{}' | Custom organization metadata |
created_at | TIMESTAMP | NOT NULL, DEFAULT NOW() | Creation timestamp |
updated_at | TIMESTAMP | NOT NULL, DEFAULT NOW() | Last update timestamp (auto-updated) |
Indexes:
idx_org_statuson(status)idx_org_createdon(created_at)
Triggers:
update_organizations_updated_at- Automatically updatesupdated_aton row modification
Example:
INSERT INTO organizations (organization_id, name, metadata)
VALUES ('org-acme', 'Acme Corporation', '{"industry": "technology"}');
team_credits¶
Tracks credit allocation and usage for teams. Credits are the billing unit where 1 credit = 1 successfully completed job.
Columns:
| Column | Type | Constraints | Description |
|---|---|---|---|
team_id | VARCHAR(255) | PRIMARY KEY | Team identifier |
organization_id | VARCHAR(255) | FOREIGN KEY → organizations | Parent organization |
credits_allocated | INTEGER | DEFAULT 0 | Total credits allocated to team |
credits_used | INTEGER | DEFAULT 0 | Credits consumed by completed jobs |
credits_remaining | INTEGER | COMPUTED STORED | Auto-calculated: allocated - used |
credit_limit | INTEGER | NULLABLE | Optional hard limit (NULL = unlimited) |
auto_refill | BOOLEAN | DEFAULT FALSE | Enable automatic credit refills |
refill_amount | INTEGER | NULLABLE | Amount to refill when auto_refill enabled |
refill_period | VARCHAR(50) | NULLABLE | Refill frequency: 'daily', 'weekly', 'monthly' |
last_refill_at | TIMESTAMP | NULLABLE | Last automatic refill timestamp |
virtual_key | VARCHAR(500) | NULLABLE | LiteLLM virtual API key for this team |
created_at | TIMESTAMP | NOT NULL, DEFAULT NOW() | Creation timestamp |
updated_at | TIMESTAMP | NOT NULL, DEFAULT NOW() | Last update timestamp |
Indexes:
idx_team_credits_orgon(organization_id)idx_team_credits_remainingon(credits_remaining)idx_team_credits_virtual_keyon(virtual_key)
Computed Column:
The credits_remaining column is a generated stored column:
This ensures consistency and prevents manual manipulation.
Triggers:
update_team_credits_updated_at- Auto-updatesupdated_aton modification
Budget Modes:
- Fixed Budget:
credit_limitset,auto_refill = FALSE - Team has a fixed allocation
-
Cannot use more than
credits_allocated -
Unlimited Budget:
credit_limit = NULL - No hard limit on usage
-
Useful for enterprise customers
-
Recurring Budget:
auto_refill = TRUE,refill_amountandrefill_periodset - Automatically refills credits on schedule
- Useful for subscription-based billing
Example:
-- Create team with 1000 credits
INSERT INTO team_credits (team_id, organization_id, credits_allocated, virtual_key)
VALUES ('team-alpha', 'org-acme', 1000, 'sk-xxxxxxxxxxxxx');
-- Setup monthly auto-refill
UPDATE team_credits
SET auto_refill = TRUE,
refill_amount = 1000,
refill_period = 'monthly',
last_refill_at = NOW()
WHERE team_id = 'team-alpha';
credit_transactions¶
Audit log of all credit operations (allocations, deductions, refunds, adjustments).
Columns:
| Column | Type | Constraints | Description |
|---|---|---|---|
transaction_id | UUID | PRIMARY KEY, DEFAULT gen_random_uuid() | Unique transaction identifier |
team_id | VARCHAR(255) | NOT NULL | Team the transaction applies to |
organization_id | VARCHAR(255) | NULLABLE | Organization for reporting |
job_id | UUID | FOREIGN KEY → jobs, NULLABLE | Associated job (for deductions) |
transaction_type | VARCHAR(50) | NOT NULL | Type: 'deduction', 'allocation', 'refund', 'adjustment' |
credits_amount | INTEGER | NOT NULL | Number of credits (positive for additions, negative for deductions) |
credits_before | INTEGER | NOT NULL | Balance before transaction |
credits_after | INTEGER | NOT NULL | Balance after transaction |
reason | VARCHAR(500) | NULLABLE | Human-readable reason for transaction |
created_at | TIMESTAMP | NOT NULL, DEFAULT NOW() | Transaction timestamp |
Indexes:
idx_credit_transactions_teamon(team_id, created_at)idx_credit_transactions_jobon(job_id)idx_credit_transactions_orgon(organization_id, created_at)idx_credit_transactions_typeon(transaction_type)
Transaction Types:
- deduction: Credit removed for successful job completion
- allocation: New credits added to team
- refund: Credit returned (e.g., job failed after deduction)
- adjustment: Manual credit correction by admin
Example:
-- Deduction for completed job
INSERT INTO credit_transactions (
team_id, organization_id, job_id, transaction_type,
credits_amount, credits_before, credits_after, reason
) VALUES (
'team-alpha', 'org-acme', 'job-uuid-123', 'deduction',
1, 1000, 999, 'Job document_analysis completed successfully'
);
jobs¶
Represents a business operation that may involve one or more LLM calls. Jobs are the core billing unit.
Columns:
| Column | Type | Constraints | Description |
|---|---|---|---|
job_id | UUID | PRIMARY KEY, DEFAULT gen_random_uuid() | Unique job identifier |
team_id | VARCHAR(255) | NOT NULL, INDEX | Team that owns this job |
user_id | VARCHAR(255) | NULLABLE, INDEX | User who initiated the job |
job_type | VARCHAR(100) | NOT NULL, INDEX | Job category (e.g., 'chat', 'document_analysis') |
status | job_status | NOT NULL, DEFAULT 'pending', INDEX | Current status (enum) |
created_at | TIMESTAMP | NOT NULL, DEFAULT NOW(), INDEX | Job creation time |
started_at | TIMESTAMP | NULLABLE | When job moved to 'in_progress' |
completed_at | TIMESTAMP | NULLABLE | When job finished (success or failure) |
job_metadata | JSONB | DEFAULT '{}' | Custom application-specific data |
error_message | VARCHAR(1000) | NULLABLE | Error description if failed |
organization_id | VARCHAR(255) | FOREIGN KEY → organizations, INDEX | Parent organization |
external_task_id | VARCHAR(255) | NULLABLE, INDEX | Reference to external system task ID |
credit_applied | BOOLEAN | DEFAULT FALSE | Whether credit was deducted |
model_groups_used | TEXT[] | DEFAULT '{}' | Array of model group names used |
Status Enum:
CREATE TYPE job_status AS ENUM (
'pending', -- Job created but not started
'in_progress', -- Job has started processing
'completed', -- Job finished successfully
'failed', -- Job encountered errors
'cancelled' -- Job was cancelled by user
);
Indexes:
idx_team_createdon(team_id, created_at)idx_team_statuson(team_id, status)idx_job_type_createdon(job_type, created_at)idx_statuson(status)idx_jobs_organizationon(organization_id, created_at)idx_jobs_external_taskon(external_task_id)idx_jobs_credit_appliedon(credit_applied)
Credit Deduction Rules:
Credits are deducted when: 1. status = 'completed' (not 'failed' or 'cancelled') 2. All associated LLM calls succeeded (no errors) 3. credit_applied = FALSE (prevents double-charging)
Example:
-- Create job
INSERT INTO jobs (team_id, job_type, organization_id, external_task_id, job_metadata)
VALUES (
'team-alpha',
'resume_analysis',
'org-acme',
'task-789',
'{"resume_id": "res-456", "priority": "high"}'
);
-- Update to in_progress
UPDATE jobs
SET status = 'in_progress', started_at = NOW()
WHERE job_id = 'job-uuid-123';
-- Complete job
UPDATE jobs
SET status = 'completed',
completed_at = NOW(),
credit_applied = TRUE
WHERE job_id = 'job-uuid-123';
llm_calls¶
Individual LLM API calls within a job. Tracks costs, performance, and usage metrics.
Columns:
| Column | Type | Constraints | Description |
|---|---|---|---|
call_id | UUID | PRIMARY KEY, DEFAULT gen_random_uuid() | Unique call identifier |
job_id | UUID | FOREIGN KEY → jobs ON DELETE CASCADE, INDEX | Parent job |
litellm_request_id | VARCHAR(255) | UNIQUE, NULLABLE | LiteLLM's internal request ID |
model_used | VARCHAR(100) | NULLABLE | Actual model that handled the request |
prompt_tokens | INTEGER | DEFAULT 0 | Input tokens consumed |
completion_tokens | INTEGER | DEFAULT 0 | Output tokens generated |
total_tokens | INTEGER | DEFAULT 0 | Sum of prompt + completion tokens |
cost_usd | NUMERIC(10,6) | DEFAULT 0.0 | Actual USD cost from LLM provider |
latency_ms | INTEGER | NULLABLE | Request latency in milliseconds |
created_at | TIMESTAMP | NOT NULL, DEFAULT NOW(), INDEX | Call timestamp |
purpose | VARCHAR(200) | NULLABLE | Description of call purpose |
request_data | JSONB | NULLABLE | Full request payload (for debugging) |
response_data | JSONB | NULLABLE | Full response payload (for debugging) |
error | VARCHAR(1000) | NULLABLE | Error message if call failed |
model_group_used | VARCHAR(100) | NULLABLE, INDEX | Model group name requested |
resolved_model | VARCHAR(200) | NULLABLE, INDEX | Primary model resolved from group |
Indexes:
idx_job_id_createdon(job_id, created_at)idx_created_aton(created_at)idx_llm_calls_model_groupon(model_group_used)idx_llm_calls_resolved_modelon(resolved_model)
Cost Calculation:
The cost_usd field stores the actual cost charged by the LLM provider (via LiteLLM). This is separate from the credit system (1 credit per job), allowing you to track both: - Internal billing (credits) - External costs (actual USD from OpenAI/Anthropic/etc.)
Example:
INSERT INTO llm_calls (
job_id, model_used, model_group_used, resolved_model,
prompt_tokens, completion_tokens, total_tokens,
cost_usd, latency_ms, purpose
) VALUES (
'job-uuid-123',
'gpt-4-turbo-2024-04-09',
'ResumeAgent',
'gpt-4-turbo',
1250,
450,
1700,
0.034000,
2340,
'Resume skills extraction'
);
job_cost_summaries¶
Aggregated cost and performance metrics per job. Generated when job completes.
Columns:
| Column | Type | Constraints | Description |
|---|---|---|---|
job_id | UUID | PRIMARY KEY, FOREIGN KEY → jobs ON DELETE CASCADE | Job identifier |
total_calls | INTEGER | DEFAULT 0 | Total number of LLM calls |
successful_calls | INTEGER | DEFAULT 0 | Calls that succeeded |
failed_calls | INTEGER | DEFAULT 0 | Calls that failed with errors |
total_prompt_tokens | INTEGER | DEFAULT 0 | Sum of all prompt tokens |
total_completion_tokens | INTEGER | DEFAULT 0 | Sum of all completion tokens |
total_tokens | INTEGER | DEFAULT 0 | Sum of all tokens |
total_cost_usd | NUMERIC(12,6) | DEFAULT 0.0 | Total actual USD cost |
avg_latency_ms | INTEGER | NULLABLE | Average latency across all calls |
total_duration_seconds | INTEGER | NULLABLE | Job duration (completed_at - created_at) |
calculated_at | TIMESTAMP | NOT NULL, DEFAULT NOW() | Summary calculation timestamp |
Example:
INSERT INTO job_cost_summaries (
job_id, total_calls, successful_calls, failed_calls,
total_prompt_tokens, total_completion_tokens, total_tokens,
total_cost_usd, avg_latency_ms, total_duration_seconds
) VALUES (
'job-uuid-123',
3, 3, 0,
3750, 1350, 5100,
0.102000,
2180,
15
);
team_usage_summaries¶
Aggregated team usage analytics by period (daily or monthly).
Columns:
| Column | Type | Constraints | Description |
|---|---|---|---|
id | UUID | PRIMARY KEY, DEFAULT gen_random_uuid() | Summary identifier |
team_id | VARCHAR(255) | NOT NULL, INDEX | Team identifier |
period | VARCHAR(50) | NOT NULL | Period string: "2024-10" or "2024-10-15" |
period_type | VARCHAR(20) | DEFAULT 'monthly' | Type: 'daily' or 'monthly' |
total_jobs | INTEGER | DEFAULT 0 | Total jobs in period |
successful_jobs | INTEGER | DEFAULT 0 | Completed jobs |
failed_jobs | INTEGER | DEFAULT 0 | Failed jobs |
cancelled_jobs | INTEGER | DEFAULT 0 | Cancelled jobs |
total_cost_usd | NUMERIC(12,2) | DEFAULT 0.0 | Total actual costs |
total_tokens | INTEGER | DEFAULT 0 | Total tokens consumed |
job_type_breakdown | JSONB | DEFAULT '{}' | Per-job-type statistics |
calculated_at | TIMESTAMP | NOT NULL, DEFAULT NOW() | Calculation timestamp |
Indexes:
idx_team_periodon(team_id, period)UNIQUE
Job Type Breakdown Format:
{
"resume_analysis": {
"count": 150,
"cost_usd": 12.45
},
"document_parsing": {
"count": 320,
"cost_usd": 8.90
}
}
Example:
INSERT INTO team_usage_summaries (
team_id, period, period_type,
total_jobs, successful_jobs, failed_jobs,
total_cost_usd, total_tokens, job_type_breakdown
) VALUES (
'team-alpha',
'2024-10',
'monthly',
470, 455, 15,
21.35,
1250000,
'{"resume_analysis": {"count": 150, "cost_usd": 12.45}, "document_parsing": {"count": 320, "cost_usd": 8.90}}'
);
Model Group Tables¶
model_groups¶
Defines named groups of models with primary and fallback configurations (e.g., "ResumeAgent", "ParsingAgent").
Columns:
| Column | Type | Constraints | Description |
|---|---|---|---|
model_group_id | UUID | PRIMARY KEY, DEFAULT gen_random_uuid() | Unique group identifier |
group_name | VARCHAR(100) | UNIQUE, NOT NULL | Group name (e.g., "ResumeAgent") |
display_name | VARCHAR(200) | NULLABLE | Human-readable name |
description | TEXT | NULLABLE | Group description |
status | VARCHAR(50) | DEFAULT 'active' | Status: active, inactive |
created_at | TIMESTAMP | NOT NULL, DEFAULT NOW() | Creation timestamp |
updated_at | TIMESTAMP | NOT NULL, DEFAULT NOW() | Last update timestamp |
Indexes:
idx_model_group_nameon(group_name)idx_model_group_statuson(status)
Triggers:
update_model_groups_updated_at- Auto-updatesupdated_at
Example:
INSERT INTO model_groups (group_name, display_name, description)
VALUES (
'ResumeAgent',
'Resume Analysis Agent',
'High-quality model for resume parsing and skills extraction'
);
model_group_models¶
Maps models to groups with priority for fallback handling.
Columns:
| Column | Type | Constraints | Description |
|---|---|---|---|
id | UUID | PRIMARY KEY, DEFAULT gen_random_uuid() | Unique mapping identifier |
model_group_id | UUID | FOREIGN KEY → model_groups ON DELETE CASCADE, NOT NULL | Parent group |
model_name | VARCHAR(200) | NOT NULL | LiteLLM model identifier |
priority | INTEGER | DEFAULT 0 | 0 = primary, 1 = first fallback, 2 = second fallback |
is_active | BOOLEAN | DEFAULT TRUE | Whether this model is currently enabled |
created_at | TIMESTAMP | NOT NULL, DEFAULT NOW() | Creation timestamp |
Indexes:
idx_model_group_models_lookupon(model_group_id, priority, is_active)idx_model_group_models_groupon(model_group_id)
Priority System:
- Priority 0: Primary model (first choice)
- Priority 1: First fallback (if primary fails or unavailable)
- Priority 2: Second fallback
- Priority N: Nth fallback
Example:
-- Add primary model
INSERT INTO model_group_models (model_group_id, model_name, priority)
VALUES ('group-uuid-123', 'gpt-4-turbo', 0);
-- Add fallbacks
INSERT INTO model_group_models (model_group_id, model_name, priority)
VALUES
('group-uuid-123', 'gpt-4', 1),
('group-uuid-123', 'gpt-3.5-turbo', 2);
team_model_groups¶
Junction table mapping teams to their assigned model groups (access control).
Columns:
| Column | Type | Constraints | Description |
|---|---|---|---|
id | UUID | PRIMARY KEY, DEFAULT gen_random_uuid() | Unique assignment identifier |
team_id | VARCHAR(255) | NOT NULL | Team identifier |
model_group_id | UUID | FOREIGN KEY → model_groups ON DELETE CASCADE, NOT NULL | Model group |
assigned_at | TIMESTAMP | NOT NULL, DEFAULT NOW() | Assignment timestamp |
Indexes:
idx_team_model_groups_teamon(team_id)idx_team_model_groups_groupon(model_group_id)
Constraints:
unique_team_model_groupUNIQUE on(team_id, model_group_id)- Prevents duplicate assignments
Example:
-- Assign model group to team
INSERT INTO team_model_groups (team_id, model_group_id)
VALUES ('team-alpha', 'group-uuid-123');
Additional Tables¶
webhook_registrations¶
Webhook endpoints for job event notifications.
Columns:
| Column | Type | Constraints | Description |
|---|---|---|---|
webhook_id | UUID | PRIMARY KEY, DEFAULT gen_random_uuid() | Unique webhook identifier |
team_id | VARCHAR(255) | NOT NULL, INDEX | Team that owns webhook |
webhook_url | VARCHAR(500) | NOT NULL | Webhook endpoint URL |
events | JSONB | DEFAULT '[]' | Array of event types to trigger on |
is_active | INTEGER | DEFAULT 1 | Active status (1=active, 0=inactive) |
created_at | TIMESTAMP | NOT NULL, DEFAULT NOW() | Creation timestamp |
last_triggered_at | TIMESTAMP | NULLABLE | Last successful trigger |
auth_header | VARCHAR(500) | NULLABLE | Optional authentication header |
Indexes:
idx_webhook_team_idon(team_id)idx_webhook_activeon(is_active)
Event Types:
job.createdjob.startedjob.completedjob.failedjob.cancelled
Example:
INSERT INTO webhook_registrations (team_id, webhook_url, events, auth_header)
VALUES (
'team-alpha',
'https://api.acme.com/webhooks/llm-jobs',
'["job.completed", "job.failed"]',
'Bearer secret-webhook-token-xyz'
);
Relationships¶
Foreign Key Constraints¶
-- Organizations → Team Credits
ALTER TABLE team_credits
ADD CONSTRAINT fk_team_credits_organization
FOREIGN KEY (organization_id) REFERENCES organizations(organization_id);
-- Organizations → Jobs
ALTER TABLE jobs
ADD CONSTRAINT fk_jobs_organization
FOREIGN KEY (organization_id) REFERENCES organizations(organization_id);
-- Jobs → LLM Calls (cascade delete)
ALTER TABLE llm_calls
ADD CONSTRAINT fk_llm_calls_job
FOREIGN KEY (job_id) REFERENCES jobs(job_id) ON DELETE CASCADE;
-- Jobs → Job Cost Summaries (cascade delete)
ALTER TABLE job_cost_summaries
ADD CONSTRAINT fk_job_cost_summaries_job
FOREIGN KEY (job_id) REFERENCES jobs(job_id) ON DELETE CASCADE;
-- Jobs → Credit Transactions
ALTER TABLE credit_transactions
ADD CONSTRAINT fk_credit_transactions_job
FOREIGN KEY (job_id) REFERENCES jobs(job_id);
-- Model Groups → Model Group Models (cascade delete)
ALTER TABLE model_group_models
ADD CONSTRAINT fk_model_group_models_group
FOREIGN KEY (model_group_id) REFERENCES model_groups(model_group_id) ON DELETE CASCADE;
-- Model Groups → Team Model Groups (cascade delete)
ALTER TABLE team_model_groups
ADD CONSTRAINT fk_team_model_groups_group
FOREIGN KEY (model_group_id) REFERENCES model_groups(model_group_id) ON DELETE CASCADE;
Cascade Behavior¶
- Jobs deleted → LLM calls and cost summaries auto-deleted (cascade)
- Model groups deleted → Model assignments and team mappings auto-deleted (cascade)
- Organizations deleted → No cascade (must manually clean up teams first)
Migration Scripts¶
Migrations are located in /scripts/migrations/ and should be run in order:
001_create_job_tracking_tables.sql- Core job tracking tables002_create_organizations.sql- Organization hierarchy003_create_model_groups.sql- Model group system004_create_team_model_groups.sql- Team-model assignments005_create_credits_tables.sql- Credit system006_extend_jobs_and_llm_calls.sql- Add organization and model group fields007_add_virtual_key_to_team_credits.sql- Add virtual key column
Running Migrations:
# Run all migrations
psql -U postgres -d saas_litellm -f scripts/migrations/001_create_job_tracking_tables.sql
psql -U postgres -d saas_litellm -f scripts/migrations/002_create_organizations.sql
# ... etc
Performance Considerations¶
Index Strategy¶
All high-traffic query patterns are covered by indexes:
- Team-based queries:
idx_team_created,idx_team_status - Time-range queries:
idx_created_at,idx_org_created - Job type analytics:
idx_job_type_created - Credit lookups:
idx_team_credits_remaining,idx_team_credits_virtual_key - Model group resolution:
idx_model_group_models_lookup
Query Optimization Tips¶
Get team jobs with costs:
SELECT
j.*,
jcs.total_cost_usd,
jcs.total_tokens
FROM jobs j
LEFT JOIN job_cost_summaries jcs ON j.job_id = jcs.job_id
WHERE j.team_id = 'team-alpha'
AND j.created_at >= '2024-10-01'
ORDER BY j.created_at DESC
LIMIT 100;
Get team credit balance:
SELECT
credits_allocated,
credits_used,
credits_remaining,
virtual_key
FROM team_credits
WHERE team_id = 'team-alpha';
Resolve model group:
SELECT
mgm.model_name,
mgm.priority
FROM model_groups mg
JOIN model_group_models mgm ON mg.model_group_id = mgm.model_group_id
JOIN team_model_groups tmg ON mg.model_group_id = tmg.model_group_id
WHERE tmg.team_id = 'team-alpha'
AND mg.group_name = 'ResumeAgent'
AND mgm.is_active = TRUE
ORDER BY mgm.priority ASC;
Backup and Maintenance¶
Recommended Backup Strategy¶
# Full database backup
pg_dump -U postgres saas_litellm > backup_$(date +%Y%m%d).sql
# Backup specific tables
pg_dump -U postgres -t credit_transactions -t team_credits saas_litellm > credits_backup.sql
Data Retention¶
Consider archiving old records:
- llm_calls: Archive calls older than 90 days to separate table
- credit_transactions: Keep all records for audit compliance
- jobs: Archive completed jobs older than 1 year
- team_usage_summaries: Keep all summaries (small table)
Vacuum and Analyze¶
-- Regular maintenance
VACUUM ANALYZE jobs;
VACUUM ANALYZE llm_calls;
VACUUM ANALYZE credit_transactions;
See Also¶
- Credit System Reference - Detailed credit allocation and deduction logic
- Model Resolution Reference - Model group resolution flow
- API Reference - API endpoints that use these tables