Skip to main content

Database Schema

Arkenos uses PostgreSQL with SQLAlchemy ORM and Alembic migrations.

Dual User Table Design

Arkenos maintains two separate user-related tables. This is intentional:
TableOwned byPurpose
"user"Better AuthAuthentication only — email, password hash, sessions, email verification
"users"Arkenos backendBusiness data — agents, voice sessions, org membership, API keys all reference this table
Why two tables? The users table acts as an auth-agnostic abstraction layer. If the auth provider changes (e.g., switching from Better Auth to Auth.js or a custom solution), only the sync logic needs updating — all business data, foreign keys, and queries remain untouched. How they sync: When a user signs up via Better Auth and makes their first API request, the backend automatically creates a corresponding users row by reading from Better Auth’s "user" table. This auto-creation happens in verify_session_token() in backend/app/dependencies.py. Organization auto-creation follows the same pattern: Better Auth manages the "organization" table, and the backend auto-creates a corresponding organizations row on first access (see _auto_create_org_from_ba() in dependencies.py).

Tables

instance_settings

Encrypted API key store (instance-level, not per-user). Managed via API Keys in the dashboard.
ColumnTypeDescription
keyString(100)Primary key (e.g., livekit_api_key)
encrypted_valueTextAES-256-GCM encrypted value
updated_atDateTimeLast update time

users

Backend user records, auto-created from Better Auth on first authenticated request.
ColumnTypeDescription
idString(36)Primary key (UUID, generated by backend)
auth_idString(255)Maps to Better Auth "user".id (unique, indexed)
emailString(255)User email (unique)
nameString(255)User display name (nullable)
created_atDateTimeAccount creation time
updated_atDateTimeLast update time

agents

Voice agent configurations.
ColumnTypeDescription
idString(36)Primary key
user_idString(36)Foreign key → users
nameString(100)Agent display name
descriptionTextAgent description (nullable)
typeEnumSTT, LLM, TTS, or PIPELINE
configJSONFull agent configuration blob
phone_numberString(50)Telephony provider number (nullable)
provider_number_sidString(255)Provider number SID (nullable)
telephony_providerString(20)Telephony provider: “twilio” or “telnyx” (nullable, default: “twilio”)
is_activeBooleanSoft delete flag
agent_modeEnumSTANDARD or CUSTOM (default: STANDARD)
storage_pathString(500)MinIO storage path for custom agents (nullable)
image_tagString(255)Docker image tag for custom agents (nullable)
build_statusEnumNONE, PENDING, BUILDING, READY, FAILED
build_errorTextLast build error message (nullable)
last_build_atDateTimeLast build timestamp (nullable)
current_versionIntegerCurrent code version (default: 0)
deployed_versionIntegerLast deployed version (nullable)
created_atDateTimeCreation time
updated_atDateTimeLast update time
The config JSON column stores the complete agent configuration:
{
  "system_prompt": "...",
  "first_message": "...",
  "stt_provider": "assemblyai",
  "voice_id": "resemble-voice-uuid",
  "functions": [
    {
      "name": "get_weather",
      "description": "Get current weather",
      "endpoint": "https://api.example.com/weather",
      "method": "GET",
      "parameters": {...}
    }
  ],
  "webhooks": {
    "pre_call": "https://...",
    "post_call": "https://..."
  }
}

voice_sessions

Call session records.
ColumnTypeDescription
idString(36)Primary key
room_nameString(255)LiveKit room name (unique, indexed)
user_idString(36)Foreign key → users
agent_idString(36)Foreign key → agents (nullable)
statusEnumCREATED, ACTIVE, COMPLETED, FAILED
started_atDateTimeCall start time (nullable)
ended_atDateTimeCall end time (nullable)
durationIntegerDuration in seconds (nullable)
call_directionEnumINBOUND or OUTBOUND (nullable)
outbound_phone_numberString(50)Dialed number for outbound calls (nullable)
call_statusEnumRINGING, ANSWERED, COMPLETED, FAILED, NO_ANSWER (nullable)
callback_urlString(2048)Webhook callback URL (nullable)
session_dataJSONMetadata including call analysis
total_costDecimal(12,6)Total session cost (nullable)
cost_breakdownJSONCost breakdown by provider (nullable)
transferred_toString(50)Transfer destination number (nullable)
transfer_typeEnumWARM or COLD (nullable)
transfer_timestampDateTimeWhen transfer occurred (nullable)
created_atDateTimeRecord creation time
updated_atDateTimeLast update time

transcripts

Per-message conversation logs.
ColumnTypeDescription
idString(36)Primary key
session_idString(36)Foreign key → voice_sessions
contentTextMessage content
speakerEnumUSER or AGENT
timestampDateTimeMessage time

usage_events

