Skip to content

Payment Plugin

Subscription and payment processing with Stripe integration.

Installation

Add the payment plugin to your config:

typescript
// nevr.config.ts
import { defineConfig } from "nevr"
import { payment } from "nevr/plugins/payment"

export const config = defineConfig({
  database: "postgresql",
  entities: [],
  plugins: [
    payment({
      provider: "stripe",
      stripe: {
        secretKey: process.env.STRIPE_SECRET_KEY!,
        webhookSecret: process.env.STRIPE_WEBHOOK_SECRET,
      },
      createCustomerOnSignUp: true,
      subscription: {
        enabled: true,
        plans: [
          { name: "free", priceId: "price_free" },
          { name: "pro", priceId: "price_pro", limits: { projects: 10 } },
          { name: "enterprise", priceId: "price_enterprise", limits: { projects: -1 } },
        ],
      },
    }),
  ],
})

export default config

Then in your server:

typescript
// src/server.ts
import { nevr } from "nevr"
import { prisma } from "nevr/drivers/prisma"
import { PrismaClient } from "@prisma/client"
import { config } from "./nevr.config.js"

const api = nevr({ ...config, driver: prisma(new PrismaClient()) })
export type API = typeof api

Generate and push or migrate the database

bash
npx nevr generate    # Generates user + session tables
npx nevr db:push     # Push to database
# or
npx nevr db:migrate  # Create migration files

Client Setup

typescript
import { createClient } from "nevr/client"
import { paymentClient } from "nevr/plugins/payment/client"
import type { API } from "./api"

const client = createClient<API>()({
  baseURL: "/api",
  plugins: [paymentClient()],
})

Configuration

typescript
payment({
  // Payment provider
  provider: "stripe", // "stripe" | "lemonsqueezy" | "paddle"

  // Default currency
  defaultCurrency: "usd",

  // Auto-create Stripe customer on user signup
  createCustomerOnSignUp: true,

  // Stripe configuration
  stripe: {
    secretKey: process.env.STRIPE_SECRET_KEY!,
    webhookSecret: process.env.STRIPE_WEBHOOK_SECRET,
  },

  // Subscription configuration
  subscription: {
    enabled: true,
    plans: [
      { 
        name: "free", 
        priceId: "price_free",
        limits: { projects: 5, storage: 1024 } 
      },
      { 
        name: "pro", 
        priceId: "price_pro",
        annualPriceId: "price_pro_annual",
        limits: { projects: 50, storage: 10240 } 
      },
      { 
        name: "enterprise", 
        priceId: "price_enterprise",
        limits: { projects: -1, storage: -1 } // unlimited
      },
    ],
    // Multi-tenant (organization billing)
    authorizeReference: async ({ user, referenceId }) => {
      return isOrgAdmin(user.id, referenceId)
    },
    // Trial period
    trialPeriodDays: 14,
    // Callbacks
    onSubscriptionComplete: async ({ subscription }) => {
      console.log("Subscription created:", subscription.id)
    },
    onSubscriptionUpdate: async ({ subscription }) => {
      console.log("Subscription updated:", subscription.status)
    },
    onTrialEnd: async ({ subscription }) => {
      console.log("Trial ended for:", subscription.referenceId)
    },
  },
})

Endpoints

Subscription Management

MethodPathDescription
POST/payment/subscription/upgradeUpgrade/create subscription
POST/payment/subscription/cancelCancel subscription
POST/payment/subscription/cancel-callbackCancel callback
POST/payment/subscription/restoreRestore canceled subscription
GET/payment/subscription/listList active subscriptions
GET/payment/subscription/successSuccess callback
POST/payment/subscription/billing-portalOpen billing portal

Webhook

MethodPathDescription
POST/payment/stripe/webhookStripe webhook handler

Webhook Setup Required

Stripe webhooks require the raw request body for signature verification. Setup depends on your adapter:

Express — Use nevrJson(express) instead of express.json():

typescript
import express from "express"
import { expressAdapter, sessionAuth, nevrJson } from "nevr/adapters/express"

const app = express()

