Skip to main content

Database ER Diagram

Generated from packages/db/prisma/schema.prisma. Every model carries a tenantId unless noted (the isolation boundary — see Multi-Tenancy); User is the deliberate exception (a global identity).

Tenancy & Identity

Tenant

FieldTypeNotes
idcuid PK
slugString uniquepath routing key — /t/{slug}
emailDomainString uniqueresolves a self-registering user's tenant
firstAdminEmailString?auto-promoted to admin on first sign-up; seeds default projects
entraDirectoryIdString?customer's Azure AD tid for verified SSO auto-approval
nameString
statusTenantStatuslogin/API allowed only when ACTIVE

User (global identity — no tenantId-based isolation)

FieldTypeNotes
idcuid PK
entraIdString? uniquenull for custom-login users
emailString uniqueglobally unique across all tenants
name, displayName, avatarUrlString(?)
isActiveBooleanglobal account kill-switch
isPlatformAdminBooleantenant-less super-role
authProviderStringteams / azure_sso / custom
passwordHashString?custom login only
emailVerified, pendingApprovalBoolean
tenantIdString?home tenant only; nullable (homeless guests)

:::note User.role / User.isAdmin were removed Per-tenant role and admin status now live on Membership. A user can be an admin in one tenant and a member in another. :::

FieldTypeNotes
userId + tenantIdunique
roleUserAppRoleOWNER / MEMBER / VIEWER (baseline)
isAdminBooleanunlocks the tenant admin panel
statusMembershipStatusACTIVE / PENDING / SUSPENDED
isActiveBooleanper-tenant disable (distinct from status)
isHomeBooleantrue for the email-domain-resolved tenant
invitedByIdString?set for guest memberships

Supporting

  • RegistrationAttempt — rate-limit ledger for the public self-serve registration endpoint (not tenant-scoped; it precedes any tenant).
  • ReapprovalRequest — a user asking to be re-enabled in a specific tenant (PENDING/APPROVED/REJECTED).
  • UserNotificationPreference — per-user level (MENTIONS_ONLY / MINIMAL / REGULAR / HIGH) and in-app/Teams/email toggles.

Projects, Roles & Subjects

Project

FieldTypeNotes
typeProjectTypeEPIC / SUPPORT / DEV / CONSULTING — fixed after create; drives roles, workflow, subjects
ticketPrefixStringe.g. ASS — max 3 uppercase letters
ticketSeqIntlast issued ticket number
isPublicBooleanpublic within the tenant
customerString?
billable / billedByBoolean / String?billing metadata
isArchivedBoolean

ProjectRole (dynamic roles)

FieldTypeNotes
keyStringslug, unique per project type
name, descriptionStringdisplay
projectTypeProjectTyperoles are scoped to a type
permissionsString[]permission codes (see Roles & Permissions)
isSystemBooleanseeded defaults

Unique: [tenantId, projectType, key].

ProjectMember

[projectId, userId] PK, plus roleIdProjectRole (RESTRICT).

Subject (folder level)

A folder between Project and Ticket. type is constrained by the project's type. Carries startDate/endDate (all subjects are time-based), sortOrder, isDefault (hidden default for Epic projects), and a fields JSON blob for type-specific extras.

Join table ([projectId, ticketId]) linking a project to the epic ticket(s) it rolls up to.

WorkflowStatus

Per-project-type ticket workflow: which TicketStatus keys a type uses, with label, color, sortOrder, isInitial, isTerminal. Unique [tenantId, projectType, statusKey].

Tickets & Tasks

Ticket

FieldTypeNotes
ticketNumberIntsequential per project
title, descriptionString / Text?
statusTicketStatussee enum
priorityPrioritysupport uses Low/Med/High/Critical/Custom; others P1–P3
subjectIdFKrequired — every ticket lives in a Subject
parentId, duplicateOfIdString?self-relations
startDate / endDateDateTime?non-support planning window
preApprovalHours, estimateHours, actualHours, billableHoursFloat?Consulting/Dev effort & billing
actualStartDate / actualEndDateDateTime?
estimateApprovedById / estimateApprovedAtLead estimate approval
dueDate, resolvedAt, closedAtDateTime?
onHoldAt / onHoldMinutesaccumulated hold time (for SLA)
deferredFromStatusTicketStatus?restored on resume from Deferred
isEscalated, escalationUrgency, escalatedAt, escalatedById, escalationCommentescalation
commitIdString?git commit a Dev fix shipped in; captured on Fixed → Review
emailMessageId (unique), emailGraphMessageId, emailMailbox, emailSender, emailCcemail-to-ticket threading
emailInboxIdFK?which inbox produced it — how notification settings are resolved. SetNull
emailRoutingRuleIdFK?which rule routed it. SetNull, and indexed so hard-deleting a rule doesn't scan the whole table

Tickets support multiple assignees via TicketAssignee.

