Skip to main content

Database Schema

warning

This tutorial is a community contribution and is not supported by the Open WebUI team. It serves only as a demonstration on how to customize Open WebUI for your specific use case. Want to contribute? Check out the contributing tutorial.

[!WARNING] This documentation reflects schema changes up to Open WebUI v0.11.1.

Open-WebUI Internal SQLite Database

For Open-WebUI, the SQLite database serves as the backbone for user management, chat history, file storage, and various other core functionalities. Understanding this structure is essential for anyone looking to contribute to or maintain the project effectively.

Internal SQLite Location

You can find the SQLite database at root -> data -> webui.db

📁 Root (/)
├── 📁 data
│   ├── 📁 cache
│   ├── 📁 uploads
│   ├── 📁 vector_db
│   └── 📄 webui.db
├── 📄 dev.sh
├── 📁 open_webui
├── 📄 requirements.txt
├── 📄 start.sh
└── 📄 start_windows.bat

Copy Database Locally

If you want to copy the Open-WebUI SQLite database running in the container to your local machine, you can use:

docker cp open-webui:/app/backend/data/webui.db ./webui.db

Alternatively, you can access the database within the container using:

docker exec -it open-webui /bin/sh

Table Overview

Here is a complete list of tables in Open-WebUI's SQLite database. The tables are listed alphabetically and numbered for convenience.

No.Table NameDescription
01access_grantStores normalized access control grants for all resources
02authStores user authentication credentials and login information
03calendarStores user-owned calendars with access control
04calendar_eventStores calendar events with recurrence (RRULE) support
05calendar_event_attendeeTracks attendee RSVPs for shared calendar events
06channelManages chat channels and their configurations
07channel_fileLinks files to channels and messages
08channel_memberTracks user membership and permissions within channels
09chatStores chat sessions and their metadata
10chat_fileLinks files to chats and messages
11chatidtagLegacy. Pre-tag chat/tag mapping; data migrated to chat and no longer used (see note below)
12configMaintains system-wide configuration settings
13documentLegacy. Pre-Knowledge documents table; data migrated to knowledge and no longer used (see note below)
14feedbackCaptures user feedback and ratings
15fileManages uploaded files and their metadata
16folderOrganizes files and content into hierarchical structures
17functionStores custom functions and their configurations
18groupManages user groups and their permissions
19group_memberTracks user membership within groups
20knowledgeStores knowledge base entries and related information
21knowledge_fileLinks files to knowledge bases
22memoryMaintains chat history and context memory
23messageStores individual chat messages and their content
24message_reactionRecords user reactions (emojis/responses) to messages
25migrate_historyTracks database schema version and migration records
26modelManages AI model configurations and settings
27noteStores user-created notes and annotations
28oauth_sessionManages active OAuth sessions for users
29promptStores templates and configurations for AI prompts
30prompt_historyTracks version history and snapshots for prompts
31shared_chatStores snapshots of shared chats for link sharing
32skillStores reusable markdown instruction sets (Skills)
33tagManages tags/labels for content categorization
34toolStores configurations for system tools and integrations
35userMaintains user profiles and account information
36automationStores user-defined scheduled automations
37automation_runStores execution history for automation runs
38pinned_noteTracks per-user note pins (each row = one user pinning one note)
39chat_messageNormalized per-message store for chat conversations
40api_keyStores per-user API keys, replacing the former user.api_key column
41knowledge_directoryNestable folders that organize files within a knowledge base
42channel_webhookStores per-channel incoming webhooks for unauthenticated posting

Note: there are two additional tables in Open-WebUI's SQLite database that are not related to Open-WebUI's core functionality, that have been excluded:

  • Alembic Version table
  • Migrate History table

Note on the document table: it is a legacy table from before the Knowledge feature. Its rows were migrated into the knowledge table (migration 6a39f3d8e55c) and nothing writes to it anymore, but no migration drops it, so it may still be present (empty) in databases that predate the Knowledge feature. There is no backing model for it in current code.

Now that we have all the tables, let's understand the structure of each table.

Access Grant Table

Column NameData TypeConstraintsDescription
idIntegerPRIMARY KEY, AUTOINCREMENTUnique identifier
resource_typeTextNOT NULLType of resource (e.g., model, knowledge, tool)
resource_idTextNOT NULLID of the specific resource
principal_typeTextNOT NULLType of grantee: user, group or anyone
principal_idTextNOT NULLID of the user or group (or * for public)
permissionTextNOT NULLPermission level: read or write
created_atBigIntegernullableGrant creation timestamp

Things to know about the access_grant table:

  • Unique constraint on (resource_type, resource_id, principal_type, principal_id, permission) to prevent duplicate grants
  • Indexed on (resource_type, resource_id) and (principal_type, principal_id) for efficient lookups
  • Replaces the former access_control JSON column that was previously embedded in each resource table
  • principal_type of user with principal_id of * represents public access, meaning every signed-in user. It does not reach visitors who are not logged in
  • principal_type of anyone (added in v0.11.0) is the no-sign-in grant behind open share links. It is only ever stored as anyone / * / read, any other combination is rejected, and it is only honoured for the shared_chat resource type. Every other resource strips it
  • Supports both group-level and individual user-level access grants

API Key Table

Added in v0.6.41 (migration b10670c03dd5), which also dropped the api_key column from the User Table after copying every existing key into this table.

Column NameData TypeConstraintsDescription
idTextPRIMARY KEY, UNIQUEUnique identifier
user_idTextFOREIGN KEY(user.id) CASCADEOwner of the key
keyTextUNIQUE, NOT NULLThe API key itself
dataJSONnullableExtensible data payload
expires_atBigIntegernullableExpiry timestamp
last_used_atBigIntegernullableTimestamp of the key's last use
created_atBigIntegerNOT NULLCreation timestamp
updated_atBigIntegerNOT NULLLast update timestamp

Things to know about the api_key table:

  • Deleting a user cascades to delete their keys.
  • Generating a key deletes the account's existing rows first, so an account holds at most one key. Rows carry the fixed id key_{user_id}.
  • Key generation, retrieval and deletion all go through /api/v1/auths/api_key. All three are refused unless the auth.enable_api_keys config key is on, and non-admins additionally need the features.api_keys permission.
  • data, expires_at and last_used_at are present in the schema and no code path writes them. Key lookup joins on key alone and does not consult expires_at.

