billSDK

Configuration

All configuration options for BillSDK

Options

const billing = billsdk({
  database: drizzleAdapter(db, { schema, provider: "pg" }),
  secret: process.env.BILLSDK_SECRET!,
  trustedOrigins: ["https://myapp.com"],
  payment: stripePayment({ ... }),
  basePath: "/api/billing",
  features: [...],
  plans: [...],
  plugins: [...],
  hooks: { before, after },
  logger: { level: "info", disabled: false },
});
OptionTypeDefaultDescription
databaseDBAdaptermemoryAdapter()Database adapter
paymentPaymentAdapterpaymentAdapter()Payment provider adapter
basePathstring/api/billingAPI base path
secretstring-Required. Secret for signing CSRF tokens and Bearer auth. Min 32 chars. Falls back to BILLSDK_SECRET env var
trustedOriginsstring[][]Origins allowed to make mutating requests. Supports wildcards (*.example.com). Falls back to BILLSDK_TRUSTED_ORIGINS env var
featuresFeatureConfig[][]Feature definitions
plansPlanConfig[][]Plan definitions
pluginsPlugin[][]Plugins
hooksobject-Request hooks
loggerobject-Logger configuration

Features

Features are capabilities you can gate per plan. Currently only boolean (on/off) features are supported.

features: [
  { code: "export", name: "Export Data" },
  { code: "api_access", name: "API Access" },
  { code: "priority_support", name: "Priority Support" },
]
PropertyTypeRequiredDescription
codestringYesUnique identifier
namestringYesDisplay name
type"boolean" | "metered" | "seats"NoFeature type (default: "boolean")

Feature codes are type-safe. When you call checkFeature, TypeScript only accepts codes you defined:

// Type error: "invalid_feature" is not a valid feature code
await billing.api.checkFeature({ customerId, feature: "invalid_feature" });

Plans

Plans define your pricing tiers. They exist only in code, not in the database.

plans: [
  {
    code: "free",
    name: "Free",
    description: "Get started",
    isPublic: true,
    prices: [{ amount: 0, interval: "monthly" }],
    features: ["export"],
  },
  {
    code: "pro",
    name: "Pro",
    prices: [
      { amount: 2000, interval: "monthly", currency: "usd" },
      { amount: 20000, interval: "yearly", trialDays: 14 },
    ],
    features: ["export", "api_access"],
  },
  {
    code: "enterprise",
    name: "Enterprise",
    isPublic: false, // Hidden from listPlans()
    prices: [{ amount: 9900, interval: "monthly" }],
    features: ["export", "api_access", "priority_support"],
  },
]

Plan Properties

PropertyTypeRequiredDescription
codestringYesUnique identifier
namestringYesDisplay name
descriptionstringNoPlan description
isPublicbooleanNoShow in listPlans() (default: true)
pricesPlanPriceConfig[]YesPricing options
featuresstring[]NoEnabled feature codes

Price Properties

PropertyTypeRequiredDescription
amountnumberYesPrice in cents (2000 = $20.00)
interval"monthly" | "quarterly" | "yearly"YesBilling cycle
currencystringNoISO 4217 code (default: "usd")
trialDaysnumberNoTrial period length

Database Adapters

Drizzle

import { drizzleAdapter } from "@billsdk/drizzle-adapter";

database: drizzleAdapter(db, {
  schema,
  provider: "pg", // "pg" | "mysql" | "sqlite"
})

Memory (Testing)

import { memoryAdapter } from "@billsdk/memory-adapter";

database: memoryAdapter()

Payment Adapters

Default

Activates subscriptions immediately without payment processing. Used automatically if you don't specify a payment adapter.

import { paymentAdapter } from "@billsdk/payment-adapter";

payment: paymentAdapter()

Use when:

  • All your plans are free
  • You're in development/testing
  • You handle payments outside BillSDK

Stripe

import { stripePayment } from "@billsdk/stripe";

payment: stripePayment({
  secretKey: process.env.STRIPE_SECRET_KEY!,
  webhookSecret: process.env.STRIPE_WEBHOOK_SECRET!,
})

Hooks

Execute code before or after every request.

hooks: {
  before: async ({ request, path, method }) => {
    // Return Response to short-circuit
    if (!isAuthenticated(request)) {
      return new Response("Unauthorized", { status: 401 });
    }
    // Return undefined to continue
  },
  after: async ({ request, path, method }) => {
    console.log(`[billing] ${method} ${path}`);
  },
}

Logger

logger: {
  level: "debug", // "debug" | "info" | "warn" | "error"
  disabled: false,
}

Security

BillSDK protects all mutating endpoints (POST, PUT, PATCH, DELETE) automatically. GET requests and the /webhook endpoint are exempt.

How It Works

Browser requests are protected by two layers:

  1. Origin validation — the Origin header must match one of your trustedOrigins
  2. CSRF token — a signed token (using your secret) is required via cookie + header

The BillSDK client handles CSRF tokens automatically. You don't need to do anything.

Server-to-server requests can bypass origin and CSRF checks by sending the secret as a Bearer token:

curl -X POST https://myapp.com/api/billing/customer \
  -H "Authorization: Bearer $BILLSDK_SECRET" \
  -H "Content-Type: application/json" \
  -d '{"externalId": "user_123", "email": "user@example.com"}'

Server-side direct calls via billing.api.* don't go through HTTP, so security checks don't apply.

Configuration

const billing = billsdk({
  // Required. Throws if missing. Falls back to BILLSDK_SECRET env var.
  secret: process.env.BILLSDK_SECRET!,

  // Origins allowed to make mutating requests from the browser.
  // Supports wildcards. Falls back to BILLSDK_TRUSTED_ORIGINS env var (comma-separated).
  trustedOrigins: ["https://myapp.com", "*.myapp.com"],
});

Generate a secret:

openssl rand -base64 32

Trusted Origins

PatternMatchesDoesn't match
https://myapp.comhttps://myapp.comhttp://myapp.com, https://sub.myapp.com
*.myapp.comhttps://sub.myapp.com, http://app.myapp.comhttps://myapp.com
https://*.myapp.comhttps://sub.myapp.comhttp://sub.myapp.com
http://localhost:3000http://localhost:3000http://localhost:3001

Webhooks

The /webhook endpoint is exempt from origin and CSRF checks. It relies on the payment provider's signature verification (e.g., Stripe's webhook secret).

On this page