AI Chat stores everything inside your application: structured data in your App's database via OrmLite, and files under App_Data. There is no external service holding your conversation history.
Database tables​
Tables are created on startup when AutoInitSchema is true (the default), using the host's IDbConnectionFactory - or a named connection if you'd rather keep chat data separate:
services.AddPlugin(new ChatFeature {
NamedConnection = "chat",
AutoInitSchema = true,
});
ChatThread​
One conversation. Complex fields are stored as raw JSON strings so the wire shape matches the OpenAI format exactly, and are only parsed at the DTO boundary.
| Column | Notes |
|---|---|
Id |
Auto-increment |
User |
Data partition key - the authenticated username, or default |
CreatedAt, UpdatedAt |
Indexed |
Title, SystemPrompt, Model |
|
ModelInfo, Modalities, Args |
JSON |
Messages |
JSON - the durable conversation |
StreamingMessage |
JSON - in-flight assistant message while streaming |
Tools, ToolHistory |
JSON |
Cost, InputTokens, OutputTokens, Stats |
Thread rollup |
Provider, ProviderModel |
|
StartedAt, CompletedAt, Status |
|
Metadata |
JSON |
StreamingMessage is deliberately separate from Messages: a failed or abandoned stream can never damage the durable conversation, and checkpointing writes one small column rather than rewriting the whole thread.
ChatRequest​
Per-completion accounting behind the Analytics dashboards - user, thread, model, provider, duration, token counts, prices, cost, finish reason, and Error/StackTrace when a completion fails.
ChatMedia​
The generated and uploaded media catalog - name, type, prompt, model, cost, seed, dimensions, size, duration, aspect ratio, content hash, reactions, caption, tags, rating and publish state. Written by the gallery extension from a cache-saved filter. See Voice & Media.
Gemini tables​
The gemini extension owns its own document catalog tables, created the same way. See Gemini RAG, Search & Analytics.
Querying​
They're ordinary OrmLite tables:
using var db = dbFactory.Open();
var recent = db.Select<ChatThread>(db.From<ChatThread>()
.Where(x => x.User == userName)
.OrderByDescending(x => x.UpdatedAt)
.Take(20));
AdminQueryChatRequests additionally exposes ChatRequest as an AutoQuery API for admins.
File storage​
Rooted at App_Data/chat by default, overridable with AppDataPath:
Per-user layout​
With RequireAuth = false everything runs as the default user, so all of the above lives under user/default/.
Path resolution is guarded: any relative path that would escape App_Data/chat throws UnauthorizedAccessException.
Static project exports​
The global user/default/share_static/config.json configures static folder sharing. By default,
exports live outside App_Data at <WebContentDirectory>/p/<user>/<project-folder>/, typically
wwwroot/p/.... A custom Directory can select another export root. The UI abbreviates this root to
~/; it does not display the full destination path.
Project staticPublication metadata remains in the user's project configuration, separately from the
remote publication link. Folder exports can be served by a static server without AI.Chat running.
Include the export directory in deployment or backup if you need to retain the served copies.
See Publishing.
The content-addressed cache​
Cache writes fire the cache_saved filters, which is how the gallery records media and how an App can hook uploads:
ctx.RegisterCacheSavedFilter(saved => {
Log.LogInformation("Cached {Url} ({Size} bytes)", saved.Url, saved.Size);
});
Cached files are served at {RoutePrefix}/~cache/{path} to authenticated users.
Seeded config files​
llms.json is seeded only when missing and preserves user settings across restarts. Provider
catalogs (providers.json and providers-extra.json) refresh from bundled defaults on startup unless
their filenames are listed in PreserveConfigs. Preserved catalog files are seeded when missing.
services.AddPlugin(new ChatFeature {
PreserveConfigs = ["providers-extra.json"],
});
Alternatively, bypass llms.json entirely by setting ChatFeature.Config in code. See Providers & Models.
PDF storage​
PdfFeature uses a separate root, App_Data/pdf by default:
See Rendering PDFs.
Backup and deployment​
Back up together:
- Your App's database (threads, requests, media, Gemini catalog)
App_Data/chat- config, cache and every user's workspaceApp_Data/pdf- including.published.jsonand.versions
A database backup on its own is not sufficient: media rows reference cache files by hash, and Gemini document rows reference cached bytes.
Deployment notes:
Retention​
AI Chat doesn't expire data on your behalf, so retention policy is yours to implement:
// example: delete threads untouched for a year
using var db = dbFactory.Open();
var cutoff = DateTime.UtcNow.AddYears(-1);
var ids = db.Column<long>(db.From<ChatThread>()
.Where(x => x.UpdatedAt < cutoff).Select(x => x.Id));
db.Delete<ChatRequest>(x => Sql.In(x.ThreadId, ids));
db.Delete<ChatThread>(x => Sql.In(x.Id, ids));
Deleting a user's folder under App_Data/chat/user/{user} removes their projects, profiles, skills, PDF workspace and preferences.
Decision, subscription, and creation state​
| Location | State |
|---|---|
App_Data/chat/user/{user}/jev/ |
Portable recipes, immutable history, index, initialization receipt, and publication journals |
App_Data/chat/user/{user}/credentials/openai_subscription.json |
Private per-user ChatGPT grant |
App_Data/chat/openai-agent-host.json |
Public-sign-in host registration identity |
| App database | Durable project-creation reservations, chat/run identities, and Gemini desired/completed work |
Use a single owning host process per App_Data root for Decision Studio and rotating subscription credentials. Stop hosts before moving profiles and preserve hidden files and pending receipts. Back up the database and App_Data together. A downgrade requires restoring the matching pre-upgrade backup rather than handing migrated state to an older binary.