npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

@creem_io/better-auth

v0.0.12

Published

Creem official Better-Auth plugin

Downloads

3,317

Readme

@creem_io/better-auth

Official Creem Better-Auth plugin for seamless payments and subscription management.

✨ Features

  • 🔐 Automatic Customer Sync - Optional automatic synchronization of the creem customer_id with your user_id
  • Checkout Sessions - Create payment sessions with product-specific checkout
  • 📊 Customer Portal - Let users manage their subscriptions, view invoices, and update payment methods
  • 🔄 Subscription Management - Cancel, retrieve, and track subscription details
  • 💰 Transaction History - Search and filter transaction records
  • 🪝 Webhook Processing - Handle Creem webhooks with signature verification
  • 💾 Database Persistence - Optional subscription data storage in your database
  • Flexible Architecture - Use Better-Auth endpoints OR direct server-side functions

📦 Installation

npm install @creem_io/better-auth better-auth creem

Required Dependencies

  • better-auth ^1.3.34 (peer dependency)
  • creem ^0.4.0 (included)
  • zod ^3.23.8 (included)

🚀 Quick Start

Required setup

Get your Creem API Key from the dashboard, under the 'Developers' menu. Ensure you are using the api-key from the correct environment: Important: Test-Mode have different API-Keys than Production.

Server Setup

Create your Better Auth configuration with the Creem plugin:

// lib/auth.ts
import { betterAuth } from "better-auth";
import { creem } from "@creem_io/better-auth";