Granular cost tracking events logged by the agent worker.
ColumnTypeDescription
idString(36)Primary key
session_idString(36)Foreign key → voice_sessions
user_idString(36)Foreign key → users (indexed)
agent_idString(36)Foreign key → agents (nullable, indexed)
providerString(50)Service provider (e.g., assemblyai, google, resemble)
event_typeEnumstt_minutes, llm_tokens, or tts_characters
quantityDecimal(12,4)Usage quantity
unit_costDecimal(12,8)Cost per unit
total_costDecimal(12,6)Calculated total cost in USD
created_atDateTimeEvent time (indexed)

agent_files

Files uploaded for custom agents, stored in MinIO.
ColumnTypeDescription
idString(36)Primary key
agent_idString(36)Foreign key → agents (unique with file_path)
file_pathString(500)Relative file path within agent storage
content_hashString(64)SHA-256 hash of file content (nullable)
size_bytesBigIntegerFile size in bytes
mime_typeString(100)MIME type (nullable)
versionIntegerCurrent version number
created_atDateTimeCreation time
updated_atDateTimeLast update time

agent_file_versions

Version history for custom agent files.
ColumnTypeDescription
idString(36)Primary key
agent_file_idString(36)Foreign key → agent_files
versionIntegerVersion number
content_hashString(64)SHA-256 hash (nullable)
minio_keyString(500)MinIO object key
size_bytesBigIntegerFile size in bytes
created_atDateTimeCreation time

agent_containers

Docker container tracking for custom agents.
ColumnTypeDescription
idString(36)Primary key
agent_idString(36)Foreign key → agents
session_idString(36)Foreign key → voice_sessions (nullable)
container_idString(100)Docker container ID (nullable)
container_typeString(50)Container type (default: “agent”)
statusEnumPENDING, RUNNING, STOPPED, FAILED
image_tagString(255)Docker image tag (nullable)
started_atDateTimeContainer start time (nullable)
stopped_atDateTimeContainer stop time (nullable)
error_messageTextError details (nullable)
created_atDateTimeCreation time

coding_agent_conversations

Chat conversations with the coding agent (per agent, per user).
ColumnTypeDescription
idString(36)Primary key
agent_idString(36)Foreign key → agents (indexed)
user_idString(255)Auth user ID
titleString(200)Conversation title (default: “New conversation”)
created_atDateTimeCreation time
updated_atDateTimeLast update time

coding_agent_messages

Individual messages within coding agent conversations.
ColumnTypeDescription
idString(36)Primary key
conversation_idString(36)Foreign key → coding_agent_conversations (indexed)
roleString(20)“user” or “assistant”
contentTextMessage content
file_changesJSONFile changes made by the assistant (nullable)
created_atDateTimeCreation time

organizations

Multi-tenancy: top-level tenant for resource scoping. Auto-created from Better Auth’s "organization" table on first access.
ColumnTypeDescription
idString(36)Primary key (UUID)
ba_org_idString(255)Better Auth organization ID (unique, indexed)
nameString(255)Organization display name
slugString(100)URL-safe slug (unique)
planString(50)Subscription plan (default: “free”)
stripe_customer_idString(255)Stripe customer ID (nullable)
stripe_subscription_idString(255)Stripe subscription ID (nullable)
usage_limit_minutesIntegerMonthly usage limit in minutes (default: 100)
created_atDateTimeCreation time
updated_atDateTimeLast update time

org_members

Organization membership with roles.
ColumnTypeDescription
idString(36)Primary key
org_idString(36)Foreign key → organizations (indexed)
user_idString(36)Foreign key → users (indexed)
roleString(20)Role: “owner”, “admin”, or “member” (default: “member”)
created_atDateTimeCreation time

org_api_keys

Per-organization encrypted API key storage.
ColumnTypeDescription
org_idString(36)Foreign key → organizations (composite PK)
key_nameString(100)Key identifier (composite PK)
encrypted_valueTextAES-256-GCM encrypted value
updated_atDateTimeLast update time

user_api_keys

Per-user encrypted API key storage (legacy, migrating to org_api_keys).
ColumnTypeDescription
user_idString(36)Foreign key → users (composite PK)
key_nameString(100)Key identifier (composite PK)
encrypted_valueTextAES-256-GCM encrypted value
updated_atDateTimeLast update time

Migrations

Database migrations are managed by Alembic and run automatically on backend startup:
# Run all pending migrations
alembic upgrade head

# Check current migration state
alembic current

# Create a new migration
alembic revision --autogenerate -m "description"

Fresh Deployment Notes

On a fresh deployment, the backend runs alembic upgrade head on startup to create all tables. If migrations fail, the backend will not start — check the logs for migration errors. Startup order matters: In Docker Compose, PostgreSQL must be healthy before the backend starts. The frontend can start independently since Better Auth tables are created via autoMigrate() in the frontend’s auth.ts.

Better Auth Tables

Better Auth manages its own tables ("user", "session", "account", "verification", "organization", "member", "invitation"). These are created automatically by the frontend’s auth configuration. Do not modify these tables via Alembic — they are owned by Better Auth.