Auth Table

Column NameData TypeConstraintsDescription
idStringPRIMARY KEYUnique identifier
emailString-User's email
passwordText-Hashed password
activeBoolean-Account status

Things to know about the auth table:

  • Uses UUID for primary key
  • One-to-One relationship with the user table (shared id)

Channel Table

Column NameData TypeConstraintsDescription
idTextPRIMARY KEYUnique identifier (UUID)
user_idText-Owner/creator of channel
typeTextnullableChannel type
nameText-Channel name
descriptionTextnullableChannel description
is_privateBooleannullablePrivate flag for group type channels
dataJSONnullableFlexible data storage
metaJSONnullableChannel metadata
created_atBigInteger-Creation timestamp (nanoseconds)
updated_atBigInteger-Last update timestamp (nanoseconds)
updated_byTextnullableUser who last updated the channel
archived_atBigIntegernullableArchive timestamp (nanoseconds)
archived_byTextnullableUser who archived the channel
deleted_atBigIntegernullableDeletion timestamp (nanoseconds)
deleted_byTextnullableUser who deleted the channel

Things to know about the channel table:

  • Uses UUID for primary key
  • Case-insensitive channel names (stored lowercase)
  • type was added in migration 3781e22d8b01. is_private, updated_by, archived_at, archived_by, deleted_at and deleted_by were added in migration 90ef40d4714e.
  • Channel listing and member queries return only rows where both archived_at and deleted_at are null, so the schema is shaped for archiving and soft deletion.
  • No code path writes archived_at, archived_by, deleted_at, deleted_by or updated_by. Deleting a channel removes the row outright, so a deleted channel leaves nothing behind for the deleted_at filter to exclude. Anyone querying this table directly will find those five columns null on every row.
  • The access_control column that this table was created with was dropped in migration f1e2d3c4b5a6. Channel access is managed through the access_grant table with resource_type = 'channel'.

Channel Member Table

Column NameData TypeConstraintsDescription
idTextPRIMARY KEY, UNIQUEUnique identifier for the channel membership
channel_idTextNOT NULLReference to the channel
user_idTextNOT NULLReference to the user
roleTextnullableMember's role within the channel
statusTextnullableMembership status: joined or left
is_activeBooleanNOT NULL, server_default=trueWhether the membership is live
is_channel_mutedBooleanNOT NULL, server_default=falsePer-member mute flag
is_channel_pinnedBooleanNOT NULL, server_default=falsePer-member pin flag
dataJSONnullableExtensible data payload
metaJSONnullableOptional metadata
invited_atBigIntegernullableInvitation timestamp (nanoseconds)
invited_byTextnullableUser who added this member
joined_atBigIntegerNOT NULLJoin timestamp (nanoseconds)
left_atBigIntegernullableLeave timestamp (nanoseconds)
last_read_atBigIntegernullableLast read timestamp, drives unread state
created_atBigInteger-Timestamp when membership was created
updated_atBigIntegernullableLast update timestamp (nanoseconds)

Things to know about the channel_member table:

  • role, invited_at and invited_by were added in migration 90ef40d4714e. status, is_active, is_channel_muted, is_channel_pinned, data, meta, joined_at, left_at, last_read_at and updated_at were added in migration 2f1211949ecc.
  • status is written as joined when the membership is created. Leaving a channel writes left, sets is_active to false and stamps left_at. The row survives.
  • is_active is the flag that channel listings and member listings filter on, so a membership someone has left drops out of those results while remaining in the table. It is also written by POST /api/v1/channels/{id}/members/active.
  • last_read_at is stamped when the membership is created and updated over the socket connection when the user reads the channel.
  • role is read in one place, to test whether a member is a manager. No code path writes it, so it is null on every row and that test never matches. Manager-only actions such as viewing and creating channel webhooks fall through to the other two branches of the check: the channel's own user_id, or an instance admin.
  • is_channel_muted has no write path and stays false. is_channel_pinned has a setter on the model, Channels.pin_channel, that nothing calls.
  • joined_at is NOT NULL in the database (migration 2f1211949ecc) while the SQLAlchemy model omits the flag. The database constraint is the one that applies.
  • Deleting a user account leaves their channel_member rows in place. Member listings, the member count on a channel and the lookup that finds the existing direct message for a set of people all skip rows whose user_id no longer matches an account, so a leftover row is not counted as a member and does not push a direct message into a second conversation.

Channel Webhook Table

Incoming webhooks that post messages into one channel without a signed-in user. Added by migration 90ef40d4714e.

Column NameData TypeConstraintsDescription
idTextPRIMARY KEY, UNIQUEUnique identifier (UUID)
channel_idTextFOREIGN KEY(channel.id) CASCADE, NOT NULLChannel the webhook posts into
user_idTextNOT NULLUser who created the webhook
nameTextNOT NULLDisplay name shown on its messages
profile_image_urlTextnullableAvatar shown on its messages
tokenTextNOT NULLSecret used to authorize posting
last_used_atBigIntegernullableTimestamp of the last post
created_atBigIntegerNOT NULLCreation timestamp
updated_atBigIntegerNOT NULLLast update timestamp

Things to know about the channel_webhook table:

  • Deleting a channel cascades to delete its webhooks.
  • token is a URL-safe random token carrying 32 bytes of entropy, generated with secrets.token_urlsafe(32), stored in plain text and carried in the request path. POST /api/v1/channels/webhooks/{webhook_id}/{token} takes no authentication and matches the id and token pair against this table, so anyone holding the pair can post to the channel.
  • last_used_at is stamped on each accepted post.
  • Listing and creating webhooks require the caller to be an instance admin or the channel's own creator. See the role note on the Channel Member Table for why the membership-based manager branch of that check never matches.
  • The webhook's id and name are copied into the posted message's meta.webhook, which is what the interface renders in place of an author account.

Channel File Table

