Skip to main content

Database Schema

All tables use a bigint auto-increment id, Laravel timestamps (created_at, updated_at), and InnoDB with utf8mb4 on MySQL (:memory: SQLite under phpunit). Migrations live in backend/database/migrations/.

The schema spans 40+ tables across nine functional clusters:

ClusterCore tables
Identity & RBACsystem_admin, users, tenants, tenant_user, roles, permissions, role_permission, role_user
Auditaudit_logs
Auth helperspersonal_access_tokens, password_reset_tokens
Patients & medicalclients, pets, visits, prescriptions, prescription_items, lab_results, medical_attachments, pet_vaccinations, pet_documents, pet_health_logs
Caregivers & sharingpet_shares, pet_caregivers
Schedulingappointments
Billinginvoices, payments
Inventorymedicines, stock_movements, suppliers, purchase_orders, purchase_order_items
Notificationsreminders, reminder_templates, communication_opt_outs, service_settings
Marketplace & commerceorder_requests (uses tenants' provider columns)
Infracache, cache_locks, jobs, failed_jobs, job_batches

Entity-relationship diagram


Identity & RBAC

system_admin

Platform operators using the Filament admin panel. Separate auth guard, no tenant_id (operates above tenancy).

ColumnTypeNotes
idbigint PK
name, email (UK), phone, passwordstrings
email_verified_at, remember_tokennullable

users

Clinic staff and pet owners share this table.

ColumnTypeNotes
idbigint PK
name, email (UK), phone, passwordstrings
account_typestringstaff or owner
current_tenant_idFK → tenants nullablecurrently selected tenant for the staff user
email_verified_at, remember_tokennullable

tenants

One SaaS customer per row. Carries every operator-editable field plus the public-marketplace columns.

ColumnTypeNotes
idbigint PK
name, slug (UK), email (UK)strings
phone, addressnullable
subscription_planenumtrial, starter, growth, premium, enterprise (default trial)
statusenumactive, suspended, cancelled
business_typestring(20)clinic, pharmacy, retail, breeder, groomer, trainer
currencystring(3)Default INR
timezonestringDefault Asia/Kolkata
latitude, longitudedecimal(10,7) nullableMarketplace geo
service_radius_kmsmallint nullable
headline, biotext nullablePublic profile copy
servicesjson nullableArray of strings
premium_featuresjson nullableSlugs unlocked by the plan
is_publicbooleanWhether tenant appears in /providers

Migrations: 2026_04_15_175000_create_tenants_table.php, 2026_04_18_090000_add_business_type_…, 2026_04_19_210000_add_locale_fields_…, 2026_04_20_090200_add_provider_fields_….

tenant_user (pivot)

ColumnTypeNotes
tenant_id, user_idFKcascade
rolestringclinic_admin, veterinarian, staff, pharmacy, retail_counter
statusstringactive, invited, disabled
is_defaultboolean

permissions

Catalog of capabilities; seeded by PermissionSeeder.

ColumnTypeNotes
idbigint PK
keystring UKe.g. appointment.create, invoice.convert, payment.refund
descriptionstringHuman-readable label

roles

Tenant-scoped roles; can be platform default (no tenant_id).

ColumnTypeNotes
idbigint PK
tenant_idFK nullablenull = platform-default role
namestring
slugstringunique within tenant
is_systembooleansystem roles can't be edited

role_permission, role_user

Standard pivot tables: (role_id, permission_id), (role_id, user_id, tenant_id).

audit_logs

Append-only audit trail of model changes and explicit events.

ColumnTypeNotes
idbigint PK
tenant_idFK nullablenull = system-level (e.g. tenant create)
actor_user_id, actor_system_admin_idFK nullableone-of
auditable_type, auditable_idmorphtarget row
actionstringcreate, update, delete, or custom (e.g. receipt.emailed)
before, afterjson nullablesnapshots
ip, user_agent, request_idstrings nullablefrom RecordAuditContext middleware

Auth helpers

personal_access_tokens

Standard Sanctum table. Used by both API + web SPAs (Bearer flow).

password_reset_tokens

ColumnTypeNotes
idbigint PK
user_idFKcascade
token_hashstring(64)SHA-256 of token sent in email
expires_at, used_atdatetime nullable
ip_issued, ip_usedstrings nullablefor audit

Active token uniqueness is enforced via a partial-unique behaviour at the model level; new requests invalidate older unused tokens.


Patients & medical

clients

Pet owners as seen by a clinic; may or may not have a paired users row (cash walk-in vs. owner-app user).

ColumnTypeNotes
idbigint PK
tenant_idFKcascade
owner_user_idFK → users nullablecascade-on-delete-set-null
full_name, email, phone, addressstrings, mostly nullable
statusenumactive, inactive

pets

ColumnTypeNotes
idbigint PK
tenant_id, client_idFK
name, speciesrequired
breed, sex (male/female/unknown), date_of_birth, weight_kg, color_markings, allergies, medical_flagsnullable
statusenumactive, inactive, deceased
public_slugstring UK nullableURL slug for the public collar-tag landing page
public_profile_enabledbooleanfeature flag
lost_mode_enabled_attimestamp nullablewhen lost-mode was toggled on
lost_mode_messagetext nullableowner's plea text

Migrations: base + 2026_04_22_100100_add_public_profile_to_pets_table.php.

visits

A consultation. Carries the SOAP narrative + vitals + status.

ColumnTypeNotes
idbigint PK
tenant_id, pet_id, client_id, veterinarian_user_idFK
visit_atdatetime
chief_complaint, subjective, objective, assessment, plantext nullableSOAP fields
weight_kg, temperature_c, heart_rate_bpm, respiratory_rate_bpmdecimal/integer nullablevitals
statusenumdraft, complete, cancelled
notestext nullable

prescriptions

A clinical Rx envelope. One visit may have one prescription.

ColumnTypeNotes
idbigint PK
tenant_id, pet_id, client_id, prescribed_by_user_idFK
visit_idFK nullablecascade-set-null when visit deleted
numberstringe.g. RX-2026-0001
issued_atdatetime
statusenumdraft, active, completed, cancelled
valid_untildate nullable
notestext nullable

prescription_items

Each line of an Rx.

ColumnTypeNotes
idbigint PK
prescription_idFKcascade
medicine_idFK nullablenullable for free-text "external" Rx
medicine_namestringsnapshot
dosage, frequency, duration, routestrings/nullablee.g. 5mg/kg, BID, 7d, oral
quantityinteger
refills_allowed, refills_remaininginteger
instructionstext nullable

lab_results

ColumnTypeNotes
idbigint PK
tenant_id, pet_id, visit_id (nullable), recorded_by_user_idFK
panelstringe.g. CBC, Biochem, Urinalysis
analytestring
value, unitstrings
reference_rangestring nullable
flagenumnormal, low, high, critical (default normal)
recorded_atdatetime
notestext nullable

medical_attachments

Polymorphic file attachments. Hangs off any clinical resource.

ColumnTypeNotes
idbigint PK
tenant_id, pet_idFK
attachable_type, attachable_idmorph nullableparent (visit / lab / prescription)
uploaded_by_user_idFK
diskstringalways private (controller hard-codes)
pathstringstorage key
original_name, mime_typestrings
size_bytesinteger
categorystringxray, ultrasound, lab_pdf, consent, other
descriptiontext nullable

pet_vaccinations

ColumnTypeNotes
idbigint PK
pet_idFKcascade
tenant_idFK nullablenull = owner-logged outside any clinic
recorded_by_user_idFK
sourceenumclinic, owner (derived from URL prefix in controller)
name, manufacturer, batchstrings/nullable
administered_ondate
next_due_atdate nullabledrives the "Due Soon" feed
notestext nullable

pet_documents

Owner-uploaded documents (insurance card, X-ray, lab PDF, ID).

ColumnTypeNotes
idbigint PK
pet_idFKcascade
uploaded_by_user_idFK
diskstringalways local
pathstringstorage key
original_name, mime_typestrings
size_bytesinteger
categoryenumlab_report, xray, ultrasound, insurance, id, other
issued_ondate nullable
notestext nullable

pet_health_logs

Owner-written + clinic-noted vitals/symptom journal.

ColumnTypeNotes
idbigint PK
pet_idFK
tenant_idFK nullablenull = home log
logged_by_user_idFK nullable
logged_atdatetime
weight_kg, temperature_cdecimal nullable
appetiteenumlow, normal, high (nullable)
symptomsjson nullablearray of strings
notestext nullable
sourceenumowner, clinic

Caregivers & sharing

pet_shares

QR / link-based pet share for clinics.

ColumnTypeNotes
idbigint PK
pet_idFKcascade
owner_user_idFKalways set server-side from authenticated user
codestring UKrandom 8-char human-friendly code
expires_atdatetime nullable
revoked_atdatetime nullable
max_redemptions, redeemed_countinteger

pet_caregivers

Email-based ongoing co-owner / family invite.

ColumnTypeNotes
idbigint PK
pet_idFKcascade
invited_by_user_idFKthe primary owner
invited_email, invited_namestrings
caregiver_user_idFK nullablebound on accept
invite_tokenstring(48) UK nullablenulled out after accept (one-shot)
access_levelenumview, full
accepted_at, revoked_attimestamps nullable

Scheduling

appointments

ColumnTypeNotes
idbigint PK
tenant_id, pet_id, client_idFK
veterinarian_user_idFK nullable
appointment_typestringconsult, vaccination, surgery, …
statusenumscheduled, confirmed, checked_in, with_vet, completed, cancelled, no_show
booking_sourceenumclinic_portal, owner_app, phone, walk_in
scheduled_start, scheduled_enddatetime
reason, notestext nullable
rescheduled_from_start_at, rescheduled_at, rescheduled_by_user_idnullablereschedule audit
cancellation_reason, cancelled_at, cancelled_by_user_idnullablecancel audit
checked_in_at, with_vet_at, completed_attimestamps nullableworkflow timestamps

Migrations: base + 2026_04_19_090000_add_reschedule_cancel_fields_… + 2026_04_24_090000_add_workflow_timestamps_….


Billing

invoices

ColumnTypeNotes
idbigint PK
tenant_id, client_id, pet_id (nullable)FK
invoice_numberstring UK(per-tenant uniqueness recommended — see audit)
invoice_typeenumestimate, invoice
statusenumdraft, pending, partial, paid, void
subtotal, tax_total, discount_total, grand_total, balance_duedecimal(12,2)
currencystring(3)
issued_at, due_atdates
notestext nullable
receipt_emailed_at, receipt_emailed_to, receipt_pdf_pathnullablepost-receipt audit

Migrations: base + 2026_04_19_150000_add_receipt_fields_to_invoices_table.php.

payments

ColumnTypeNotes
idbigint PK
tenant_id, invoice_idFK
amountdecimal(12,2)
payment_methodenumcash, card, upi, bank_transfer, online
statusenumpending, captured, failed, refunded
reference_numberstring nullable
paid_atdatetime nullable
gatewaystring nullablee.g. razorpay
gateway_order_id, gateway_payment_id, gateway_signaturestrings nullable
raw_payloadjson nullableresponse body kept for forensics
refund_id, refunded_at, refund_reasonnullablerefund audit

Migration: base + 2026_04_21_090000_add_provider_columns_to_payments_table.php.


Inventory

See Inventory module for the operational story.

medicines

Tenant-scoped catalog. Used by clinic, pharmacy, and retail tenants.

ColumnTypeNotes
idbigint PK
tenant_idFK
skustring(64)unique per tenant
name, generic_name, manufacturerstrings/nullable
categorystring(60)Antibiotic, NSAID, Food, …
kindstring(20)medicine, vaccine, pet_food, supplement, accessory, grooming
form, strength, unitnullable
stock_quantity, minimum_stock, maximum_stock, reorder_levelintegers
unit_price, selling_pricedecimal(12,2)
requires_prescriptionboolean
batch_number, expiry_date, storage_instructionsnullable
indications, side_effectsjson nullable
status, notesnullable

stock_movements

Append-only ledger of every stock change.

ColumnTypeNotes
idbigint PK
tenant_id, medicine_id, performed_by_user_id (nullable), pet_id (nullable)FK
typeenumpurchase, dispense, return, wastage, adjustment, transfer
quantity, previous_stock, new_stockintegers
unit_cost, total_valuedecimal nullable
reference, vendor, invoice_number, prescription_numberstrings nullable
batch_number, expiry_datenullable
reason, notesnullable
statusenumcompleted, pending_approval, cancelled

suppliers, purchase_orders, purchase_order_items

Standard inventory shape with PO lifecycle: draft → ordered → partial → received → cancelled. Receive flow writes one stock_movements row per received item via PurchaseOrderController::receive.


Notifications

reminders

ColumnTypeNotes
idbigint PK
tenant_idFK
client_id, pet_idFK nullable
titlestring
reminder_typeenumvaccination, deworming, follow_up, payment_due, general
channelenumemail, sms, push, in_app
due_atdatetime
statusenumpending, sent, dismissed, failed
payloadjson nullable
attempts, last_attempt_atinteger / timestampretry counter
sent_at, last_error, provider_message_id, to_address, template_slugnullablepost-send audit

Migrations: base + 2026_04_19_120000_extend_reminders_and_create_comms_tables.php.

reminder_templates

ColumnTypeNotes
idbigint PK
tenant_idFK nullablenull = platform default
slugstringunique per tenant (or per platform)
channelenumemail, sms, push, in_app
subject, bodystrings/textwith {{variable}} interpolation
html_bodytext nullablefor HTML email
from_namestring nullableper-template From: override

Seeded slugs: general_reminder, appointment_reminder, vaccination_due, payment_due, receipt_email, caregiver_invite, order_request_received. See database/migrations/2026_04_23_090100_seed_transactional_email_templates.php.

communication_opt_outs

ColumnTypeNotes
idbigint PK
tenant_idFK
channelenumemail, sms, push
contactstringnormalised lower-cased trimmed
reasonstring nullable
opted_out_attimestamp

Unique index (tenant_id, channel, contact).

service_settings

Per-tenant key/value store for notification + integration config. Backs the Filament admin "Notification settings" page.

ColumnTypeNotes
idbigint PK
tenant_idFK nullablenull = platform default
groupstringmail, sms, push, razorpay, …
keystringscoped to group
valuetext nullableencrypted at rest for secrets

Marketplace & commerce

order_requests

Owner-initiated refill / reorder ping to a pharmacy or retail tenant.

ColumnTypeNotes
idbigint PK
target_tenant_idFKthe seller
pet_id, requester_user_idFK
source_prescription_idFK nullablewhen refill draft was used
kindenumrefill, reorder, custom
statusenumpending, accepted, fulfilled, rejected, cancelled
itemsjson[{medicine_name, quantity, notes}, …]
delivery_preferenceenumpickup, delivery
delivery_addresstext nullable
notestext nullable
accepted_at, fulfilled_at, cancelled_at, rejected_attimestamps nullable
reject_reasonstring nullable

Tenants' provider columns (is_public, latitude, longitude, business_type) drive the marketplace listing this is paired with.


Foreign-key conventions

  • All FKs use ->foreignId('xxx_id')->constrained()->cascadeOnDelete() unless the parent can vanish without invalidating the row, in which case ->nullOnDelete().
  • Tenant scoping is always modelled with a non-nullable tenant_id column except for resources that legitimately exist outside any tenant (caregivers, public-share lookups, platform-default templates, pet-owner-only health logs).

Indexes worth knowing

  • appointments(tenant_id, scheduled_start) — calendar.
  • invoices(tenant_id, status) — dashboard aggregations.
  • reminders(tenant_id, due_at, status) — dispatch scheduler.
  • pets(public_slug) — lost-pet lookup.
  • tenants(is_public, business_type, latitude, longitude) — marketplace shortlist.
  • communication_opt_outs(tenant_id, channel, contact) — UNIQUE; the lookup happens on the same triple.
  • pet_caregivers(invite_token) — token-redeem lookup.

Indexes to consider adding (audit)

See AUDIT.md for full context. Most relevant deferred items:

  • Composite unique (tenant_id, invoice_number) rather than global unique (multi-tenant clash today).
  • audit_logs(tenant_id, auditable_type, auditable_id) for entity history queries.
  • order_requests(target_tenant_id, status, created_at) for the seller's inbox.