Task (sub-task)

taskNumber (per-ticket), status (TaskStatus), single assigneeId, dueDate, completedAt. Supports tags, comments, attachments, time entries, and work logs.

Custom attributes

  • ProjectCustomAttributefieldName (snake_case), label, type (CustomAttributeType), mandatory, options JSON (string[] or parent::child for cascading), dependsOn (parent field), defaultValue, sortOrder. Unique [projectId, fieldName].
  • TicketCustomAttributeValue — polymorphic value JSON. Unique [ticketId, attributeId].

Collaboration

  • Comment — on a ticket or task; threaded via parentId; supports attachments and reactions.
  • CommentReaction — one row per (comment, user); the compound unique is what enforces one reaction per person, so toggling updates or deletes the row rather than adding a duplicate. emoji is the native unicode character, not an enum. userId/tenantId are plain scalars with no @relation — isolation comes from the tenant extension keying off the column name.
  • Follower — a user following exactly one of a ticket or task (the pair is enforced in Zod, mirroring Notification). role records why they have tenant access: VIEWER if they already belonged, EX_VIEWER if a guest membership was created purely to support this follow. Unique per [userId, ticketId] and [userId, taskId].
  • AttachmententityType (TICKET/TASK/COMMENT), Azure Blob blobKey (unique); tenantId is re-checked before issuing a download SAS URL.
  • Activity — human-readable event feed per ticket/task/project.
  • AuditLog — immutable CREATE/UPDATE/DELETE/RESTORE record with IP/user-agent.
  • Notificationtenant-scoped, per-user, with event, isRead/readAt, optional ticket/task link.

Time & Productivity

  • TimeEntry — minutes logged against a ticket or sub-task (Consulting), with spentOn.
  • WorkLog (+ WorkLogAssignee) — a daily work log: date, summary, free-text ticketRefs[], optional taskId. See Work Log.
  • Note (+ NoteAssignee) — title + content; tenantId derived from the creator.
  • Reminder (+ ReminderAssignee) — content, scheduledAt, repeat (ReminderRepeat), optional ticket link; fires REMINDER_DUE.
  • MeetingLogtitle, meetingDate, mom; has MeetingLogAttendee, tags, and MeetingItemLog action items (status, owner, due date), each optionally linked to tickets via MeetingItemLogTicket.

Master Calendar

  • Calendar — one isMaster row per tenant; weekStart (display).
  • CalendarRegiontimezone (IANA), support hours as minutes-from-midnight (startMinute/endMinute, or is24x7), isDefault.
  • CalendarWeekendRule — one clause of a region's weekend set: ordinal (WeekOrdinal) + weekday.
  • Holiday — dated per-year row, type (HolidayType).
  • HolidayRegion — which regions observe a holiday; empty set = all regions.

See Master Calendar.

Email System

EmailInbox (table still named ProjectEmailConfig)

Owned by the tenant, not a project. It used to be the reverse — projectId was unique, one inbox per project — but a shared support mailbox routinely feeds several projects, so the project became a destination and the old owning column became defaultProjectId. The table name is kept via @@map so the rename cost no data movement.

FieldNotes
defaultProjectIdRequired fallback destination. onDelete: Restrict — an inbox is shared, so cascading would let deleting one project destroy a mailbox several others receive through
providermicrosoft, gmail, …
connectionModegraph_app | imap_oauth | imap_basic. Orthogonal to provider
graphSubscriptionId, subscriptionExpiresAtPush mode only
oauthRefreshToken (encrypted), oauthAccountEmailDelegated mode. The signed-in account is stored separately from mailboxAddress because an admin can type one and authenticate as another
imapHost/Port/Username/Password/Tls/Folder, smtp*IMAP modes; passwords encrypted, and null under imap_oauth where XOAUTH2 replaces them
afterProcessAction, processedFolderMARK_READ | MOVE | DELETE | NONE
lastPolledAtThe IMAP counterpart of subscriptionExpiresAt. Not a cursor — unread state on the server drives the poll, so a lost write can't lose or replay mail
sendAckEmail, notifyOnComment, notifyOnStatusChangeOutbound notification gates
ignoreNoreplyEmailsDrops no-reply senders and bounces/delivery reports
circuitOpen, consecutiveFailures, lastErrorAt/MsgCircuit breaker — trips at 5 consecutive failures
autoCreateTicket, allowedFromDomains, blockedFromDomainsDead — no longer read by ingestion; kept to avoid a no-op migration

Inboxes are soft-deleted (deletedAt), so the tickets they created keep their link.

EmailRoutingRule

Routes one inbox's mail to several projects. Scoped per inbox, not per tenant — two mailboxes in one tenant may route the same domain differently.

