Backend
Backend overview: API route tree, shared helpers, auth pattern
Backend
The Odys backend serves as the central nervous system for the application, orchestrating data flow, managing user interactions, and integrating with external services. It is built upon Next.js API routes, providing a structured and familiar environment for handling HTTP requests. This document delves into the architecture of the backend, exploring its API surface, database schema, shared utility functions, and authentication patterns.
Overview
The backend exposes a comprehensive set of 23 API routes, primarily under the /api/ prefix, to manage various aspects of the application. These routes cover core functionalities such as /api/booking for scheduling, /api/messages for communication, and /api/follows for user engagement. Dynamic routes, like /api/appointments/[id], allow for operations on specific resources.
At the heart of the data layer lies a Drizzle ORM schema comprising 10 distinct tables. Key entities include professionals, storing detailed information about service providers, and clients, representing the individuals who book services. The relationships between these entities are crucial:
professionalsis linked toavailability(defining working hours),clients(clients associated with a professional),clientNotes,recurringSchedules,appointments,messages,follows, andreviews. Many of these relationships useprofessionalIdas a foreign key, often withonDelete: cascadeto ensure data integrity.appointmentstracks individual service bookings, linkingprofessionalIdandclientId, and can optionally referencerecurringSchedules.messagesfacilitates communication between professionals and clients, also linking to bothprofessionalIdandclientId.
Beyond standard API interactions, the backend incorporates specialized functionalities:
- Cron Jobs: Two scheduled tasks,
/api/cron/remindersand/api/cron/whatsapp-watchdog, run daily at0 8 * * *and0 9 * * *respectively, automating critical background processes. - Rate Limiting: To prevent abuse and ensure fair usage, various endpoints are protected by rate limits. For instance,
/api/bookingis limited to5requests per10 m, while general API access (/api/) is capped at60requests per1 m. Specific limits also apply toonboarding,register,whatsapp,aichatroutes. - AI Integration: The system includes
4AI tools:get_appointments_today,get_customer_by_phone,get_revenue_summary, andupdate_appointment_status, accessible via routes like/api/ai/chat. - Subscription Plans: Odys offers
4distinct subscription plans:free(at0),basic(at39),pro(at79), andpremium(at149), each with associated Stripe Price IDs. - WhatsApp Integration: The platform uses
19predefined WhatsApp message templates for various notifications, such asmsgBookingConfirmedandmsgReminder24h, indicating a deep integration with the messaging service. - Stripe Webhooks: The backend listens for
checkout.session.completed,customer.subscription.updated, andcustomer.subscription.deletedevents from Stripe to manage subscriptions and payments.
The project relies on a modern JavaScript ecosystem, utilizing next version 16.2.4 and react version 19.2.5. Key dependencies for backend operations include drizzle for ORM, supabase for authentication and database access, stripe for payment processing, and groqSdk for AI interactions.
Shared API Helpers (src/lib/api.ts)
The src/lib/api.ts file centralizes a collection of shared helpers designed to streamline API route development, enforce consistency, and enhance security. These helpers are categorized into response utilities, authentication mechanisms, and general utilities.
Response Helpers
A set of functions provides standardized JSON responses with appropriate HTTP status codes:
ok(data: object): Returns a200 OKresponse with the provided data.unauthorized(): Sends a401 Unauthorizedresponse, typically used when a user is not logged in.forbidden(msg = "Sem permissão"): Returns a403 Forbiddenresponse, indicating the user is authenticated but lacks permission for the requested action.notFound(msg = "Não encontrado"): Provides a404 Not Foundresponse for non-existent resources.badRequest(msg: string): Generates a400 Bad Requestresponse for invalid client input.conflict(msg: string): Returns a409 Conflictresponse, often used when a request attempts to create a resource that already exists.tooManyRequests(): Sends a429 Too Many Requestsresponse, indicating rate limit exhaustion.serverError(context: string, err: unknown): This critical helper logs the internal error usinglogger.errorwith a specificcontextand returns a generic500 Internal Server Errorto the client, preventing sensitive internal details from being exposed.
This structured approach to responses ensures that clients receive predictable feedback and that server-side errors are handled gracefully without compromising security.
Auth Helpers
Authentication in the backend follows a two-step process:
getUser(): This asynchronous function interacts with Supabase to retrieve the currently authenticated user. It returns the Supabase user object if logged in, ornullotherwise. This is the primary gatekeeper for any authenticated request.getProfessional(userId: string): Once a Supabase user is identified, this function queries the Drizzle ORM to fetch the corresponding professional profile from theprofessionalstable using theuserId. This separation allows the system to first verify a user's identity via Supabase and then retrieve their application-specific professional data.
This pattern ensures that only authenticated users can access protected resources, and that their professional context is correctly loaded for subsequent operations.
Utility Helpers
The src/lib/api.ts file also includes general utility functions:
getIp(req: NextRequest): Extracts the client's IP address from thex-forwarded-forheader, which is essential for implementing and enforcing rate limiting policies. It defaults to "anonymous" if the IP cannot be determined.safeEqual(a: string, b: string): This private helper performs a constant-time string comparison using Node.js'stimingSafeEqual. It first checks if either input is empty or if their lengths differ, returningfalseimmediately in such cases. This is a crucial security measure, preventing timing side-channel attacks when comparing sensitive values like API keys or secrets.isCronAuthorized(req: NextRequest): This function verifies if an incoming request is authorized to trigger a cron job. It checks for aCRON_SECRETenvironment variable and compares it against either thex-cron-secretheader or theAuthorization: Bearerheader, usingsafeEqualto ensure secure comparison. This prevents unauthorized external entities from triggering scheduled tasks.
Design Decisions
The architectural choices in the Odys backend reflect a commitment to maintainability, security, and developer experience:
- Centralized API Helpers: Grouping response, auth, and utility functions in
src/lib/api.tspromotes code reuse and consistency across all API routes. Instead of duplicating error handling or authentication logic, developers can import and use these standardized building blocks, reducing the likelihood of errors and simplifying future modifications. - Two-Step Authentication (
getUserthengetProfessional): This design separates generic user authentication (handled by Supabase) from application-specific user roles (handled by theprofessionalstable). This allows for flexibility, as a Supabase user might exist without being a registered professional, and ensures that professional-specific actions are only performed by verified professionals. - Drizzle ORM: The choice of Drizzle ORM for database interactions provides a type-safe and performant way to interact with the PostgreSQL database. It allows developers to define the schema in TypeScript, benefiting from compile-time checks and a fluent query builder, which enhances developer productivity and reduces runtime errors compared to raw SQL.
- Next.js API Routes: Utilizing Next.js API routes simplifies the deployment model, allowing the frontend and backend to coexist within a single repository and deployment pipeline. This monolithic approach can accelerate development and reduce operational overhead, especially for smaller to medium-sized applications.
- Constant-Time Comparison (
safeEqual): The inclusion ofsafeEqualfor comparing secrets, particularly inisCronAuthorized, is a deliberate security decision. It mitigates timing attacks, where an attacker could infer parts of a secret by measuring the time it takes for a comparison function to return, thus protecting sensitive credentials. - Explicit Error Responses: The comprehensive set of error response helpers (
badRequest,unauthorized,serverError, etc.) ensures that API clients receive clear, actionable feedback. TheserverErrorhelper's design to log internal errors while returning a generic message to the client is a critical security practice, preventing information leakage.
Potential Improvements
While the current backend structure is well-designed, several areas offer opportunities for refinement:
- Refine
okhelper's type signature: Theokhelper insrc/lib/api.tscurrently acceptsdata: object. While functional, this type is very broad. Refining it to accept a generic typeT(e.g.,export const ok = <T extends object>(data: T) => ...) would allow API routes to return more specific, type-safe JSON payloads, improving developer experience and reducing potential runtime errors by ensuring the returned data conforms to an expected structure. - Centralize rate limiting logic: The
getIphelper is provided, indicating rate limiting is in use, and therateLimitsfact lists specific limits for various routes. However, the actual rate limiting logic (e.g., using a Redis store to track requests) is not present insrc/lib/api.ts. If this logic is currently duplicated across various API routes, centralizing it into a reusable middleware or decorator that consumes therateLimitsconfiguration would reduce boilerplate, ensure consistent application of policies, and simplify future adjustments to rate limits. - Optimize
getProfessionalcalls: ThegetProfessionalfunction performs a database query on theprofessionalstable for every call. For frequently accessed routes that require professional data, this could lead to redundant database hits. Considering a caching layer (e.g., Redis) for professional profiles or integrating theprofessionalIddirectly into the session token (if appropriate for the authentication flow) could reduce database load and improve response times. This would involve careful consideration of data freshness and invalidation strategies. - Enhance
CRON_SECRETvalidation: InisCronAuthorized, ifprocess.env.CRON_SECRETis not set, the function simply returnsfalse. While this prevents unauthorized access, it might silently fail in a misconfigured environment. Adding a warning log or throwing an error during application startup ifCRON_SECRETis missing in a production environment could help prevent deployment issues and ensure cron jobs are properly secured.
References
src/lib/api.ts