MMetiss AI|Docs
Technical Design Document

Orion Portal — Existing Data Model

Complete reference for the current PostgreSQL schema as of July 2026. Source of truth: database/prisma/schema.prisma in the Metiss-AI/orion repo.

Status
AS-IS — current state
Models
27 models across 6 domains
Database
PostgreSQL 16 · Cloud SQL
Version
v1.0 — 2026-07-08
AS-ISData ModelPostgreSQLPrisma
Schema file: database/prisma/schema.prisma in Metiss-AI/orion. Issues in this model are tracked in the Data Model Execution Plan. Background and rationale in the Architecture Review.

01Domain map

27 models across 6 domains. Dashed arrows indicate foreign key direction (child → parent).

AUTHUserSessionForgotPasswordTokenSYSTEMEventLogAuditLogSavedSearchPARTNERSPartnerUserPartnerJOBSJobNotesTaskTaskNotesNotificationsINVOICINGInvoiceDetailsInvoiceNoteEmailThreadEmailMessageSUBCONTRACTORSOrganizationAddressContactBillingBillingContactOrganizationContactSUB — COMPLIANCEMarketPricingLicenseCertificationDocumentNOTE JOIN TABLESOrganizationNotePricingNoteLicenseNoteCertificationNoteUserNoteForeign key / belongs to

02Auth domain

User

Central identity record. All audit trails and assignments reference this model.

FieldTypeNotes
idUUID (PK)Primary key
firstName, lastNameVarChar(80)
nameVarChar(120)Computed full name — redundant with first+last
emailVarChar(255) UNIQUE
roleString → enum DM-02"Executive" | "Admin" | "Project Coordinator"
statusString → enum DM-02"Active" | "Inactive" | "Suspended"
communicationPreferenceString → enum DM-02"Email" | "Text Message"
passwordHashVarChar(255)bcrypt hash
invalidLoginAttemptsInt default 0Incremented on failed login
jobTitle, phoneNumber, avatarUrlVarChar / nullable
createdAt, updatedAtTimestamptz

Session

FieldTypeNotes
idUUID (PK)
tokenHashVarChar(255) UNIQUEHTTP-only cookie value, hashed
userIdUUID → UserCascade delete on user removal
expiresAtTimestamptzIndexed
revokedAtTimestamptz?Null = active session

ForgotPasswordToken

FieldTypeNotes
idBigInt (PK autoincrement)
uuidUUID UNIQUEExposed in reset URL
tokenHashVarChar(255) UNIQUEOne-time reset token, hashed
userIdUUID → UserCascade delete
expiresAt, usedAtTimestamptzusedAt set when consumed

03Jobs domain

Job

The central operational record. Every field service assignment starts here.

FieldTypeNotes
idUUID (PK)
jobIdVarChar(80) UNIQUEHuman-readable identifier (e.g. ORN-00123)
partnerIdUUID → PartnerRequired — which partner owns this job
userIdUUID → User?Assigned Metiss team member
organizationIdBigInt → Organization? See DM-07Subcontractor FK
subContractorString? DM-07: deprecateLegacy free-text subcontractor name; coalesced with org.name
jobStatusString → enum DM-03"New" | "Lead" | "Assigned" | "Scheduled" | "Rescheduled" | "In Partner Review" | "Completed" | "Cancelled" | "On Hold" | "Rejected" | "Revisit Needed" | "Closed"
serviceTypeString?"Pre-Install Services" | "Installation Services" | "Post Install Services" | "Diagnostics & Service" | "Roofing" | "Removal and Reinstall" | "Panel Cleaning" | "Critter Guard Install" | "Other"
serviceSubTypeString?Sub-type per service type; values defined in shared-types
customerName, customerPhone, customerEmail, customerAddressString?Flat customer fields — no Customer entity yet. See Architecture Review §3.2
city, state, zipCodeString?
latitude, longitudeFloat?
dateAssigned, completedDate, cancelledDate, holdDate, partnerAssignedDate, createdDateDate?Status-transition dates
storageProviderStorageProvider enumGOOGLE_DRIVE | ONE_DRIVE (ONE_DRIVE not yet implemented)
storageUrlText?Root folder URL in storage provider
fileNamesString[]File list; denormalized — no FileAttachment entity
cancellationReasonText?
notesString?Legacy single-field notes — prefer Notes model
externalIdVarChar?ID in upstream partner system
createdByUserId, updatedByUserIdUUID → User?

Notes

Job-scoped notes. Note: a separate UserNote model exists for org/task/ invoice notes — see Architecture Review §3.4.

