Setup

1. Install Packages

Server SDK

npm install @getlumen/server

UI Components

For React apps, you have two options:

Option A: shadcn components (Recommended)

npx shadcn@latest add https://getlumen.dev/pricing-table.json

Option B: Pre-built React package

npm install @getlumen/react

The shadcn components give you full control over styling and can be customized directly or with an LLM of your choice. The React package provides ready-to-use components but it might not suit the styling in your app.

2. Environment Variables

Add these to your environment variables:

# Get these from https://getlumen.dev/developer/apikeys
LUMEN_API_KEY=sk_live_...                    # Secret key for backend operations
NEXT_PUBLIC_LUMEN_PUBLISHABLE_KEY=pk_live_... # Publishable key for frontend components

If you're not using Next.js, instead of setting NEXT_PUBLIC_LUMEN_PUBLISHABLE_KEY, you can set the env var as per your framework - for eg, VITE_LUMEN_PUBLISHABLE_KEY for Vite.

This variable needs to be passed into the UI components later.

3. Backend Proxy Setup

Lumen requires a backend proxy route to securely check entitlements and usage. This prevents direct frontend-to-Lumen communication for sensitive operations.

The flow is: Frontend → Your Backend → Lumen

Create app/api/lumen/[...all]/route.ts:

import { NextRequest, NextResponse } from "next/server";
import { auth } from "@clerk/nextjs/server";
import { lumenNextHandler } from "@getlumen/server";

const handler = async (request: NextRequest) => {
  const { userId } = await auth();

  if (!userId) {
    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
  }

  return NextResponse.json(
    await lumenNextHandler({
      request,
      userId,
    })
  );
};

export {
  handler as GET,
  handler as POST,
  handler as PUT,
  handler as DELETE,
  handler as PATCH,
  handler as OPTIONS,
  handler as HEAD,
};
import { NextRequest, NextResponse } from "next/server";
import { createClient } from "@/lib/supabase/server";
import { lumenNextHandler } from "@getlumen/server";

const handler = async (request: NextRequest) => {
  const supabase = await createClient();
  const { data: { user } } = await supabase.auth.getUser();

  if (!user) {
    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
  }

  return NextResponse.json(
    await lumenNextHandler({
      request,
      userId: user.id,
    })
  );
};

export {
  handler as GET,
  handler as POST,
  handler as PUT,
  handler as DELETE,
  handler as PATCH,
  handler as OPTIONS,
  handler as HEAD,
};
import { NextRequest, NextResponse } from "next/server";
import { auth } from "@/lib/auth";
import { lumenNextHandler } from "@getlumen/server";

const handler = async (request: NextRequest) => {
  const session = await auth.api.getSession({ headers: request.headers });
  if (!session?.user) {
    return Response.json({ error: "Unauthorized" }, { status: 401 });
  }

  return NextResponse.json(
    await lumenNextHandler({
      request,
      userId: session.user.id,
    })
  );
};

export {
  handler as GET,
  handler as POST,
  handler as PUT,
  handler as DELETE,
  handler as PATCH,
  handler as OPTIONS,
  handler as HEAD,
};

For Express, create a catch-all route and use our handler:

import express from 'express';
import { lumenExpressHandler } from '@getlumen/server';

const app = express();

// Your auth middleware (must set req.user or equivalent)
app.use('/api/lumen', requireAuth);
// Ensure JSON parsing for POST/PUT/PATCH bodies
app.use(express.json());

app.all(
  '/api/lumen/*',
  lumenExpressHandler({
    getUserId: (req) => req.user?.id,
    mountPath: '/api/lumen',
  })
);

For Hono, use the Hono handler with auth middleware:

import { Hono } from 'hono';
import { lumenHonoHandler } from '@getlumen/server';

const app = new Hono();

// Your auth middleware - set userId in context
const requireAuth = async (c, next) => {
  // your auth middleware
};

app.all(
  '/api/lumen/*',
  requireAuth,
  lumenHonoHandler({
    getUserId: (c) => c.get('userId'),
    mountPath: '/api/lumen',
  })
);

Lumen works best if all your customers have a subscription attached, even those on the free plan. This enables entitlements to work (check if a user has a feature or not, grant credits and check usage) and provides a smooth upgrade path to paid plans.

Read the quickstart to see how to create a free plan. You need to set up webhook forwarding from your auth provider to Lumen so that we can automatically create free subscriptions for new users.

Lumen supports webhooks from Clerk, Supabase and Better Auth. If you're using a different auth provider, you'll need to manually callt he Lumen SDK to create a free subscription for new users. See this guide.

  1. Navigate to Configure > Webhooks in Clerk dashboard. Create a new webhook subscribed to the event user.created.

  2. Set the webhook URL to:

    https://api.getlumen.dev/v1/webhooks/clerk

    Clerk Webhook

  3. After the webhook is created, click on your new webhook, then on the Advanced tab and add the following header:

    • Key: Authorization
    • Value: Bearer your_lumen_secret_api_key

    If you don't have an API Key, you can create one at API Keys.

    Clerk webhook header

  1. Activate webhooks for your project if not already activated

  2. Go to Webhooks > Create a new hook in your Supabase dashboard triggered on insert into auth.users

  3. Set the webhook URL to: https://api.getlumen.dev/v1/webhooks/supabase

  4. Add this header:

    • Header name: Authorization
    • Header value: Bearer your_secret_lumen_api_key

    If you don't have an API Key, you can create one at API Keys.

Supabase webhook

  1. Install the Better Auth plugin:
npm install @getlumen/better-auth
  1. Add the plugin to your Better Auth configuration:
import { betterAuth } from "better-auth";
import { LumenPlugin } from "@getlumen/better-auth";

export const auth = betterAuth({
  // ... your other Better Auth configuration

  plugins: [
    LumenPlugin({
      apiKey: process.env.LUMEN_API_KEY!,
    }),
  ],
});

If you don't have an API Key, you can create one at API Keys.

Advanced Usage with Callbacks

import { betterAuth } from "better-auth";
import { LumenPlugin } from "@getlumen/better-auth";

export const auth = betterAuth({
  // ... your other Better Auth configuration

  plugins: [
    LumenPlugin({
      apiKey: process.env.LUMEN_API_KEY!,
      enabled: true,
      prodOnly: process.env.NODE_ENV !== "development",
      onSuccess: (user, subscription) => {
        console.log(
          `Free subscription created for ${user.email}:`,
          subscription
        );
        // Optional: Send notification, update analytics, etc.
      },
      onError: (user, error) => {
        console.error(
          `Failed to create free subscription for ${user.email}:`,
          error
        );
        // Optional: Send error notification, log to monitoring service, etc.
      },
      logger: (message, ...args) => {
        console.log(`[FreeSubscription] ${message}`, ...args);
      },
    }),
  ],
});

Configuration Options

OptionTypeDefaultDescription
apiKeystringRequiredAPI key for authenticating with the subscription service
enabledbooleantrueWhether the plugin is enabled
prodOnlybooleanfalseWhether to only run in production/staging environments
onSuccessfunctionOptionalCallback function called when subscription is created successfully
onErrorfunctionOptionalCallback function called when subscription creation fails
loggerfunctionconsole.logCustom logger function for debugging

Development vs Production

When prodOnly is set to true, the plugin will skip execution in development mode. This is useful for preventing test subscriptions from being created during development.