BirrJS
Get started

Installation

Install BirrJS, configure Chapa, and mount the route handler in your app.

Want to skip the manual setup? Run npx @birrjs/cli init to scaffold the full configuration with framework detection, route handler, and plan templates. See the CLI reference.

Install the core package and provider

BirrJS ships as a monorepo with a core package and separate provider packages.

pnpm add @birrjs/core @birrjs/chapa
npm install @birrjs/core @birrjs/chapa
yarn add @birrjs/core @birrjs/chapa
bun add @birrjs/core @birrjs/chapa

Create the BirrJS instance

Create a file named birrjs.ts — usually in src/lib/ or src/server/.

src/lib/birrjs.ts
import { createBirr } from "@birrjs/core";

export const birrjs = createBirr({
  // ...
});

Configure provider

BirrJS uses provider adapters for payment processing. Here's the setup for Chapa:

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

export const birrjs = createBirr({
  provider: chapa({ 
    secretKey: process.env.CHAPA_SECRET_KEY!, 
    webhookSecret: process.env.CHAPA_WEBHOOK_SECRET!, 
    callbackUrl: process.env.CALLBACK_URL!, 
    returnUrl: process.env.RETURN_URL!, 
  }),
});
.env
CHAPA_SECRET_KEY=your-chapa-secret-key
CHAPA_WEBHOOK_SECRET=your-webhook-secret
CALLBACK_URL=https://your-app.com/api/webhook

CHAPA_SECRET_KEY — get yours from the Chapa dashboard → Settings → API Keys. Use test mode keys for development.

CALLBACK_URL — a publicly reachable URL for payment notifications. Chapa sends a GET callback here after payment (?trx_ref=...&status=success). Also paste this URL in your Chapa dashboard → Settings → Webhooks for POST notifications. For local testing, use a tunnel like ngrok or Cloudflare Tunnel.

RETURN_URL (optional) — where Chapa sends the user's browser after payment. Set this to a frontend page like /account or /plans?success=true. Without it, users stay on Chapa's checkout page after paying.

CHAPA_WEBHOOK_SECRET — a secret you invent yourself. Click Generate above to create a random one (copied automatically).

Don't forget to configure both CALLBACK_URL and CHAPA_WEBHOOK_SECRET in your Chapa dashboard → Profile Settings → Webhooks: paste your URL into Webhook URL, your generated string into Secret Hash,

Enable Receive Webhook for failed Payments. BirrJS handles charge.failed events to mark pending subscriptions as failed — without it, only success events arrive and pending subscriptions pile up until the scheduled cron sweep clears them.

Configure database

BirrJS needs PostgreSQL to store subscriptions, entitlements, and customer data.

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

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!,
    returnUrl: process.env.RETURN_URL!,
  }),
});

Add to your .env:

DATABASE_URL="postgres://user:password@localhost:5432/birrjs"

Configure identify

The identify function links HTTP requests to customers. Required for the client SDK to resolve the current customer from incoming requests.

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

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!,
    returnUrl: process.env.RETURN_URL!,
  }),
  identify: async (request) => { 
    // Replace with your auth logic
    return { customerId: "cus_abc123" }; 
  }, 
});

Mount the route handler

BirrJS needs a catch-all route to handle API requests and webhooks.

Create a catch-all route file:

app/api/birrjs/[[...slug]]/route.ts
import { birrHandler } from "@birrjs/core";
import { birrjs } from "@/lib/birrjs";

export const { GET, POST } = birrHandler(birrjs);
src/routes/api/birrjs.$.ts
import { birrjs } from "@/lib/birrjs";
import { createFileRoute } from "@tanstack/react-router";

export const Route = createFileRoute("/api/birrjs/$")({
  server: {
    handlers: {
      GET: ({ request }) => birrjs.handler(request),
      POST: ({ request }) => birrjs.handler(request),
    },
  },
});

Use the native Request handler:

src/index.ts
import { Hono } from "hono";
import { birrjs } from "./birrjs";

const app = new Hono();

app.all("/api/*", async (c) => {
  return birrjs.handler(c.req.raw);
});

Wrap the standard handler:

src/index.ts
import express from "express";
import { birrjs } from "./birrjs";

const app = express();

app.all("/api/*", async (req, res) => {
  const request = new Request(`http://localhost${req.originalUrl}`, {
    method: req.method,
    headers: req.headers as HeadersInit,
    body: JSON.stringify(req.body),
  });
  const response = await birrjs.handler(request);
  res.status(response.status).json(await response.json());
});

The core handler is a standard Web API function that takes a Request and returns a Response. Use it with any framework that supports the Web API standard.

server.ts
import { birrjs } from "./birrjs";

// birrjs.handler: (request: Request) => Promise<Response>
// Mount it on any path that catches /api/birrjs/*
app.all("/api/birrjs/*", (req) => birrjs.handler(req));

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>();

Define plans

Create a file for your products:

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

const storage = feature({ id: "storage", type: "metered" });
const apiCalls = feature({ id: "api_calls", type: "metered" });
const advancedReports = feature({ id: "advanced_reports", type: "boolean" });

export const free = plan({
  id: "free",
  name: "Free",
  default: true,
  includes: [
    storage({ limit: 1000, reset: "month" }),
    apiCalls({ limit: 1000, reset: "month" }),
  ],
});

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

Then wire them into your instance:

src/lib/birrjs.ts
import { createBirr } from "@birrjs/core";
import { chapa } from "@birrjs/chapa";
import { free, pro } from "./birrjs-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!,
    returnUrl: process.env.RETURN_URL!,
  }),
  plans: [free, pro], 
  identify: async (request) => {
    return { customerId: "cus_abc123" };
  },
});

Sync to the database

Run migrations to create tables and sync your plan definitions:

birrjs push

Verify the setup:

birrjs status

This checks your database connection, pending migrations, and plan sync status.

You're now ready to use BirrJS! 🚀

Want SMS notifications on subscription events? Add the Afromessage plugin — or build your own with the Your First Plugin guide.