This guide explains how all components work together to create a complete subscription management system.
Architecture Overview
The application follows a modern serverless architecture with clear separation of concerns:
Core Components
Frontend Layer (Next.js 15)
The application uses Next.js 15 with the App Router for optimal performance and developer experience.
App Router File-based routing with layouts
/app/page.tsx - Landing page with auth redirect
/app/login/page.tsx - Google OAuth sign-in
/app/dashboard/page.tsx - Subscription management
/app/layout.tsx - Root layout with theme provider
Server Components Default server-side rendering export default async function DashboardPage () {
const userRes = await getUser ();
const productRes = await getProducts ();
const subscriptionRes = await getUserSubscription ();
return < Dashboard { ... data } />;
}
Component Structure
components/
├── auth/
│ └── google-signin.tsx # OAuth sign-in button
├── dashboard/
│ ├── dashboard.tsx # Main dashboard container
│ ├── subscription-management.tsx # Plan details and actions
│ ├── invoice-history.tsx # Payment records
│ ├── cancel-subscription-dialog.tsx
│ ├── update-plan-dialog.tsx
│ └── restore-subscription-dialog.tsx
├── layout/
│ └── header.tsx # Navigation header
├── ui/
│ └── ... # shadcn/ui components
└── theme-provider.tsx # Dark mode support
Authentication Layer (Supabase Auth)
Two-client architecture for secure authentication:
lib/supabase/client.ts
lib/supabase/server.ts
lib/supabase/admin.ts
// Browser client for client-side operations
import { createBrowserClient } from "@supabase/ssr" ;
export const createClient = () =>
createBrowserClient (
process . env . NEXT_PUBLIC_SUPABASE_URL ! ,
process . env . NEXT_PUBLIC_SUPABASE_ANON_KEY !
);
The service role key has admin privileges and should never be exposed to the client. Only use it in server-side code.
Database Layer (PostgreSQL + Drizzle)
PostgreSQL database hosted on Supabase with type-safe ORM access:
Schema Relationships
// User → Subscription relationship (one-to-many)
export const usersRelations = relations ( users , ({ one , many }) => ({
currentSubscription: one ( subscriptions , {
fields: [ users . currentSubscriptionId ],
references: [ subscriptions . subscriptionId ],
}),
subscriptions: many ( subscriptions ),
}));
// Subscription → User relationship (many-to-one)
export const subscriptionsRelations = relations ( subscriptions , ({ one }) => ({
user: one ( users , {
fields: [ subscriptions . userId ],
references: [ users . supabaseUserId ],
}),
}));
Data Flow
User signs up → Supabase Auth creates user → Server action creates user record
User subscribes → Dodo Payments creates subscription → Webhook updates database
Payment succeeds → Webhook creates payment record → User tier updated
User cancels → API call to Dodo → Webhook marks subscription as cancelled
Payment Layer (Dodo Payments)
Integration with Dodo Payments for subscription billing:
// lib/dodo-payments/client.ts
import DodoPayments from "dodopayments" ;
export const dodoClient = new DodoPayments ({
bearerToken: process . env . DODO_PAYMENTS_API_KEY ! ,
environment: process . env . DODO_PAYMENTS_ENVIRONMENT !
});
Key Operations
Create Subscription
Change Plan
Cancel Subscription
Fetch Products
// actions/change-plan.ts
const customer = await dodoClient . customers . create ({
email: user . email ! ,
name: user . user_metadata . full_name ,
});
await dodoClient . miscellaneous . createSubscription ({
customer_id: customer . customer_id ,
product_id: productId ,
});
// actions/change-plan.ts
await dodoClient . miscellaneous . changeSubscriptionPlan ({
subscription_id: currentSubscription . subscriptionId ,
product_id: newProductId ,
});
// actions/cancel-subscription.ts
await dodoClient . subscriptions . update ( subscriptionId , {
cancel_at_next_billing_date: true ,
});
// actions/get-products.ts
const products = await dodoClient . products . list ();
return { success: true , data: products . items };
Webhook Handler (Supabase Edge Function)
Deno-based serverless function deployed to Supabase:
// supabase/functions/dodo-webhook/index.ts
Deno . serve ( async ( req ) => {
// Verify webhook signature
const webhook = new Webhook ( dodoWebhookSecret );
await webhook . verify ( rawBody , webhookHeaders );
// Route events to handlers
switch ( event . type ) {
case "payment.succeeded" :
await managePayment ( event );
break ;
case "subscription.active" :
await manageSubscription ( event );
await updateUserTier ( event . data );
break ;
case "subscription.cancelled" :
await manageSubscription ( event );
await downgradeToHobbyPlan ( event . data );
break ;
}
});
Webhook Functions
Upserts payment records to the payments table: async function managePayment ( event : any ) {
const data = {
payment_id: event . data . payment_id ,
status: event . data . status ,
total_amount: event . data . total_amount ,
currency: event . data . currency ,
customer_email: event . data . customer . email ,
webhook_data: event ,
// ... more fields
};
await supabase . from ( "payments" ). upsert ( data , {
onConflict: "payment_id" ,
});
}
Upserts subscription records to the subscriptions table: async function manageSubscription ( event : any ) {
const data = {
subscription_id: event . data . subscription_id ,
status: event . data . status ,
product_id: event . data . product_id ,
next_billing_date: event . data . next_billing_date ,
// ... more fields
};
await supabase . from ( "subscriptions" ). upsert ( data , {
onConflict: "subscription_id" ,
});
}
Links active subscription to user: async function updateUserTier ( props ) {
await supabase
. from ( "users" )
. update ({ current_subscription_id: props . subscriptionId })
. eq ( "dodo_customer_id" , props . dodoCustomerId );
}
Removes subscription reference from user: async function downgradeToHobbyPlan ( props ) {
await supabase
. from ( "users" )
. update ({ current_subscription_id: null })
. eq ( "dodo_customer_id" , props . dodoCustomerId );
}
Data Flow Diagrams
User Registration Flow
Subscription Creation Flow
Payment Processing Flow
Server Actions
All data mutations go through Next.js server actions for security:
// actions/ directory structure
actions /
├── get - user . ts # Fetch authenticated user
├── get - products . ts # List available plans
├── get - user - subscription . ts # Get current subscription
├── get - invoices . ts # Fetch payment history
├── create - user . ts # Initialize user record
├── create - dodo - customer . ts # Register with Dodo
├── change - plan . ts # Upgrade / downgrade
├── cancel - subscription . ts # Cancel at next billing
├── restore - subscription . ts # Undo cancellation
└── delete - account . ts # Remove user data
Type-Safe Action Pattern
"use server" ;
import { ServerActionRes } from "@/types/server-action" ;
export async function getUser () : ServerActionRes < User > {
try {
const supabase = await createClient ();
const { data : { user } } = await supabase . auth . getUser ();
if ( ! user ) {
return { success: false , error: "User not found" };
}
return { success: true , data: user };
} catch ( error ) {
return { success: false , error: "Failed to get user" };
}
}
All server actions return a standardized response type with success boolean and either data or error fields.
Security Considerations
Environment Variables
Public Variables Safe to expose to client (prefixed with NEXT_PUBLIC_):
NEXT_PUBLIC_SUPABASE_URL
NEXT_PUBLIC_SUPABASE_ANON_KEY
Secret Variables Server-side only (never expose to client):
SUPABASE_SERVICE_ROLE_KEY
DODO_PAYMENTS_API_KEY
DODO_WEBHOOK_SECRET
DATABASE_URL
Webhook Security
// Verify webhook signature before processing
const webhook = new Webhook ( dodoWebhookSecret );
try {
await webhook . verify ( rawBody , webhookHeaders );
} catch ( error ) {
return new Response ( "Invalid webhook signature" , { status: 400 });
}
Row Level Security (RLS)
This starter kit uses service role keys in server actions, which bypass RLS. For production applications, implement proper RLS policies on your Supabase tables.
Deployment Architecture
Recommended Hosting
Vercel (Frontend)
Supabase (Backend)
Dodo Payments (Billing)
Deploy Next.js application to Vercel:
Connect GitHub repository
Configure environment variables
Deploy with automatic CI/CD
Benefits:
Zero-config deployment
Edge network distribution
Automatic HTTPS
Preview deployments
Backend services hosted on Supabase:
PostgreSQL database
Authentication service
Edge Functions for webhooks
File storage (if needed)
Benefits:
Managed PostgreSQL
Built-in auth
Serverless functions
Real-time subscriptions
Payment processing through Dodo:
Subscription management
Payment processing
Invoice generation
Webhook events
Benefits:
Compliance handling
Multi-currency support
Tax calculations
Global payment methods
Production Checklist
Environment Setup
Switch from test_mode to live_mode in Dodo Payments configuration
Database Migration
Run bun run db:push to apply schema to production database
Webhook Deployment
Deploy webhook function with production credentials: bun run deploy:webhook --project-ref YOUR_PROD_PROJECT
Configure Webhooks
Update webhook URL in Dodo Payments dashboard to production endpoint
Test Payment Flow
Make a test subscription to verify end-to-end flow
Next.js Features
Turbopack : Fast development builds
React Server Components : Reduced client JavaScript
Server Actions : Eliminates API route overhead
Automatic Code Splitting : Optimized bundle sizes
Database Optimizations
// Indexed columns for fast lookups
supabaseUserId : text ( "supabase_user_id" ). primaryKey ()
dodoCustomerId : text ( "dodo_customer_id" ). notNull ()
subscriptionId : text ( "subscription_id" ). primaryKey ()
Caching Strategy
Consider implementing React Server Component caching for product lists and subscription data to reduce database queries.
Extending the Architecture
Adding New Features
Email Notifications Add transactional emails using Resend or SendGrid triggered by webhook events
Usage Tracking Implement metered billing by tracking usage and syncing with Dodo Payments
Team Management Extend schema to support team subscriptions with multiple users
Admin Dashboard Build admin panel using Supabase service role to manage users and subscriptions
Integration Points
The architecture is designed for extensibility:
Webhook handler : Add new event types in the switch statement
Server actions : Create new actions for additional business logic
Database schema : Extend with migrations using Drizzle Kit
UI components : Add new pages using the established patterns