Column NameData TypeConstraintsDescription
idTextPRIMARY KEYUnique identifier (UUID)
user_idTextNOT NULLOwner of the relationship
channel_idTextFOREIGN KEY(channel.id) CASCADE, NOT NULLReference to the channel
file_idTextFOREIGN KEY(file.id) CASCADE, NOT NULLReference to the file
message_idTextFOREIGN KEY(message.id) CASCADE, nullableReference to associated message
created_atBigIntegerNOT NULLCreation timestamp
updated_atBigIntegerNOT NULLLast update timestamp

Things to know about the channel_file table:

  • Unique constraint on (channel_id, file_id) to prevent duplicate entries
  • All three foreign keys cascade on delete, message_id included, so deleting the message a file was attached to removes the row here as well as deleting the channel or the file
  • Indexed on channel_id, file_id, and user_id for performance

Chat Table

Column NameData TypeConstraintsDescription
idStringPRIMARY KEYUnique identifier (UUID)
user_idString-Owner of the chat
titleText-Chat title
chatJSON-Chat content and history
created_atBigInteger-Creation timestamp
updated_atBigInteger-Last update timestamp
share_idTextUNIQUE, nullableSharing identifier
archivedBooleandefault=FalseArchive status
pinnedBooleandefault=False, nullablePin status
metaJSONserver_default=""Metadata including tags
folder_idTextnullableParent folder ID
tasksJSONnullableChat-level task/todo list used by agentic workflows
summaryTextnullableOptional chat summary text
last_read_atBigIntegernullableLast read timestamp used for unread indicators
current_message_idTextnullableCurrent (active leaf) message of the chat's history
variablesJSONnullableValues filled in for the model's chat variables

Things to know about the chat table:

  • tasks and summary support structured planning/status UX in chat sessions.
  • last_read_at is used by sidebar unread state logic (compare with updated_at).
  • share_id references the shared_chat.id token when the chat has an active share link.
  • current_message_id was added in v0.11.0 (migration 9a1b2c3d4e5f). It records the chat's current message, the leaf of the active branch that a new reply continues from, and is backfilled from the existing history when the migration runs. Context compaction and context-usage resolution read it so they work on the branch actually in play rather than the whole message tree.
  • variables was added in v0.11.0 (migration c49178636c78). It holds the values a user filled in for the chat variables declared by the model's system prompt, as a flat map keyed by variable name, and is copied along when a chat is forked or cloned. Temporary chats keep their values in the request instead, so nothing is stored.
  • A migration (242a2047eae0) adds an old_chat column (Text) that backs up the original JSON chat blob as text. It is a migration safety net, not part of the active model, and is not read at runtime.

Shared Chat Table

Column NameData TypeConstraintsDescription
idTextPRIMARY KEYShare token (UUID) used in /s/{id} URLs
chat_idTextFOREIGN KEY(chat.id) CASCADE, NOT NULLReference to the original chat
user_idTextNOT NULLUser who created the share
titleTextnullableChat title at time of sharing
chatJSONnullableSnapshot of chat content at share time
created_atBigIntegernullableShare creation timestamp
updated_atBigIntegernullableLast re-snapshot timestamp

Things to know about the shared_chat table:

  • Replaces the previous pattern of storing shared chat snapshots as phantom rows in the chat table with user_id set to shared-{chat_id}.
  • Each row is an immutable snapshot of the original chat at the time of sharing (or last re-share). The snapshot is updated when the user clicks "Update and Copy Link".
  • Deleting the original chat cascades to delete the shared snapshot.
  • Access control for shared chats is managed via the access_grant table with resource_type = 'shared_chat'.

Chat Message Table

The chat_message table is the normalized per-message store for chat conversations: one row per message, separate from the JSON history blob in chat.chat and distinct from the channel Message Table (which holds channel/thread messages, not chat-model turns).

Column NameData TypeConstraintsDescription
idTextPRIMARY KEYUnique identifier (UUID)
chat_idTextFOREIGN KEY(chat.id) CASCADE, NOT NULLParent chat
user_idTextindexedAuthor of the message
roleTextNOT NULLMessage role: user, assistant, or system
parent_idTextnullableParent message id (for branched conversations)
contentJSONnullableMessage content (a string or a list of content blocks)
outputJSONnullableGenerated output payload
model_idTextnullable, indexedModel that produced the message
filesJSONnullableAttached files
sourcesJSONnullableRetrieval/citation sources
embedsJSONnullableEmbedded artifacts
metaJSONnullableMessage metadata; marks internal sub-agent and timer messages (added in v0.11.0)
doneBooleandefault=TrueWhether generation completed
status_historyJSONnullableStreamed status updates during generation
errorJSONnullableError payload when generation failed
usageJSONnullableToken/usage statistics
context_summaryTextnullablePer-message context summary (added in v0.10.0)
created_atBigIntegerindexedCreation timestamp
updated_atBigInteger-Last update timestamp

Things to know about the chat_message table:

  • Deleting a chat cascades to delete its messages (chat_id foreign key with ON DELETE CASCADE).
  • Composite indexes back the common access patterns: (chat_id, parent_id), (model_id, created_at), and (user_id, created_at).
  • context_summary was added in v0.10.0 (migration 4c5ce3d2f27f) to store a summary of the message's context.
  • meta was added in v0.11.0 (migration 856c5b02fb54). It carries per-message metadata and is what marks the messages Open WebUI injects on a user's behalf, such as a sub-agent result or a fired timer, so the interface can render them differently from a message the user typed.
  • Chat search reads a different set of stores per backend. On PostgreSQL it matches chat_message.content as well as the history.messages map and the older flat messages array inside the chat.chat blob. On SQLite it matches those two JSON locations only.

Automation Table

Column NameData TypeConstraintsDescription
idTextPRIMARY KEYUnique identifier (UUID)
user_idTextNOT NULLOwner of the automation
folder_idTextnullableFolder the runs' chats are created in
nameTextNOT NULLAutomation display name
dataJSONNOT NULLAutomation payload (prompt, model_id, rrule, optional terminal config)
metaJSONnullableOptional metadata
is_activeBooleanNOT NULL, default=TrueActive/paused state
last_run_atBigIntegernullableLast execution time
next_run_atBigIntegernullableNext scheduled execution time
created_atBigIntegerNOT NULLCreation timestamp
updated_atBigIntegerNOT NULLLast update timestamp

