Tiago Fortunato
ProjectsOdysTech Decisions

Supabase

Why Supabase (Postgres + Auth + Storage)

Supabase

Odys relies on Supabase as a foundational backend-as-a-service platform, providing a PostgreSQL database, authentication services, and file storage capabilities. This integrated approach simplifies infrastructure management and accelerates development by offering a suite of essential backend features out of the box. The project uses the @supabase/ssr package, version ^0.10.2, and the core @supabase/supabase-js library, version ^2.100.1, to interact with these services across both server and client environments within the Next.js application.

Overview

Supabase serves as the central data store and identity provider for Odys. Its PostgreSQL database hosts all application data, structured across 10 distinct tables. These tables manage core entities such as professionals, clients, appointments, messages, and reviews. Each table is meticulously defined, with id columns typically serving as uuid primary keys, often with default values.

For instance, the professionals table, with its 24 columns, includes a userId column of type text which is both non-nullable and unique, directly linking a professional's profile to a Supabase authentication user. Similarly, the clients table, with 8 columns, also features a userId column, though it is nullable, indicating that not all clients are required to have a direct Supabase user account. Many tables, such as availability, clients, clientNotes, recurringSchedules, appointments, messages, follows, and reviews, establish foreign key relationships back to the professionals table via a professionalId column. A notable design choice is the consistent application of ON DELETE CASCADE for these professionalId foreign keys, ensuring that when a professional's record is removed, all associated data (like their availability, clients, or appointments) is automatically cleaned up. However, the appointments.recurringScheduleId column, while referencing recurring_schedules, does not have ON DELETE CASCADE, implying that individual appointments persist even if their originating recurring schedule is deleted.

Beyond the database, Supabase Auth handles user registration, login, and session management. This is crucial for distinguishing between professionals and their clients, and for securing access to specific data. Supabase Storage is utilized for managing user-uploaded files, such as avatarUrl for both professionals and clients, and fileUrl for attachments within messages.

Database Integration

While Supabase provides the underlying PostgreSQL database, Odys interacts with it through Drizzle ORM, specifically using drizzle-orm version ^0.45.2. Drizzle allows for defining the database schema in TypeScript, providing strong type safety and a more idiomatic way to construct queries compared to raw SQL or Supabase's client-side query builders. The project uses drizzle-kit for schema generation and migrations, as evidenced by the db:generate and db:migrate scripts in package.json. This abstraction layer enhances developer experience and reduces the likelihood of runtime database errors.

Authentication and Storage

Supabase Auth is integrated to manage user identities. The userId column in the professionals table, being unique and non-nullable, directly maps to a user managed by Supabase Auth. This allows for a clear association between an authenticated user and their professional profile within the application.

For file management, Supabase Storage buckets are used. This is evident from columns like avatarUrl in both professionals and clients tables, and fileUrl in the messages table, all of which are of type text and store URLs pointing to assets hosted in Supabase Storage. This offloads file serving and management to a dedicated service, reducing the complexity of the application server.

Supabase Client Implementations

Odys employs distinct Supabase client implementations for server-side and client-side operations, a common pattern in Next.js applications to handle different execution environments and security contexts.

The server-side client, defined in src/lib/supabase/server.ts, provides an async createClient function that uses createServerClient from @supabase/ssr. This client is designed to run in server components and API routes, where it can securely access and manipulate HTTP cookies. The cookies object from next/headers is used to manage Supabase session tokens, ensuring that user sessions are correctly propagated and maintained across server requests. The setAll method includes a try...catch block, which silently handles potential errors during cookie setting.

For browser-side interactions, src/lib/supabase/client.ts provides a createClient function that wraps createBrowserClient. A notable design choice here is the use of a JavaScript Proxy. This proxy defers the actual createBrowserClient call until the first property access (e.g., createClient().auth.signIn). This was implemented to circumvent a specific issue during next build where, in certain CI environments (like Dependabot), process.env.NEXT_PUBLIC_SUPABASE_URL might be empty. Without the proxy, createBrowserClient would throw an error during static pre-rendering, halting the build process. The proxy ensures that the client is only initialized when needed at runtime, allowing the build to complete successfully.

Design decisions

The decision to adopt Supabase as a unified backend-as-a-service was primarily driven by the desire for rapid development and reduced operational overhead. Instead of separately managing a PostgreSQL database, an authentication service, and a file storage solution, Supabase provides these components as a cohesive, managed offering. This allows the development team to focus more on application logic rather than infrastructure.

The integration of Drizzle ORM alongside Supabase's PostgreSQL was a deliberate choice to enhance developer experience. While Supabase offers its own client libraries for database interaction, Drizzle provides superior type safety, a more expressive query API, and robust schema migration tools, which are critical for maintaining a complex schema with 10 tables and numerous relationships. This trade-off introduces an additional dependency but pays dividends in code quality and maintainability.

The use of separate server and client Supabase clients is a direct consequence of building with Next.js. createServerClient is essential for secure, authenticated data fetching in server components and API routes, where direct cookie manipulation is possible. Conversely, createBrowserClient is necessary for client-side interactions, such as user sign-up or profile updates from the browser. The specific implementation of a Proxy in src/lib/supabase/client.ts for the browser client is a pragmatic solution to a build-time environment variable challenge, prioritizing successful builds over a marginally simpler client initialization pattern.

The consistent use of ON DELETE CASCADE for professionalId foreign keys across most related tables reflects a design decision to maintain data integrity and simplify cleanup. When a professional account is deleted, all their associated data, such as clients, appointments, and messages, is automatically removed. The exception for appointments.recurringScheduleId not having ON DELETE CASCADE suggests a deliberate choice to preserve historical appointment data even if the recurring schedule that generated it is removed, which could be important for auditing or client history.

Potential improvements

  1. Centralize Supabase Client Configuration: The NEXT_PUBLIC_SUPABASE_URL! and NEXT_PUBLIC_SUPABASE_ANON_KEY! environment variables are currently passed directly to createServerClient in src/lib/supabase/server.ts and createBrowserClient in src/lib/supabase/client.ts. Centralizing these environment variable accesses into a single configuration utility or constant would reduce repetition and make it easier to manage or update these values in the future.
  2. Improve Error Handling in Server Client: The setAll method within createClient in src/lib/supabase/server.ts uses a try {} catch {} block that silently swallows any errors during cookie setting. While this might prevent crashes, it obscures potential issues with cookie management. Logging the error or providing a more explicit fallback mechanism could aid in debugging and ensure session integrity.
  3. Refine Proxy Type Safety: In src/lib/supabase/client.ts, the target[prop as string] assertion within the Proxy's get handler bypasses some of TypeScript's type checking. While functional, exploring ways to provide more explicit type definitions for the proxied object, perhaps by using conditional types or a more advanced proxy pattern, could improve type safety and developer confidence when interacting with the client.

References

  • src/lib/supabase/server.ts
  • src/lib/supabase/client.ts
  • package.json

On this page