FieldTypeNotes
idUUID (PK)
jobIdUUID → Job?Nullable — note may be user-level
userIdUUID → User?
typeNoteType enumCUSTOMER | SUB_CONTRACTOR | METISS | PARTNER | SOW
notesString?Note body
fileNamesString[]Attached file names
createdBy, updatedByVarChar(120)?Name string — not a FK to User
isDeletedBoolean default falseSoft delete

Task

FieldTypeNotes
idBigInt (PK autoincrement)
uuidUUID UNIQUE
assignedToUUID → User
jobIdUUID → Job?Optional — tasks can exist without a job
descriptionText
dueDateTimestamptz
statusVarChar(50)"Pending" | "Completed" | …
isDeletedBoolean default false

Notifications

FieldTypeNotes
idBigInt (PK autoincrement)
userIdUUID → UserRecipient
jobIdUUID → Job?
threadIdBigInt → EmailThread?
organizationIdBigInt → Organization?
invoiceDetailsIdBigInt → InvoiceDetails?
contentText
isReadBoolean default false
receivedAtTimestamptz

04Partners domain

Partner

FieldTypeNotes
idUUID (PK)
nameVarChar(120)
statusString → enum DM-04"Active" | "Inactive"
isDeletedBoolean default false

UserPartner

Many-to-many join between User and Partner — controls which partner(s) a user can see.

FieldTypeNotes
idUUID (PK)
userIdUUID → UserIndexed
partnerIdUUID → PartnerIndexed
isDeletedBoolean default false

05Subcontractors domain

Organization

Subcontractor company. The authoritative entity for subcontractor identity.

FieldTypeNotes
idBigInt (PK autoincrement)See Architecture Review §3.1
uuidUUID UNIQUE
nameVarChar(120)
statusString? → enum DM-04
businessTypeVarChar(120)?
website, dba, notesText?
yearFormedDate?
crewCapacityString? DM-01: fix defaultStored as string, coerced to Number at read time. Bad default: 'crew_capacity'
mailingAddressIdBigInt → Address? (UNIQUE)
warehouseAddressIdBigInt → Address? (UNIQUE)
isDeletedBoolean default false

Address

FieldTypeNotes
idBigInt (PK autoincrement)
streetAddress, secondAddressVarChar(255)?
city, stateVarChar(120)?
postalCodeVarChar(20)?
isDeletedBoolean default false

Contact

FieldTypeNotes
idBigInt (PK autoincrement)
firstName, lastNameString?
roleVarChar(120)?
emailVarChar(120)?
mobilePhoneString?
isDeletedBoolean default false

Billing DM-08: encrypt credentials

FieldTypeNotes
idBigInt (PK autoincrement)
organizationIdBigInt → OrganizationCascade delete
addressIdBigInt → Address?
routingNumberText?PLAINTEXT — see DM-08
accountNumberText?PLAINTEXT — see DM-08
bankName, accountTypeVarChar(120)?
remittanceEmailVarChar(120)?
paymentTermString?
isDeletedBoolean default false

Market

FieldTypeNotes
idBigInt (PK autoincrement)
organizationIdBigInt → OrganizationCascade delete
stateVarChar(120)?
zipCodeVarChar(20)?
serviceType, serviceSubTypeVarChar(120)?Mirror of Job service fields
isDeletedBoolean default false

Pricing

FieldTypeNotes
idBigInt (PK autoincrement)
organizationIdBigInt → Organization
marketIdBigInt → Market? (Cascade)
subContractorPriceString?Stored as string, not Decimal
addersText?
notesText?

License

FieldTypeNotes
idBigInt (PK autoincrement)
organizationIdBigInt → OrganizationCascade delete
licenseTypeString?
marketVarChar(120)?Free-text string — not FK to Market model
receiveDate, expirationDateTimestamptz?
notesText?

Certification

FieldTypeNotes
idBigInt (PK autoincrement)
organizationIdBigInt → OrganizationCascade delete
certificationTypeVarChar(120)?
marketVarChar(120)?Free-text string — not FK to Market model
receiveDate, expirationDateTimestamptz?

Document

FieldTypeNotes
idBigInt (PK autoincrement)
organizationIdBigInt → OrganizationCascade delete
documentType, documentNameVarChar(120)?
statusVarChar(120)?
executionDate, expirationDateTimestamptz?
isDeletedBoolean default false

06Invoicing domain

InvoiceDetails

FieldTypeNotes
idBigInt (PK autoincrement)
jobIdUUID → Job
emailThreadIdsBigInt[] DM-06: migrate to FKArray of EmailThread IDs — breaks normalization; redundant with EmailThread.invoiceDetailsId FK
invoicedBoolean? default true
invoiceDate, paymentDateDate?
invoiceStatusVarChar(120)?
invoiceAmount, customInvoiceNumberVarChar(120)?Amount stored as string, not Decimal
typeVarChar(120)
splitInvoiceBoolean?
customerInvoiceAmount, customerInvoiceStatus, customerInvoiceDate, customerInvoicedVarious?Split-invoice customer side
isDeletedBoolean default false

EmailThread

FieldTypeNotes
idBigInt (PK autoincrement)
uuidUUID DM-09: add @unique@default(uuid()) present but @unique missing
userIdBigInt → UserThread owner
jobIdUUID → Job?
organizationIdBigInt → Organization?
invoiceDetailsIdBigInt → InvoiceDetails?Proper FK — counterpart to the emailThreadIds array
isReadBoolean default false
lastActivityAtTimestamptz

EmailMessage

FieldTypeNotes
idBigInt (PK autoincrement)
uuidUUID DM-09: add @unique@default(uuid()) present but @unique missing
threadIdBigInt → EmailThreadCascade delete
messageIdString? UNIQUESMTP Message-ID header
inReplyToString?SMTP In-Reply-To header
referencesString[]SMTP References header
directionDirection enumSENT | RECEIVED
fromEmailString
toEmails, ccEmails, bccEmailsString[]
subjectString
bodyHtmlString?
attachmentsGroupIdUUID?Groups attachments across messages
receivedAtTimestamptzIndexed with threadId

07System domain

EventLog

Local event store. Used when EVENT_PUBLISHER_DRIVER=local. In production, domain mutations should publish to GCP Pub/Sub instead — see Architecture Review §4.4.

FieldTypeNotes
idUUID (PK)
eventType, aggregateTypeVarChar(80)
aggregateIdUUIDID of the entity that changed
providerVarChar(60)"local" | "gcp-pubsub"
statusVarChar(60)
payloadJsonStringFull event payload as JSON string

AuditLog

FieldTypeNotes
idBigInt (PK autoincrement)
schemaName, tableNameVarChar(120)
entityId, entityUuidBigInt? / UUID?Indexed with tableName
operationVarChar(120)"INSERT" | "UPDATE" | "DELETE"
versionIntMonotonic version counter per entity
afterJson?Snapshot of row after mutation

SavedSearch

FieldTypeNotes
idBigInt (PK autoincrement)
uuidUUID UNIQUE
viewVarChar(32) default 'active'Which job board tab this search applies to
nameVarChar(120)
searchCriteriaTextJSON-serialised filter state
shareSettingsVarChar(255)?
selectedUserIdsString[]Users the search is shared with
createdByUserId, updatedByUserIdUUID → User
isDeletedBoolean default false
SavedSearchHistory mirrors SavedSearch in the history schema with a composite PK of (id, version) — populated by a PostgreSQL trigger on every update to track change history. Its uuid field carries the entity's UUID repeated across all version rows and has no @default(uuid()). The absence of @unique here is intentional — multiple rows share the same UUID by design.

08Note join tables

UserNote is a shared note body used by five entity-specific join tables. This is a parallel note system to Notes — see Architecture Review §3.4.

Join tableParent entityFK fields
OrganizationNoteOrganizationorganizationId → Organization, noteId → UserNote
PricingNotePricingpricingId → Pricing, noteId → UserNote
LicenseNoteLicenselicenseId → License, noteId → UserNote
CertificationNoteCertificationcertificationId → Certification, noteId → UserNote
TaskNotesTasktaskId → Task, noteId → UserNote
InvoiceNoteInvoiceDetailsinvoiceDetailsId → InvoiceDetails, userNotesId → UserNote

UserNote (shared note body)

FieldTypeNotes
idBigInt (PK autoincrement)
userIdUUID → User?Author
notesText?Note body
createdBy, updatedByVarChar(120)?Name string — not FK
isDeletedBoolean default false

09Enum reference

EnumValuesStatus
DirectionSENT | RECEIVEDIn schema
NoteTypeCUSTOMER | SUB_CONTRACTOR | METISS | PARTNER | SOWIn schema
StorageProviderGOOGLE_DRIVE | ONE_DRIVEIn schema
UserRole"Executive" | "Admin" | "Project Coordinator"DM-02 — add to schema
UserStatus"Active" | "Inactive" | "Suspended"DM-02 — add to schema
CommunicationPreference"Email" | "Text Message"DM-02 — add to schema
JobStatus"New" | "Lead" | "Assigned" | "Scheduled" | "Rescheduled" | "In Partner Review" | "Completed" | "Cancelled" | "On Hold" | "Rejected" | "Revisit Needed" | "Closed"DM-03 — add to schema
EntityStatus (Partner, Org)"Active" | "Inactive"DM-04 — add to schema
Technical Design Document · v1.0 · 2026-07-08 · AS-IS stateSource: database/prisma/schema.prisma