Better Helpdesk
Database

Tables

The tables of the helpdesk schema, every column with its type and default, and what each table holds.

Better Helpdesk keeps everything in the host's database: in one schema, helpdesk, on Postgres and SQL Server, and in tables prefixed helpdesk_ on MySQL and SQLite. better-helpdesk-migrate creates and updates them. The types below are Postgres's; Choose and connect a database says what differs on the others. Ids are uuid, generated by the app, and every timestamp is timestamptz, written from the app's clock; the defaults the columns still carry are left from before and unused. The TypeScript types (Conversation, Message) use the column names in camel case: contact_id is contactId. Related guide: Report from your own database.

A minor release only adds columns and tables. Changing, renaming or dropping one takes a major release, and its notes say so.

TableHolds
companyOrganizations, mapped to the host's by external_org_id
contactPeople: customers, visitors, leads
identityThe ways a contact is known
agentSupport team members
conversationConversations
participantContacts added to a conversation besides its own
messageMessages and internal notes
conversation_eventA conversation's timeline
attachmentFiles, by storage key
audit_logWhat agents changed through the agent routes
canned_replySaved answers
settingValues the package and agents keep
dealDeals
activityNotes, calls, meetings and host events on a timeline
jobQueued work
rate_limitHourly counters
conversation_tag, contact_tag, company_tagTags, in the order they were added
counterThe next conversation number

The migrator records what it applied in migration, and takes migration_lock while it runs. __migrations lists the migrations from before the adapters, which the first of the new ones reads; the sequence reference_seq is no longer used.

company

ColumnTypeNullDefaultNotes
iduuidnorandom
nametextno
external_org_idtextyesThe host's organization id. Unique. null for a company an agent made.
domaintextyesLower-cased. Indexed.
lead_stagetextyesOne of leadStages when written; a stage the host later removed stays until an agent changes it.
customjsonbno{}Values of customFields.company.
created_attimestamptznonow()

contact

ColumnTypeNullDefaultNotes
iduuidnorandom
nametextyes
emailtextyesLower-cased. Indexed.
company_iduuidyescompany.id; set to null when the company is deleted.
lead_stagetextyesOne of leadStages when written; a stage the host later removed stays until an agent changes it. A merge keeps the target's stage, or takes the source's when it has none.
customjsonbno{}Values of customFields.contact. A merge keeps both contacts' fields, the target's value where both have one.
localetextyesDecides the language of the customer's emails. A merge keeps the target's, or takes the source's when it has none.
blockedbooleannofalseRefused as a sender, and their conversations left out of the inbox. Partially indexed.
created_attimestamptznonow()
last_seen_attimestamptzyesThe last time the contact used the widget, signed in or as a visitor. Written at most once every 5 minutes. A merge keeps the later of the two values.

identity

ColumnTypeNullDefaultNotes
iduuidnorandom
contact_iduuidnocontact.id; deleted with the contact.
channeltextnohost, email or visitor. The type also allows github, which the package does not write.
external_idtextnoThe host user id, the email address, or the SHA-256 of a visitor token.
verifiedbooleannoThe host or a DKIM signature proved it.
created_attimestamptznonow()
last_used_attimestamptznonow()A visitor token unused for 30 days stops opening its contact.

A verified (channel, external_id) belongs to one contact only. (channel, external_id, contact_id) is unique.

agent

ColumnTypeNullDefaultNotes
iduuidnorandom
external_user_idtextnoThe host's user id. Unique.
nametextyesFrom identify, rewritten on each agent request.
emailtextyesWhere agent emails go. Rewritten on each agent request.
avatar_urltextyesFrom the identity's image, rewritten on each agent request.
roletextnoagentagent, supervisor or viewer, from the identity's role, rewritten on each agent request.
away_untiltimestamptzyesSet while away; the widget promises a reply after it.
last_seen_attimestamptznonow()Updated on each agent request.
deactivated_attimestamptzyesSet when the agent is removed from the team; cleared on their next agent request.
viewing_iduuidyesconversation.id the agent last opened, set to null when it is deleted; others see them on it while viewing_at is recent.
viewing_attimestamptzyes

A row is written on an agent's first request, keyed by the host's user id. The package never deletes agent rows.

conversation