FieldNotes
matchTypeRECIPIENT_ADDRESS | SENDER_DOMAIN. The enum order is the precedence
patternLowercased; a bare domain or a full address depending on matchType
projectIdDestination. onDelete: Cascade
isEnabledPark a rule without deleting and retyping it
overridePriority, overrideSendAck, overrideNotifyOnComment, overrideNotifyOnStatusChangeNull means inherit from the inbox, so an inbox-level change still propagates
overrideSubjectId, overrideAssigneeIdBoth SetNull — losing the embellishment beats deleting the rule and silently re-routing that customer

Unique on [inboxId, matchType, pattern], so "which rule wins" is unambiguous within a type.

Others

  • EmailMessage — inbound message (RFC messageId dedup key), status (EmailProcessingStatus), the inbox it arrived on and the routed ticket.
  • OutboundEmail — threaded replies sent from a ticket.
  • MicrosoftMailConfigdead. Per-tenant Azure app registrations were removed; mail now uses the platform SSO app against Tenant.entraDirectoryId. The table remains pending cleanup.

Auth Tables (Better Auth)

Session, Account, Verification — used by the web app (Azure SSO + custom login). The Teams SSO path does not use these.

Config

  • StatusConfig — display name/color overrides for ticket/task statuses, per tenant.
  • AppConfig — per-tenant settings (e.g. ticketArchiveDays).
  • IncomingWebhook — HTTP endpoints that create tickets from external tools.

Enumerations

EnumValues
TicketStatusOPEN, ASSIGNED, IN_PROGRESS, ON_HOLD, REVIEW, FIXED, CLOSED, REOPENED, DEFERRED
TaskStatusOPEN, ASSIGNED, IN_PROGRESS, COMPLETED, REOPENED
PriorityLOW, MEDIUM, HIGH, CRITICAL, CUSTOM, P1, P2, P3
ProjectTypeEPIC, SUPPORT, DEV, CONSULTING
FollowerRoleVIEWER, EX_VIEWER
EmailMatchTypeRECIPIENT_ADDRESS, SENDER_DOMAIN (also the precedence order)
EmailProcessingStatusPENDING, PROCESSING, PROCESSED, FAILED, DUPLICATE, IGNORED
NotificationEventTICKET_CREATED/ASSIGNED/UPDATED/CLOSED/REOPENED/ON_HOLD/ESCALATED, TASK_CREATED/ASSIGNED/COMPLETED, PROJECT_ASSIGNED, SLA_BREACHED, USER_PENDING, USER_REAPPROVAL_REQUESTED/APPROVED/REJECTED, COMMENT_ADDED, COMMENT_MENTION, COMMENT_REACTION, COMMENT_REPLY, REMINDER_DUE, FOLLOWER_ADDED
NotificationLevelMENTIONS_ONLY, MINIMAL (default), REGULAR, HIGH
TenantStatusPENDING_APPROVAL, ACTIVE, SUSPENDED, REJECTED
MembershipStatusACTIVE, PENDING, SUSPENDED
UserAppRoleOWNER, MEMBER, VIEWER
EscalationUrgencyDelayed_Resolution, UNRESOLVED_RECURRING, URGENCY_BUSINESS_IMPACT, CRITICAL_SHOWSTOPPER, MISCOMMUNICATION_ERROR
NotificationEventTICKET_CREATED, TICKET_ASSIGNED, TICKET_UPDATED, TICKET_CLOSED, TICKET_REOPENED, TICKET_ON_HOLD, TICKET_ESCALATED, TASK_CREATED, TASK_ASSIGNED, TASK_COMPLETED, PROJECT_ASSIGNED, SLA_BREACHED, USER_PENDING, USER_REAPPROVAL_REQUESTED/APPROVED/REJECTED, COMMENT_ADDED, COMMENT_MENTION, REMINDER_DUE
NotificationLevelMENTIONS_ONLY, MINIMAL, REGULAR, HIGH
ReapprovalStatusPENDING, APPROVED, REJECTED
CustomAttributeTypeTEXT_INPUT, TEXT_AREA, SINGLE_SELECT, MULTI_SELECT, DATE_PICKER, DATE_RANGE_PICKER
ReminderRepeatNEVER, DAILY, WEEKLY, MONTHLY
MeetingItemStatusOPEN, IN_PROGRESS, DONE
WeekOrdinalFIRST, SECOND, THIRD, FOURTH, FIFTH, LAST, ALL
HolidayTypeCOMPULSORY, REGIONAL, OPTIONAL
AttachmentEntityTypeTICKET, TASK, COMMENT
ActivityEntityTypeTICKET, TASK, PROJECT
AuditActionCREATE, UPDATE, DELETE, RESTORE
EmailProcessingStatusPENDING, PROCESSING, PROCESSED, FAILED, DUPLICATE, IGNORED

:::note Legacy enum ProjectMemberRole (CREATOR / MEMBER / VIEWER) is retained only for the migration that backfilled ProjectMember.roleId. New code resolves roles via ProjectRole. :::