// Use nevrJson(express) instead of express.json()
// This preserves rawBody for webhook signature verification
app.use(nevrJson(express))

app.use("/api", expressAdapter(api, {
  getUser: sessionAuth(driver),
}))

Next.js — No extra setup needed. The Next.js adapter automatically preserves rawBody.

Hono — No extra setup needed. The Hono adapter automatically preserves rawBody.

Client SDK

The payment plugin uses the unified client pattern with createClient:

typescript
import { createClient } from "nevr/client"
import { paymentClient } from "nevr/plugins/payment/client"
import type { API } from "./api"

// Use curried pattern for full type inference
export const client = createClient<API>()({
  baseURL: "http://localhost:3000",
  plugins: [
    authClient(),
    paymentClient(),
  ],
})

// Upgrade subscription
const { data } = await client.payment.subscription.upgrade({
  plan: "pro",
  successUrl: "/dashboard",
  cancelUrl: "/pricing",
})
if (data?.url) window.location.href = data.url

// List active subscriptions
const { data: subs } = await client.payment.subscription.list()

// Cancel subscription
const { data: cancel } = await client.payment.subscription.cancel({
  returnUrl: "/account",
})
if (cancel?.url) window.location.href = cancel.url

// Restore a canceled subscription
await client.payment.subscription.restore()

// Open billing portal
const { data: portal } = await client.payment.subscription.billingPortal({
  returnUrl: "/settings",
})
if (portal?.url) window.location.href = portal.url

Multi-Tenant Billing

Use referenceId for organization billing:

typescript
// Upgrade for organization
await client.payment.subscription.upgrade({
  plan: "pro",
  referenceId: organizationId, // Bill to organization
  successUrl: "/success",
  cancelUrl: "/cancel",
})

// Get organization's subscriptions
const { data } = await client.payment.subscription.list({
  referenceId: organizationId,
})

Type Inference

The plugin exports types for SDK inference:

typescript
import type { InferPlugin } from "nevr"
import { payment } from "nevr/plugins/payment"

type PaymentPlugin = InferPlugin<typeof payment>

// Available types:
// PaymentPlugin["Subscription"]
// PaymentPlugin["PaymentPlan"]

Helper Functions

typescript
import { 
  hasActiveSubscription, 
  getCurrentPlan, 
  hasFeatureAccess,
  getFeatureLimit 
} from "nevr/plugins/payment"

// Check if reference has active subscription
const isActive = await hasActiveSubscription(driver, referenceId)

// Get current plan name
const plan = await getCurrentPlan(driver, referenceId)

// Check feature access
const canCreate = await hasFeatureAccess(driver, referenceId, "projects", plans)

// Get feature limit
const limit = await getFeatureLimit(driver, referenceId, "projects", plans)

Schema

Subscription

FieldTypeDescription
idstringUnique identifier
referenceIdstringUser/Org reference
stripeCustomerIdstringStripe customer ID
stripeSubscriptionIdstringStripe subscription ID
planstringPlan name
priceIdstringStripe price ID
statusstringactive, canceled, past_due, trialing
periodStartdatetimePeriod start
periodEnddatetimePeriod end
trialStartdatetime?Trial start
trialEnddatetime?Trial end
cancelAtPeriodEndbooleanWill cancel at period end
seatsnumber?Number of seats

PaymentPlan

FieldTypeDescription
namestringPlan name
priceIdstringStripe price ID
annualPriceIdstring?Annual price ID
limitsobject?Feature limits
trialDaysnumber?Trial period

Webhook Events

The plugin handles these Stripe events:

EventDescription
checkout.session.completedCheckout completed
customer.subscription.createdSubscription created
customer.subscription.updatedSubscription updated
customer.subscription.deletedSubscription canceled
invoice.paidInvoice paid
invoice.payment_failedPayment failed
customer.subscription.trial_will_endTrial ending soon

Database Hooks

The payment plugin automatically:

  1. Creates Stripe customer on signup - If createCustomerOnSignUp: true
  2. Links existing customers - Finds customer by email before creating
  3. Syncs email changes - Updates Stripe when user email changes

Released under the MIT License.