Things to know about the automation table:

  • next_run_at is indexed for efficient due-run polling.
  • data.rrule defines recurrence and drives scheduler calculations.
  • folder_id was added in v0.11.0 (migration 959eaac8f909) together with a (user_id, folder_id) index, so an owner's automations can be listed per folder. It is not a foreign key: deleting a folder clears the column on that owner's automations instead of deleting the automation, and a run whose folder has disappeared in the meantime clears the column and files its chat outside any folder.

Automation Run Table

Column NameData TypeConstraintsDescription
idTextPRIMARY KEYUnique identifier (UUID)
automation_idTextNOT NULLReference to automation
chat_idTextnullableChat created by this run (if available)
statusTextNOT NULLRun status (success / error)
errorTextnullableError details when status is error
created_atBigIntegerNOT NULLExecution record timestamp

Things to know about the automation_run table:

  • Indexed by automation_id for fast per-automation run history queries.
  • Rows are deleted when an automation is deleted.

Calendar Table

Column NameData TypeConstraintsDescription
idTextPRIMARY KEYUnique identifier (UUID)
user_idTextNOT NULLOwner of the calendar
nameTextNOT NULLCalendar display name
colorTextnullableDisplay color (hex, e.g. #3b82f6)
is_defaultBooleanNOT NULL, default=FalseWhether this is the user's default calendar
dataJSONnullableExtensible data payload
metaJSONnullableOptional metadata
created_atBigIntegerNOT NULLCreation timestamp
updated_atBigIntegerNOT NULLLast update timestamp

Things to know about the calendar table:

  • Indexed on user_id for efficient per-user calendar listing.
  • A default "Personal" calendar is auto-created on first access.
  • The "Scheduled Tasks" calendar is virtual: it is not stored in this table. Instead, the API synthesizes it at response time (with constant ID __scheduled_tasks__) for users who have Automations access. Automation RRULE future runs and past execution records are rendered as virtual events on this calendar.
  • Access control is managed via the access_grant table with resource_type = 'calendar', enabling calendar sharing between users and groups.
  • A user can only delete non-default calendars. Deleting a calendar cascades to all its events, attendees, and access grants.

Calendar Event Table

Column NameData TypeConstraintsDescription
idTextPRIMARY KEYUnique identifier (UUID)
calendar_idTextNOT NULLReference to parent calendar
user_idTextNOT NULLUser who created the event
titleTextNOT NULLEvent title
descriptionTextnullableEvent description
start_atBigIntegerNOT NULLStart time (epoch nanoseconds)
end_atBigIntegernullableEnd time (epoch nanoseconds)
all_dayBooleanNOT NULL, default=FalseWhether this is an all-day event
rruleTextnullableiCalendar RRULE for recurrence
colorTextnullablePer-event color override
locationTextnullableEvent location
dataJSONnullableExtensible data payload
metaJSONnullableOptional metadata (e.g. automation_id)
is_cancelledBooleanNOT NULL, default=FalseSoft-cancel flag
created_atBigIntegerNOT NULLCreation timestamp
updated_atBigIntegerNOT NULLLast update timestamp

Things to know about the calendar_event table:

  • Composite index on (calendar_id, start_at) for efficient range queries within a calendar.
  • Composite index on (user_id, start_at) for efficient per-user date range queries.
  • Recurring events store an rrule string and are expanded into individual instances at query time (server-side Python expansion using dateutil). Expansion runs in the user's time zone, so every instance keeps the local clock time of the stored start_at, including across daylight saving changes. Accounts with no usable time zone fall back to the server's.
  • Cancelled events (is_cancelled = True) are excluded from range queries but retained in the database.

Calendar Event Attendee Table

Column NameData TypeConstraintsDescription
idTextPRIMARY KEYUnique identifier (UUID)
event_idTextNOT NULLReference to the calendar event
user_idTextNOT NULLUser invited to the event
statusTextNOT NULL, default='pending'RSVP status: pending, accepted, declined, tentative
metaJSONnullableOptional metadata
created_atBigIntegerNOT NULLCreation timestamp
updated_atBigIntegerNOT NULLLast update timestamp

Things to know about the calendar_event_attendee table:

  • Unique constraint on (event_id, user_id) to prevent duplicate attendee entries.
  • Indexed on (user_id, status) for efficient lookups of events a user is invited to.
  • Attendees are replaced in bulk when an event is updated with a new attendee list.
  • Deleting an event cascades to delete all attendee records.

Chat File Table

Column NameData TypeConstraintsDescription
idTextPRIMARY KEYUnique identifier (UUID)
user_idTextNOT NULLUser associated with the file
chat_idTextFOREIGN KEY(chat.id), NOT NULLReference to the chat
file_idTextFOREIGN KEY(file.id), NOT NULLReference to the file
message_idTextnullableReference to associated message
created_atBigIntegerNOT NULLCreation timestamp
updated_atBigIntegerNOT NULLLast update timestamp

Things to know about the chat_file table:

  • Unique constraint on (chat_id, file_id) to prevent duplicate entries
  • Foreign key relationships with CASCADE delete
  • Indexed on chat_id, file_id, message_id, and user_id for performance

Why this table was added:

  • Query Efficiency: Before this, files were embedded in message objects. This table allows direct indexed lookups for finding all files in a chat without iterating through every message.
  • Data Consistency: Acts as a single source of truth for file associations. In multi-node deployments, all nodes query this table instead of relying on potentially inconsistent embedded data.
  • Deduplication: The database-level unique constraint prevents duplicate file associations, which is more reliable than application-level checks.

Chat ID Tag Table

Column NameData TypeConstraintsDescription
idStringPRIMARY KEYUnique identifier
tag_nameStringnullableName of the tag
chat_idStringnullableReference to chat
user_idStringnullableReference to user
timestampBigIntegernullableCreation timestamp

Note on the chatidtag table: it is a legacy table from before chat tags moved onto the chat record. Its rows were read out into the chat table (migration 1af9b942657b), which wrote each tag into chat.meta.tags and turned the pinned tag into chat.pinned, and nothing writes to it anymore. That migration leaves the rows where they are and no migration drops the table, so an older database may still hold stale rows in it. There is no backing model for it in current code. Chat tags now live in the tag table.

Config

As of v0.10.0 the config table is per-key: every setting is its own row keyed by a dot-notation path, replacing the previous single-row JSON blob.

Column NameData TypeConstraintsDescription
keyTextPRIMARY KEYConfig key in dot notation (e.g. audio.stt.engine)
valueJSONNOT NULLThe stored value for this key
updated_atBigIntegernullableLast update timestamp (epoch)

Things to know about the config table:

  • Reshaped in migration 3ff2c63645b8. The old single-row schema (id INTEGER PK, data JSON, version INTEGER, created_at/updated_at DATETIME) was migrated by exploding the JSON blob into one row per key.
  • The pre-migration table is preserved as config_old (renamed, not dropped) so the change is reversible; it is not used at runtime.
  • Reads/writes go through individual keys, which avoids rewriting the entire configuration blob on every change.

Feedback Table

Column NameData TypeConstraintsDescription
idTextPRIMARY KEYUnique identifier (UUID)
user_idText-User who provided feedback
versionBigIntegerdefault=0Feedback version number
typeText-Type of feedback
dataJSONnullableFeedback data including ratings
metaJSONnullableMetadata (arena, chat_id, etc)
snapshotJSONnullableAssociated chat snapshot
created_atBigInteger-Creation timestamp
updated_atBigInteger-Last update timestamp

File Table

Column NameData TypeConstraintsDescription
idStringPRIMARY KEYUnique identifier
user_idString-Owner of the file
hashTextnullableFile hash/checksum
filenameText-Name of the file
pathTextnullableFile system path
dataJSONnullableFile-related data
metaJSONnullableFile metadata
created_atBigInteger-Creation timestamp
updated_atBigInteger-Last update timestamp

The meta field's expected structure:

{
    "name": string,          # Optional display name
    "content_type": string,  # MIME type
    "size": integer,         # File size in bytes
    # Additional metadata supported via ConfigDict(extra="allow")
}

Folder Table

Column NameData TypeConstraintsDescription
idTextPK (composite)Unique identifier (UUID)
parent_idTextnullableParent folder ID for hierarchy
user_idTextPK (composite)Owner of the folder
nameText-Folder name
itemsJSONnullableFolder contents
dataJSONnullableAdditional folder data
metaJSONnullableFolder metadata
is_expandedBooleandefault=FalseUI expansion state
created_atBigInteger-Creation timestamp
updated_atBigInteger-Last update timestamp

Things to know about the folder table:

  • Primary key is composite (id, user_id)
  • Folders can be nested (parent_id reference)
  • Root folders have null parent_id
  • Folder names must be unique within the same parent

Function Table

Column NameData TypeConstraintsDescription
idStringPRIMARY KEYUnique identifier
user_idString-Owner of the function
nameText-Function name
typeText-Function type
contentText-Function content/code
metaJSON-Function metadata
valvesJSON-Function control settings
is_activeBoolean-Function active status
is_globalBoolean-Global availability flag
created_atBigInteger-Creation timestamp
updated_atBigInteger-Last update timestamp

Things to know about the function table:

  • type is one of: pipe, filter, action, event (the event type was added in v0.10.0). The type is auto-detected from the top-level class name in the function's source code.

Group Table

Column NameData TypeConstraintsDescription
idTextPRIMARY KEY, UNIQUEUnique identifier (UUID)
user_idText-Group owner/creator
nameText-Group name
descriptionText-Group description
dataJSONnullableAdditional group data
metaJSONnullableGroup metadata
permissionsJSONnullablePermission configuration
created_atBigInteger-Creation timestamp
updated_atBigInteger-Last update timestamp

Note: The user_ids column has been migrated to the group_member table.

Group Member Table

Column NameData TypeConstraintsDescription
idTextPRIMARY KEY, UNIQUEUnique identifier (UUID)
group_idTextFOREIGN KEY(group.id), NOT NULLReference to the group
user_idTextFOREIGN KEY(user.id), NOT NULL, indexedReference to the user
created_atBigIntegernullableCreation timestamp
updated_atBigIntegernullableLast update timestamp

Things to know about the group_member table:

  • Unique constraint on (group_id, user_id) to prevent duplicate memberships, which also serves lookups that start from a group
  • Indexed on (user_id, group_id) for efficient lookups of the groups a user belongs to (migration 1ce6ade7d93b). The unique constraint leads with group_id and cannot answer that question, so without this index every permission and access check reads the whole membership table, and the cost grows with the total number of memberships on the instance rather than with the number a single user has
  • Foreign key relationships with CASCADE delete to group and user tables

Knowledge Table

Column NameData TypeConstraintsDescription
idTextPRIMARY KEY, UNIQUEUnique identifier (UUID)
user_idText-Knowledge base owner
nameText-Knowledge base name
descriptionText-Knowledge base description
dataJSONnullableKnowledge base content
metaJSONnullableAdditional metadata
created_atBigInteger-Creation timestamp
updated_atBigInteger-Last update timestamp

Knowledge Directory Table

Nestable folders that organize the files inside one knowledge base. Added by migration 3c9b0ca343fd, which also added directory_id to the Knowledge File Table.

Column NameData TypeConstraintsDescription
idTextPRIMARY KEYUnique identifier (UUID)
knowledge_idTextFOREIGN KEY(knowledge.id) CASCADE, NOT NULLParent knowledge base
parent_idTextFOREIGN KEY(knowledge_directory.id) CASCADE, nullableParent directory for nesting
nameTextNOT NULLDirectory name
user_idTextNOT NULLUser who created the directory
created_atBigIntegerNOT NULLCreation timestamp
updated_atBigIntegerNOT NULLLast update timestamp

Things to know about the knowledge_directory table:

  • Directories nest through the self-referencing parent_id. Root directories have a null parent_id.
  • Unique constraint on (knowledge_id, parent_id, name) (uq_knowledge_directory_knowledge_parent_name), so directory names are unique among siblings within one knowledge base.
  • Indexed on knowledge_id and on parent_id (ix_knowledge_directory_knowledge_id, ix_knowledge_directory_parent_id).
  • Deleting a knowledge base cascades to its directories. Deleting a directory cascades to the directories nested inside it.

Knowledge File Table

Column NameData TypeConstraintsDescription
idTextPRIMARY KEYUnique identifier (UUID)
user_idTextNOT NULLOwner of the relationship
knowledge_idTextFOREIGN KEY(knowledge.id), NOT NULLReference to the knowledge base
file_idTextFOREIGN KEY(file.id), NOT NULLReference to the file
directory_idTextFOREIGN KEY(knowledge_directory.id) SET NULL, nullable, indexedDirectory holding the file
created_atBigIntegerNOT NULLCreation timestamp
updated_atBigIntegerNOT NULLLast update timestamp

Things to know about the knowledge_file table:

  • Unique constraint on (knowledge_id, file_id) to prevent duplicate entries
  • Foreign key relationships with CASCADE delete
  • Indexed on knowledge_id, file_id, and user_id for performance
  • directory_id was added in migration 3c9b0ca343fd, with the index ix_knowledge_file_directory_id. It is the one foreign key here that does not cascade: deleting a directory sets directory_id back to null on its files rather than deleting them, and a null directory_id puts the file at the root of its knowledge base

Access control for resources (models, knowledge bases, tools, prompts, notes, files, channels) is managed through the access_grant table rather than embedded JSON. Each grant entry specifies a resource, a principal (user or group), and a permission level (read or write). See the Access Grant Table section above for details.

Memory Table

Column NameData TypeConstraintsDescription
idStringPRIMARY KEYUnique identifier (UUID)
user_idStringindexedMemory owner
typeStringdefault context, indexedMemory type: user or context (added in v0.10.0)
pathTextnullableOptional path for organizing memories into a hierarchy (added in v0.10.0)
contentText-Memory content
metaJSONnullableOptional metadata (added in v0.10.0)
created_atBigInteger-Creation timestamp
updated_atBigInteger-Last update timestamp

Things to know about the memory table:

  • type distinguishes user memories (explicit, user-curated facts) from context memories (learned from conversation); it is indexed for per-type lookups. Added in migration 7b3f2a9c1d4e (with an index fixup migration following it).
  • path and meta (added in a later v0.10.0 migration) back the expanded builtin memory tools, which let the model organize memories under paths and attach structured metadata.
  • A covering index on (id, user_id) was added in v0.11.0 (migration 55f1302ac17c) to speed up per-user memory lookups.

Message Table

Column NameData TypeConstraintsDescription
idTextPRIMARY KEYUnique identifier (UUID)
user_idText-Message author
channel_idTextnullableAssociated channel
reply_to_idTextnullableMessage this one quotes in its reply
parent_idTextnullableThread root for threaded replies
is_pinnedBooleanNOT NULL, server_default=falseWhether the message is pinned in its channel
pinned_atBigIntegernullablePin timestamp (nanoseconds)
pinned_byTextnullableUser who pinned the message
contentText-Message content
dataJSONnullableAdditional message data
metaJSONnullableMessage metadata
created_atBigInteger-Creation timestamp (nanoseconds)
updated_atBigInteger-Last update timestamp (nanoseconds)

Things to know about the message table:

  • parent_id was added in migration 3781e22d8b01 and reply_to_id in migration a5c220713937. is_pinned, pinned_at and pinned_by were added in migration 2f1211949ecc.
  • parent_id and reply_to_id do different jobs. parent_id is the thread root: a channel's message list selects rows with a null parent_id, and a thread is read by selecting rows whose parent_id is the root message. reply_to_id points at a single message that the reply quotes, and the quoted message is fetched and attached when a message is served. It has no effect on which list a message appears in.
  • is_pinned, pinned_at and pinned_by are written together by POST /api/v1/channels/{id}/messages/{message_id}/pin. Unpinning clears pinned_at and pinned_by back to null. GET /api/v1/channels/{id}/messages/pinned reads them.
  • meta carries the webhook identity for messages posted through a channel webhook, under a webhook key, which is what lets those messages render with the webhook's name rather than a user account.

Message Reaction Table

Column NameData TypeConstraintsDescription
idTextPRIMARY KEY, UNIQUEUnique identifier (UUID)
user_idTextNOT NULLUser who reacted
message_idTextNOT NULLAssociated message
nameTextNOT NULLReaction name/emoji
created_atBigIntegernullableReaction timestamp

Things to know about the message_reaction table:

  • user_id, message_id and name are NOT NULL in the database (migration 3781e22d8b01) while the SQLAlchemy model declares them without the flag. The database constraint is the one that applies.

Model Table

Column NameData TypeConstraintsDescription
idTextPRIMARY KEYModel identifier
user_idText-Model owner
base_model_idTextnullableParent model reference
nameText-Display name
paramsJSON-Model parameters
metaJSON-Model metadata
is_activeBooleandefault=TrueActive status
created_atBigInteger-Creation timestamp
updated_atBigInteger-Last update timestamp

Note Table

Column NameData TypeConstraintsDescription
idTextPRIMARY KEYUnique identifier
user_idTextnullableOwner of the note
titleTextnullableNote title
dataJSONnullableNote content and data
metaJSONnullableNote metadata
created_atBigIntegernullableCreation timestamp
updated_atBigIntegernullableLast update timestamp

The note body lives in data under content.md and is markdown text. A row that holds an object or an array there instead, which a model can produce by passing structured data to the note tools, is served with that content rendered as a fenced json code block, and the row is rewritten to match the next time the note is saved.

Pin state is no longer stored on this table. The legacy is_pinned column was removed in migration 4de81c2a3af1 and replaced by a per-user Pinned Note Table. Pre-existing pins were backfilled to the note owner; the API surfaces is_pinned as a per-request join against the calling user's rows.

Pinned Note Table

Per-user note pins. Each row records that one user has pinned one note for their own sidebar. Pinning is private and does not affect any other user with access to the same note.

Column NameData TypeConstraintsDescription
idTextPRIMARY KEYUnique identifier (UUID)
user_idTextNOT NULLThe user who pinned the note
note_idTextNOT NULL, FOREIGN KEY(note.id) ON DELETE CASCADEThe pinned note
created_atBigIntegerNOT NULLPin creation timestamp (used for ordering)

A UNIQUE(user_id, note_id) constraint prevents duplicate pins for the same user/note pair. The pinned-note list is ordered by created_at DESC per user, so the most recently pinned note appears first. Deleting a note cascades through this table; toggling a pin does not modify note.updated_at.

OAuth Session Table

Column NameData TypeConstraintsDescription
idTextPRIMARY KEY, UNIQUEUnique session identifier
user_idTextFOREIGN KEY(user.id) CASCADE, NOT NULLAssociated user
providerTextNOT NULLOAuth provider (e.g., 'google')
tokenTextNOT NULLEncrypted OAuth token set
expires_atBigIntegerNOT NULLToken expiration timestamp
created_atBigIntegerNOT NULLSession creation timestamp
updated_atBigIntegerNOT NULLSession last update timestamp

Things to know about the oauth_session table:

  • The relationship to user is one-to-many. Each sign-in inserts a new row, so several rows can exist for the same user and the same provider. OAUTH_MAX_SESSIONS_PER_USER (default 10) caps how many a user may hold per provider; on sign-in the oldest rows above the cap are deleted.
  • Indexed on user_id, on expires_at and on (user_id, provider) (idx_oauth_session_user_id, idx_oauth_session_expires_at, idx_oauth_session_user_provider, all from migration 38d63c18f30f).
  • Deleting a user cascades to delete their sessions.
  • token holds the provider's token set (access token, ID token and refresh token) as JSON, encrypted at rest with Fernet before it is written and decrypted on read. The key comes from OAUTH_SESSION_TOKEN_ENCRYPTION_KEY, which falls back to WEBUI_SECRET_KEY. A key that is not already 44 characters is hashed with SHA-256 and base64-encoded to the length Fernet requires.

Prompt Table

Column NameData TypeConstraintsDescription
idTextPRIMARY KEYUnique identifier (UUID)
commandStringUNIQUE, INDEXUnique command identifier
user_idStringNOT NULLOwner of the prompt
nameTextNOT NULLDisplay name of the prompt
contentTextNOT NULLPrompt content/template
dataJSONnullableAdditional prompt data
metaJSONnullablePrompt metadata
is_activeBooleandefault=TrueActive status
version_idTextnullableCurrent version identifier
tagsJSONnullableAssociated tags
created_atBigIntegerNOT NULLCreation timestamp
updated_atBigIntegerNOT NULLLast update timestamp

Prompt History Table

Column NameData TypeConstraintsDescription
idTextPRIMARY KEYUnique identifier (UUID)
prompt_idTextFOREIGN KEY(prompt.id), INDEXReference to the prompt
parent_idTextnullableReference to the parent version
snapshotJSONNOT NULLSnapshot of the prompt at version
user_idTextNOT NULLUser who created the version
commit_messageTextnullableVersion commit message
created_atBigIntegerNOT NULLCreation timestamp

Skill Table

Column NameData TypeConstraintsDescription
idTextPRIMARY KEYUnique identifier (UUID)
user_idTextNOT NULLOwner/creator of the skill
nameTextNOT NULLDisplay name of the skill
descriptionTextnullableShort description (used in manifest)
contentTextNOT NULLFull skill instructions (Markdown)
dataJSONnullableAdditional skill data
metaJSONnullableSkill metadata
is_activeBooleandefault=TrueActive status
created_atBigIntegerNOT NULLCreation timestamp
updated_atBigIntegerNOT NULLLast update timestamp

Things to know about the skill table:

  • Uses UUID for primary key
  • Access control is managed through the access_grant table (resource_type skill)
  • description is injected into the system prompt as part of the manifest; content is loaded on-demand via the view_skill builtin tool

Tag Table

Column NameData TypeConstraintsDescription
idStringPK (composite)Normalized tag identifier
nameString-Display name
user_idStringPK (composite)Tag owner
metaJSONnullableTag metadata

Things to know about the tag table:

  • Primary key is composite (id, user_id)

Tool Table

Column NameData TypeConstraintsDescription
idStringPRIMARY KEYUnique identifier
user_idString-Tool owner
nameText-Tool name
contentText-Tool content/code
specsJSON-Tool specifications
metaJSON-Tool metadata
valvesJSON-Tool control settings
created_atBigInteger-Creation timestamp
updated_atBigInteger-Last update timestamp

User Table

Column NameData TypeConstraintsDescription
idStringPRIMARY KEYUnique identifier
usernameString(50)nullableUser's handle
nameStringNOT NULLUser's name
emailString-User's email
roleStringdefault='pending'User's role
profile_image_urlText-Profile image path
profile_banner_image_urlTextnullableProfile banner image path
bioTextnullableUser's biography
genderTextnullableUser's gender
date_of_birthDatenullableUser's date of birth
timezoneStringnullableIANA time zone name
presence_stateStringnullablePresence state
status_emojiStringnullableEmoji shown with status
status_messageTextnullableStatus text
status_expires_atBigIntegernullableStatus expiry timestamp
last_active_atBigInteger-Last activity timestamp
updated_atBigInteger-Last update timestamp
created_atBigInteger-Creation timestamp
settingsJSONnullableUser preferences
infoJSONnullableAdditional user info
variablesJSONnullableUser variables substituted into system prompts
oauthJSONnullableIdentity provider subjects, keyed by provider
scimJSONnullableSCIM provisioning data

Things to know about the user table:

  • Uses UUID for primary key
  • One-to-One relationship with auth table (shared id)
  • One-to-Many relationship with oauth_session table (via user_id foreign key), one row per sign-in
  • email is unique case-insensitively, enforced by the partial unique index uq_user_email_lower on lower(email) where email is not null (migration f0bd01a18a3d). An upgrade onto a database that already holds two accounts differing only in capitalisation stops and names them rather than choosing between them; see Duplicate Emails.
  • The email column carries no plain UNIQUE constraint in the database, which is why the row above shows none. The SQLAlchemy model declares unique=True on it, but the schema is built only from the migrations and none of them creates that constraint, so the index above is the whole of the enforcement.
  • variables was added in v0.11.0 (migration b0018471bbbe). It holds the user's own user variables as a flat map of string keys to string values, substituted into system prompts at request time. It is excluded from user API responses and is read through its own endpoints instead.
  • oauth replaced the single oauth_sub text column in v0.6.41 (migration b10670c03dd5), so one account can hold a subject from several identity providers at once. It stores {"<provider>": {"sub": "<subject>"}}. Databases that were still older than v0.6.41 when they were upgraded on a v0.9.6 or newer build had that value written as text rather than as an object, which locked the affected accounts out; migration 6d09d1bf1f23 rewrites those rows on startup and leaves every other row alone. See Existing accounts cannot sign in after a long-delayed upgrade.
  • The api_key column was removed in v0.6.41 (the same migration b10670c03dd5). API keys are now rows in the dedicated API Key Table. The migration copies each existing key across before dropping the column.
  • profile_banner_image_url, timezone, presence_state, status_emoji, status_message and status_expires_at were all added in v0.6.41 (migration b10670c03dd5).
  • timezone holds an IANA zone name (for example Europe/Vienna), written by POST /api/v1/auths/update/timezone. Calendar recurrence expansion, automation scheduling and the per-user usage statistics read it. An unset or unrecognised value falls back to the server's zone for calendars and automations, and to UTC for usage statistics.
  • status_emoji, status_message and status_expires_at hold the status a user sets for themselves. They are written through POST /api/v1/users/user/status/update, which is refused unless the users.enable_status config key is on, and they are returned with the signin response and with channel member listings.
  • presence_state is returned in channel member listings and in the socket session payload. No code path writes it.

The scim field's expected structure:

{
    "<provider>": {
        "external_id": string,  # externalId from the identity provider
    },
    # Multiple providers can be stored simultaneously
    # Example:
    # "microsoft": { "external_id": "abc-123" },
    # "okta": { "external_id": "def-456" }
}

Why this column was added:

  • SCIM account linking: Stores per-provider externalId values from SCIM provisioning, enabling identity providers (like Azure AD, Okta) to match users by their external identifiers rather than relying solely on email.
  • Multi-provider support: The per-provider key structure allows a single user to be provisioned from multiple identity providers simultaneously, each storing their own externalId.
  • OAuth fallback: When looking up a user by externalId, the system falls back to matching it against the subject stored in the oauth field for the same provider if no scim entry is found, enabling seamless linking of SCIM-provisioned and OAuth-authenticated accounts.

Entity Relationship Diagram

To help visualize the relationship between the tables, refer to the below Entity Relationship Diagram (ERD) generated with Mermaid.


Database Encryption with SQLCipher

For enhanced security, Open WebUI supports at-rest encryption for its primary SQLite database using SQLCipher. This is recommended for deployments handling sensitive data where using a larger database like PostgreSQL is not needed.

Prerequisites

SQLCipher encryption requires additional dependencies that are not included by default. Before using this feature, you must install:

  • The SQLCipher system library (e.g., libsqlcipher-dev on Debian/Ubuntu, sqlcipher on macOS via Homebrew)
  • The sqlcipher3-wheels Python package (pip install sqlcipher3-wheels)

For Docker users, this means building a custom image with these dependencies included.

Configuration

To enable encryption, set the following environment variables:

# Required: Set the database type to use SQLCipher
DATABASE_TYPE=sqlite+sqlcipher

# Required: Set a secure password for database encryption
DATABASE_PASSWORD=your-secure-password

When these are set and a full DATABASE_URL is not explicitly defined, Open WebUI will automatically create and use an encrypted database file at ./data/webui.db.

Important Notes

danger
  • The DATABASE_PASSWORD environment variable is required when using sqlite+sqlcipher.
  • The DATABASE_TYPE variable tells Open WebUI which connection logic to use. Setting it to sqlite+sqlcipher activates the encryption feature.
  • Keep the password secure, as it is needed to decrypt and access all application data.
  • Losing the password means losing access to all data in the encrypted database.
Migrating Existing Data to SQLCipher

Open WebUI does not support automatic migration from an unencrypted SQLite database to an encrypted SQLCipher database. If you enable SQLCipher on an existing installation, the application will fail to read your existing unencrypted data.

To use SQLCipher with existing data, you must either:

  1. Start fresh: Enable SQLCipher on a new installation and have users export/re-import their chats manually
  2. Manual database migration: Use external SQLite/SQLCipher tools to export data from the unencrypted database and import it into a new encrypted database (advanced users only)
  3. Use filesystem-level encryption: Consider alternatives like LUKS (Linux) or BitLocker (Windows) for at-rest encryption without database-level changes
  4. Switch to PostgreSQL: For multi-user deployments, PostgreSQL with TLS provides encryption in transit and can be combined with encrypted storage
VariableDefaultDescription
DATABASE_TYPENoneSet to sqlite+sqlcipher for encrypted SQLite
DATABASE_PASSWORD-Encryption password (required for SQLCipher)
DATABASE_ENABLE_SQLITE_WALFalseEnable Write-Ahead Logging for better performance
DATABASE_SQLITE_PRAGMA_SYNCHRONOUSNORMALSQLite sync mode (safe with WAL, avoids fsync per txn)
DATABASE_SQLITE_PRAGMA_BUSY_TIMEOUT5000Write-lock wait time in milliseconds
DATABASE_SQLITE_PRAGMA_CACHE_SIZE-65536Page cache size (negative = KiB; ≈ 64 MB)
DATABASE_SQLITE_PRAGMA_TEMP_STOREMEMORYTemp table storage (MEMORY keeps temps in RAM)
DATABASE_SQLITE_PRAGMA_MMAP_SIZE268435456Memory-mapped I/O size in bytes (≈ 256 MB)
DATABASE_SQLITE_PRAGMA_JOURNAL_SIZE_LIMIT67108864Max WAL file size after checkpoint (≈ 64 MB)
DATABASE_POOL_SIZENoneDatabase connection pool size
DATABASE_POOL_TIMEOUT30Pool connection timeout in seconds
DATABASE_POOL_RECYCLE3600Pool connection recycle time in seconds

For more details, see the Environment Variable Configuration documentation.

This content is for informational purposes only and does not constitute a warranty, guarantee, or contractual commitment. Open WebUI is provided "as is." See your license for applicable terms.