Skip to content

Database Schema

A guide to the tables you're most likely to query when writing SQL reports against a self-hosted Quackback database. It lists key columns only. The source of truth is the Drizzle schema in packages/db/src/schema/ in the Quackback repository.

Read from the database, but make changes through the app, the REST API, or MCP. Writing to tables directly can skip validation, events, webhooks, and search indexing.

Overview

  • IDs: primary and foreign keys are stored as PostgreSQL uuid. The app and API show them as TypeIDs with a type prefix, such as post_01h455vb4pex5vsknk084sn02q. The prefix is not stored.
  • Timestamps: every timestamp column is timestamp with time zone.
  • Soft deletes: most content tables have a deleted_at column. Filter on deleted_at IS NULL to match what the app shows.
  • Search: posts, conversation messages, and help center articles have a generated search_vector column for full-text search. Posts, changelog entries, and articles also store an embedding (pgvector) when AI is configured.
  • People are principals: authors, voters, agents, and customers are all referenced by principal_id, not user_id. Join principal to user for names and emails.

Renamed in 0.14

Upgrading from 0.13 renames several tables. Update any saved SQL.

0.13 name0.14 name
votespost_votes
commentspost_comments
tagspost_tags (the tag catalogue)
post_tags (post-to-tag join)post_tag_assignments
comment_reactionspost_comment_reactions
comment_edit_historypost_comment_edit_history
merge_suggestionspost_merge_suggestions
chat_messagesconversation_messages
chat_message_mentions, chat_message_reactions, chat_message_flagsconversation_message_mentions, conversation_message_reactions, conversation_message_flags
chat_tagsconversation_tags (the tag catalogue)
conversation_tags (conversation-to-tag join)conversation_tag_assignments

Roadmaps are now built from post statuses, and the post_roadmaps table is gone. On installs upgraded from 0.13, the old curated placements are kept in post_roadmaps_archived for reference until a later release. Nothing reads or writes it.

Table Categories

CategoryTables
People and accessuser, principal, session, account, invitation, roles, permissions, role_permissions, principal_role_assignments, teams, team_members
Feedbackboards, posts, post_statuses, post_votes, post_comments, post_comment_reactions, post_tags, post_tag_assignments, post_notes, post_edit_history, post_comment_edit_history
Roadmapsroadmaps, roadmap_columns
Changelogchangelog_entries, changelog_entry_posts
Supportconversations, conversation_messages, conversation_tags, conversation_tag_assignments, conversation_participants, tickets, ticket_statuses, ticket_conversations, ticket_links
Help centerkb_categories, kb_articles, kb_article_translations, kb_search_queries
Customerscompanies, segments, user_segments
AIpost_merge_suggestions, post_sentiment
Notificationspost_subscriptions, notification_preferences, in_app_notifications
Integrations and APIintegrations, post_external_links, ticket_external_links, webhooks, api_keys
Events and jobsevents, job_queue
Workspacesettings

People and Access

user

One row per person who has signed in or been identified.

ColumnDescription
idUser ID
nameDisplay name
emailEmail address. Anonymous visitors have a placeholder address.
email_verifiedWhether the email is verified
external_idID from your own system, set by widget identify
locale, countryDetected locale and country
created_atWhen the user was created

principal

The identity that owns content and activity. A principal usually points to a user, but service principals (API keys, integrations) don't.

ColumnDescription
idPrincipal ID
user_idThe linked user, if any
typeuser, anonymous, service, or support
roleAccess tier: admin or member for team members, user for portal users. Fine-grained roles are in principal_role_assignments.
display_nameName shown for service principals
company_idThe customer's companies row, if any
blocked_atWhen the principal was blocked
created_atCreation time

To list your team with their roles:

SELECT u.name, u.email, r.name AS role
FROM principal p
JOIN "user" u ON u.id = p.user_id
JOIN principal_role_assignments pra ON pra.principal_id = p.id
JOIN roles r ON r.id = pra.role_id
WHERE p.type = 'user' AND p.role IN ('admin', 'member');

roles, permissions, role_permissions, principal_role_assignments

Role-based access control. roles holds the built-in roles (Owner, Admin, Manager, Contributor) and any custom roles. role_permissions maps roles to permissions. principal_role_assignments gives a principal a role, optionally scoped to a team.

TableKey columns
rolesid, key, name, is_system
permissionsid, key, category
role_permissionsrole_id, permission_id
principal_role_assignmentsprincipal_id, role_id, team_id, granted_by_principal_id, created_at

session, account, invitation

TableKey columns
sessionuser_id, expires_at, ip_address, user_agent, scope (dashboard, widget, or portal)
accountuser_id, provider_id (sign-in method, such as credential, github, or an OIDC provider), account_id
invitationemail, role_id, status, kind (team or portal), expires_at, inviter_id

teams, team_members

TableKey columns
teamsid, name, is_default, assignment_method, deleted_at
team_membersteam_id, principal_id

Feedback

boards

ColumnDescription
idBoard ID
slug, name, descriptionBoard identity
accessJSON. Who can see and post on the board.
deleted_atSoft delete

posts

ColumnDescription
idPost ID
board_idThe board
title, contentTitle and plain-text body. content_json holds the rich-text document.
principal_idAuthor
status_idThe post_statuses row
owner_principal_idAssigned teammate
vote_count, comment_countDenormalised counts
moderation_statepublished, pending, spam, archived, closed, or deleted
canonical_post_id, merged_atSet when the post was merged into another
etaPlanned date, used by date roadmaps
created_at, updated_at, deleted_atTimestamps

Merged posts keep their row. Filter on canonical_post_id IS NULL to count only canonical posts.

post_statuses

ColumnDescription
id, name, slug, colorStatus identity
categoryactive, complete, or closed
show_on_roadmapWhether posts with this status appear on roadmaps
is_defaultStatus given to new posts
positionDisplay order

post_votes

ColumnDescription
post_idThe post
principal_idThe voter
added_by_principal_idTeammate who added the vote on someone's behalf, if any
source_type, source_external_urlWhere a proxy vote came from
created_atWhen the vote was cast

post_comments

ColumnDescription
post_idThe post
parent_idParent comment for replies
principal_idAuthor
contentPlain-text body
is_team_memberWritten by a team member
is_privateInternal comment, hidden from the portal
status_change_from_id, status_change_to_idSet when the comment records a status change
moderation_stateSame values as posts
created_at, deleted_atTimestamps

post_tags, post_tag_assignments

post_tags is the tag catalogue. post_tag_assignments links tags to posts.

TableKey columns
post_tagsid, name, color, is_public, deleted_at
post_tag_assignmentspost_id, tag_id, auto_tagged
SELECT t.name, count(*) AS posts
FROM post_tag_assignments a
JOIN post_tags t ON t.id = a.tag_id
JOIN posts p ON p.id = a.post_id
WHERE p.deleted_at IS NULL AND t.deleted_at IS NULL
GROUP BY t.name
ORDER BY posts DESC;

Other post tables

TableKey columns
post_comment_reactionscomment_id, principal_id, emoji
post_notespost_id, principal_id, content (internal notes)
post_edit_historypost_id, editor_principal_id, previous_title, previous_content
post_comment_edit_historycomment_id, editor_principal_id, previous_content

Roadmaps

Roadmaps are views over posts. A post appears on a roadmap because of its status (column roadmaps) or its eta (date roadmaps), not because it was placed there.

TableKey columns
roadmapsid, slug, name, type (column or date), base_filter (JSON), date_source, frequency, visibility (public, team, or segment), visible_segment_ids, deleted_at
roadmap_columnsroadmap_id, status_id, name, position

Changelog

TableKey columns
changelog_entriesid, title, content, principal_id, published_at (null while a draft), view_count, deleted_at
changelog_entry_postschangelog_entry_id, post_id (posts the entry shipped)

Support

conversations