export const auth = betterAuth({
  database: {
    // your database config
  },
  plugins: [
    creem({
      apiKey: process.env.CREEM_API_KEY,
      testMode: true, // Use test mode for development
  ],
});

Note: Webhooks are only enabled if you provide a valid webhook secret.

// lib/auth.ts
import { betterAuth } from "better-auth";
import { creem } from "@creem_io/better-auth";

export const auth = betterAuth({
  database: {
    // your database config
  },
  plugins: [
    creem({
      apiKey: process.env.CREEM_API_KEY,
      webhookSecret: process.env.CREEM_WEBHOOK_SECRET,
      testMode: true, // Use test mode for development
  ],
});

Using persistSubscriptions automatically synchronizes your subscription data with your database.
Read more about the database schema that Creem creates in your application automatically below.

// lib/auth.ts
import { betterAuth } from "better-auth";
import { creem } from "@creem_io/better-auth";

export const auth = betterAuth({
  database: {
    // your database config
  },
  plugins: [
    creem({
      apiKey: process.env.CREEM_API_KEY!,
      webhookSecret: process.env.CREEM_WEBHOOK_SECRET,
      testMode: true, // Use test mode for development
      defaultSuccessUrl: "/success",
      persistSubscriptions: true, // Enable database persistence (default: true)
    }),
  ],
});
// lib/auth.ts
import { betterAuth } from "better-auth";
import { creem } from "@creem_io/better-auth";

export const auth = betterAuth({
  database: {
    // your database config
  },
  plugins: [
    creem({
      apiKey: process.env.CREEM_API_KEY!,
      webhookSecret: process.env.CREEM_WEBHOOK_SECRET,
      testMode: true, // Use test mode for development
      defaultSuccessUrl: "/success",
      persistSubscriptions: true, // Enable database persistence (default: true)

      // Optional: Webhook handlers
      onGrantAccess: async ({ customer, product, metadata, reason }) => {
        const userId = metadata?.referenceId as string;
        console.log(`Granting access (${reason}) to ${customer.email}`);
        // Update your database to grant access
      },

      onRevokeAccess: async ({ customer, product, metadata, reason }) => {
        const userId = metadata?.referenceId as string;
        console.log(`Revoking access (${reason}) from ${customer.email}`);
        // Update your database to revoke access
      },
    }),
  ],
});
// lib/auth.ts
import { betterAuth } from "better-auth";
import { creem } from "@creem_io/better-auth";

export const auth = betterAuth({
  database: {
    // your database config
  },
  plugins: [
    creem({
      apiKey: process.env.CREEM_API_KEY!,
      webhookSecret: process.env.CREEM_WEBHOOK_SECRET,
      testMode: true, // Use test mode for development
      defaultSuccessUrl: "/success",
    }),
  ],
});

Client Setup (Option 1: Standard)

// lib/auth-client.ts
import { createAuthClient } from "better-auth/react";
import { creemClient } from "@creem_io/better-auth/client";

export const authClient = createAuthClient({
  baseURL: process.env.NEXT_PUBLIC_APP_URL,
  plugins: [creemClient()],
});

Client Setup (Option 2: Improved TypeScript Support for React)

For even better TypeScript support with cleaner IntelliSense:

// lib/auth-client.ts
import { createCreemAuthClient } from "@creem_io/better-auth/create-creem-auth-client";
import { creemClient } from "@creem_io/better-auth/client";

export const authClient = createCreemAuthClient({
  baseURL: process.env.NEXT_PUBLIC_APP_URL,
  plugins: [creemClient()],
});

// Now you get the cleanest possible type hints!

The createCreemAuthClient wrapper improves TypeScript parameter types and autocomplete. Note: It is primarily designed for use with the Creem plugin and may not support all other better-auth plugins. If you encounter any issues, please open an issue or pull request at https://github.com/armitage-labs/creem-betterauth.

Migrate the Database

If you’re using database persistence (persistSubscriptions: true), generate the database schema:

npx @better-auth/cli generate

Or run migrations:

npx @better-auth/cli migrate

See the Schema section for manual setup. Depending on your database adapter, additional setup steps may be required. Refer to the BetterAuth documentation for adapter-specific instructions: https://www.better-auth.com/docs/adapters/mysql

Set Up Webhooks

  1. Create a webhook endpoint in your Creem dashboard pointing to:
https://your-domain.com/api/auth/creem/webhook

(/api/auth is the default path for the Better Auth server)

  1. Copy the webhook signing secret and set it in your .env file as CREEM_WEBHOOK_SECRET.

  2. (Optional) For local development and testing, use a tool like ngrok to expose your local server. Add the public ngrok URL to your Creem dashboard webhook settings.

💻 Usage

Client-Side (Better Auth Endpoints)

Create Checkout

Create a checkout session for a product. The plugin automatically attaches the authenticated user's email.

"use client";

import { authClient } from "@/lib/auth-client";
import type { CreateCheckoutInput } from "@creem_io/better-auth";

export function SubscribeButton({ productId }: { productId: string }) {
  const handleCheckout = async () => {
    const { data, error } = await authClient.creem.createCheckout({
      productId, // Required
    });

    if (data?.url) {
      //Redirect user to checkout
      window.location.href = data.url;
    }
  };

  return <button onClick={handleCheckout}>Subscribe Now</button>;
}

You can also access advanced checkout features directly through the endpoint:

"use client";

import { authClient } from "@/lib/auth-client";
import type { CreateCheckoutInput } from "@creem_io/better-auth";

export function SubscribeButton({ productId }: { productId: string }) {
  const handleCheckout = async () => {
    const { data, error } = await authClient.creem.createCheckout({
      productId, // Required
      units: 1, // Optional, defaults to 1
      successUrl: "/pro-plan/thank-you", // Optional
      discountCode: "SUMMER2024", // Optional
      metadata: { foo: "bar", icecream, "smooth" } // Optional: Arbitrary key-value pair you can set from your application
    });

    if (data?.url) {
      window.location.href = data.url;
    }
  };

  return <button onClick={handleCheckout}>Subscribe Now</button>;
}

Parameters:

  • productId (required) - The Creem product ID
  • units - Number of units (default: 1)
  • successUrl - Success redirect URL
  • discountCode - Discount code to apply
  • customer - Customer info (defaults to session user)
  • metadata - Additional metadata (auto-includes user ID as referenceId)
  • requestId - Idempotency key

Create Customer Portal

Open the Creem customer portal where users can manage subscriptions (Uses logged-in user):

const handlePortal = async () => {
  const { data, error } = await authClient.creem.createPortal();

  if (data?.url) {
    window.location.href = data.url;
  }
};

Cancel Subscription

If database persistence is enabled, the subscription for the logged-in user is found automatically. Otherwise, you must provide the subscription ID in the request.

const handleCancel = async (subscriptionId: string) => {
  const { data, error } = await authClient.creem.cancelSubscription({
    id: subscriptionId,
  });

  if (data?.success) {
    console.log(data.message);
  }
};

Retrieve Subscription

If database persistence is enabled, the subscription will be retrieved automatically for the logged-in user. Otherwise, you must provide the subscription ID in the request.

const getSubscription = async (subscriptionId: string) => {
  const { data } = await authClient.creem.retrieveSubscription({
    id: subscriptionId,
  });

  if (data) {
    console.log(`Status: ${data.status}`);
    console.log(`Product: ${data.product.name}`);
    console.log(`Price: ${data.product.price} ${data.product.currency}`);
  }
};

Search Transactions

By default, uses the logged-in user's creemCustomerId. If not available, it will use the customerId provided in the request body.

const { data } = await authClient.creem.searchTransactions({
  customerId: "cust_abc123", // Optional
  productId: "prod_xyz789", // Optional
  pageNumber: 1, // Optional
  pageSize: 50, // Optional
});

if (data?.transactions) {
  data.transactions.forEach((tx) => {
    console.log(`${tx.type}: ${tx.amount} ${tx.currency}`);
  });
}

Check Access

Check whether the currently logged-in user has an active subscription for the current period. This function requires database persistence to be enabled.

For example, if a user purchases a yearly plan and cancels after one month, this function will still return true as long as the current date is within the active subscription period that was paid for.

const { data } = await authClient.creem.hasAccessGranted();

if (data?.hasAccess) {
  // User has active subscription
}

🖥️ Server-Side Utilities

Use these functions directly in Server Components, Server Actions, or API routes without going through Better Auth endpoints. These functions can be used independently from your plugin configuration. You may specify different options, such as a separate API key or test mode, when calling them.

Import Server Utilities

import {
  createCreemClient,
  createCheckout,
  createPortal,
  cancelSubscription,
  retrieveSubscription,
  searchTransactions,
  checkSubscriptionAccess,
  getActiveSubscriptions,
  isActiveSubscription,
  formatCreemDate,
  getDaysUntilRenewal,
  validateWebhookSignature,
} from "@creem_io/better-auth/server";

Server Component Example

// app/dashboard/page.tsx
import { checkSubscriptionAccess } from "@creem_io/better-auth/server";
import { auth } from "@/lib/auth";
import { headers } from "next/headers";
import { redirect } from "next/navigation";

export default async function DashboardPage() {
  const session = await auth.api.getSession({ headers: await headers() });

  if (!session?.user) {
    redirect('/login');
  }

  // Database mode (when persistSubscriptions: true)
  const status = await checkSubscriptionAccess(
    {
      apiKey: process.env.CREEM_API_KEY!,
      testMode: true
    },
    {
      database: auth.options.database,
      userId: session.user.id
    }
  );

  if (!status.hasAccess) {
    redirect('/subscribe');
  }

  return (
    <div>
      <h1>Dashboard</h1>
      <p>Status: {status.status}</p>
      {status.expiresAt && (
        <p>Renews: {status.expiresAt.toLocaleDateString()}</p>
      )}
    </div>
  );
}

Server Action Example

// app/actions.ts
"use server";

import { createCheckout } from "@creem_io/better-auth/server";
import { auth } from "@/lib/auth";
import { headers } from "next/headers";
import { redirect } from "next/navigation";

export async function startCheckout(productId: string) {
  const session = await auth.api.getSession({ headers: await headers() });

  if (!session?.user) {
    throw new Error("Not authenticated");
  }

  const { url } = await createCheckout(
    {
      apiKey: process.env.CREEM_API_KEY!,
      testMode: true,
    },
    {
      productId,
      customer: { email: session.user.email },
      successUrl: "/success",
      metadata: { userId: session.user.id },
    },
  );

  redirect(url);
}

Middleware Example

// middleware.ts
import { checkSubscriptionAccess } from "@creem_io/better-auth/server";
import { auth } from "@/lib/auth";
import { NextRequest, NextResponse } from "next/server";

export async function middleware(request: NextRequest) {
  const session = await auth.api.getSession({
    headers: request.headers,
  });

  if (!session?.user) {
    return NextResponse.redirect(new URL("/login", request.url));
  }

  const status = await checkSubscriptionAccess(
    {
      apiKey: process.env.CREEM_API_KEY!,
      testMode: true,
    },
    {
      database: auth.options.database,
      userId: session.user.id,
    },
  );

  if (!status.hasAccess) {
    return NextResponse.redirect(new URL("/subscribe", request.url));
  }

  return NextResponse.next();
}

export const config = {
  matcher: ["/dashboard/:path*"],
};

Utility Functions

import {
  isActiveSubscription,
  formatCreemDate,
  getDaysUntilRenewal,
} from "@creem_io/better-auth/server";

// Check if status grants access
if (isActiveSubscription(subscription.status)) {
  // User has access
}

// Format Creem timestamps
const renewalDate = formatCreemDate(subscription.next_billing_date);
console.log(renewalDate.toLocaleDateString());

// Calculate days until renewal
const days = getDaysUntilRenewal(subscription.current_period_end_date);
console.log(`Renews in ${days} days`);

Custom Webhook Handler

// app/api/webhooks/custom/route.ts
import { validateWebhookSignature } from "@creem_io/better-auth/server";

export async function POST(req: Request) {
  const payload = await req.text();
  const signature = req.headers.get("creem-signature");

  if (
    !validateWebhookSignature(
      payload,
      signature,
      process.env.CREEM_WEBHOOK_SECRET!,
    )
  ) {
    return new Response("Invalid signature", { status: 401 });
  }

  const event = JSON.parse(payload);
  // Custom webhook handling logic

  return Response.json({ received: true });
}

🔄 Database Mode vs API Mode

The plugin supports two operational modes:

Database Mode (Recommended)

When persistSubscriptions: true (default), subscription data is stored in your database.

Benefits:

  • ✅ Fast access checks (no API calls)
  • ✅ Offline access to subscription data
  • ✅ Query subscriptions with SQL
  • ✅ Automatic sync via webhooks

Usage:

// lib/auth.ts
import { betterAuth } from "better-auth";
import { creem } from "@creem_io/better-auth";

export const auth = betterAuth({
  database: {
    // your database config
  },
  plugins: [
    creem({
      apiKey: process.env.CREEM_API_KEY!,
      testMode: true, // Use test mode for development
      persistSubscriptions: true, // Enable database persistence (default: true)
  ],
});

📊 Schema

When persistSubscriptions: true, the plugin creates these database tables:

subscription Table

| Field | Type | Description | | --------------------- | ------- | --------------------- | | id | string | Primary key | | productId | string | Creem product ID | | referenceId | string | Your user/org ID | | creemCustomerId | string | Creem customer ID | | creemSubscriptionId | string | Creem subscription ID | | creemOrderId | string | Creem order ID | | status | string | Subscription status | | periodStart | date | Period start date | | periodEnd | date | Period end date | | cancelAtPeriodEnd | boolean | Cancel flag |

user Table Extension

| Field | Type | Description | | ----------------- | ------ | ---------------------------- | | creemCustomerId | string | Links user to Creem customer |

API Mode

When persistSubscriptions: false, all data comes directly from Creem API.

Benefits:

  • ✅ No database schema needed
  • ✅ Simpler setup

Limitations:

  • ⚠️ Requires API call for each check
  • ⚠️ Some features require custom implementation

Usage:

// lib/auth.ts
import { betterAuth } from "better-auth";
import { creem } from "@creem_io/better-auth";

export const auth = betterAuth({
  database: {
    // your database config
  },
  plugins: [
    creem({
      apiKey: process.env.CREEM_API_KEY!,
      testMode: true, // Use test mode for development
      persistSubscriptions: false, // Enable database persistence (default: true)
  ],
});

Note: In API mode, some functions like checkSubscriptionAccess and getActiveSubscriptions have limited functionality and may require custom implementation with the Creem SDK.

🪝 Webhook Handling

The plugin provides two types of webhook handlers:

1. Event-Specific Handlers

Handle specific webhook events with all properties flattened:

creem({
  apiKey: process.env.CREEM_API_KEY!,
  webhookSecret: process.env.CREEM_WEBHOOK_SECRET!,

  onCheckoutCompleted: async (data) => {
    const {
      webhookEventType, // "checkout.completed"
      webhookId,
      product,
      customer,
      order,
      subscription,
    } = data;

    console.log(`${customer.email} purchased ${product.name}`);
  },

  onSubscriptionActive: async (data) => {
    const { product, customer, status } = data;
    // Handle active subscription
  },
});

2. Access Control Handlers (Recommended)

Use high-level onGrantAccess and onRevokeAccess for simpler access management:

creem({
  apiKey: process.env.CREEM_API_KEY!,
  webhookSecret: process.env.CREEM_WEBHOOK_SECRET!,

  // Triggered for: active, trialing, and paid subscriptions
  onGrantAccess: async ({ reason, product, customer, metadata }) => {
    const userId = metadata?.referenceId as string;

    await db.user.update({
      where: { id: userId },
      data: { hasAccess: true, subscriptionStatus: reason },
    });

    console.log(`Granted ${reason} to ${customer.email}`);
  },

  // Triggered for: paused, expired, and canceled subscriptions considering current date and billing period end
  onRevokeAccess: async ({ reason, product, customer, metadata }) => {
    const userId = metadata?.referenceId as string;

    await db.user.update({
      where: { id: userId },
      data: { hasAccess: false, subscriptionStatus: reason },
    });

    console.log(`Revoked access (${reason}) from ${customer.email}`);
  },
});

Grant Reasons:

  • subscription_active - Subscription is active
  • subscription_trialing - Subscription is in trial
  • subscription_paid - Subscription payment received

Revoke Reasons:

  • subscription_paused - Subscription paused
  • subscription_expired - Subscription expired

⚙️ Configuration Options

Main Options

interface CreemOptions {
  /** Creem API key (required) */
  apiKey: string;

  /** Webhook secret for signature verification */
  webhookSecret?: string;

  /** Use test mode (default: false) */
  testMode?: boolean;

  /** Default success URL for checkouts */
  defaultSuccessUrl?: string;

  /** Persist subscription data to database (default: true) */
  persistSubscriptions?: boolean;

  // Webhook Handlers
  onCheckoutCompleted?: (data: FlatCheckoutCompleted) => void; // Great for One Time Payments
  onRefundCreated?: (data: FlatRefundCreated) => void;
  onDisputeCreated?: (data: FlatDisputeCreated) => void;
  onSubscriptionActive?: (
    data: FlatSubscriptionEvent<"subscription.active">,
  ) => void;
  onSubscriptionTrialing?: (
    data: FlatSubscriptionEvent<"subscription.trialing">,
  ) => void;
  onSubscriptionCanceled?: (
    data: FlatSubscriptionEvent<"subscription.canceled">,
  ) => void;
  onSubscriptionPaid?: (
    data: FlatSubscriptionEvent<"subscription.paid">,
  ) => void;
  onSubscriptionExpired?: (
    data: FlatSubscriptionEvent<"subscription.expired">,
  ) => void;
  onSubscriptionUnpaid?: (
    data: FlatSubscriptionEvent<"subscription.unpaid">,
  ) => void;
  onSubscriptionUpdate?: (
    data: FlatSubscriptionEvent<"subscription.update">,
  ) => void;
  onSubscriptionPastDue?: (
    data: FlatSubscriptionEvent<"subscription.past_due">,
  ) => void;
  onSubscriptionPaused?: (
    data: FlatSubscriptionEvent<"subscription.paused">,
  ) => void;

  // Access Control (High-level)
  onGrantAccess?: (context: GrantAccessContext) => void | Promise<void>;
  onRevokeAccess?: (context: RevokeAccessContext) => void | Promise<void>;
}

📚 Type Exports

Server-Side Types

import type {
  CreemOptions,
  GrantAccessContext,
  RevokeAccessContext,
  GrantAccessReason,
  RevokeAccessReason,
  FlatCheckoutCompleted,
  FlatRefundCreated,
  FlatDisputeCreated,
  FlatSubscriptionEvent,
} from "@creem_io/better-auth";

Client-Side Types

import type {
  CreateCheckoutInput,
  CreateCheckoutResponse,
  CheckoutCustomer,
  CreatePortalInput,
  CreatePortalResponse,
  CancelSubscriptionInput,
  CancelSubscriptionResponse,
  RetrieveSubscriptionInput,
  SubscriptionData,
  SearchTransactionsInput,
  SearchTransactionsResponse,
  TransactionData,
  HasAccessGrantedResponse,
} from "@creem_io/better-auth";

Server Utility Types

import type { CreemServerConfig } from "@creem_io/better-auth/server";

🎯 TypeScript Tips

  1. Hover for Documentation - Hover over any method to see full JSDoc documentation
  2. Autocomplete - Let TypeScript suggest available options
  3. Type Inference - Response types are automatically inferred
  4. Import Types - Import types explicitly when needed for function parameters

🌍 Environment Variables

# Required
CREEM_API_KEY=your_api_key_here

# Optional
CREEM_WEBHOOK_SECRET=your_webhook_secret_here

🔧 Troubleshooting

Webhook Issues

  • Check webhook URL is correct in Creem dashboard
  • Verify webhook signing secret matches
  • Ensure all necessary events are selected
  • Check server logs for errors

Subscription Status Issues

  • Make sure webhooks are being received
  • Check creemCustomerId and creemSubscriptionId fields are populated
  • Verify reference IDs match between app and Creem

Testing Webhooks Locally

Use a tool like ngrok:

# Using ngrok
ngrok http 3000

# Then use the ngrok URL in Creem dashboard:
# https://abc123.ngrok.io/api/auth/creem/webhook

Database Mode Not Working

  • Ensure persistSubscriptions: true (default)
  • Run migrations: npx @better-auth/cli migrate
  • Check database connection
  • Verify schema tables exist

API Mode Limitations

Some functions require database mode:

  • checkSubscriptionAccess with userId
  • getActiveSubscriptions with userId

Either enable database mode or implement custom logic with the Creem SDK directly.

📖 Additional Resources

📄 License

MIT

🤝 Support

For issues or questions: