BirrJS
Get started

Quick Start

Build your first subscription flow in 5 minutes.

This guide assumes you've completed Installation and have a working birrjs instance with a mounted route handler.

1. Define a plan

plans.ts
import { feature, plan } from "@birrjs/core";

const messages = feature({ id: "messages", type: "metered" });
const analytics = feature({ id: "analytics", type: "boolean" });

export const free = plan({
  id: "free",
  name: "Free",
  default: true,
  price: { amount: 0, interval: "monthly" },
  includes: [messages({ limit: 100, reset: "month" })],
});

export const pro = plan({
  id: "pro",
  name: "Pro",
  price: { amount: 29, interval: "monthly" },
  includes: [messages({ limit: 5000, reset: "month" }), analytics()],
});

2. Add plans to your instance

src/lib/birrjs.ts
import { createBirr } from "@birrjs/core";
import { chapa } from "@birrjs/chapa";
import { free, pro } from "./plans";

export const birrjs = createBirr({
  database: process.env.DATABASE_URL!,
  provider: chapa({
    secretKey: process.env.CHAPA_SECRET_KEY!,
    webhookSecret: process.env.CHAPA_WEBHOOK_SECRET!,
    callbackUrl: process.env.CALLBACK_URL!,
  }),
  plans: [free, pro], 
  identify: async (request) => {
    const session = await auth.api.getSession({ headers: request.headers });
    if (!session) return null;
    return { customerId: session.user.id, email: session.user.email };
  },
});

3. Sync plans to the database

birrjs push

This runs pending migrations and syncs your code-first plans into the database.

4. How customers work

Customers are created automatically when identify returns a user ID on the first client request. The customerId you return becomes the BirrJS customer record key — no separate creation step needed.

src/lib/birrjs.ts
identify: async (request) => {
  const session = await auth.api.getSession({ headers: request.headers });
  if (!session) return null;
  return { customerId: session.user.id, email: session.user.email };
},

BirrJS doesn't handle authidentify receives the raw Request and returns your app's user ID. Wire in your own session resolver (BetterAuth, NextAuth, Clerk etc.).

For server-side code (admin dashboards, cron jobs) where there is no HTTP Request object, use createCustomer directly:

const customer = await birrjs.createCustomer({
  email: "user@example.com",
  name: "Jane Doe",
});
// customer.id is auto-generated

5. Subscribe to a plan

The identify option links each request to a customer automatically. On the client, you don't need to pass a customerId — the SDK resolves it from the session.

First create the client:

src/lib/birrjs-client.ts
import { createBirrJSClient } from "@birrjs/core/client";
import type { birrjs } from "./birrjs";

export const client = createBirrJSClient<typeof birrjs>();

Subscribe can be called from the client or the server. Depending on your provider, it returns either a checkoutUrl (redirect providers like Chapa) or paymentInstructions (Verification providers like Vodit).

subscribe-button.tsx
import { client } from "./birrjs-client";

async function handleSubscribe() {
  const { checkoutUrl } = await client.subscribe({ planId: "pro" });
  window.location.href = checkoutUrl;
}
subscribe-button.tsx
import { client } from "./birrjs-client";

async function handleSubscribe() {
  const { paymentInstructions } = await client.subscribe({ planId: "pro" });
  // Show paymentInstructions.amount and channels to the user
}
app/api/subscribe/route.ts
import { birrjs } from "../lib/birrjs";

export async function POST(req: Request) {
  const { customerId } = await req.json();
  const { checkoutUrl } = await birrjs.subscribe({
    customerId,
    planId: "pro",
  });
  return Response.json({ checkoutUrl });
}
app/api/subscribe/route.ts
import { birrjs } from "../lib/birrjs";

export async function POST(req: Request) {
  const { customerId } = await req.json();
  const { paymentInstructions } = await birrjs.subscribe({
    customerId,
    planId: "pro",
  });
  return Response.json({ paymentInstructions });
}

For free plans, the subscription activates immediately with no redirect or payment instructions.

6. Check usage

Use check to verify a customer has remaining balance, then report to decrement. Call these server-side with the customer ID.

app/api/chat/route.ts
import { birrjs } from "./birrjs";

async function sendMessage(customerId: string) {
  const { allowed } = await birrjs.check({
    customerId,
    featureId: "messages",
    required: 1,
  });

  if (!allowed) {
    throw new Error("Monthly message limit reached");
  }

  // process the message...

  await birrjs.report({
    customerId,
    featureId: "messages",
    amount: 1,
  });
}

For boolean features like analytics, check returns allowed without any balance tracking.

Need to react to billing events? Use the on option on createBirr() for simple handlers (email, SMS), or build a plugin for advanced use cases.