> ## Documentation Index
> Fetch the complete documentation index at: https://nenyax.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Database Schema

> PostgreSQL database tables and relationships.

# Database Schema

Nenyax uses PostgreSQL with SQLAlchemy ORM and Alembic migrations.

## Dual User Table Design

Nenyax maintains two separate user-related tables. This is intentional:

| Table     | Owned by       | Purpose                                                                                   |
| --------- | -------------- | ----------------------------------------------------------------------------------------- |
| `"user"`  | Better Auth    | Authentication only — email, password hash, sessions, email verification                  |
| `"users"` | Nenyax backend | Business 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.

| Column           | Type        | Description                           |
| ---------------- | ----------- | ------------------------------------- |
| key              | String(100) | Primary key (e.g., `livekit_api_key`) |
| encrypted\_value | Text        | AES-256-GCM encrypted value           |
| updated\_at      | DateTime    | Last update time                      |

### users

Backend user records, auto-created from Better Auth on first authenticated request.

| Column      | Type        | Description                                       |
| ----------- | ----------- | ------------------------------------------------- |
| id          | String(36)  | Primary key (UUID, generated by backend)          |
| auth\_id    | String(255) | Maps to Better Auth `"user".id` (unique, indexed) |
| email       | String(255) | User email (unique)                               |
| name        | String(255) | User display name (nullable)                      |
| created\_at | DateTime    | Account creation time                             |
| updated\_at | DateTime    | Last update time                                  |

### agents

Voice agent configurations.

| Column                | Type        | Description                                                            |
| --------------------- | ----------- | ---------------------------------------------------------------------- |
| id                    | String(36)  | Primary key                                                            |
| user\_id              | String(36)  | Foreign key → users                                                    |
| name                  | String(100) | Agent display name                                                     |
| description           | Text        | Agent description (nullable)                                           |
| type                  | Enum        | STT, LLM, TTS, or PIPELINE                                             |
| config                | JSON        | Full agent configuration blob                                          |
| phone\_number         | String(50)  | Telephony provider number (nullable)                                   |
| provider\_number\_sid | String(255) | Provider number SID (nullable)                                         |
| telephony\_provider   | String(20)  | Telephony provider: "twilio" or "telnyx" (nullable, default: "twilio") |
| is\_active            | Boolean     | Soft delete flag                                                       |
| agent\_mode           | Enum        | STANDARD or CUSTOM (default: STANDARD)                                 |
| storage\_path         | String(500) | MinIO storage path for custom agents (nullable)                        |
| image\_tag            | String(255) | Docker image tag for custom agents (nullable)                          |
| build\_status         | Enum        | NONE, PENDING, BUILDING, READY, FAILED                                 |
| build\_error          | Text        | Last build error message (nullable)                                    |
| last\_build\_at       | DateTime    | Last build timestamp (nullable)                                        |
| current\_version      | Integer     | Current code version (default: 0)                                      |
| deployed\_version     | Integer     | Last deployed version (nullable)                                       |
| created\_at           | DateTime    | Creation time                                                          |
| updated\_at           | DateTime    | Last update time                                                       |

The `config` JSON column stores the complete agent configuration:

