PayKit Best Practices
General configuration reference for PayKit.
createPayKit Options
import { createPayKit } from "paykitjs"
export const paykit = createPayKit({
database: process.env.DATABASE_URL!,
provider: stripe({ ... }),
plans: [free, pro, enterprise],
basePath: "/paykit",
identify: async (request) => { ... },
on: { ... },
plugins: [dash()],
logging: { ... },
testing: { ... },
})| Option | Required | Description |
|---|---|---|
database | Yes | PostgreSQL connection string or pg.Pool instance |
provider | Yes | Payment provider config (e.g., stripe()) |
plans | No | Array of plans defined with plan() |
basePath | No | API route prefix. Default: "/paykit" |
identify | No | Resolve customer from HTTP request |
on | No | Event handlers |
plugins | No | Array of plugins |
logging | No | Logging configuration |
testing | No | Testing mode configuration |
Customer Identification
The identify function resolves a customer from an incoming HTTP request. Required for client-side SDK usage.
export const paykit = createPayKit({
// ...
identify: async (request) => {
const session = await getSession(request)
if (!session) return null
return {
customerId: session.user.id,
email: session.user.email,
name: session.user.name,
}
},
})Rules:
- Return
{customerId, email?, name?}ornull - When
identifyis set, client requests are authenticated through it - If the resolved
customerIddoesn't match a request's explicitcustomerId, PayKit rejects with 403 - Without
identify, client SDK methods won't work. Only server-side calls are available
Event Handlers
export const paykit = createPayKit({
// ...
on: {
"customer.updated": ({ payload }) => {
// Fires when subscriptions or entitlements change
console.log(`Customer ${payload.customerId} updated`)
console.log("Subscriptions:", payload.subscriptions)
},
"*": ({ event }) => {
// Wildcard: fires on every event
console.log(`Event: ${event.name}`)
},
},
})Use customer.updated to sync billing state to your app (e.g., update user roles, invalidate caches).
Database
PayKit uses PostgreSQL with Drizzle ORM internally. All tables are prefixed with paykit_.
Connection options:
// Connection string
database: "postgresql://user:pass@localhost:5432/mydb"
// pg.Pool instance (for connection pooling)
import pg from "pg"
database: new pg.Pool({ connectionString: process.env.DATABASE_URL })Schema management:
npx paykitjs push # Create tables + sync plans to DB and Stripe
npx paykitjs status # Check sync stateNever edit PayKit's database tables directly. Use the API methods.
Customer Management
// Create or update a customer
await paykit.upsertCustomer({
id: "user_123",
email: "jane@example.com",
name: "Jane Doe",
})
// Get customer with subscriptions and entitlements
const customer = await paykit.getCustomer({ id: "user_123" })
// List customers with pagination
const { data, total, hasMore } = await paykit.listCustomers({
limit: 50,
offset: 0,
planIds: ["pro"],
})
// Delete a customer
await paykit.deleteCustomer({ id: "user_123" })Note: upsertCustomer auto-subscribes to the default plan if the customer doesn't exist yet.
Type Inference
PayKit infers plan and feature IDs from your configuration:
// Access inferred types
type PlanId = typeof paykit.$infer.planId // "free" | "pro" | ...
type FeatureId = typeof paykit.$infer.featureId // "messages" | "pro_models" | ...
// Client inherits types from the instance
const client = createPayKitClient<typeof paykit>()
// client.subscribe({ planId: "pro" }) // planId is typedRoute Handler
Mount PayKit's API routes in your framework:
Next.js (App Router):
// app/paykit/[[...slug]]/route.ts
import { paykitHandler } from "paykitjs/handlers/next"
import { paykit } from "@/lib/paykit"
export const { GET, POST } = paykitHandler(paykit)The basePath option in createPayKit must match the route path.
Testing Mode
For tests, use testing mode to avoid hitting Stripe:
const paykit = createPayKit({
// ...
testing: {
enabled: true,
},
})Troubleshooting
| Problem | Fix |
|---|---|
| Client methods return 401 | identify is missing or returning null |
| Plans not showing in Stripe | Run npx paykitjs push |
| Tables not created | Run npx paykitjs push |
| Customer not auto-subscribed | Ensure a plan has default: true in its group |
basePath mismatch | createPayKit({basePath}) must match your route handler path |