Documentation
IntroductionArchitectureMonorepo & ToolingRunning Locally
Screen Flow MapUser JourneysPersonas & RolesWeb ScreensClient Portal
Convex OverviewData ModelFunctions ReferenceAuth & SessionsRoles & PermissionsJobs & Providers
Web Design SystemAdmin Console DesignPatterns & Conventions
Web AppiOS AppMarketing Website
Testing & HarnessCI Pipeline

Data Model

Data Model

All 31 tables in packages/backend/convex/schema.ts, grouped by domain. Every operational row carries orgId; tenancy is enforced by the orgQuery/orgMutation seam, never by ad-hoc filters. Money is always integer cents. Timestamps are epoch milliseconds unless noted.

Shared validators defined at the top of the schema: role, visitStatus, fulfillmentStatus, paymentStatus, productType, timeEntryType, careEventType, messageChannel, petStatus, issueType, issueSeverity, scheduleStyle — all sourced from @pawdentity/domain enums.

orgs
tenant · slug · check-in windows
every table below carries orgId
Identity
sessions
users
memberships
1:N
1:N
joins org
membership = role · pay · PIN · feedToken — the row every permission check reads
Facility
locations
rooms
1:N
hours + per-location tax (bps) · room kinds: run / suite / room / grooming station
CRM & Operations
clients
pets
visits
careEvents
1:N
1:N
care log
signatures
medicationNeeds
petIssues
documents
signed
standing meds
issue lifecycle
uploads
Commerce
orders
orderItems
payments
paymentMethods
1:N
ledger
vaulted
products
line item
orders link back to clients and visits (deposits) · retail sales decrement products.inventoryCount · payments is append-only with idempotency keys
visit ↔ order (deposit)
Communication & agreements
threads
messages
notes
agreements
1:N
agreements version by seriesKey; signatures live in CRM above
shifts · timeEntries · foodBrands · rolePolicies · reminderConfigs · reminderLogs · applications · auditLogs — staff ops, config, and the landlord surface
Key relationships — every operational table also carries orgId (enforced by the wrapper, drawn once here)

Identity & tenancy

TablePurposeKey fieldsIndexes
orgsA tenant businessname, slug (subdomain in prod, /o/{slug} in dev), timezone, logoUrl?, accentColor?, checkInEarlyMinutes, checkInLateMinutes, checkOutLateMinutes, paymentProvider (stripe/fluidpay/manual), activeby_slug
usersGlobal person/loginemail, firstName, lastName, phone?, avatarUrl?, passwordHash?, passwordSalt? (local provider; Clerk replaces in prod)by_email
sessionsOpaque bearer tokensuserId, token, expiresAt (14-day TTL)by_token, by_user
membershipsUser↔org joinorgId, userId, role, pin? (4-digit desk PIN), payRateCents?, payType? (hourly/salary), title?, feedToken? (calendar feed auth), activeby_org, by_user, by_org_user, by_feed_token

Facility

TablePurposeKey fieldsIndexes
locationsPhysical sitesname, address?, phone?, color?, hours[] ({day, open, close, closed}), taxRateBps? (925 = 9.25%), activeby_org
roomsBookable spaceslocationId, name, kind (run/suite/room/grooming_station), capacity, activeby_org, by_location

CRM

TablePurposeKey fieldsIndexes
clientsPet ownersuserId? (portal login link), firstName, lastName, email?, phone?, address?, contactMethod?, isVip (policy-masked), notes?, archived, searchText (denormalized), stripeCustomerId?by_org, by_org_user, by_org_name (alphabetical cursor pagination), by_stripe_customer; search index search_name
petsAnimalsclientId, name, species (dog/cat/other), breed?, color?, sex?, fixed?, weightLbs?, birthDate? (ISO), photoUrl?/photoStorageId?, feedingNotes?/medicationNotes?/behaviorNotes?, feeding? (structured: dryFood, dryPortion, wetFood, wetPortion, frequency, notes), allergies?[], status? (petStatus), vaccines[] ({name, expiresAt?}), issues[] (legacy flags, superseded by petIssues), microchip?, archived, searchTextby_org, by_client, by_org_name; search index search_name
petPhotosGallerypetId, storageId?, url?, caption?, takenBy?by_org, by_pet

Operations

