The Postgres schema, summarized. Every table that holds tenant data has
a workspace_id column (directly or transitively via
agent_id) and the corresponding model uses
BelongsToWorkspace or BelongsToAgent.
Identity
| Table | Notes |
users | Email + password (bcrypt). 2FA fields. role: customer or super_admin (the PlatformRole enum). default_workspace_id for switcher pinning. |
workspaces | Name + Stripe customer/subscription IDs + plan_id. app_settings-style overrides live in settings JSON column. |
workspace_users | Pivot. (user_id, workspace_id, role) where role is owner/admin/member. |
invitations | Email + role + token + 7-day expiry. Belongs to a workspace. |
Agents & their config
| Table | Notes |
agents | Persona, theme, system_prompt, guardrails, starter_prompts, allowed_origins, confidence_threshold, language_default, auto_index_visited_pages, is_published, published_version_id. |
agent_versions | Immutable snapshots created on every Publish. The runtime reads from these, not the agent row. |
behavior_rules | (agent_id, kind, conditions JSON, action JSON, enabled, priority). |
cta_rules | Specialized behavior rules for clickable calls-to-action. |
curated_answers | (agent_id, triggers[], answer_md, citation_url). |
experiments | A/B configurations on behavior rules. |
Knowledge
| Table | Notes |
sources | (agent_id, type, status, config JSON, last_synced_at, error). type: url/sitemap/feed/text/notion/google_doc/auto. |
documents | (source_id, url, title, content, lang). One per crawled page or pasted text. |
chunks | (document_id, idx, content, token_count). The unit that's embedded. |
integration_connections | (workspace_id, provider, encrypted_token, scope). Notion, Google Drive, Slack. |
Chunk embeddings live only in the vector store
(Vectorize / Qdrant) โ the Postgres chunks table holds
text + metadata, not vectors. Metadata is mirrored as labels on the
vector point so retrieval can filter by agent_id.
Conversations & messages
| Table | Notes |
visitors | (agent_id, anonymous_id, ip_hash, ua, first_seen_at, last_seen_at, visit_count). The anon_id ties widget reloads to the same row. |
conversations | (agent_id, visitor_id, page_url, started_at, lang, claimed_by_user_id, claimed_at, cleared_at, is_playground). |
messages | (conversation_id, role, content, citations JSON, latency_ms, created_at). Roles: user / assistant / human-agent. |
leads | (agent_id, conversation_id, name, email, phone, fields JSON). Unique on (agent_id, email) for dedup. |
content_gaps | (agent_id, sample_question, occurrence_count, sample_conversations[]). Created by DetectGapJob when low-confidence is flagged. |
Billing
| Table | Notes |
plans | (name, slug, monthly_conversations, price_cents, features JSON, is_active, stripe_product_id, stripe_price_id). |
plan_subscriptions | Cashier subscription mirror. (workspace_id, stripe_subscription_id, stripe_status, ends_at). |
usage_events | (workspace_id, kind, quantity, occurred_at). Aggregate by month for the quota gate. |
app_settings | Singleton row. Stripe / mail / branding overrides + LLM / vector / crawler config not in env. Encrypted casts on every secret. |
Operations
| Table | Notes |
audit_logs | (actor_id, workspace_id, action, target_type, target_id, metadata, created_at). Every privileged action. |
jobs / failed_jobs | Standard Laravel queue tables. failed_jobs drives /admin/jobs/failed. |
notifications | Standard Laravel notifications. |
Indexes that matter
conversations(agent_id, visitor_id, started_at desc) โ visitor resume lookup runs every init.
messages(conversation_id, created_at) โ chat history hydration.
chunks(document_id) โ fast deletes on source removal.
usage_events(workspace_id, occurred_at) โ quota gate.
leads(agent_id, email) โ dedup constraint, unique.
workspace_users(user_id, workspace_id) โ unique pivot.
Encryption at rest
Sensitive columns use Laravel's encrypted cast: integration
OAuth tokens, Stripe secrets in app_settings, mail
passwords, custom LLM API keys. The encryption key is the standard
APP_KEY โ back it up the same way you back up the database.
Soft deletes
Most tables hard-delete on cascade. The exceptions:
plans โ never destructively deleted (FK from workspaces, FK from invoices). Soft via is_active.
invitations โ cleared by a daily cleanup job after expiry.