BetterAuth Integration

This guide shows how to integrate mavi pay with BetterAuth to gate access to content and features based on purchase status and subscription entitlements.


Overview

BetterAuth handles user authentication. mavi pay handles payments and entitlements. Together, they let you:

  • Require a valid purchase before granting access to protected routes.
  • Check subscription status to gate premium features.
  • Sync mavi pay customer data with your BetterAuth user records.

Installation

npm install @mavi-pay/sdk better-auth

Setup

1. Configure BetterAuth

Set up BetterAuth as described in their documentation. You need a working authentication flow before adding mavi pay.

2. Create the mavi pay Client

// lib/mavi-pay.ts
import { MaviPay } from '@mavi-pay/sdk'

export const maviPay = new MaviPay({
  accessToken: process.env.MAVI_PAY_ACCESS_TOKEN!,
  baseUrl: 'https://api.mavifinans.sh',
})

When a customer completes a checkout, use a webhook to link the mavi pay customer ID to the BetterAuth user.

// Handle the checkout.completed webhook event
async function handleCheckoutCompleted(event: any) {
  const { customer_email, customer_id } = event.data

  // Find the BetterAuth user by email
  const user = await auth.getUserByEmail(customer_email)

  if (user) {
    // Store the mavi pay customer ID on the user record
    await auth.updateUser(user.id, {
      metadata: {
        maviPayCustomerId: customer_id,
      },
    })
  }
}

Gating Access by Purchase

Server-Side Middleware

Create a middleware that checks whether the authenticated user has an active purchase or subscription.

// middleware/require-purchase.ts
import { maviPay } from '@/lib/mavi-pay'

export async function requirePurchase(
  userId: string,
  productId: string
): Promise<boolean> {
  const user = await auth.getUser(userId)
  const customerId = user?.metadata?.maviPayCustomerId

  if (!customerId) {
    return false
  }

  const orders = await maviPay.orders.list({
    customerId,
    productId,
  })

  return orders.items.some(
    (order) => order.status === 'paid' || order.status === 'active'
  )
}

Protecting a Route

// app/premium/page.tsx
import { auth } from '@/lib/auth'
import { requirePurchase } from '@/middleware/require-purchase'
import { redirect } from 'next/navigation'

export default async function PremiumPage() {
  const session = await auth.getSession()

  if (!session) {
    redirect('/login')
  }

  const hasAccess = await requirePurchase(session.user.id, 'prod_xxxxxxxx')

  if (!hasAccess) {
    redirect('/pricing')
  }

  return (
    <div>
      <h1>Premium Content</h1>
      <p>You have access to this content.</p>
    </div>
  )
}

Checking Subscription Status

For subscription products, check the subscription status rather than individual orders.

// lib/entitlements.ts
import { maviPay } from '@/lib/mavi-pay'

export async function hasActiveSubscription(
  customerId: string,
  productId: string
): Promise<boolean> {
  const subscriptions = await maviPay.subscriptions.list({
    customerId,
    productId,
  })

  return subscriptions.items.some(
    (sub) => sub.status === 'active'
  )
}

Webhook Setup

Register a webhook endpoint in your mavi pay dashboard to keep user entitlements in sync:

  • checkout.completed -- Grant access after purchase.
  • subscription.cancelled -- Revoke access when subscription ends.
  • subscription.renewed -- Confirm continued access.

See the Next.js integration guide for a webhook handler example.


Environment Variables

MAVI_PAY_ACCESS_TOKEN=your_api_key_here
MAVI_PAY_WEBHOOK_SECRET=your_webhook_secret_here
BETTER_AUTH_SECRET=your_auth_secret_here

Next Steps