Payment Flows
PIX flow (client → pro, off-Stripe) + Stripe subscription flow (pro → Odys)
Payment Flows
Odys manages two distinct payment flows, each serving a different relationship within the platform: payments from clients to professionals for services rendered, and subscription payments from professionals to Odys for platform access. Understanding these mechanisms is crucial for comprehending how value is exchanged and how the platform's business model operates.
Overview
The Odys platform facilitates financial transactions through two primary channels. First, clients pay professionals directly for appointments, primarily using Brazil's instant payment system, PIX. This flow is largely "off-Stripe," meaning Odys provides the tools for PIX generation but does not intermediate the funds. Second, professionals subscribe to Odys's services, paying a recurring fee to access various features. This subscription model is handled entirely through Stripe, a third-party payment processor, ensuring automated billing and plan management.
The core database schema reflects these payment structures. The professionals table is central, storing details for both payment types: pixKeyType and pixKey for client payments, and stripeCustomerId, stripeSubscriptionId, plan, and trialEndsAt for their subscription to Odys. The appointments table includes a paymentStatus column, which is relevant for tracking client payments, regardless of the method.
Client-to-Professional Payments via PIX
For transactions between a client and a professional, Odys integrates with PIX, Brazil's instant payment system. This allows professionals to receive payments directly from their clients.
Professionals configure their PIX details within their profile, which are stored in the professionals table under the pixKeyType and pixKey columns. These fields are nullable, indicating that a professional might not offer PIX as a payment option or might not have configured it yet.
The src/lib/pix.ts file contains the logic for generating a PIX static QR code payload. The buildPixPayload function takes the professional's key, name, an optional city (defaulting to "Brasil"), and the amount of the payment (in cents) to construct a valid EMV BR Code. This function adheres to the Banco Central do Brasil's standards for PIX initiation. It uses helper functions like field to format data according to the PIX specification and crc16 to calculate the Cyclic Redundancy Check, ensuring the integrity of the generated payload.
When a client books an appointment, the system can generate a PIX QR code based on the professional's configured sessionPrice from the professionals table. The appointments table tracks the paymentStatus for each booking, which would typically be updated once the professional confirms receipt of the PIX payment. The "PIX integrado" feature is available even on the "Free" plan, as defined in src/lib/stripe/plans.ts, highlighting its foundational role in client-professional transactions.
Professional-to-Odys Payments via Stripe Subscriptions
Professionals subscribe to Odys's services through a tiered plan structure, managed by Stripe. This flow handles the recurring payments from professionals to Odys.
The available subscription plans—"Free", "Basic", "Pro", and "Premium"—are defined in src/lib/stripe/plans.ts within the PLANS object. Each plan specifies its label, price, priceId (referencing a Stripe Price ID), limits (e.g., clients, appointmentsPerMonth), and a list of features. The free plan also includes a note field for additional details. The PLAN_FEATURES array provides a master list of all features, used for comparison displays.
When a professional decides to upgrade their plan, the process is initiated through the POST /api/stripe/checkout API route.
- Authentication and Authorization: The route first verifies the user's identity using
supabase.auth.getUser(). It also applies a rate limit usinggetApiLimiter()to prevent abuse. - Plan Selection: The request body specifies the desired
plan(e.g., "basic", "pro", "premium"). The system validates this against thePLANSobject, rejecting requests for invalid or "free" plans. - Professional Lookup: The system fetches the professional's details from the
professionalstable using theiruserId. - Trial Management: If the professional is upgrading to the "pro" plan and still within their trial period (checked via
trialDaysLeft), Stripe is instructed to defer payment collection until the trial ends. ThetrialEndtimestamp is passed to Stripe's session creation. - Stripe Checkout Session: A Stripe Checkout Session is created using
stripe.checkout.sessions.create. This session is configured forsubscriptionmode, acceptscardpayments, and includes thepriceIdfor the selected plan. Crucially,metadatacontainingprofessionalIdandplanis attached to the session, which is vital for later reconciliation via webhooks. Thesuccess_urlandcancel_urldirect the user back to the Odys dashboard after the checkout process.
After a professional completes the checkout process on Stripe, or if their subscription status changes, Stripe sends events to the POST /api/stripe/webhook endpoint. This webhook is critical for keeping Odys's database synchronized with Stripe's subscription state.
- Signature Verification: The webhook first verifies the
stripe-signatureheader to ensure the request genuinely originated from Stripe, preventing spoofing. checkout.session.completed: When a new subscription payment is successfully completed, this event is triggered. The system extractsprofessionalIdandplanfrom the session'smetadata. It then updates theprofessionalstable, setting theplan,stripeCustomerId, andstripeSubscriptionId. Analytics events likesubscription_startedare also captured.customer.subscription.updated: If a professional changes their subscription (e.g., upgrades or downgrades) via the Stripe customer portal, this event updates theplanin theprofessionalstable to reflect the new subscription tier based on thepriceIdof the active subscription item.customer.subscription.deleted: When a subscription is canceled, this event sets the professional'splanback to "free" and clears theirstripeSubscriptionIdin theprofessionalstable.
This webhook-driven approach ensures that the professional's plan status within Odys is always consistent with their active Stripe subscription, enabling or disabling features accordingly.
Design Decisions
The payment architecture reflects a clear separation of concerns and leverages specialized third-party services where appropriate.
The decision to use PIX for client-to-professional payments, rather than an integrated payment gateway, stems from PIX's prevalence and ease of use in Brazil. By providing the buildPixPayload utility, Odys empowers professionals to accept payments directly without incurring platform fees on these transactions. This design choice prioritizes direct financial relationships between professionals and their clients, keeping Odys out of the money flow for services. The trade-off is that Odys has less direct control over the payment process and relies on professionals to manually confirm paymentStatus for appointments.
For professional subscriptions, Stripe was chosen for its comprehensive platform for recurring billing. This offloads the complexity of payment processing, subscription management, and tax compliance to a dedicated service. The use of webhooks (/api/stripe/webhook) is a fundamental design choice, ensuring that Odys's internal professionals table remains the single source of truth for subscription status, synchronized with Stripe. This asynchronous, event-driven approach is more resilient than polling and handles various subscription lifecycle events (creation, updates, cancellations) automatically. The PLANS object in src/lib/stripe/plans.ts centralizes all plan-related data, making it easy to manage and display subscription options consistently across the application.
The integration of trial periods, specifically for the "pro" plan, demonstrates a strategic decision to encourage adoption of higher-tier services by reducing initial commitment. The trialDaysLeft function and the conditional trial_end parameter in the Stripe Checkout Session creation (src/app/api/stripe/checkout/route.ts) are key to implementing this.
Potential Improvements
- Automate PIX Payment Confirmation: Currently, the PIX flow is off-platform, meaning
appointments.paymentStatuslikely requires manual updates by the professional. Implementing a mechanism to automatically confirm PIX payments would significantly enhance the user experience and reduce administrative burden. This could involve integrating with a PIX API that provides webhooks for payment notifications or developing a reconciliation process that periodically checks bank statements for incoming PIX transactions linked to appointments. - Enforce Plan Limits: The
PLANSobject insrc/lib/stripe/plans.tsdefineslimitssuch asclientsandappointmentsPerMonth. However, the provided code does not show where these limits are actively enforced within the application logic. For example, when a professional on the "Free" plan attempts to add an 11th client or book a 21st appointment, the system should prevent the action and prompt an upgrade. Implementing this enforcement logic, perhaps in the/api/bookingor/api/client-profileroutes, is crucial for the integrity of the subscription model. - Refine Stripe Webhook Error Handling: In
src/app/api/stripe/webhook/route.ts, thelogger.error("stripe_webhook_invalid_sig", { err })call logs the raw error object. While useful for debugging, it might expose sensitive details in production logs. Consider refining the error logging to extract only relevant, non-sensitive information from theerrobject, or ensure that the logging infrastructure is configured to handle sensitive data appropriately. - PIX Key Validation: The
buildPixPayloadfunction insrc/lib/pix.tsaccepts akeyas a string without explicit validation of its format (e.g., CPF, CNPJ, email, phone). While the PIX system itself would reject an invalid key, adding client-side or server-side validation when the professional configures theirpixKeyin theprofessionalstable would improve data quality and user experience.
References
src/lib/pix.tssrc/lib/stripe/plans.tssrc/app/api/stripe/checkout/route.tssrc/app/api/stripe/webhook/route.ts
Booking Flow
The customer journey from discovering a professional to confirming an appointment, including slot selection, data submission, and conflict resolution.
Frontend Overview
An in-depth look at the Odys frontend architecture, built with Next.js 16 App Router, React Server Components, Tailwind CSS, and shadcn/ui.