Creem official Better-Auth plugin
npm install @creem_io/better-authOfficial Creem Better-Auth plugin for seamless payments and subscription management.
- π 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
``bash`
npm install @creem_io/better-auth better-auth creem
- better-auth ^1.3.34 (peer dependency)creem
- ^0.4.0 (included)zod
- ^3.23.8 (included)
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.
Create your Better Auth configuration with the Creem plugin:
`typescript
// 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
],
});
`
Optional: Add your webhook secret to securely receive and verify webhooks from Creem
Note: Webhooks are only enabled if you provide a valid webhook secret.
`typescript
// 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
],
});
`
Recommended: Enable
persistSubscriptions for automatic database sync
Using persistSubscriptions automatically synchronizes your subscription data with your database.
Read more about the database schema that Creem creates in your application automatically below.
`typescript
// 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: Add webhook handlers to run your own logic when certain events occur
`typescript
// 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
},
}),
],
});
Optional: Set a default success URL to redirect your customers after checkout
`typescript
// 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",
}),
],
});
`
`typescript
// 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()],
});
`
For even better TypeScript support with cleaner IntelliSense:
`typescript
// 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.
If youβre using database persistence (persistSubscriptions: true), generate the database schema:
`bash`
npx @better-auth/cli generate
Or run migrations:
`bash`
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
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)
2. Copy the webhook signing secret and set it in your .env file as CREEM_WEBHOOK_SECRET.
3. (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.
#### Create Checkout
Create a checkout session for a product. The plugin automatically attaches the authenticated user's email.
`typescript
"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 ;
}
`
You can also access advanced checkout features directly through the endpoint:
`typescript
"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 ;
}
`
Parameters:
- productId (required) - The Creem product IDunits
- - Number of units (default: 1)successUrl
- - Success redirect URLdiscountCode
- - Discount code to applycustomer
- - 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):
`typescript
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.
`typescript
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.
`typescript
const getSubscription = async (subscriptionId: string) => {
const { data } = await authClient.creem.retrieveSubscription({
id: subscriptionId,
});
if (data) {
console.log(Status: ${data.status});Product: ${data.product.name}
console.log();Price: ${data.product.price} ${data.product.currency}
console.log();`
}
};
#### Search Transactions
By default, uses the logged-in user's creemCustomerId. If not available, it will use the customerId provided in the request body.
`typescript
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.
`typescript
const { data } = await authClient.creem.hasAccessGranted();
if (data?.hasAccess) {
// User has active subscription
}
`
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
`typescript`
import {
createCreemClient,
createCheckout,
createPortal,
cancelSubscription,
retrieveSubscription,
searchTransactions,
checkSubscriptionAccess,
getActiveSubscriptions,
isActiveSubscription,
formatCreemDate,
getDaysUntilRenewal,
validateWebhookSignature,
} from "@creem_io/better-auth/server";
#### Server Component Example
`typescript
// 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 (
Status: {status.status}
Renews: {status.expiresAt.toLocaleDateString()}
#### Server Action Example
`typescript
// 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
`typescript
// 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
`typescript
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
`typescript
// 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:
$3
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:
`typescript
// 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:$3
| 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 |$3
| Field | Type | Description |
| ----------------- | ------ | ---------------------------- |
|
creemCustomerId | string | Links user to Creem customer |
$3
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:
`typescript
// 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:
$3
Handle specific webhook events with all properties flattened:
`typescript
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
},
});
`$3
Use high-level
onGrantAccess and onRevokeAccess for simpler access management:`typescript
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 receivedRevoke Reasons:
-
subscription_paused - Subscription paused
- subscription_expired - Subscription expiredβοΈ Configuration Options
$3
`typescript
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;
onRevokeAccess?: (context: RevokeAccessContext) => void | Promise;
}
`π Type Exports
$3
`typescript
import type {
CreemOptions,
GrantAccessContext,
RevokeAccessContext,
GrantAccessReason,
RevokeAccessReason,
FlatCheckoutCompleted,
FlatRefundCreated,
FlatDisputeCreated,
FlatSubscriptionEvent,
} from "@creem_io/better-auth";
`$3
`typescript
import type {
CreateCheckoutInput,
CreateCheckoutResponse,
CheckoutCustomer,
CreatePortalInput,
CreatePortalResponse,
CancelSubscriptionInput,
CancelSubscriptionResponse,
RetrieveSubscriptionInput,
SubscriptionData,
SearchTransactionsInput,
SearchTransactionsResponse,
TransactionData,
HasAccessGrantedResponse,
} from "@creem_io/better-auth";
`$3
`typescript
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
`env
Required
CREEM_API_KEY=your_api_key_hereOptional
CREEM_WEBHOOK_SECRET=your_webhook_secret_here
`π§ Troubleshooting
$3
- Check webhook URL is correct in Creem dashboard
- Verify webhook signing secret matches
- Ensure all necessary events are selected
- Check server logs for errors
$3
- Make sure webhooks are being received
- Check
creemCustomerId and creemSubscriptionId fields are populated
- Verify reference IDs match between app and Creem$3
Use a tool like ngrok:
`bash
Using ngrok
ngrok http 3000Then use the ngrok URL in Creem dashboard:
https://abc123.ngrok.io/api/auth/creem/webhook
`$3
- Ensure
persistSubscriptions: true (default)
- Run migrations: npx @better-auth/cli migrate
- Check database connection
- Verify schema tables exist$3
Some functions require database mode:
-
checkSubscriptionAccess with userId
- getActiveSubscriptions with userId`Either enable database mode or implement custom logic with the Creem SDK directly.
- Creem Documentation
- Better-Auth Documentation
- GitHub Repository
MIT
For issues or questions:
- Open an issue on GitHub
- Contact Creem support at support@creem.io