ColumnDescription
idConversation ID
visitor_principal_idThe customer
assigned_agent_principal_id, assigned_team_idAssignment
statusopen, snoozed, or closed
snoozed_untilWhen a snoozed conversation reopens
channelmessenger, email, or github
priorityTriage priority (none when unset)
subject, last_message_atSummary fields
csat_rating, csat_commentSatisfaction rating
resolved_at, end_reasonWhen and why it was closed
created_atWhen it started

conversation_messages

ColumnDescription
conversation_idThe conversation
ticket_idSet when the message belongs to a ticket thread
principal_idSender
sender_typevisitor, agent, or system
contentMessage body
is_internalInternal note, not shown to the customer
created_at, deleted_atTimestamps

conversation_tags, conversation_tag_assignments

conversation_tags is the tag catalogue (id, name, color, deleted_at). conversation_tag_assignments links tags to conversations (conversation_id, conversation_tag_id). conversation_participants lists extra teammates on a conversation.

tickets

ColumnDescription
id, numberTicket ID and human-readable number
typecustomer, back_office, or tracker
ticket_type_idThe configured ticket type
titleTitle
status_idThe ticket_statuses row
priorityPriority
requester_principal_id, assignee_principal_id, assignee_team_idPeople
company_idCustomer company
first_response_at, due_at, resolved_atSLA timestamps
created_at, deleted_atTimestamps
TableKey columns
ticket_statusesid, name, category (open, pending, or closed), public_stage
ticket_conversationsticket_id, conversation_id
ticket_linkstracker_ticket_id, linked_ticket_id, relation

Help Center

TableKey columns
kb_categoriesid, parent_id, slug, name, is_public, position, deleted_at
kb_articlesid, category_id, slug, title, content, principal_id, published_at (null while a draft), view_count, helpful_count, not_helpful_count, deleted_at
kb_article_translationsarticle_id, locale, title, content, status
kb_search_queriesquery, locale, results_count, created_at

Customers

TableKey columns
companiesid, name, external_id, plan, mrr_cents, size, industry, custom_attributes
segmentsid, name, slug, type (manual or dynamic), rules, deleted_at
user_segmentsprincipal_id, segment_id, added_by, added_at

AI

TableKey columns
post_merge_suggestionssource_post_id, target_post_id, status, llm_confidence, resolved_at
post_sentimentpost_id, sentiment (positive, neutral, or negative), confidence, processed_at

Notifications

TableKey columns
post_subscriptionspost_id, principal_id, reason, notify_comments, notify_status_changes
notification_preferencesprincipal_id, email_status_change, email_new_comment, email_muted
in_app_notificationsprincipal_id, type, title, post_id

Integrations and API

TableKey columns
integrationsid, integration_type, status, connected_by_principal_id. Credentials are encrypted.
post_external_linkspost_id, integration_type, external_id, external_url, status
ticket_external_linksticket_id, integration_type, external_id, external_url, status
webhooksid, url, events, board_ids, status, failure_count, last_triggered_at, deleted_at
api_keysid, name, key_prefix, scopes, created_by_id, last_used_at, expires_at, revoked_at. Only a hash of the key is stored.

Events and Jobs

Background work runs on PostgreSQL. There is no separate queue service.

TableKey columns
eventsid, type (such as post.created), entity_type, entity_id, actor_type, actor_id, payload, occurred_at, published_at
job_queueid, queue, status (pending, running, succeeded, or failed), run_at, attempts, max_attempts, last_error, created_at, finished_at

Domain events are written to events in the same transaction as the change that raised them, then fanned out to webhooks, notifications, and workflows through job_queue. Finished jobs are deleted after the retention period (JOB_RETENTION_MS, 7 days by default).

To see recent failed jobs:

SELECT queue, last_error, attempts, finished_at
FROM job_queue
WHERE status = 'failed'
ORDER BY finished_at DESC
LIMIT 20;

Workspace

settings

A single row holding workspace configuration: name, slug, logos, and JSON (stored as text) for authentication, portal, branding, widget, help center, and feature flags. Change these through Admin → Settings or the config file, not with SQL.

Next steps