### Install Project Dependencies Source: https://context7.com/dodopayments/dodo-supabase-subscription-starter/llms.txt Installs all necessary project dependencies using Bun. This is the first step before running any other development or build commands. ```bash # Install dependencies bun install ``` -------------------------------- ### Run Development Server Source: https://github.com/dodopayments/dodo-supabase-subscription-starter/blob/main/README.md Start the local development server using Bun, npm, or pnpm. Access the application at http://localhost:3000. ```bash # Start the development server bun run dev # or npm run dev # or pnpm run dev ``` -------------------------------- ### Install Project Dependencies Source: https://github.com/dodopayments/dodo-supabase-subscription-starter/blob/main/README.md Install project dependencies using your preferred package manager (bun, npm, or pnpm). Ensure Node.js v18 or higher is installed. ```bash bun install # or npm install # or pnpm install ``` -------------------------------- ### Start Development Server Source: https://context7.com/dodopayments/dodo-supabase-subscription-starter/llms.txt Starts the Next.js development server with Turbopack. The server will automatically redirect to /login or /dashboard based on authentication status. Accessible at http://localhost:3000. ```bash # Start development server (Next.js with Turbopack) bun run dev # → http://localhost:3000 (auto-redirects to /login or /dashboard) ``` -------------------------------- ### Checkout API Routes Source: https://context7.com/dodopayments/dodo-supabase-subscription-starter/llms.txt Handles both static and dynamic checkout sessions. GET requests are for pre-configured links, while POST requests create dynamic checkout sessions. ```APIDOC ## GET /checkout and POST /checkout ### Description Proxy routes for Dodo Payments checkout. GET handles static checkout links; POST creates a dynamic session checkout. ### Method GET, POST ### Endpoint /checkout ### Query Parameters (for GET) - **product_id** (string) - Required - The ID of the product for static checkout. - **customer_email** (string) - Required - The email of the customer. ### Request Body (for POST) - **product_cart** (array) - Required - An array of products in the cart. - **product_id** (string) - Required - The ID of the product. - **quantity** (number) - Required - The quantity of the product. - **customer** (object) - Required - Customer details. - **email** (string) - Required - The customer's email address. - **name** (string) - Optional - The customer's name. ### Response (for POST) #### Success Response (200) - **checkout_url** (string) - The URL for the dynamic checkout session. ### Request Example (POST) ```json { "product_cart": [ { "product_id": "prod_abc123", "quantity": 1 } ], "customer": { "email": "jane@example.com", "name": "Jane Doe" } } ``` ### Response Example (POST) ```json { "checkout_url": "https://checkout.dodopayments.com/session/sess_xyz" } ``` ``` -------------------------------- ### API Route for Dodo Payments Checkout Source: https://context7.com/dodopayments/dodo-supabase-subscription-starter/llms.txt Handles both static and dynamic checkout links. GET requests are for pre-configured links, while POST requests create dynamic sessions based on a product cart. ```typescript // app/checkout/route.ts — wraps @dodopayments/nextjs Checkout helper // Static checkout (GET) — for pre-configured payment links: // GET /checkout?product_id=prod_abc123&customer_email=jane@example.com // → Redirects user to Dodo-hosted static checkout page // → On completion, redirects back to /dashboard // Session checkout (POST) — for dynamic cart-based checkout: // POST /checkout // Content-Type: application/json // Body: // { // "product_cart": [{ "product_id": "prod_abc123", "quantity": 1 }], // "customer": { "email": "jane@example.com", "name": "Jane Doe" } // } // Response: // { "checkout_url": "https://checkout.dodopayments.com/session/sess_xyz" } // The client then does: window.location.href = checkout_url ``` -------------------------------- ### GET /api/auth/callback Source: https://context7.com/dodopayments/dodo-supabase-subscription-starter/llms.txt Handles the OAuth callback from Supabase. It exchanges the authorization code for a Supabase session, creates a user record if necessary, and redirects the user to the dashboard. ```APIDOC ## API Route: GET /api/auth/callback OAuth callback handler. Exchanges the authorization code for a Supabase session, bootstraps the user record, then redirects to the dashboard. ### Method GET ### Endpoint `/api/auth/callback` ### Parameters #### Query Parameters - **code** (string) - Required - The authorization code received from the OAuth provider. ### Request Example `GET https://your-app.com/api/auth/callback?code=abc123xyz` ### Response #### Success Response Redirects to `/dashboard` upon successful authentication and user bootstrapping. #### Error Response Redirects to `/login?error=Could not authenticate` if authentication fails. ### Notes - This route is triggered automatically by Supabase after the OAuth flow completes. - The Redirect URI to configure in Google Cloud Console and Supabase is: `https://[your-project-ref].supabase.co/auth/v1/callback`. - The process involves `supabase.auth.exchangeCodeForSession(code)`, `createUser()` (if the user does not exist), and finally a redirect. ``` -------------------------------- ### getUser Action - Retrieve Authenticated User Source: https://context7.com/dodopayments/dodo-supabase-subscription-starter/llms.txt Fetches the currently logged-in Supabase Auth user from the server-side session. This action should be called within Server Components or Server Actions to get user details. ```typescript // actions/get-user.ts import { getUser } from "@/actions/get-user"; // In a Server Component or Server Action: const userRes = await getUser(); if (!userRes.success) { // userRes.error === "User not found" | "Failed to get user" redirect("/login"); } const user = userRes.data; // user.id → Supabase Auth UUID // user.email → "jane@example.com" // user.user_metadata.name → "Jane Doe" ``` -------------------------------- ### Configure Environment Variables Source: https://github.com/dodopayments/dodo-supabase-subscription-starter/blob/main/README.md Set up your environment variables in a .env.local file. This includes Supabase and Dodo Payments API keys and URLs. ```env NEXT_PUBLIC_SUPABASE_URL=https://your-project-ref.supabase.co NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key SUPABASE_SERVICE_ROLE_KEY=your-key DATABASE_URL=postgresql://postgres:[password]@db.[project-ref].supabase.co:5432/postgres DODO_PAYMENTS_API_KEY=your-dodo-api-key DODO_WEBHOOK_SECRET=your-webhook-secret DODO_PAYMENTS_ENVIRONMENT=test_mode ``` -------------------------------- ### Build and Type Check Project Source: https://context7.com/dodopayments/dodo-supabase-subscription-starter/llms.txt Commands for building the project for production and performing type checking. Essential for ensuring code quality and preparing for deployment. ```bash # Build for production bun run build # Type check bun run typecheck ``` -------------------------------- ### changePlan Source: https://context7.com/dodopayments/dodo-supabase-subscription-starter/llms.txt Immediately switches a subscription to a different product, applying prorated billing. If the user is new, this action initiates a new checkout session. ```APIDOC ## changePlan — Upgrade or Downgrade Subscription Immediately switches a subscription to a different product with prorated billing. If the user has no existing subscription, the dashboard initiates a new checkout session instead. ### Method Not specified (client-side function call for plan switch, POST for checkout initiation) ### Parameters #### Path Parameters None #### Query Parameters None #### Request Body (for existing subscribers) - **subscriptionId** (string) - Required - The ID of the current subscription. - **productId** (string) - Required - The product ID of the target plan from the Dodo dashboard. ### Request Example (for existing subscribers) ```typescript await changePlan({ subscriptionId: "sub_abc123", productId: "prod_new456", }); ``` ### Request Example (for new users - checkout initiation) ```javascript fetch("/checkout", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ product_cart: [{ product_id: "prod_new456", quantity: 1 }], customer: { email: "jane@example.com", name: "Jane Doe" }, return_url: "https://your-app.com/dashboard", }), }); ``` ### Response (for existing subscribers) #### Success Response - **success** (boolean) - Indicates if the operation was successful. #### Error Response (for existing subscribers) - **success** (boolean) - Indicates if the operation was successful. - **error** (string) - Description of the error if `success` is false. ### Response (for new users - checkout initiation) #### Success Response - **checkout_url** (string) - The URL to redirect the user to for checkout. ### Notes - For existing subscribers, Dodo bills/credits the prorated difference immediately. The `subscription.plan_changed` webhook will sync the updated subscription to the DB. - For new users, the dashboard component redirects to a Dodo-hosted checkout session. ``` -------------------------------- ### Expose Local Server for Webhook Testing Source: https://context7.com/dodopayments/dodo-supabase-subscription-starter/llms.txt Exposes the local development server to the internet using ngrok, which is necessary for testing webhooks. The ngrok URL must then be registered in the Dodo dashboard. ```bash # Expose local server for webhook testing (requires ngrok account) bun run ngrok # Then register https://YOUR_NGROK_URL/functions/v1/dodo-webhook in Dodo dashboard ``` -------------------------------- ### Deploy Supabase Webhook Handler Source: https://github.com/dodopayments/dodo-supabase-subscription-starter/blob/main/README.md Deploy the webhook handler function to Supabase. Replace `[your-project-ref]` with your actual Supabase project reference ID. ```bash bunx supabase login # or npx supabase login bun run deploy:webhook --project-ref [your-project-ref] # or npm run deploy:webhook -- --project-ref [your-project-ref] # or pnpm run deploy:webhook --project-ref [your-project-ref] ``` -------------------------------- ### Environment Configuration (.env.local) Source: https://context7.com/dodopayments/dodo-supabase-subscription-starter/llms.txt Environment variables required for Supabase, PostgreSQL, and Dodo Payments integration. Ensure these are set in your .env.local file before running the application. ```env # .env.local # Supabase NEXT_PUBLIC_SUPABASE_URL=https://your-project-ref.supabase.co NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... # PostgreSQL (from Supabase → Settings → Database → Connection String) DATABASE_URL=postgresql://postgres:password@db.your-project-ref.supabase.co:5432/postgres # Dodo Payments DODO_PAYMENTS_API_KEY=sk_test_... DODO_WEBHOOK_SECRET=whsec_... DODO_PAYMENTS_ENVIRONMENT=test_mode # or "live_mode" ``` -------------------------------- ### Initiate Checkout from Dashboard Source: https://context7.com/dodopayments/dodo-supabase-subscription-starter/llms.txt Initiates a new subscription checkout process by sending product and customer details to a checkout endpoint. Redirects the user to the generated checkout URL. Requires the user's email and name, and a return URL. ```typescript // Checkout initiation from Dashboard (new subscription): const response = await fetch(`${window.location.origin}/checkout`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ product_cart: [{ product_id: selectedProductId, quantity: 1 }], customer: { email: user.email, name: user.user_metadata.name }, return_url: `${window.location.origin}/dashboard`, }), }); const { checkout_url } = await response.json(); window.location.href = checkout_url; ``` -------------------------------- ### Deploy Supabase Webhook Function Source: https://context7.com/dodopayments/dodo-supabase-subscription-starter/llms.txt Deploys the Supabase webhook Edge Function. Requires logging into Supabase via `bunx supabase login` and specifying the project reference. ```bash # Deploy Supabase webhook Edge Function bunx supabase login bun run deploy:webhook -- --project-ref YOUR_PROJECT_REF ``` -------------------------------- ### Free/Hobby Plan Configuration Source: https://context7.com/dodopayments/dodo-supabase-subscription-starter/llms.txt Defines the default free plan details when a user has no active paid subscription. Paid plan features are dynamically loaded from Dodo Payments product metadata. ```typescript // lib/config/plans.ts export const freePlan = { name: "Hobby Plan", description: "Free plan for Dodo Supabase subscription starter", price: 0, currency: "USD", features: [ "Access to basic tools", "Single user support", "5 projects limit", "Community support", "Basic analytics", ], }; // In SubscriptionManagement component: // If user has no paid subscription → shows freePlan.name, freePlan.features // If user has paid subscription → looks up ProductListResponse by productId: const currentPlanDetails = products.find(p => p.product_id === currentPlan?.productId); const features = currentPlanDetails ? JSON.parse(currentPlanDetails.metadata.features || "[]") : freePlan.features; // To add a paid plan, create a product in the Dodo Payments dashboard with: // Metadata → { "features": ["Feature A", "Feature B", "Feature C"] } ``` -------------------------------- ### Dodo Payments Client Initialization Source: https://context7.com/dodopayments/dodo-supabase-subscription-starter/llms.txt Initializes a singleton Dodo Payments client using environment variables for API key and environment mode. This client is used for all server-side interactions with the Dodo Payments API. ```typescript // lib/dodo-payments/client.ts import DodoPayments from "dodopayments"; export type DodoPaymentsEnvironment = "live_mode" | "test_mode"; export const dodoClient = new DodoPayments({ bearerToken: process.env.DODO_PAYMENTS_API_KEY!, environment: process.env.DODO_PAYMENTS_ENVIRONMENT! as DodoPaymentsEnvironment, }); // Example: list all products const products = await dodoClient.products.list(); // products.items → ProductListResponse[] // Example: create a customer const customer = await dodoClient.customers.create({ email: "user@example.com", name: "Jane" }); // customer.customer_id → string // Example: update a subscription await dodoClient.subscriptions.update("sub_abc123", { cancel_at_next_billing_date: true }); // Example: change subscription plan (prorated immediately) await dodoClient.subscriptions.changePlan("sub_abc123", { product_id: "prod_new123", proration_billing_mode: "prorated_immediately", quantity: 1, }); ``` -------------------------------- ### Push Database Schema to Supabase Source: https://github.com/dodopayments/dodo-supabase-subscription-starter/blob/main/README.md Apply the database schema to your Supabase project. This command creates the necessary tables for users, subscriptions, and payments. ```bash bun run db:push # or npm run db:push # or pnpm run db:push ``` -------------------------------- ### `getUserSubscription` — Fetch User's Current Subscription Source: https://context7.com/dodopayments/dodo-supabase-subscription-starter/llms.txt Retrieves the user's database record along with their active subscription details. Returns null if the user is on the free or hobby plan. ```APIDOC ## `getUserSubscription` — Fetch User's Current Subscription ### Description Returns both the user's database record and their active subscription (or `null` if on the free/hobby plan). ### Method Signature `getUserSubscription()` ### Returns - `success` (boolean): Indicates if the operation was successful. - `data` ({ subscription: SelectSubscription | null, user: SelectUser }): An object containing the user record and their subscription details. `subscription` will be `null` if on the free tier. - `error` (string): An error message if the operation failed (e.g., "User not found"). ``` -------------------------------- ### `getProducts` — List Dodo Payments Products Source: https://context7.com/dodopayments/dodo-supabase-subscription-starter/llms.txt Fetches the entire product catalog from Dodo Payments. Product metadata includes a `features` array for display in the billing UI. ```APIDOC ## `getProducts` — List Dodo Payments Products ### Description Fetches the full product catalog from Dodo Payments. Product metadata contains the `features` array rendered in the billing UI. ### Method Signature `getProducts()` ### Returns - `success` (boolean): Indicates if the operation was successful. - `data` (ProductListResponse[]): An array of product objects if successful. - `error` (string): An error message if the operation failed (e.g., "Failed to fetch products"). ``` -------------------------------- ### Fetch User's Current Subscription Source: https://context7.com/dodopayments/dodo-supabase-subscription-starter/llms.txt Retrieves the user's database record along with their active subscription details. Returns `null` for the subscription if the user is on the free or hobby plan. Handles cases where user or subscription details might be missing. ```typescript // actions/get-user-subscription.ts import { getUserSubscription } from "@/actions/get-user-subscription"; import { SelectSubscription, SelectUser } from "@/lib/drizzle/schema"; const res = await getUserSubscription(); if (!res.success) { console.error(res.error); // "User not found" | "User details not found" return; } const { subscription, user } = res.data; // user: SelectUser → {supabaseUserId, dodoCustomerId, currentSubscriptionId, ... } // subscription: SelectSubscription | null if (subscription === null) { // User is on free/hobby plan (currentSubscriptionId is empty or null) console.log("Free tier"); } else { console.log(subscription.status); // "active" console.log(subscription.productId); // "prod_abc123" console.log(subscription.nextBillingDate); // "2025-08-01T00:00:00+00:00" console.log(subscription.cancelAtNextBillingDate); // false console.log(subscription.recurringPreTaxAmount); // 1999 (cents) } ``` -------------------------------- ### List Dodo Payments Products Source: https://context7.com/dodopayments/dodo-supabase-subscription-starter/llms.txt Fetches the complete product catalog from Dodo Payments. Product metadata includes a `features` array, which is stored as a JSON string and can be parsed for display in the billing UI. ```typescript // actions/get-products.ts import { getProducts } from "@/actions/get-products"; import type { ProductListResponse } from "dodopayments/resources/index.mjs"; const res = await getProducts(); if (!res.success) { console.error(res.error); // "Failed to fetch products" return; } const products: ProductListResponse[] = res.data; products.forEach((product) => { console.log(product.product_id); // "prod_abc123" console.log(product.name); // "Pro Plan" console.log(product.price); // 1999 (in cents) console.log(product.description); // "Full access to all features" // Features are stored as a JSON string in product metadata: const features: string[] = JSON.parse(product.metadata.features || "[]"); // ["Unlimited projects", "Priority support", "Advanced analytics"] }); ``` -------------------------------- ### Database Management Scripts Source: https://context7.com/dodopayments/dodo-supabase-subscription-starter/llms.txt Provides commands for managing the Supabase PostgreSQL database schema. Includes pushing schema changes, generating migration files, and running pending migrations. ```bash # Push database schema to Supabase PostgreSQL bun run db:push # Generate Drizzle migration files bun run db:generate # Run pending migrations bun run db:migrate # Open Drizzle Studio (visual DB browser) bun run db:studio ``` -------------------------------- ### `createUser` — Bootstrap New User Record Source: https://context7.com/dodopayments/dodo-supabase-subscription-starter/llms.txt Automatically called after OAuth login to create a Dodo Payments customer and a user record in the `users` table if one doesn't exist. ```APIDOC ## `createUser` — Bootstrap New User Record ### Description Called automatically after OAuth login. Creates a Dodo Payments customer and inserts a row into the `users` table if one does not already exist. ### Method Signature `createUser()` ### Returns - `success` (boolean): Indicates if the operation was successful. - `true`: User created or already exists. - `false`: An error occurred. - `data` (string | null): "User created" or "User already exists" if successful. - `error` (string | null): "User not found" or "Failed to create customer" if an error occurred. ``` -------------------------------- ### Change Subscription Plan Source: https://context7.com/dodopayments/dodo-supabase-subscription-starter/llms.txt Immediately switches a subscription to a different product with prorated billing. If the user has no existing subscription, this initiates a new checkout session. Use for upgrading or downgrading plans. ```typescript // actions/change-plan.ts import { changePlan } from "@/actions/change-plan"; // For existing subscribers — plan switch: const res = await changePlan({ subscriptionId: "sub_abc123", productId: "prod_new456", // Target plan product ID from Dodo dashboard }); if (!res.success) { console.error(res.error); return; } // Dodo bills/credits the prorated difference immediately. // The webhook (subscription.plan_changed) will sync the updated subscription to the DB. // For new users — the Dashboard component falls through to a checkout session: const response = await fetch("/checkout", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ product_cart: [{ product_id: "prod_new456", quantity: 1 }], customer: { email: "jane@example.com", name: "Jane Doe" }, return_url: "https://your-app.com/dashboard", }), }); const { checkout_url } = await response.json(); window.location.href = checkout_url; // Redirects to Dodo-hosted checkout ``` -------------------------------- ### Create User Record with Dodo Customer Source: https://context7.com/dodopayments/dodo-supabase-subscription-starter/llms.txt Automatically called after OAuth login to create a Dodo Payments customer and a user record in the `users` table if they don't exist. It handles user lookup, Dodo customer creation, and persisting user data with a Dodo customer ID. ```typescript // actions/create-user.ts import { createUser } from "@/actions/create-user"; // Automatically invoked in app/api/auth/callback/route.ts after exchangeCodeForSession: const res = await createUser(); // res.success === true, res.data === "User created" | "User already exists" // res.success === false, res.error === "User not found" | "Failed to create customer" // Internal flow: // 1. getUser() → get Supabase Auth user // 2. db.query.users.findFirst(...) → check if already registered // 3. createDodoCustomer({ email, name }) → create Dodo customer // 4. db.insert(users).values({ ... }) → persist user with dodoCustomerId ``` -------------------------------- ### `getInvoices` — Fetch Payment History Source: https://context7.com/dodopayments/dodo-supabase-subscription-starter/llms.txt Retrieves all payment records for the authenticated user's Dodo customer ID from the local `payments` table. ```APIDOC ## `getInvoices` — Fetch Payment History ### Description Returns all payment records for the authenticated user's Dodo customer ID from the local `payments` table. ### Method Signature `getInvoices()` ### Returns - `success` (boolean): Indicates if the operation was successful. - `data` (SelectPayment[]): An array of payment records if successful. - `error` (string): An error message if the operation failed (e.g., "Subscription not found"). ``` -------------------------------- ### Fetch Payment History Source: https://context7.com/dodopayments/dodo-supabase-subscription-starter/llms.txt Retrieves all payment records for the authenticated user's Dodo customer ID from the local `payments` table. This function provides details for each invoice, including payment ID, status, amount, currency, card information, and creation date. ```typescript // actions/get-invoices.ts import { getInvoices } from "@/actions/get-invoices"; import { SelectPayment } from "@/lib/drizzle/schema"; const res = await getInvoices(); if (!res.success) { console.error(res.error); // "Subscription not found" | "Failed to get invoices" return; } const invoices: SelectPayment[] = res.data; invoices.forEach((invoice) => { console.log(invoice.paymentId); // "pay_abc123" console.log(invoice.status); // "succeeded" | "failed" | "processing" console.log(invoice.totalAmount); // 1999 console.log(invoice.currency); // "USD" console.log(invoice.cardLastFour); // "4242" console.log(invoice.cardNetwork); // "visa" console.log(invoice.createdAt); // "2025-07-01T10:00:00+00:00" }); ``` -------------------------------- ### API Route: OAuth Callback Handler Source: https://context7.com/dodopayments/dodo-supabase-subscription-starter/llms.txt Handles the OAuth callback from Supabase, exchanging the authorization code for a session, bootstrapping the user record, and redirecting to the dashboard. Handles both successful authentication and errors. ```typescript // app/api/auth/callback/route.ts // Triggered automatically by Supabase after Google OAuth flow completes. // Redirect URI to configure in Google Cloud Console and Supabase: // https://[your-project-ref].supabase.co/auth/v1/callback // Supabase then redirects to your app: // https://your-app.com/api/auth/callback?code=AUTHORIZATION_CODE // GET https://your-app.com/api/auth/callback?code=abc123xyz // →supabase.auth.exchangeCodeForSession("abc123xyz") // →createUser() (creates Dodo customer + DB user record if not exists) // →redirect("/dashboard") // On error: // →redirect("/login?error=Could not authenticate") ``` -------------------------------- ### Supabase Client Factories Source: https://context7.com/dodopayments/dodo-supabase-subscription-starter/llms.txt Provides factory functions to create Supabase clients tailored for different environments: Server Components/Actions, Client Components (browser), and server-side privileged operations. ```typescript // lib/supabase/server.ts — for Server Components and Server Actions (cookie-based session) import { createServerClient } from "@supabase/ssr"; import { cookies } from "next/headers"; export async function createClient() { const cookieStore = await cookies(); return createServerClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, { cookies: { getAll() { return cookieStore.getAll(); }, setAll(cookiesToSet) { try { cookiesToSet.forEach(({ name, value, options }) => cookieStore.set(name, value, options) ); } catch { /* Safe to ignore in Server Components */ } }, }, } ); } ``` ```typescript // lib/supabase/client.ts — for Client Components (browser) import { createBrowserClient } from "@supabase/ssr"; export const createClient = () => createBrowserClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY! ); ``` ```typescript // lib/supabase/admin.ts — for privileged server-side operations (service role key) import { createClient } from "@supabase/supabase-js"; const supabase = createClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.SUPABASE_SERVICE_ROLE_KEY!, { auth: { autoRefreshToken: false, persistSession: false } } ); export const adminAuthClient = supabase.auth.admin; // Usage: await adminAuthClient.deleteUser(userId) ``` -------------------------------- ### Configure Dodo Payments Webhook Source: https://github.com/dodopayments/dodo-supabase-subscription-starter/blob/main/README.md Add this URL to your Dodo Payments dashboard to receive payment and subscription events. Ensure all relevant events are selected. ```text https://[your-project-ref].supabase.co/functions/v1/dodo-webhook ``` -------------------------------- ### Create Dodo Payments Customer Source: https://context7.com/dodopayments/dodo-supabase-subscription-starter/llms.txt Creates a customer object in Dodo Payments. If a name is not provided, it defaults to using the prefix of the email address before the '@' symbol as the customer name. ```typescript // actions/create-dodo-customer.ts import { createDodoCustomer } from "@/actions/create-dodo-customer"; import type { Customer } from "dodopayments/resources/index.mjs"; const res = await createDodoCustomer({ email: "jane@example.com", name: "Jane Doe" }); if (res.success) { const customer: Customer = res.data; console.log(customer.customer_id); // "cus_abc123" console.log(customer.email); // "jane@example.com" } else { console.error(res.error); // "Failed to create customer" } // Name fallback example (no name provided): await createDodoCustomer({ email: "jane@example.com" }); // → name becomes "jane" (prefix before @) ``` -------------------------------- ### Load Dashboard Data Server-Side Source: https://context7.com/dodopayments/dodo-supabase-subscription-starter/llms.txt Fetches necessary data on the server before rendering the dashboard. Ensures all required data is available as props for the client component. Handles potential errors during data fetching by redirecting to login. ```typescript // app/dashboard/page.tsx + components/dashboard/dashboard.tsx // Server-side data loading (page.tsx): const [userRes, productRes, userSubscriptionRes, invoicesRes] = await Promise.all([ getUser(), getProducts(), getUserSubscription(), getInvoices(), ]); if (!userRes.success) redirect("/login"); // Dashboard component props: // Tab structure: // "manage-subscription" → (current plan, upgrade/cancel/restore) // "payments" → (paginated payment records) // "account" → (profile info, delete account) ``` -------------------------------- ### restoreSubscription Source: https://context7.com/dodopayments/dodo-supabase-subscription-starter/llms.txt Reverses a pending subscription cancellation, ensuring the subscription remains active through the next billing cycle. This updates both the Dodo API and the local database. ```APIDOC ## restoreSubscription — Undo Scheduled Cancellation Reverses a pending cancellation, keeping the subscription active through the next billing cycle. ### Method Not specified (client-side function call) ### Parameters #### Path Parameters None #### Query Parameters None #### Request Body - **subscriptionId** (string) - Required - The ID of the subscription to restore. ### Request Example ```typescript await restoreSubscription({ subscriptionId: "sub_abc123" }); ``` ### Response #### Success Response - **success** (boolean) - Indicates if the operation was successful. #### Error Response - **success** (boolean) - Indicates if the operation was successful. - **error** (string) - Description of the error if `success` is false. ### Notes - After success, Dodo sets `cancel_at_next_billing_date = false`. - Local DB: `subscriptions.cancelAtNextBillingDate = false`. - UI reverts to showing `CancelSubscriptionDialog` instead of `RestoreSubscriptionDialog`. ``` -------------------------------- ### Supabase Edge Function for Dodo Payments Webhooks Source: https://context7.com/dodopayments/dodo-supabase-subscription-starter/llms.txt A Deno-based Supabase Edge Function to receive and verify Dodo Payments webhook events. It upserts data into Supabase tables for subscriptions, payments, and users. Ensure DODO_WEBHOOK_SECRET is set in Supabase function secrets. ```typescript // supabase/functions/dodo-webhook/index.ts // Deploy with: bun run deploy:webhook --project-ref YOUR_PROJECT_REF // Webhook URL to register in Dodo Payments dashboard: // https://[your-project-ref].supabase.co/functions/v1/dodo-webhook // Handled event types and their side-effects: // ───────────────────────────────────────────────────────────────────── // payment.succeeded | .failed | .processing | .cancelled // → upserts payments table row (managePayment) // // subscription.active | .plan_changed // → upserts subscriptions table row (manageSubscription) // → sets users.current_subscription_id = subscriptionId (updateUserTier) // // subscription.renewed | .on_hold // → upserts subscriptions table row (manageSubscription) // // subscription.cancelled | .expired | .failed // → upserts subscriptions table row (manageSubscription) // → sets users.current_subscription_id = null (downgradeToHobbyPlan) // ───────────────────────────────────────────────────────────────────── // Example incoming payload (subscription.active): const exampleEvent = { type: "subscription.active", data: { subscription_id: "sub_abc123", product_id: "prod_abc123", status: "active", currency: "USD", recurring_pre_tax_amount: 1999, tax_inclusive: false, next_billing_date: "2025-08-01T00:00:00Z", previous_billing_date: "2025-07-01T00:00:00Z", quantity: 1, billing: { city: "San Francisco", country: "US", state: "CA", street: "123 Main St", zipcode: "94102" }, customer: { customer_id: "cus_abc123", email: "jane@example.com", name: "Jane Doe" }, cancel_at_next_billing_date: false, }, }; // Webhook signature verification (must pass before any DB writes): // Headers required: webhook-id, webhook-signature, webhook-timestamp // Secret: DODO_WEBHOOK_SECRET env var (set in Supabase function secrets) const webhook = new Webhook(process.env.DODO_WEBHOOK_SECRET); await webhook.verify(rawBody, { "webhook-id": "...", "webhook-signature": "...", "webhook-timestamp": "..." }); // Required Supabase function secrets (set via Supabase dashboard or CLI): // SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY, DODO_WEBHOOK_SECRET ``` -------------------------------- ### Add Metadata to Dodo Payments Product Source: https://github.com/dodopayments/dodo-supabase-subscription-starter/blob/main/README.md When creating a product in Dodo Payments, use the metadata section to include custom features. This JSON will be dynamically displayed in your application's pricing UI. ```json { "features": ["Feature 1", "Feature 2", "Feature 3"] } ``` -------------------------------- ### cancelSubscription Source: https://context7.com/dodopayments/dodo-supabase-subscription-starter/llms.txt Schedules a subscription for cancellation at the next billing date. This action flags the subscription in the Dodo API and updates the local database to reflect the pending cancellation. ```APIDOC ## cancelSubscription — Schedule Subscription Cancellation Flags a subscription for cancellation at the next billing date via the Dodo API and updates the local database. ### Method Not specified (client-side function call) ### Parameters #### Path Parameters None #### Query Parameters None #### Request Body - **subscriptionId** (string) - Required - The ID of the subscription to cancel. ### Request Example ```typescript await cancelSubscription({ subscriptionId: "sub_abc123" }); ``` ### Response #### Success Response - **success** (boolean) - Indicates if the operation was successful. #### Error Response - **success** (boolean) - Indicates if the operation was successful. - **error** (string) - Description of the error if `success` is false. ### Notes - After success, Dodo sets `cancel_at_next_billing_date = true` on the subscription. - Local DB: `subscriptions.cancelAtNextBillingDate = true`. - UI will show a "Scheduled for cancellation" badge and expose `RestoreSubscriptionDialog`. ``` -------------------------------- ### Schedule Subscription Cancellation Source: https://context7.com/dodopayments/dodo-supabase-subscription-starter/llms.txt Flags a subscription for cancellation at the next billing date via the Dodo API and updates the local database. Use this when a user requests to cancel their subscription but should retain access until the current billing period ends. ```typescript // actions/cancel-subscription.ts import { cancelSubscription } from "@/actions/cancel-subscription"; const res = await cancelSubscription({ subscriptionId: "sub_abc123" }); if (!res.success) { console.error(res.error); // Dodo API error message or "An unknown error occurred" return; } // After success: // - Dodo sets cancel_at_next_billing_date = true on the subscription // - Local DB: subscriptions.cancelAtNextBillingDate = true // - UI will show "Scheduled for cancellation" badge and expose RestoreSubscriptionDialog ``` -------------------------------- ### `createDodoCustomer` — Create Dodo Payments Customer Source: https://context7.com/dodopayments/dodo-supabase-subscription-starter/llms.txt Creates a customer object in Dodo Payments. If no name is provided, it defaults to the prefix of the email address. ```APIDOC ## `createDodoCustomer` — Create Dodo Payments Customer ### Description Creates a customer object in Dodo Payments. Falls back to the email prefix as the customer name when no name is provided. ### Method Signature `createDodoCustomer({ email: string, name?: string })` ### Parameters #### Request Body - **email** (string) - Required - The email address of the customer. - **name** (string) - Optional - The name of the customer. Defaults to the part of the email before the '@' symbol if not provided. ### Returns - `success` (boolean): Indicates if the operation was successful. - `data` (Customer): The created customer object if successful. - `error` (string): An error message if the operation failed. ``` -------------------------------- ### Database Schema Definition (Drizzle ORM) Source: https://context7.com/dodopayments/dodo-supabase-subscription-starter/llms.txt Defines the PostgreSQL tables for users, subscriptions, and payments using Drizzle ORM. These schemas are managed via Drizzle Kit and can be pushed to the database using `bun run db:push`. ```typescript // lib/drizzle/schema.ts import { pgTable, text, boolean, integer, real, timestamp, jsonb } from "drizzle-orm/pg-core"; import { relations } from "drizzle-orm"; // Users table — links Supabase Auth UIDs to Dodo customer IDs export const users = pgTable("users", { supabaseUserId: text("supabase_user_id").primaryKey(), dodoCustomerId: text("dodo_customer_id").notNull(), currentSubscriptionId: text("current_subscription_id"), // null = free/hobby plan createdAt: timestamp("created_at", { mode: "string", withTimezone: true }).notNull(), updatedAt: timestamp("updated_at", { mode: "string", withTimezone: true }).notNull(), deletedAt: timestamp("deleted_at", { mode: "string", withTimezone: true }), }); // Subscriptions table — synced from Dodo webhook events export const subscriptions = pgTable("subscriptions", { subscriptionId: text("subscription_id").primaryKey().notNull(), userId: text("user_id").references(() => users.supabaseUserId), recurringPreTaxAmount: real("recurring_pre_tax_amount").notNull(), taxInclusive: boolean("tax_inclusive").notNull(), currency: text("currency").notNull(), status: text("status").notNull(), // "active" | "on_hold" | "cancelled" | ... productId: text("product_id").notNull(), quantity: integer("quantity").notNull(), nextBillingDate: timestamp("next_billing_date", { mode: "string", withTimezone: true }).notNull(), previousBillingDate: timestamp("previous_billing_date", { mode: "string", withTimezone: true }).notNull(), customerId: text("customer_id").notNull(), customerEmail: text("customer_email").notNull(), cancelAtNextBillingDate: boolean("cancel_at_next_billing_date"), billing: jsonb("billing").notNull(), // ... (trialPeriodDays, addons, metadata, discountId, etc.) createdAt: timestamp("created_at", { mode: "string", withTimezone: true }).notNull(), }); // Payments table — synced from Dodo payment webhook events export const payments = pgTable("payments", { paymentId: text("payment_id").primaryKey(), status: text("status").notNull(), totalAmount: real("total_amount").notNull(), currency: text("currency").notNull(), paymentMethod: text("payment_method"), customerId: text("customer_id").notNull(), customerEmail: text("customer_email").notNull(), subscriptionId: text("subscription_id").notNull(), cardLastFour: text("card_last_four"), cardNetwork: text("card_network"), webhookData: jsonb("webhook_data").notNull(), billing: jsonb("billing").notNull(), createdAt: timestamp("created_at", { mode: "string", withTimezone: true }).notNull(), // ... (refunds, disputes, settlement fields, etc.) }); export type SelectUser = typeof users.$inferSelect; export type SelectSubscription = typeof subscriptions.$inferSelect; export type SelectPayment = typeof payments.$inferSelect; // Drizzle Kit config (drizzle.config.ts): // schema: "./lib/drizzle/schema.ts" // dialect: "postgresql" // out: "./lib/drizzle/migrations" ``` -------------------------------- ### Dodo Webhook Handler Source: https://context7.com/dodopayments/dodo-supabase-subscription-starter/llms.txt A Supabase Edge Function to receive, verify, and process Dodo Payments webhook events, updating relevant database tables. ```APIDOC ## Supabase Edge Function: `dodo-webhook` ### Description A Deno-based Supabase Edge Function that receives Dodo Payments webhook events, verifies signatures, and updates `subscriptions`, `payments`, and `users` tables. ### Endpoint `https://[your-project-ref].supabase.co/functions/v1/dodo-webhook` ### Method POST ### Headers Required for Verification - **webhook-id** (string) - **webhook-signature** (string) - **webhook-timestamp** (string) ### Environment Variables Required - **DODO_WEBHOOK_SECRET**: The secret key for webhook signature verification. - **SUPABASE_URL**: The URL of your Supabase project. - **SUPABASE_SERVICE_ROLE_KEY**: The Supabase service role key. ### Event Handling - **payment.succeeded, payment.failed, payment.processing, payment.cancelled**: Upserts a row in the `payments` table. - **subscription.active, subscription.plan_changed**: Upserts a row in the `subscriptions` table and updates `users.current_subscription_id`. - **subscription.renewed, subscription.on_hold**: Upserts a row in the `subscriptions` table. - **subscription.cancelled, subscription.expired, subscription.failed**: Upserts a row in the `subscriptions` table and sets `users.current_subscription_id` to null. ### Request Example (Incoming Payload - subscription.active) ```json { "type": "subscription.active", "data": { "subscription_id": "sub_abc123", "product_id": "prod_abc123", "status": "active", "currency": "USD", "recurring_pre_tax_amount": 1999, "tax_inclusive": false, "next_billing_date": "2025-08-01T00:00:00Z", "previous_billing_date": "2025-07-01T00:00:00Z", "quantity": 1, "billing": { "city": "San Francisco", "country": "US", "state": "CA", "street": "123 Main St", "zipcode": "94102" }, "customer": { "customer_id": "cus_abc123", "email": "jane@example.com", "name": "Jane Doe" }, "cancel_at_next_billing_date": false } } ``` ``` -------------------------------- ### Soft-Delete User Account Source: https://context7.com/dodopayments/dodo-supabase-subscription-starter/llms.txt Deletes the user from Supabase Auth and soft-deletes the local user row by stamping `deletedAt`. This action automatically logs the user out by invalidating their session. ```typescript // actions/delete-account.ts import { deleteAccount } from "@/actions/delete-account"; const res = await deleteAccount(); if (!res.success) { console.error(res.error); // "User not found" | Supabase admin error message return; } // After success: // - adminAuthClient.deleteUser(userId) removes the Supabase Auth account // - DB: users.deletedAt = new Date().toISOString() (soft delete — row is retained) // The user is logged out automatically as their session is invalidated. ``` -------------------------------- ### Undo Scheduled Subscription Cancellation Source: https://context7.com/dodopayments/dodo-supabase-subscription-starter/llms.txt Reverses a pending cancellation, keeping the subscription active through the next billing cycle. Use this when a user decides to continue their subscription after initially scheduling a cancellation. ```typescript // actions/restore-subscription.ts import { restoreSubscription } from "@/actions/restore-subscription"; const res = await restoreSubscription({ subscriptionId: "sub_abc123" }); if (!res.success) { console.error(res.error); return; } // After success: // - Dodo sets cancel_at_next_billing_date = false // - Local DB: subscriptions.cancelAtNextBillingDate = false // - UI reverts to showing CancelSubscriptionDialog instead of RestoreSubscriptionDialog ``` -------------------------------- ### deleteAccount Source: https://context7.com/dodopayments/dodo-supabase-subscription-starter/llms.txt Soft-deletes a user account by removing them from Supabase Auth and marking the local user row as deleted. ```APIDOC ## deleteAccount — Soft-Delete User Account Deletes the user from Supabase Auth and soft-deletes the local user row by stamping `deletedAt`. ### Method Not specified (client-side function call) ### Parameters None ### Request Example ```typescript await deleteAccount(); ``` ### Response #### Success Response - **success** (boolean) - Indicates if the operation was successful. #### Error Response - **success** (boolean) - Indicates if the operation was successful. - **error** (string) - Description of the error if `success` is false. Possible errors include "User not found" or Supabase admin error messages. ### Notes - After success, `adminAuthClient.deleteUser(userId)` removes the Supabase Auth account. - DB: `users.deletedAt = new Date().toISOString()` (soft delete). - The user is automatically logged out as their session is invalidated. ``` -------------------------------- ### ServerActionRes Type Definition Source: https://context7.com/dodopayments/dodo-supabase-subscription-starter/llms.txt Defines a discriminated union type for server action return values, ensuring consistent success or error reporting. Use this type for all server actions to standardize responses. ```typescript // types/server-action.ts export type ServerActionRes = Promise; ``` ```typescript // Usage pattern in any server action: import { ServerActionRes } from "@/types/server-action"; async function myAction(): ServerActionRes { try { return { success: true, data: "result" }; } catch (e) { return { success: false, error: e instanceof Error ? e.message : "Unknown error" }; } } ``` ```typescript // Caller pattern: const res = await myAction(); if (!res.success) { console.error(res.error); // TypeScript narrows to { success: false; error: string } return; } console.log(res.data); // TypeScript narrows to { success: true; data: string } ``` === COMPLETE CONTENT === This response contains all available snippets from this library. No additional content exists. Do not make further requests.