ColumnTypeNullDefaultNotes
iduuidnorandom
numberintegernoThe number in the reference, from counter. Unique.
inboxtextnoA key of inboxes.
typetextnoOne of types.
kindtextnoticketchat for a message the customer wrote, ticket for a form with a subject or an email.
statustextnoopenopen, pending or resolved.
prioritytextnonormalOne of priorities: low, normal, high or urgent unless the host set its own.
tiertextyesOne of tiers, from resolveTier or an agent; picks the SLA policy.
channeltextnowidgetWhere it came in: widget, email, agent or api.
subjecttextyesWhat the customer called it; the only name they see.
titletextyesThe team's name for it, from triage or a rename.
contact_iduuidnoThe contact it belongs to; deleted with the contact.
company_iduuidyescompany.id; deleted with the company.
shared_with_companybooleannofalseOther contacts of the company may see and join it.
assignee_iduuidyesagent.id; set to null when the agent row is deleted.
contextjsonbno{}What the widget captured: url, title, userAgent, viewport, locale, appVersion, host, errors, landingPage, referrer, utm.
ai_suggestionjsonbyesTriage: type, priority, title, summary, duplicates (conversationId, reference, reason), and acceptedAt once accepted.
waiting_sincetimestamptzyesSet while the customer has the last word.
reminded_attimestamptzyesWhen the last reminder was queued.
snoozed_untiltimestamptzyesOn a pending conversation, when runJobs reopens it.
customer_seen_attimestamptzyesWhen the customer last read it in the widget.
agent_seen_attimestamptzyeslast_message_at as of the last time any agent opened it.
agent_typing_untiltimestamptzyesSet while an agent writes a reply; the customer sees them typing until it passes.
last_message_attimestamptznonow()
resolved_attimestamptzyesRetention counts from it.
merged_into_iduuidyesThe conversation this one was merged into; set to null when that one is deleted.
ratingtextyesgood or bad.
rating_scoreintegeryes1 to 5 on the five-point scale; rating is derived from it.
rating_commenttextyes
rated_attimestamptzyes
first_responded_attimestamptzyesThe first public agent reply. Backfilled from the messages when the column arrived.
respond_bytimestamptzyesWhen the first reply is due, from the SLA policy; kept once answered, so a report can tell whether it was met.
resolve_bytimestamptzyesWhen the resolution is due. Moves forward by the time the clock was paused, so it is read as of the last resume.
sla_due_attimestamptzyesThe nearest deadline still to meet while the status is open; the inbox sorts by it. Indexed.
sla_paused_attimestamptzyesSet while the clock is paused: the status is pending or resolved.
sla_paused_secondsintegerno0Time the clock has spent paused, in the hours the target counts.
respond_noticetextyesThe last notice sent about the response target: warning or breach.
resolve_noticetextyesThe same for the resolution target.
reply_sincetimestamptzyesWhen the customer's oldest unanswered message after the first response came in, while the next-reply clock runs.
reply_bytimestamptzyesWhen the answer to it is due. Cleared by a public agent reply, or when the conversation leaves open.
reply_noticetextyesThe last notice sent about the next-reply target.
created_attimestamptznonow()
searchtsvectorgeneratedFrom title and subject, with the simple configuration.

participant

ColumnTypeNullNotes
conversation_iduuidnoDeleted with the conversation.
contact_iduuidnoDeleted with the contact.

The primary key is the pair.

message

ColumnTypeNullDefaultNotes
iduuidnorandom
conversation_iduuidnoDeleted with the conversation.
author_typetextnocontact, agent or system.
contact_iduuidyesThe author, for a contact's message; set to null when the contact is deleted.
agent_iduuidyesThe author, for an agent's message; set to null when the agent row is deleted.
bodytextnoThe text, with Markdown formatting.
internalbooleannofalseA note only agents see.
verifiedbooleanyesA customer message whose author proved who they are: a host user with a verified email, or mail their domain signed. null on messages stored before the column existed.
email_message_idtextyesMessage-ID of the email it came from. Unique.
created_attimestamptznonow()
searchtsvectorgeneratedFrom body, with the simple configuration.

conversation_event

ColumnTypeNullDefaultNotes
iduuidnorandom
conversation_iduuidnoDeleted with the conversation.
agent_iduuidyesThe agent who made the change, if one did; set to null when the agent row is deleted.
kindtextnoSee below.
datajsonbno{}
created_attimestamptznonow()
kinddata
status, priority, tier, assigneeId, type, inbox, title, tags, snoozedUntil, rating{ from, to }, plus by: 'customer' when the customer changed it. Removing an agent from the team writes an assigneeId row with no agent_id for each conversation it unassigns.
sla.warning, sla.breach{ target, due }: response or resolution, and the deadline as an ISO timestamp
reopenednone; a customer wrote on a resolved conversation
email.sent{ kind, to }: the email kind (agent-sla among them), or agent-reopened, and the list of addresses
mentioned{ agentIds }
participant.added{ contactId }
merged.into, merged.from{ conversationId, reference }
suggestion.accepted, suggestion.dismissednone

