Database
Database overview: Drizzle on Supabase Postgres
Database
The odys application relies on a robust and well-structured database to manage its core business logic, from professional profiles and client bookings to messaging and notifications. This document delves into the database architecture, explaining the choices made for its implementation using Drizzle ORM with a Supabase-hosted PostgreSQL backend. Understanding this foundation is crucial for anyone looking to extend or maintain the application's data layer.
Overview
At its heart, odys leverages a PostgreSQL database, managed by Supabase, and interacts with it through the Drizzle ORM. This combination provides a powerful, type-safe, and developer-friendly experience for data operations. The database schema, defined declaratively in src/lib/db/schema.ts, consists of 10 distinct tables, each designed to encapsulate a specific domain entity within the application.
The connection to the database is established in src/lib/db/index.ts, where the postgres client is initialized and then wrapped by Drizzle. For schema migrations and type generation, Drizzle Kit is configured via drizzle.config.ts, pointing to the schema definition and specifying the PostgreSQL dialect.
The core entities revolve around professionals, clients, and appointments, forming the backbone of the booking and service provision system. Supporting these are tables for availability, clientNotes, recurringSchedules, messages, follows, reviews, and notifications, each playing a vital role in the application's functionality.
Schema Deep Dive
Let's explore the key tables and their relationships:
-
professionals: This is the central table for service providers, containing 24 columns. It stores essential information likename,phone,email, andprofession. Crucially, it links to the Supabase authentication system viauserId(atextcolumn, which is unique), and provides a uniqueslugfor public profile URLs. Financial aspects are covered bysessionDuration,sessionPrice,plan(defaulting to "free"),paymentType(defaulting to "upfront"),paymentPercentage(defaulting to 100), and integration with Stripe (stripeCustomerId,stripeSubscriptionId) and Pix (pixKeyType,pixKey). Other fields includebio,avatarUrl,trialEndsAt, andwelcomeMessage. Operational settings likeautoConfirmandactivestatus are also present, alongside standardcreatedAtandupdatedAttimestamps. -
availability: This table defines when aprofessionalis available for bookings. It links toprofessionalsviaprofessionalIdwith anonDelete: "cascade"rule, meaning if a professional is removed, their availability slots are automatically deleted. It capturesdayOfWeek(anintegerfrom 0 for Sunday to 6 for Saturday),startTime, andendTimeastextfields. An index onprofessionalIdensures efficient lookups. -
clients: Represents the individuals booking services. It has 8 columns, including a nullableuserId(alsotext), allowing clients to book without necessarily having a full Supabase account. It links toprofessionalsviaprofessionalIdwithonDelete: "cascade". Key client details likename,phone,email, andavatarUrlare stored here. Indexes onprofessionalIdanduserIdfacilitate quick access. -
clientNotes: This table provides a private space for professionals to record notes about their clients. It links to bothprofessionalsandclientswithonDelete: "cascade"rules, ensuring notes are cleaned up if either the professional or client is deleted. Thecontentis stored astext. -
recurringSchedules: Manages recurring appointments, such as weekly therapy sessions. It links toprofessionalsandclientswithonDelete: "cascade". It defines thefrequency(e.g., "weekly", "biweekly", "twice_weekly"),dayOfWeek,secondDayOfWeek(for twice-weekly schedules), andtimeastext. Theactiveboolean controls whether the schedule is currently in effect. -
appointments: This is the core booking table, tracking individual sessions. It has 13 columns and links toprofessionalsandclientswithonDelete: "cascade". It also has a nullable reference torecurringSchedules.id, but notably, without anonDelete: "cascade"rule, meaning a recurring schedule can be deleted without affecting past or future individual appointments generated from it. It storesstartsAtandendsAtastimestampvalues, along withstatus(defaulting to "pending_confirmation") andpaymentStatus(defaulting to "none"). Stripe payment intent IDs (stripePaymentIntentId) are stored here, as are flags forreminderSent24handreminderSent1h. An optionalnotesfield is also available. Multiple indexes are defined onprofessionalId,clientId,startsAt, andstatusto optimize common queries. -
messages: Facilitates communication between professionals and clients. It links toprofessionalsandclientswithonDelete: "cascade". It records thesender("professional" or "client"),type(defaulting to "text"),content, and optionally afileUrl. AreadAttimestamp tracks when a message was viewed. A composite index onprofessionalIdandclientIdsupports efficient message retrieval for specific conversations. -
follows: Tracks when a client (identified byuserId) follows aprofessional. It links toprofessionalswithonDelete: "cascade". Indexes onprofessionalIdanduserIdare present. -
reviews: Stores client feedback after an appointment. It links toprofessionals,clients, andappointmentswithonDelete: "cascade". TheappointmentIdis unique, ensuring only one review per appointment. It captures arating(anintegerfrom 1-5) and an optionalcomment. An index onprofessionalIdhelps aggregate reviews for a professional. -
notifications: A generic system for sending notifications to users. It usesrecipientId(atextfield, likely a Supabase user ID) andrecipientType("professional" or "client") to target notifications. It includestype,title,body, and areadstatus (defaulting tofalse). An index onrecipientIdallows for quick retrieval of a user's notifications.
Design Decisions
The database schema and its interaction layer reflect several deliberate design choices aimed at balancing development speed, data integrity, and application performance.
-
Drizzle ORM for Type Safety and Developer Experience: The decision to use Drizzle ORM was primarily driven by the desire for strong type safety throughout the data layer. Drizzle allows defining the schema in TypeScript, which then generates types for queries and results, significantly reducing runtime errors and improving developer confidence. Its SQL-like query builder syntax also offers a familiar and intuitive way to interact with the database, abstracting away raw SQL while retaining control over query optimization.
-
Supabase Postgres for Managed Backend: Hosting the PostgreSQL database on Supabase simplifies infrastructure management. Supabase provides a fully managed Postgres instance, handling backups, scaling, and security, allowing the
odysteam to focus on application features rather than database operations. Its integrated authentication system also complements theuserIdcolumns found in tables likeprofessionals,clients,follows, andnotifications. -
UUID Primary Keys: All tables utilize
uuidfor their primary keys, generated withdefaultRandom(). This choice offers several advantages: it avoids sequential IDs that could be guessed, provides globally unique identifiers, and is well-suited for distributed systems where ID generation might occur across multiple services without central coordination. -
onDelete: "cascade"for Referential Integrity: Many foreign key relationships, such asprofessionalIdinavailability,clients,clientNotes,recurringSchedules,appointments,messages,follows, andreviews, are configured withonDelete: "cascade". This is a pragmatic choice to maintain referential integrity and simplify data cleanup. When a parent record (e.g., aprofessional) is deleted, all associated child records (e.g., theiravailabilityslots,clients,appointments) are automatically removed by the database. This prevents orphaned records and ensures data consistency. The notable exception isrecurringScheduleIdinappointments, which does not cascade, allowing individual appointments to persist even if their recurring schedule is removed. This suggests a design where individual appointments are considered distinct events, even if initially generated by a recurring rule. -
Flexible
textTypes for Enums and Time: Several columns, such asprofessionals.profession,professionals.plan,professionals.paymentType,professionals.pixKeyType,recurringSchedules.frequency,appointments.status,messages.sender,messages.type,notifications.recipientType, andnotifications.type, are defined astextrather than PostgreSQL enum types. While this offers flexibility for adding new categories without schema migrations, it shifts the responsibility for data validation to the application layer. Similarly,availability.startTime,availability.endTime, andrecurringSchedules.timeare stored astext(e.g., "09:00"). This simplifies storage and retrieval but means the database doesn't enforce time format or validity, relying on application logic for correct parsing and manipulation. -
Default Values for Consistency: Many columns are assigned default values, such as
defaultRandom()for UUIDs,defaultNow()forcreatedAtandupdatedAttimestamps, and specific literal defaults for fields likesessionDuration(50 minutes),sessionPrice(0 cents),plan("free"),paymentType("upfront"),autoConfirm(false),active(true),recurringSchedules.active(true),appointments.status("pending_confirmation"),appointments.paymentStatus("none"),appointments.reminderSent24h(false),appointments.reminderSent1h(false),messages.type("text"), andnotifications.read(false). These defaults ensure that new records have sensible initial values, reducing the burden on application code to explicitly set every field and promoting data consistency.
Potential Improvements
While the current database design is functional and well-structured, several areas could be enhanced to improve data integrity, type safety, and query capabilities.
-
Introduce Drizzle Enum Types for Categorical Data: Many columns like
professionals.profession,professionals.plan,professionals.paymentType,professionals.pixKeyType,recurringSchedules.frequency,appointments.status,appointments.paymentStatus,messages.sender,messages.type,notifications.recipientType, andnotifications.typecurrently usetextcolumns to store categorical values. While flexible, this approach lacks database-level validation. Migrating these to Drizzle'spgEnumtypes would enforce valid values at the database level, preventing invalid data entry and improving type safety in the application. For example,professionals.professioncould be an enum of('psychologist', 'personal_trainer', 'nutritionist'). -
Align
userIdTypes with Supabaseuuid: TheuserIdcolumn inprofessionals,clients,follows, andnotifications.recipientIdis defined astext. Supabase'sauth.userstable typically usesuuidfor its primary key. Whiletextcan store UUID strings, using Drizzle'suuidtype for these columns insrc/lib/db/schema.tswould provide stronger type enforcement and better reflect the underlying data type from Supabase, potentially enabling more direct foreign key relationships or clearer integration points if Drizzle were to support cross-database FKs or custom type mappings. -
Utilize Native Time Types for
startTime/endTime: Theavailability.startTime,availability.endTime, andrecurringSchedules.timecolumns are currently stored astext. PostgreSQL offers nativetimeorintervaltypes that are specifically designed for storing time-of-day values. Using these native types would allow the database to enforce valid time formats, enable more efficient time-based queries (e.g., finding overlaps), and simplify time arithmetic directly within SQL, reducing the need for application-level parsing and validation. -
Implement Composite Unique Constraints for Relationships: For tables like
follows, a user should only be able to follow a professional once. Currently, this is likely enforced at the application level. Adding a composite unique constraint onfollowsfor(userId, professionalId)would enforce this uniqueness directly in the database, preventing duplicate entries and ensuring data integrity even if application-level checks are bypassed or have bugs. Similarly,availabilitycould benefit from a unique constraint on(professionalId, dayOfWeek, startTime, endTime)to prevent overlapping or duplicate availability slots.
References
src/lib/db/index.tssrc/lib/db/schema.tsdrizzle.config.ts