```json theme={null}
{
  "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.

| Column                  | Type          | Description                                                 |
| ----------------------- | ------------- | ----------------------------------------------------------- |
| id                      | String(36)    | Primary key                                                 |
| room\_name              | String(255)   | LiveKit room name (unique, indexed)                         |
| user\_id                | String(36)    | Foreign key → users                                         |
| agent\_id               | String(36)    | Foreign key → agents (nullable)                             |
| status                  | Enum          | CREATED, ACTIVE, COMPLETED, FAILED                          |
| started\_at             | DateTime      | Call start time (nullable)                                  |
| ended\_at               | DateTime      | Call end time (nullable)                                    |
| duration                | Integer       | Duration in seconds (nullable)                              |
| call\_direction         | Enum          | INBOUND or OUTBOUND (nullable)                              |
| outbound\_phone\_number | String(50)    | Dialed number for outbound calls (nullable)                 |
| call\_status            | Enum          | RINGING, ANSWERED, COMPLETED, FAILED, NO\_ANSWER (nullable) |
| callback\_url           | String(2048)  | Webhook callback URL (nullable)                             |
| session\_data           | JSON          | Metadata including call analysis                            |
| total\_cost             | Decimal(12,6) | Total session cost (nullable)                               |
| cost\_breakdown         | JSON          | Cost breakdown by provider (nullable)                       |
| transferred\_to         | String(50)    | Transfer destination number (nullable)                      |
| transfer\_type          | Enum          | WARM or COLD (nullable)                                     |
| transfer\_timestamp     | DateTime      | When transfer occurred (nullable)                           |
| created\_at             | DateTime      | Record creation time                                        |
| updated\_at             | DateTime      | Last update time                                            |

### transcripts

Per-message conversation logs.

| Column      | Type       | Description                   |
| ----------- | ---------- | ----------------------------- |
| id          | String(36) | Primary key                   |
| session\_id | String(36) | Foreign key → voice\_sessions |
| content     | Text       | Message content               |
| speaker     | Enum       | USER or AGENT                 |
| timestamp   | DateTime   | Message time                  |

### usage\_events

Granular cost tracking events logged by the agent worker.

| Column      | Type          | Description                                           |
| ----------- | ------------- | ----------------------------------------------------- |
| id          | String(36)    | Primary key                                           |
| session\_id | String(36)    | Foreign key → voice\_sessions                         |
| user\_id    | String(36)    | Foreign key → users (indexed)                         |
| agent\_id   | String(36)    | Foreign key → agents (nullable, indexed)              |
| provider    | String(50)    | Service provider (e.g., assemblyai, google, resemble) |
| event\_type | Enum          | `stt_minutes`, `llm_tokens`, or `tts_characters`      |
| quantity    | Decimal(12,4) | Usage quantity                                        |
| unit\_cost  | Decimal(12,8) | Cost per unit                                         |
| total\_cost | Decimal(12,6) | Calculated total cost in USD                          |
| created\_at | DateTime      | Event time (indexed)                                  |

### agent\_files

Files uploaded for custom agents, stored in MinIO.

| Column        | Type        | Description                                   |
| ------------- | ----------- | --------------------------------------------- |
| id            | String(36)  | Primary key                                   |
| agent\_id     | String(36)  | Foreign key → agents (unique with file\_path) |
| file\_path    | String(500) | Relative file path within agent storage       |
| content\_hash | String(64)  | SHA-256 hash of file content (nullable)       |
| size\_bytes   | BigInteger  | File size in bytes                            |
| mime\_type    | String(100) | MIME type (nullable)                          |
| version       | Integer     | Current version number                        |
| created\_at   | DateTime    | Creation time                                 |
| updated\_at   | DateTime    | Last update time                              |

### agent\_file\_versions

Version history for custom agent files.

| Column          | Type        | Description                |
| --------------- | ----------- | -------------------------- |
| id              | String(36)  | Primary key                |
| agent\_file\_id | String(36)  | Foreign key → agent\_files |
| version         | Integer     | Version number             |
| content\_hash   | String(64)  | SHA-256 hash (nullable)    |
| minio\_key      | String(500) | MinIO object key           |
| size\_bytes     | BigInteger  | File size in bytes         |
| created\_at     | DateTime    | Creation time              |

### agent\_containers

Docker container tracking for custom agents.

| Column          | Type        | Description                              |
| --------------- | ----------- | ---------------------------------------- |
| id              | String(36)  | Primary key                              |
| agent\_id       | String(36)  | Foreign key → agents                     |
| session\_id     | String(36)  | Foreign key → voice\_sessions (nullable) |
| container\_id   | String(100) | Docker container ID (nullable)           |
| container\_type | String(50)  | Container type (default: "agent")        |
| status          | Enum        | PENDING, RUNNING, STOPPED, FAILED        |
| image\_tag      | String(255) | Docker image tag (nullable)              |
| started\_at     | DateTime    | Container start time (nullable)          |
| stopped\_at     | DateTime    | Container stop time (nullable)           |
| error\_message  | Text        | Error details (nullable)                 |
| created\_at     | DateTime    | Creation time                            |

### coding\_agent\_conversations

Chat conversations with the coding agent (per agent, per user).

| Column      | Type        | Description                                      |
| ----------- | ----------- | ------------------------------------------------ |
| id          | String(36)  | Primary key                                      |
| agent\_id   | String(36)  | Foreign key → agents (indexed)                   |
| user\_id    | String(255) | Auth user ID                                     |
| title       | String(200) | Conversation title (default: "New conversation") |
| created\_at | DateTime    | Creation time                                    |
| updated\_at | DateTime    | Last update time                                 |

### coding\_agent\_messages

Individual messages within coding agent conversations.

| Column           | Type       | Description                                          |
| ---------------- | ---------- | ---------------------------------------------------- |
| id               | String(36) | Primary key                                          |
| conversation\_id | String(36) | Foreign key → coding\_agent\_conversations (indexed) |
| role             | String(20) | "user" or "assistant"                                |
| content          | Text       | Message content                                      |
| file\_changes    | JSON       | File changes made by the assistant (nullable)        |
| created\_at      | DateTime   | Creation time                                        |

### organizations

Multi-tenancy: top-level tenant for resource scoping. Auto-created from Better Auth's `"organization"` table on first access.

| Column                   | Type        | Description                                   |
| ------------------------ | ----------- | --------------------------------------------- |
| id                       | String(36)  | Primary key (UUID)                            |
| ba\_org\_id              | String(255) | Better Auth organization ID (unique, indexed) |
| name                     | String(255) | Organization display name                     |
| slug                     | String(100) | URL-safe slug (unique)                        |
| plan                     | String(50)  | Subscription plan (default: "free")           |
| stripe\_customer\_id     | String(255) | Stripe customer ID (nullable)                 |
| stripe\_subscription\_id | String(255) | Stripe subscription ID (nullable)             |
| usage\_limit\_minutes    | Integer     | Monthly usage limit in minutes (default: 100) |
| created\_at              | DateTime    | Creation time                                 |
| updated\_at              | DateTime    | Last update time                              |

### org\_members

Organization membership with roles.

| Column      | Type       | Description                                             |
| ----------- | ---------- | ------------------------------------------------------- |
| id          | String(36) | Primary key                                             |
| org\_id     | String(36) | Foreign key → organizations (indexed)                   |
| user\_id    | String(36) | Foreign key → users (indexed)                           |
| role        | String(20) | Role: "owner", "admin", or "member" (default: "member") |
| created\_at | DateTime   | Creation time                                           |

### org\_api\_keys

Per-organization encrypted API key storage.

| Column           | Type        | Description                                |
| ---------------- | ----------- | ------------------------------------------ |
| org\_id          | String(36)  | Foreign key → organizations (composite PK) |
| key\_name        | String(100) | Key identifier (composite PK)              |
| encrypted\_value | Text        | AES-256-GCM encrypted value                |
| updated\_at      | DateTime    | Last update time                           |

### user\_api\_keys

Per-user encrypted API key storage (legacy, migrating to org\_api\_keys).

| Column           | Type        | Description                        |
| ---------------- | ----------- | ---------------------------------- |
| user\_id         | String(36)  | Foreign key → users (composite PK) |
| key\_name        | String(100) | Key identifier (composite PK)      |
| encrypted\_value | Text        | AES-256-GCM encrypted value        |
| updated\_at      | DateTime    | Last update time                   |

## Migrations

Database migrations are managed by Alembic and run automatically on backend startup:

```bash theme={null}
# 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.