attachment

ColumnTypeNullDefaultNotes
iduuidnorandom
conversation_iduuidnoDeleted with the conversation.
message_iduuidyesDeleted with the message.
keytextnoThe storage key.
filenametextno
content_typetextno
sizeintegernoBytes.
uploadedbooleannofalseThe file is in storage and shows in the conversation. A file added to a reply or a note stays false until its message is sent.
created_attimestamptznonow()

The files themselves live with the storage adapter.

audit_log

ColumnTypeNullDefaultNotes
iduuidno
agent_iduuidyesThe agent; set to null when the agent row is deleted.
actiontextnoThe method and route, such as PATCH conversations/:id.
targettextyesThe id in the route, when it has one.
datajsonbno{ fields }: the names of the fields the request sent, never their values.
created_attimestamptzno

One row per agent request that is not a GET and succeeded, except the typing indicator and marking notifications read. The package never deletes these rows; retentionDays and the delete functions leave them, so what an agent did to a conversation since deleted is still on record.

canned_reply

ColumnTypeNullDefaultNotes
iduuidnorandom
titletextno
bodytextno
localetextyesen, de, fr, it, or null for any language.
created_attimestamptznonow()

setting

ColumnTypeNullDefaultNotes
keytextnoPrimary key.
valuejsonbno
updated_attimestamptznonow()
keyHolds
confirmationWhat the widget says after a first message, per locale.
aiThe AI switches the team turned off or on; a missing one is on.
closedThe days closed in the agent UI's settings, each { date, name? }, in date order.
recipientsThe agent ids chosen to receive emails about new requests; empty means everyone seen in the last 30 days.
rating-link-secretThe key that signs rating links, generated on first use.
views:sharedSaved views shared with the team.
views:agent:{agentId}An agent's own saved views.
notifications-seen:{agentId}When the agent last read their notifications.

deal

ColumnTypeNullDefaultNotes
iduuidnorandom
titletextno
company_iduuidyesDeleted with the company.
contact_iduuidyesSet to null when the contact is deleted.
stagetextnoOne of dealStages when written; a stage the host later removed stays until an agent moves the deal.
stage_changed_attimestamptznonow()
valuenumeric(14, 2)yes
currencytextnoCHFThree letters.
expected_close_attimestamptzyes
owner_iduuidyesagent.id; set to null when the agent row is deleted.
customjsonbno{}Values of customFields.deal.
created_attimestamptznonow()

activity

ColumnTypeNullDefaultNotes
iduuidnorandom
kindtextnonote, call or meeting from an agent; event from helpdesk.track.
contact_iduuidyesDeleted with the contact.
company_iduuidyesDeleted with the company.
deal_iduuidyesDeleted with the deal.
agent_iduuidyesThe agent who logged it; set to null when the agent row is deleted.
bodytextyes
eventtextyesThe host event's name.
propsjsonbyesThe host event's properties.
occurred_attimestamptznonow()

job

ColumnTypeNullDefaultNotes
iduuidnorandom
kindtextnoSee below.
payloadjsonbno
run_attimestamptznonow()Due from then. 31 December 9999 after the fifth failed attempt, so it is never claimed again.
attemptsintegerno0
locked_untiltimestamptzyesA claim, two minutes long.
last_errortextyesThe last failure's message, cut at 2,000 characters.
created_attimestamptznonow()

A job's row is deleted once it completes.

kindDoes
notify-agentsSends agent-new.
notify-mentionedSends agent-mention.
notify-customerSends customer-reply, two minutes after the reply.
send-receiptSends customer-receipt.
agent-reminderSends agent-reminder.
sla-noticeSends agent-sla.
ai-triageRuns AI triage.

rate_limit

ColumnTypeNullDefaultNotes
keytextnoWhat is counted, such as ip:{address} or contact:{id}.
window_starttimestamptznoThe start of the hour.
countintegerno0

The primary key is (key, window_start). Windows older than a day are deleted as new hits arrive.

Tags

conversation_tag, contact_tag and company_tag hold one row per tag, unique per owner. Conversation tags are lower-cased; a contact's include the answers to inbox qualifying questions. A contact merge adds the source's tags after the target's.

ColumnTypeNullDefaultNotes
conversation_id, contact_id or company_iduuidnoThe owner; its tags go with it.
tagtextnoIndexed.
positionintegernoThe order the tag was added in, from 0.

The primary key is the owner and tag.

counter

ColumnTypeNullDefaultNotes
nametextnoreference. Primary key.
valueintegernoThe number the next conversation gets; from 1000 on a new install.

On this page