WarmDesk uses GORM with automatic migration (AutoMigrate) β there are no separate migration files.
Every time the server starts it compares the model structs to the live schema and adds any missing columns or tables.
You should never need to touch the database directly for routine upgrades.
Supported engines: SQLite (default), PostgreSQL, and MySQL.
As of this writing autoMigrate() (backend/database/database.go) migrates 64 model structs. GORMβs default naming
strategy pluralizes struct names to derive table names (e.g. CardHistory β card_histories, SlaPolicy β sla_policies),
which is why some table names below look slightly different from their Go type name.
Design decisions
Fractional position ordering
Columns, cards, epics, sprints, sprint-card links, and both card and ticket checklist items use a position REAL (float)
column. Inserting between two items assigns the midpoint of the surrounding values.
This avoids renumbering the entire list on every drag-and-drop.
Atomic card numbering
Each project has a card_counter INTEGER column (not exposed via the API β its JSON tag is "-" β but it is a real,
persisted column). The backend increments it atomically when creating a card and uses the result as the human-readable
number (e.g. PRJ-42).
Polymorphic attachments
The attachments table stores files through two columns: owner_type (string) and owner_id (integer), rather than a
separate table per owner kind. Valid owner types: card, card_comment, chat_message, conv_message, ticket,
ticket_message. checkAttachmentAccess (handlers/attachment.go) walks the ownership chain for each type back to a
project membership or conversation membership check before serving a download.
message_reactions uses the same polymorphic pattern for emoji reactions, but is limited to chat_message and
conv_message.
System settings
Operational settings (SMTP, branding, locale defaults, session timeout, β¦) live in a system_settings key/value table
and are read at request time via loadAllSettings() so changes take effect without a restart. The table has only two
columns β key (primary key) and value β with no surrogate id and no timestamps. All valid keys are defined as
constants in handlers/system.go.
Core tables
users
| Column | Type | Constraints | Notes |
|---|---|---|---|
| BIGINT | PK, auto | |
| TIMESTAMP | GORM auto-field | |
| TIMESTAMP | GORM auto-field | |
| TIMESTAMP | nullable, index | Soft-delete support |
| VARCHAR(255) | unique, not null | Login identifier |
| VARCHAR(100) | unique, not null | Display/login name |
| VARCHAR | not null | bcrypt, cost 12; never serialized ( |
| VARCHAR(100) | nullable | |
| VARCHAR(100) | nullable | |
| VARCHAR(150) | nullable | Overrides first/last name in the UI when set |
| VARCHAR(500) | nullable | Profile picture path |
| VARCHAR | not null, default |
|
| VARCHAR(10) | default | UI language preference |
| VARCHAR(20) | default |
|
| VARCHAR(50) | default | Drives every date/time rendering via |
| VARCHAR(100) | default | |
| VARCHAR(100) | default | |
| VARCHAR(10) | default | |
| VARCHAR(10) | default | |
| BOOLEAN | default true | |
| VARCHAR(20) | default |
|
| TIMESTAMP | nullable | |
| TIMESTAMP | nullable | |
| BOOLEAN | default true | Account enabled |
| BOOLEAN | default true | |
| BOOLEAN | default false | |
| BOOLEAN | default false | Read-only access to others' time reports |
| BOOLEAN | default true | |
| BOOLEAN | default true | |
| BOOLEAN | default false | Gates the ticketing module ( |
| VARCHAR(10) | default |
|
| VARCHAR(10) | default |
|
| VARCHAR(10) | default |
|
| VARCHAR(10) | default |
|
| BOOLEAN | default true | Desktop (Tauri) setting |
| BOOLEAN | default true | Desktop (Tauri) setting |
| VARCHAR(5) | default | Wall-clock |
| VARCHAR(5) | default | |
| VARCHAR(5) | default | |
| VARCHAR(5) | default | |
| VARCHAR(5) | default | |
| VARCHAR(5) | nullable, no default | Empty = non-working day |
| VARCHAR(5) | nullable, no default | Empty = non-working day |
| INTEGER | default 30 | |
| VARCHAR(64) | nullable | Never serialized ( |
| BOOLEAN | default false | MFA active flag |
| VARCHAR(64) | nullable, index | Never serialized ( |
| TIMESTAMP | nullable | Never serialized ( |
| TIMESTAMP | nullable | |
| BOOLEAN | default false | Force password reset on next login |
gravatar_url and can_view_reports are computed at read time (gorm:"-") and never persisted β the former in
User.AfterFind, the latter by handlers that check report permissions.
projects
| Column | Type | Constraints | Notes |
|---|---|---|---|
| BIGINT | PK, auto | |
| TIMESTAMP | ||
| TIMESTAMP | ||
| TIMESTAMP | nullable, index | |
| VARCHAR(200) | not null | Display name |
| TEXT | nullable | |
| VARCHAR(100) | unique, not null | URL-safe identifier used in all API routes |
| VARCHAR(7) | nullable | Hex accent colour for the project card |
| VARCHAR(500) | nullable | |
| BOOLEAN | default false | |
| BOOLEAN | default false | Hidden from sidebar/listing; see "Project closed state" |
| VARCHAR(10) | unique, not null, default | Prefix for card numbers (e.g. |
| INTEGER | default 0 | Sidebar/list display order |
| INTEGER | default 0 | Atomic counter, incremented per card; |
| BIGINT | FK β customers, nullable, index | Owning customer |
| BIGINT | FK β contracts, nullable, index | |
| VARCHAR(20) | default |
|
| BOOLEAN | default false | Project exists only to log undeclarable/travel time, no board |
| INTEGER | default 0 | Minutes subtracted from every logged entry before it counts as billable |
| BIGINT | FK β users, not null |
project_members
| Column | Type | Constraints | Notes |
|---|---|---|---|
| BIGINT | PK, auto | |
| TIMESTAMP | ||
| TIMESTAMP | ||
| BIGINT | FK β projects, not null, uniqueIndex( | |
| BIGINT | FK β users, not null, uniqueIndex( | |
| VARCHAR | not null, default |
|
| BIGINT | User ID of the inviter |
Board tables
epics
Top-level grouping for cards, independent of Scrum sprints β usable on both kanban and scrum boards.
| Column | Type | Constraints | Notes |
|---|---|---|---|
| BIGINT | PK, auto | |
| TIMESTAMP | ||
| TIMESTAMP | ||
| TIMESTAMP | nullable, index | |
| BIGINT | FK β projects, not null, index | |
| VARCHAR(200) | not null | |
| TEXT | nullable | |
| VARCHAR(7) | default | |
| VARCHAR(20) | default | |
| REAL | default 0 | Fractional ordering |
card_count and done_count are computed per request (gorm:"-"), not stored.
columns
| Column | Type | Constraints | Notes |
|---|---|---|---|
| BIGINT | PK, auto | |
| TIMESTAMP | ||
| TIMESTAMP | ||
| TIMESTAMP | nullable, index | |
| BIGINT | FK β projects, not null, index | |
| VARCHAR(200) | not null | |
| REAL | not null, default 0 | Fractional ordering |
| VARCHAR(7) | nullable | Column header accent |
| INTEGER | nullable |
|
cards
| Column | Type | Constraints | Notes |
|---|---|---|---|
| BIGINT | PK, auto | |
| TIMESTAMP | ||
| TIMESTAMP | ||
| TIMESTAMP | nullable, index | |
| BIGINT | FK β columns, not null, index | Current column |
| BIGINT | FK β projects, not null, index | |
| VARCHAR(500) | not null | |
| TEXT | nullable | Markdown body |
| REAL | not null, default 0 | Fractional ordering within column |
| TIMESTAMP | nullable | |
| TIMESTAMP | nullable | |
| VARCHAR(20) | default |
|
| BIGINT | FK β users, nullable | Primary assignee (see also many-to-many |
| BIGINT | FK β users, not null | Who created the card |
| INTEGER | default 0 | Human-readable number ( |
| INTEGER | default 0 | Aggregated from linked time entries / comments |
| INTEGER | nullable | Scrum only |
| BOOLEAN | default false | |
| TIMESTAMP | nullable | |
| VARCHAR(2000) | nullable | Link to an external tracker issue |
| VARCHAR(200) | nullable | External trackerβs issue key/number |
| BIGINT | FK β epics, nullable, index | |
| BIGINT | FK β cards, nullable, index | Sub-card relationship |
Two many-to-many relationships are declared only via GORM tags on Card (no dedicated struct beyond the explicit join
tables listed elsewhere in this document, or β for watchers β no struct at all):
Card.Labels []Label `gorm:"many2many:card_labels"`β seecard_labelsbelow, which does have an explicit model.Card.Assignees []User `gorm:"many2many:card_assignees"`β seecard_assigneesbelow, which does have an explicit model.Card.Watchers []User `gorm:"many2many:card_watchers"`β no Go struct at all; GORM auto-creates a barecard_watchersjoin table with justcard_idanduser_idcolumns (composite PK, FK constraints tocardsandusers). There is intentionally no===subsection for it below.
sub_card_count and sub_cards_done are computed per request (gorm:"-"), not stored.
card_assignees
Join table for multiple card assignees. Composite primary key β no surrogate id column.
| Column | Type | Constraints | Notes |
|---|---|---|---|
| BIGINT | PK, FK β cards | |
| BIGINT | PK, FK β users |
card_checklist_items
| Column | Type | Constraints | Notes |
|---|---|---|---|
| BIGINT | PK, auto | |
| TIMESTAMP | ||
| TIMESTAMP | ||
| BIGINT | FK β cards, not null, index | |
| TEXT | not null | |
| BOOLEAN | default false | |
| REAL | default 0 | Fractional ordering |
card_comments
| Column | Type | Constraints | Notes |
|---|---|---|---|
| BIGINT | PK, auto | |
| TIMESTAMP | ||
| TIMESTAMP | ||
| TIMESTAMP | nullable, index | |
| BIGINT | FK β cards, not null, index | |
| BIGINT | not null | |
| TEXT | not null | Markdown |
| BOOLEAN | default false | |
| INTEGER | default 0 | Time logged alongside this comment |
| BIGINT | FK β time_entries, nullable, index | Linked time entry, if the comment created one |
labels
| Column | Type | Constraints | Notes |
|---|---|---|---|
| BIGINT | PK, auto | |
| TIMESTAMP | ||
| TIMESTAMP | ||
| TIMESTAMP | nullable, index | |
| BIGINT | FK β projects, not null, index | |
| VARCHAR(100) | not null | |
| VARCHAR(7) | not null | Hex colour |
card_labels
Explicit join table backing Card.Labels / Label.Cards (many2many:card_labels).
| Column | Type | Constraints | Notes |
|---|---|---|---|
| BIGINT | PK, FK β cards | |
| BIGINT | PK, FK β labels | |
| TIMESTAMP |
card_references
Bidirectional "relates to" link between two cards. The link is stored once (source β target); both sides are shown
when listing. There is no ref_type column β every row means the same generic "relates to" relationship.
| Column | Type | Constraints | Notes |
|---|---|---|---|
| BIGINT | PK, auto | |
| BIGINT | FK β cards, not null, index, uniqueIndex( | |
| BIGINT | FK β cards, not null, index, uniqueIndex( | |
| TIMESTAMP |
card_histories
Audit log for card activity (creation, column moves, field changes, etc.). Table name is card_histories (GORM
pluralizes History β Histories), not card_history.
| Column | Type | Constraints | Notes |
|---|---|---|---|
| BIGINT | PK, auto | |
| TIMESTAMP | ||
| BIGINT | not null, index | |
| BIGINT | not null | Actor |
| VARCHAR(50) | default | Event kind, e.g. |
| VARCHAR(500) | nullable | Free-text description of what changed |
| BIGINT | FK β columns | Only meaningful for |
| BIGINT | FK β columns | Only meaningful for |
card_tags
Free-text tags on a card (distinct from labels, which are project-scoped and colour-coded).
| Column | Type | Constraints | Notes |
|---|---|---|---|
| BIGINT | PK, auto | |
| TIMESTAMP | ||
| BIGINT | not null, uniqueIndex( | |
| VARCHAR(100) | not null, uniqueIndex( | Composite unique with |
Customer, contract & invoicing tables
customers
| Column | Type | Constraints | Notes |
|---|---|---|---|
| BIGINT | PK, auto | |
| TIMESTAMP | ||
| TIMESTAMP | ||
| VARCHAR(200) | not null | |
| TEXT | nullable | |
| VARCHAR(500) | nullable | Path to uploaded logo |
| INTEGER | default 0 | Display order in the customer list |
| BOOLEAN | default false | Hides the customer from non-admin pickers while keeping it usable |
| BOOLEAN | default false | Hides from helpdesk/board views; visible in time-tracking only |
| BIGINT | FK β users, nullable, index | User who created the record (nil = system / seeded) |
| VARCHAR(300) | nullable | |
| VARCHAR(200) | nullable | |
| VARCHAR(20) | nullable | |
| VARCHAR(100) | nullable | |
| VARCHAR(50) | nullable | |
| VARCHAR(100) | nullable | Default purchase-order reference for invoices |
contracts
| Column | Type | Constraints | Notes |
|---|---|---|---|
| BIGINT | PK, auto | |
| TIMESTAMP | ||
| TIMESTAMP | ||
| BIGINT | FK β customers, not null, index | |
| VARCHAR(200) | not null | |
| TEXT | nullable | |
| TIMESTAMP | nullable | |
| TIMESTAMP | nullable | |
| REAL | nullable | Base hourly rate |
| REAL | nullable | Base per-kilometre travel rate |
| VARCHAR(3) | default | Displayed alongside monetary values |
contract_time_slots
Defines an alternative rate window on a contract β for example a standby/on-call surcharge outside regular hours.
When end_time < start_time the slot crosses midnight; end_day_offset says how many calendar days later the end
time falls (1 = next morning, 3 = Monday morning for a FridayβMonday weekend slot).
| Column | Type | Constraints | Notes |
|---|---|---|---|
| BIGINT | PK, auto | |
| BIGINT | FK β contracts, not null, index | |
| VARCHAR(100) | nullable | Free-text name, e.g. |
| VARCHAR(5) | not null | Wall-clock |
| VARCHAR(5) | not null | Wall-clock |
| VARCHAR(100) | default |
|
| INTEGER | default 0 | Calendar days after anchor day when |
| REAL | nullable | Multiplier on |
| REAL | nullable | Flat rate override; multiplied by |
customer_favorites
Composite primary key β no surrogate id column.
| Column | Type | Constraints | Notes |
|---|---|---|---|
| BIGINT | PK, FK β users | |
| BIGINT | PK, FK β customers |
customer_accesses
Grants a non-admin user explicit visibility of a customer. Table name is customer_accesses (GORM pluralizes
Access β Accesses).
Non-admin users with no row (direct or via group) cannot see the customer at all.
Admins always see all customers regardless of this table.
| Column | Type | Constraints | Notes |
|---|---|---|---|
| BIGINT | PK, FK β users | |
| BIGINT | PK, FK β customers | |
| VARCHAR | default |
|
customer_contacts
A named contact person at a customer (used as the "reply to" party on invoices/tickets).
| Column | Type | Constraints | Notes |
|---|---|---|---|
| BIGINT | PK, auto | |
| TIMESTAMP | ||
| TIMESTAMP | ||
| BIGINT | not null, index | |
| VARCHAR(200) | nullable | |
| VARCHAR(200) | nullable | |
| VARCHAR(100) | nullable | |
| VARCHAR(200) | nullable | |
| BOOLEAN | default false |
invoices
Billable document generated from time entries for a customer.
| Column | Type | Constraints | Notes |
|---|---|---|---|
| BIGINT | PK, auto | |
| VARCHAR(50) | unique | |
| BIGINT | FK β customers, not null, index | |
| TIMESTAMP | ||
| TIMESTAMP | ||
| VARCHAR(20) | default |
|
| VARCHAR(10) | default | |
| TEXT | JSON-encoded array of line items (see note below) β not a separate table | |
| REAL | ||
| REAL | default 0 | |
| REAL | ||
| REAL | ||
| TIMESTAMP | nullable | |
| TEXT | nullable | |
| TIMESTAMP | nullable | |
| REAL | nullable | |
| VARCHAR(200) | nullable | |
| VARCHAR(50) | nullable | |
| BIGINT | FK β invoices, nullable, index | Set on a |
| BIGINT | FK β users, nullable, index | |
| TIMESTAMP | ||
| TIMESTAMP |
Note | line_items is a JSON-encoded []InvoiceLineItem (date, project name, description, minutes, hourly rate,
distance, price per km, amount, currency, quantity, unit price, manual/comment flags). InvoiceLineItem has no
corresponding database table β it only ever exists serialized inside invoices.line_items or
invoice_templates.line_items. |
invoice_templates
Reusable set of line items for quickly creating invoices.
| Column | Type | Constraints | Notes |
|---|---|---|---|
| BIGINT | PK, auto | |
| VARCHAR(200) | not null | |
| TEXT | JSON-encoded | |
| REAL | default 0 | |
| VARCHAR(10) | default | |
| TEXT | nullable | |
| TIMESTAMP | ||
| TIMESTAMP |
Groups tables
user_groups
| Column | Type | Constraints | Notes |
|---|---|---|---|
| BIGINT | PK, auto | |
| VARCHAR(200) | unique, not null | |
| TEXT | nullable | |
| VARCHAR(500) | nullable | |
| BIGINT | FK β conversations, nullable, index | Linked group-chat conversation; auto-created for groups that predate this feature by |
| TIMESTAMP | ||
| TIMESTAMP |
group_members
Composite primary key β no surrogate id column.
| Column | Type | Constraints | Notes |
|---|---|---|---|
| BIGINT | PK, FK β user_groups | |
| BIGINT | PK, FK β users |
group_project_accesses
Composite primary key β no surrogate id column. Table name is group_project_accesses.
| Column | Type | Constraints | Notes |
|---|---|---|---|
| BIGINT | PK, FK β user_groups | |
| BIGINT | PK, FK β projects | |
| VARCHAR | not null, default |
|
group_customer_accesses
Composite primary key β no surrogate id column. Table name is group_customer_accesses.
| Column | Type | Constraints | Notes |
|---|---|---|---|
| BIGINT | PK, FK β user_groups | |
| BIGINT | PK, FK β customers | |
| VARCHAR | not null, default |
|
Scrum & release planning tables
sprints
| Column | Type | Constraints | Notes |
|---|---|---|---|
| BIGINT | PK, auto | |
| TIMESTAMP | ||
| TIMESTAMP | ||
| TIMESTAMP | nullable, index | |
| BIGINT | FK β projects, not null, index | |
| VARCHAR(200) | not null | |
| TEXT | nullable | Sprint goal description |
| REAL | default 0 | Fractional ordering among sprints |
| VARCHAR(20) | default |
|
| TIMESTAMP | nullable | |
| TIMESTAMP | nullable |
total_points, completed_points, card_count, and card_ids are computed per request (gorm:"-"), not stored.
sprint_cards
Join table linking cards to sprints. Composite primary key, hard deletes only.
| Column | Type | Constraints | Notes |
|---|---|---|---|
| BIGINT | PK, FK β sprints | |
| BIGINT | PK, FK β cards | |
| REAL | default 0 | Fractional ordering of the card within the sprint backlog |
| TIMESTAMP |
releases
A named milestone that groups one or more sprints toward a shared target date.
| Column | Type | Constraints | Notes |
|---|---|---|---|
| BIGINT | PK, auto | |
| TIMESTAMP | ||
| TIMESTAMP | ||
| TIMESTAMP | nullable, index | |
| BIGINT | FK β projects, not null, index | |
| VARCHAR(200) | not null | |
| TEXT | nullable | |
| TIMESTAMP | nullable |
sprints is populated at query time (gorm:"-") from release_sprints, not stored on the row itself.
release_sprints
Join table linking releases to sprints. Composite primary key, no timestamps.
| Column | Type | Constraints | Notes |
|---|---|---|---|
| BIGINT | PK, FK β releases | |
| BIGINT | PK, FK β sprints |
Time tracking
time_entries
| Column | Type | Constraints | Notes |
|---|---|---|---|
| BIGINT | PK, auto | |
| TIMESTAMP | ||
| TIMESTAMP | ||
| BIGINT | FK β users, not null, index | |
| BIGINT | FK β customers, nullable, index | |
| BIGINT | FK β projects, nullable, index | |
| BIGINT | FK β contracts, nullable, index | |
| BIGINT | FK β tickets, nullable, index | Entry logged against a helpdesk ticket instead of a project/card |
| TIMESTAMP | not null, index | |
| INTEGER | not null | Duration in minutes |
| TEXT | nullable | |
| BOOLEAN | default false | |
| VARCHAR(5) | nullable | Wall-clock |
| VARCHAR(5) | nullable | Wall-clock |
| REAL | nullable | Travel distance, used with |
Note | There is no card_id column on time_entries β time is linked to a project/customer/contract/ticket, not
directly to a card (a card-level comment can separately create a linked entry via card_comments.time_entry_id). |
time_entry_row_orders
Persists the userβs custom row ordering for the time-tracking grid. One row per user.
| Column | Type | Constraints | Notes |
|---|---|---|---|
| BIGINT | PK | |
| TEXT | JSON array of row-key strings |
time_entry_week_row_orders
Persists row-key order for a specific ISO week, including empty rows, plus per-row comments.
| Column | Type | Constraints | Notes |
|---|---|---|---|
| BIGINT | PK | |
| INTEGER | PK | |
| INTEGER | PK | ISO week number |
| TEXT | JSON array of row-key strings | |
| TEXT | JSON object mapping row key β comment text |
time_macro_libraries
Stores a userβs time-tracking macro templates as JSON (mirrors the frontendβs timeTracking.macroTemplates.v2
localStorage value, persisted server-side for cross-device sync).
| Column | Type | Constraints | Notes |
|---|---|---|---|
| BIGINT | PK | |
| TEXT | not null | JSON blob |
Discussion tables
topics
| Column | Type | Constraints | Notes |
|---|---|---|---|
| BIGINT | PK, auto | |
| TIMESTAMP | ||
| TIMESTAMP | ||
| TIMESTAMP | nullable, index | |
| BIGINT | FK β projects, not null, index | |
| BIGINT | not null | Author |
| VARCHAR(500) | not null | |
| TEXT | nullable | Markdown |
| BOOLEAN | default false | |
| BOOLEAN | default false |
reply_count is computed per request (gorm:"-"), not stored. There is no is_locked column β topics never
prevent new replies at the data-model level.
topic_replies
| Column | Type | Constraints | Notes |
|---|---|---|---|
| BIGINT | PK, auto | |
| TIMESTAMP | ||
| TIMESTAMP | ||
| TIMESTAMP | nullable, index | |
| BIGINT | FK β topics, not null, index | |
| BIGINT | not null | |
| TEXT | not null | Markdown |
| BOOLEAN | default false |
Chat tables
chat_messages
Project-level channel messages.
| Column | Type | Constraints | Notes |
|---|---|---|---|
| BIGINT | PK, auto | |
| TIMESTAMP | ||
| TIMESTAMP | ||
| BIGINT | FK β projects, not null, index | |
| BIGINT | default 0 | 0 for system/bot-authored rows |
| TEXT | not null | Markdown |
| BOOLEAN | default false | |
| BOOLEAN | default false | Soft-hidden in the UI; row is kept for thread integrity |
| BOOLEAN | default false | Messages from webhook/CI integrations |
| VARCHAR(100) | nullable | Display name shown for bot-authored messages |
conversations
Direct messages and group DMs.
| Column | Type | Constraints | Notes |
|---|---|---|---|
| BIGINT | PK, auto | |
| TIMESTAMP | ||
| TIMESTAMP | ||
| VARCHAR(200) | nullable | Set for group conversations |
| VARCHAR(500) | nullable | |
| BOOLEAN | default false | |
| BIGINT | not null, index |
conversation_members
Composite primary key β no surrogate id column.
| Column | Type | Constraints | Notes |
|---|---|---|---|
| BIGINT | PK, FK β conversations | |
| BIGINT | PK, FK β users | |
| TIMESTAMP |
conversation_messages
| Column | Type | Constraints | Notes |
|---|---|---|---|
| BIGINT | PK, auto | |
| TIMESTAMP | ||
| TIMESTAMP | ||
| BIGINT | FK β conversations, not null, index | |
| BIGINT | FK β users, not null, index | Column is |
| TEXT | not null | Markdown |
| BOOLEAN | default false | |
| BOOLEAN | default false |
direct_messages
Legacy/simple 1:1 message table, distinct from the conversations model.
| Column | Type | Constraints | Notes |
|---|---|---|---|
| BIGINT | PK, auto | |
| TIMESTAMP | ||
| TIMESTAMP | ||
| BIGINT | FK β users, not null, index | |
| BIGINT | FK β users, not null, index | |
| TEXT | not null | Markdown |
| BOOLEAN | default false | |
| BOOLEAN | default false |
message_reactions
Polymorphic emoji reaction on either a chat message or a conversation message (see "Polymorphic attachments" above for the pattern).
| Column | Type | Constraints | Notes |
|---|---|---|---|
| BIGINT | PK, auto | |
| TIMESTAMP | ||
| VARCHAR(50) | not null, uniqueIndex( |
|
| BIGINT | not null, uniqueIndex( | |
| BIGINT | not null, uniqueIndex( | |
| VARCHAR(10) | not null, uniqueIndex( | The four columns together form a unique constraint β one reaction per emoji per user per message |
Files
attachments
Polymorphic file table β one row per uploaded file, regardless of where it is attached.
| Column | Type | Constraints | Notes |
|---|---|---|---|
| BIGINT | PK, auto | |
| TIMESTAMP | ||
| VARCHAR(50) | not null, index( |
|
| BIGINT | not null, index( | PK of the owning row |
| BIGINT | not null | |
| VARCHAR(255) | not null | Original filename as uploaded |
| VARCHAR(255) | not null | Randomised hex name on disk; never serialized ( |
| VARCHAR(100) | nullable | Detected server-side from file content |
| BIGINT | Bytes; column is |
Note | MIME type is detected from the first 512 bytes of the file using net/http.DetectContentType.
The Content-Type header sent by the client is ignored. |
Integration tables
card_links
Git commit, PR, and issue links attached to cards β created automatically when a webhook payload references a card
number like PRJ-42.
| Column | Type | Constraints | Notes |
|---|---|---|---|
| BIGINT | PK, auto | |
| TIMESTAMP | ||
| TIMESTAMP | ||
| BIGINT | not null, index | |
| VARCHAR(20) | not null |
|
| VARCHAR(20) | not null |
|
| VARCHAR(500) | nullable | PR/commit/issue title |
| VARCHAR(2000) | nullable | |
| VARCHAR(200) | nullable | Commit SHA, or PR/issue number |
| VARCHAR(200) | nullable | |
| VARCHAR(20) | nullable | e.g. |
| VARCHAR(300) | nullable |
project_webhooks
Inbound webhooks that push events from external systems into a projectβs chat.
| Column | Type | Constraints | Notes |
|---|---|---|---|
| BIGINT | PK, auto | |
| TIMESTAMP | ||
| BIGINT | not null, index | |
| VARCHAR(100) | nullable | |
| VARCHAR(64) | nullable | Legacy plaintext token column, kept only for migrating pre-existing webhooks; never serialized ( |
| VARCHAR(64) | unique | SHA-256(token); used for request authentication; never serialized ( |
| VARCHAR(8) | nullable | Short fragment shown in the UI so admins can recognise a key |
| VARCHAR(20) | not null, default |
|
| BIGINT | not null |
There is no active boolean column β a webhook is either present (active) or deleted.
api_keys
Long-lived API keys, primarily for CI/CD automation via the Ticket API.
| Column | Type | Constraints | Notes |
|---|---|---|---|
| BIGINT | PK, auto | |
| TIMESTAMP | ||
| TIMESTAMP | nullable | |
| BIGINT | FK β users, not null, index | Owning user |
| BIGINT | FK β projects, nullable, index |
|
| VARCHAR(100) | not null | Human label |
| VARCHAR(64) | not null, unique | SHA-256 of the raw key; never serialized ( |
| VARCHAR(12) | not null | First 12 characters of the raw key, shown in the UI (e.g. |
There is no expires_at column in the current model β keys do not expire on their own.
Helpdesk & ticketing tables
tickets
| Column | Type | Constraints | Notes |
|---|---|---|---|
| BIGINT | PK, auto | |
| BIGINT | FK β customers, nullable, index | |
| VARCHAR(500) | not null | |
| TEXT | nullable | |
| VARCHAR(30) | not null, default |
|
| VARCHAR(20) | not null, default |
|
| VARCHAR(20) | not null, default |
|
| BIGINT | not null | |
| BIGINT | FK β users, nullable, index | |
| BIGINT | FK β users, nullable, index | |
| BIGINT | FK β user_groups, nullable, index | Ticket assigned to a whole team/group |
| BIGINT | FK β sla_policies, nullable, index | |
| TIMESTAMP | nullable | |
| TIMESTAMP | nullable | Computed by |
| TIMESTAMP | nullable | |
| BOOLEAN | Refreshed on every | |
| BOOLEAN | ||
| TIMESTAMP | nullable | Used for |
| TIMESTAMP | nullable | Used for |
| BOOLEAN | default false | Excluded from default listing unless |
| BIGINT | FK β ticket_checklist_templates, nullable, index | |
| VARCHAR(998) | unique, nullable | RFC 5322 |
| VARCHAR(254) | nullable | |
| VARCHAR(150) | nullable | |
| TEXT | nullable | Full raw source of the originating email, if created via IMAP |
| TIMESTAMP | ||
| TIMESTAMP |
ticket_messages
| Column | Type | Constraints | Notes |
|---|---|---|---|
| BIGINT | PK, auto | |
| BIGINT | FK β tickets, not null, index | |
| BIGINT | not null | |
| TEXT | not null | |
| VARCHAR(150) | nullable | Sender display name for inbound emails |
| BOOLEAN | default false | Whether this reply was emailed to the ticketβs originator |
| BOOLEAN | default false | Internal note β not emailed, hidden from |
| BIGINT | FK β ticket_messages, nullable, index | Threaded reply support |
| TIMESTAMP | ||
| TIMESTAMP |
ticket_views
Records the last time each user viewed a ticket (drives unread indicators).
| Column | Type | Constraints | Notes |
|---|---|---|---|
| BIGINT | PK, auto | |
| BIGINT | not null, uniqueIndex( | |
| BIGINT | not null, uniqueIndex( | |
| TIMESTAMP |
ticket_histories
Audit log for ticket activity. Table name is ticket_histories, not ticket_history.
| Column | Type | Constraints | Notes |
|---|---|---|---|
| BIGINT | PK, auto | |
| TIMESTAMP | ||
| BIGINT | not null, index | |
| BIGINT | not null | |
| VARCHAR(50) | not null | |
| VARCHAR(500) | nullable |
ticket_tags
| Column | Type | Constraints | Notes |
|---|---|---|---|
| BIGINT | PK, auto | |
| TIMESTAMP | ||
| BIGINT | not null, uniqueIndex( | |
| VARCHAR(100) | not null, uniqueIndex( |
ticket_links
Cross-reference between two tickets, mirroring card_references.
| Column | Type | Constraints | Notes |
|---|---|---|---|
| BIGINT | PK, auto | |
| BIGINT | not null, index, uniqueIndex( | |
| BIGINT | not null, index, uniqueIndex( | |
| TIMESTAMP |
ticket_card_links
Links a helpdesk ticket to a board card (e.g. "this incident is tracked by PRJ-42").
| Column | Type | Constraints | Notes |
|---|---|---|---|
| BIGINT | PK, auto | |
| BIGINT | not null, index, uniqueIndex( | |
| BIGINT | not null, index, uniqueIndex( | |
| BIGINT | ||
| TIMESTAMP |
ticket_checklist_templates
Admin-managed named checklist that can be applied to a ticket.
| Column | Type | Constraints | Notes |
|---|---|---|---|
| BIGINT | PK, auto | |
| VARCHAR(200) | not null | |
| VARCHAR(500) | nullable | |
| TEXT | JSON-encoded array of item-body strings; expanded into | |
| BOOLEAN | not null, default true | |
| INTEGER | not null, default 0 | |
| TIMESTAMP | ||
| TIMESTAMP |
ticket_checklist_items
Actual checklist items on a specific ticket (post-instantiation from a template, or added ad hoc).
| Column | Type | Constraints | Notes |
|---|---|---|---|
| BIGINT | PK, auto | |
| TIMESTAMP | ||
| TIMESTAMP | ||
| BIGINT | not null, index | |
| TEXT | not null | |
| BOOLEAN | default false | |
| REAL | default 0 | Fractional ordering |
sla_policies
Admin-managed response/resolution time targets, matched to tickets by priority.
| Column | Type | Constraints | Notes |
|---|---|---|---|
| BIGINT | PK, auto | |
| VARCHAR(200) | not null | |
| INTEGER | not null, default 0 | |
| INTEGER | not null, default 0 | |
| VARCHAR(200) | nullable | Comma-separated priorities this policy applies to; empty = catch-all |
| BOOLEAN | not null, default true | |
| TIMESTAMP | ||
| TIMESTAMP |
macros
Reusable, admin-managed sequences of ticket actions (set_status, set_priority, set_type, add_tag,
add_message) applied by agents in one click.
| Column | Type | Constraints | Notes |
|---|---|---|---|
| BIGINT | PK, auto | |
| VARCHAR(200) | not null | |
| VARCHAR(500) | nullable | |
| TEXT | JSON-encoded array of | |
| BOOLEAN | not null, default true | |
| INTEGER | not null, default 0 | |
| TIMESTAMP | ||
| TIMESTAMP |
Auth & security tables
passkey_credentials
WebAuthn/FIDO2 passkey registered by a user.
| Column | Type | Constraints | Notes |
|---|---|---|---|
| BIGINT | PK, auto | |
| BIGINT | not null, index | Never serialized ( |
| VARCHAR(100) | nullable | User-supplied label for the passkey |
| BLOB | not null, unique | WebAuthn credential ID; never serialized ( |
| BLOB | not null | COSE public key; never serialized ( |
| BLOB | nullable | Authenticator model identifier; never serialized ( |
| INTEGER | uint32 | Anti-cloning replay counter; never serialized ( |
| VARCHAR(200) | nullable | JSON-encoded transport hints ( |
| TIMESTAMP | ||
| TIMESTAMP | nullable |
mfa_trusted_devices
A device that has completed an MFA challenge and elected to be remembered for 7 or 30 days
(per the admin mfa_remember_devices policy).
| Column | Type | Constraints | Notes |
|---|---|---|---|
| BIGINT | PK, auto | |
| BIGINT | not null, index | Never serialized ( |
| VARCHAR(64) | not null, unique | SHA-256 of the trust token; never serialized ( |
| VARCHAR(200) | nullable | |
| TIMESTAMP | ||
| TIMESTAMP | Tightening the admin trust-device policy revokes incompatible rows | |
| TIMESTAMP |
login_histories
Security audit trail of authentication-related events. Table name is login_histories, not login_history.
| Column | Type | Constraints | Notes |
|---|---|---|---|
| BIGINT | PK, auto | |
| BIGINT | index | Subject of the event; |
| VARCHAR(100) | nullable | Subjectβs username at the time |
| BIGINT | index | Who performed the action (equals |
| VARCHAR(100) | nullable | |
| VARCHAR(64) | not null | e.g. |
| VARCHAR(128) | nullable | Free-text context; password-reset tokens are truncated to the first 8 characters |
| VARCHAR(64) | nullable | |
| VARCHAR(128) | nullable | User-Agent or Tauri client identifier |
| TIMESTAMP |
System settings & miscellany
system_settings
Only two columns β no surrogate id, no timestamps.
| Column | Type | Constraints | Notes |
|---|---|---|---|
| VARCHAR(100) | PK | Setting identifier |
| TEXT | String value |
Settings are read at request time so changes take effect without restarting the server.
All valid keys are defined as constants in handlers/system.go.
news_items
Admin-authored dashboard announcements.
| Column | Type | Constraints | Notes |
|---|---|---|---|
| BIGINT | PK, auto | |
| TIMESTAMP | ||
| TIMESTAMP | ||
| TIMESTAMP | nullable, index | |
| VARCHAR(200) | not null | |
| TEXT | not null | |
| TIMESTAMP | nullable | |
| TIMESTAMP | nullable | |
| BOOLEAN | not null, default true | |
| VARCHAR(20) | nullable | |
| BOOLEAN | not null, default false | Shown as a modal on next login instead of only in the sidebar feed |
Dismissed news IDs are tracked client-side only, in localStorage (dashboard_news_dismissed_ids) β there is no
per-user "read" column on this table.
starred_projects
Composite primary key β no surrogate id column.
| Column | Type | Constraints | Notes |
|---|---|---|---|
| BIGINT | PK, index | |
| BIGINT | PK, index | |
| TIMESTAMP |
favorite_users
A user marking another user as a quick-access favourite (e.g. for chat). Composite primary key.
| Column | Type | Constraints | Notes |
|---|---|---|---|
| BIGINT | PK | |
| BIGINT | PK |
Entity-relationship overview
users ββββββββββββββββββ project_members ββββ projects ββ epics
β β β
β columns cards
β β β
βββ group_members ββ user_groups cards ββββββββ
β β β
β tickets ββββββ group_project_accesses
β β group_customer_accesses ββββ customers ββ contracts
β ticket_messages β β
β sla_policies contacts time_slots
β ticket_checklist_templates β
β invoices
β
βββ time_entries ββββββββββββββββββββββββββ
β
βββ projects / customers / contracts / tickets
sprints ββ sprint_cards ββ cards releases ββ release_sprints ββ sprintsKey relationships:
A project belongs to a customer (optional) and a contract (optional), and has many columns, cards, and epics.
project_members is the direct userβproject join table; group_project_accesses grants access to all group members at once, and group_customer_accesses does the same for customers.
cards belong to a column and a project; they can have a
parent_card_idfor sub-cards, anepic_idfor grouping, and many-to-many assignees/watchers/labels.sprints and releases are project-scoped Scrum-extension entities:
sprint_cardslinks cards into a sprintβs backlog, andrelease_sprintsgroups sprints under a release milestone.tickets belong to a customer and can be linked to a card (
ticket_card_links), to each other (ticket_links), assigned to a user or a user_group, and matched against an sla_policy.invoices and invoice_templates belong to a customer and store their line items as JSON rather than a child table; customer_contacts gives a customer named contact people.
attachments and message_reactions are polymorphic:
owner_type+owner_idpoint at any of several owner tables (see "Polymorphic attachments" above) rather than using a foreign key.time_entries link a user to any combination of customer, project, contract, and ticket.
passkey_credentials, mfa_trusted_devices, and login_histories all extend
userswith authentication and security-audit data, but live in their own tables rather than as columns onusers.