TablePurposeKey fieldsIndexes
visitsBookings / stayspetId, clientId, locationId, roomId?, orderId?, productId?, status (state machine), scheduleStyle, scheduledStart/End, checkInAt?/By?, checkOutAt?/By?, notes?by_org, by_org_status, by_pet, by_client, by_org_start
careEventsCare logvisitId, petId, type (feeding/medication/potty/play/walk/grooming/incident/photo/note), at, byUserId, note?, photoStorageId?, medicationNeedId? (pairs a dose to a standing need)by_org, by_visit, by_pet, by_need
medicationNeedsStanding meds per pet (v1 parity)petId, name, dosage?, frequency?, timeOfDay?, withFood?, note?, activeby_org, by_pet
petIssuesIssue lifecycle (v1 dog_issues parity)petId, visitId?, type, severity (normal/serious), title, note?, status (open/resolved), openedBy, resolvedBy?/At?, resolutionNote?by_org, by_pet, by_org_status
documentsUploaded filespetId?/visitId?/clientId?, storageId, name, mimeType (allowlisted), sizeBytes, kind (vet_record/document), uploadedByby_org, by_pet, by_visit, by_client

Commerce

TablePurposeKey fieldsIndexes
productsCataloglocationId?, roomId? (default room), type (service/product/boarding), name, sku, priceCents, salePriceCents?, depositCents?, durationMinutes?, scheduleStyle?, inventoryCount? (retail on-hand, decremented on sale), taxable, textColor?/bgColor?, parentId? (variants), activeby_org, by_org_type
ordersSalesclientId, locationId, number (per-org, starts 1000), fulfillment, paymentStatus (unpaid/authorized/deposit_due/deposit_met/paid/refund_due), subtotalCents, taxCents, discountCents, totalCents, paidCents, depositCents?, placedBy?, notes?by_org, by_org_number, by_client, by_org_fulfillment
orderItemsLine itemsorderId, productId, petId?, visitId?, description, quantity, unitPriceCents, note?by_org, by_order
orderEventsHistory feedorderId, at, byUserId? (absent = system), kind (status_change/payment/refund/note/pickup), summaryby_org, by_order
paymentMethodsVaulted cards (display data + provider ref only)clientId, provider (stripe/fluidpay/dev), providerRef, brand, last4, expMonth/Year, isDefault, activeby_org, by_client
paymentsCharge/refund ledgerorderId, kind (charge/refund), provider (stripe/fluidpay/manual/dev), paymentMethodId?, methodType (cash/card/bank/wallet), amountCents, status (succeeded/pending/failed), last4?, byUserId, refundOfId?, idempotencyKey?by_org, by_order, by_idempotency

Staff operations

TablePurposeKey fieldsIndexes
shiftsScheduled shiftslocationId, userId, start, end, note?by_org, by_user, by_org_start
timeEntriesTime-clock punchesuserId, type (work/pto/sick/holiday/break/unpaid), clockInAt, clockOutAt?, approvedBy?/At?, note?, editedBy?by_org, by_user, by_org_open ([orgId, clockOutAt]; open = clockOutAt undefined)

Communication

TablePurposeKey fieldsIndexes
notesInternal broadcast boardtitle, body, pinned, requiresAck?, authorId, readBy[]by_org
threadsClient message threadsclientId, petId?, subject, lastMessageAtby_org, by_client, by_org_last
messagesThread messagesthreadId, direction (outbound/inbound), channel (email/sms/in_app), body, sentBy?, readAt?, attachmentIds?[]by_org, by_thread

Agreements

TablePurposeKey fieldsIndexes
agreementsVersioned liability docsseriesKey, version (append-only publishing), title, body, requireForBooking, activeby_org, by_org_series
signaturesSigned agreementsagreementId, clientId, visitId?, signedAt, signedName, capturedBy? (staff desk-capture)by_org, by_agreement, by_client

Config

TablePurposeKey fieldsIndexes
foodBrandsCurated food list (v1 parity)species (dog/cat), type (dry/wet), name, priority, activeby_org
rolePoliciesPer-org permission overridesaction (PolicyAction), minRoleby_org
reminderConfigsAutomated reminder ruleskind (upcoming_visit/vaccine_expiry/agreement_missing), offsetHours, channel, template, activeby_org
reminderLogsSent-reminder dedupeconfigId, clientId, sentAt, summary, dedupeKey?by_org, by_config, by_dedupe

Platform (landlord)

TablePurposeKey fieldsIndexes
applicationsTenant signups from /applybusinessName, contactName, email, phone?, vertical, status (submitted/under_review/approved/rejected), orgId?, plus business detail fieldsby_status
auditLogsOrg activity trailactorId, action, entity, entityId, summary, atby_org, by_org_at

Modeling notes

  • Denormalized searchText on clients and pets ("first last email phone") powers the global search index alongside Convex search indexes.
  • State machines over booleans: visit status and payment status are explicit unions with transition rules in the domain package — never derived flags.
  • Append-only where history matters: agreements version instead of edit; orderEvents and payments are ledgers; auditLogs never update.
  • Legacy bridges: pets.issues[] (string flags) is superseded by the petIssues table but kept for v1 import compatibility.