Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
65 commits
Select commit Hold shift + click to select a range
49989e1
refactor: adding adapters folder to work on redis resolver and rework…
EggerMarc Oct 22, 2025
2b3228f
refactor(adapters): queries library
EggerMarc Oct 22, 2025
b02401e
feat(adapter): getUsage now syncs if necessary
EggerMarc Oct 22, 2025
3ae1b0f
fix(endpoints): import fixes
EggerMarc Oct 22, 2025
0cda39f
feat(redis): lua script and redis object
EggerMarc Oct 23, 2025
c7d99dd
feat(redis): create usage cache
EggerMarc Oct 23, 2025
9470b39
feat(realtime): add realtime support
EggerMarc Oct 30, 2025
173c77f
📝 CodeRabbit Chat: Add comprehensive Bun unit tests for core modules
coderabbitai[bot] Oct 30, 2025
5ea9450
📝 CodeRabbit Chat: Add comprehensive unit tests for cache, queries, a…
coderabbitai[bot] Oct 30, 2025
cf2f2ee
📝 CodeRabbit Chat: Add comprehensive Bun unit test suite for package
coderabbitai[bot] Oct 30, 2025
bd133a1
CodeRabbit Generated Unit Tests: Add comprehensive test suite with do…
coderabbitai[bot] Oct 30, 2025
b3522e3
📝 CodeRabbit Chat: Add comprehensive Bun unit test suite
coderabbitai[bot] Oct 30, 2025
ba9dd52
📝 Add docstrings to `optimization`
coderabbitai[bot] Oct 30, 2025
45b7790
Delete TEST_SUMMARY.md
EggerMarc Oct 30, 2025
bd8234b
Merge branch 'optimization' into coderabbitai/utg/5ea9450
EggerMarc Oct 30, 2025
9b8f267
Merge pull request #7 from EggerMarc/coderabbitai/utg/5ea9450
EggerMarc Oct 30, 2025
b77657a
Merge pull request #9 from EggerMarc/coderabbitai/docstrings/b3522e3
EggerMarc Oct 30, 2025
abdf632
merge
EggerMarc Oct 30, 2025
c5db8ec
refactor(tests): tests are now in their dedicated folder
EggerMarc Oct 30, 2025
3bacdb6
refactor: add resolveGetUsage, some code rabbit comment fixes
EggerMarc Oct 30, 2025
de95458
fix: comment fixes
EggerMarc Oct 30, 2025
d29897f
fix: more comment fixes, and better caching on adapter and endpoint c…
EggerMarc Oct 30, 2025
52a2b81
fix: on message now also inserts and event to cache
EggerMarc Oct 31, 2025
755fb99
refactor(resolvers): some resolver refactoring
EggerMarc Oct 31, 2025
35ba4d9
refactor(adapter): queries handle only db write/read. perform only th…
EggerMarc Oct 31, 2025
f952bac
refactor(resolvers): resolvers now handle the bulk logic and cache + …
EggerMarc Oct 31, 2025
4ddb523
feat(resolvers): add error handling at the resolver and endpoint layer
EggerMarc Oct 31, 2025
7465b47
chore(resolvers): handling errors in all resolvers
EggerMarc Oct 31, 2025
dce80b0
CodeRabbit Generated Unit Tests: Add comprehensive test suite for res…
coderabbitai[bot] Oct 31, 2025
3fa9ea6
📝 Add docstrings to `optimization-refactor`
coderabbitai[bot] Oct 31, 2025
7c66b3d
Merge pull request #12 from EggerMarc/coderabbitai/docstrings/7465b47
EggerMarc Oct 31, 2025
b1124cc
Merge pull request #11 from EggerMarc/coderabbitai/utg/7465b47
EggerMarc Nov 1, 2025
8adb7fa
fix: add customerMiddleware to check customer existance on dependent …
EggerMarc Nov 1, 2025
7b5a3ca
Merge pull request #10 from EggerMarc/optimization-refactor
EggerMarc Nov 1, 2025
a1d66d2
chore(examples): nextjs scaffold
EggerMarc Nov 2, 2025
200a2b9
chore(example): drizzle and better auth for nextjs example
EggerMarc Nov 3, 2025
c7e8c5a
chore(example): auth schema migration
EggerMarc Nov 3, 2025
0128f09
chore(example): usage setup
EggerMarc Nov 3, 2025
2eff896
fix: better-auth update
EggerMarc Nov 3, 2025
ddc59d8
chore: add counter to example, update bau's version, customer check e…
EggerMarc Nov 4, 2025
4cbc09c
merge
EggerMarc Nov 4, 2025
339dd0e
fix: merge fixes
EggerMarc Nov 4, 2025
40611f9
fix(endpoints): get adapters within endpoint body, not plugin init
EggerMarc Nov 4, 2025
b36b45e
fix(cache): a bunch of cache fixes, shape missmatches. we still have …
EggerMarc Nov 5, 2025
c8a1fc5
chore: add claude
EggerMarc Mar 21, 2026
edf7a98
chore: add claude
EggerMarc Mar 21, 2026
5679f73
chore: rm DS_Store and .claude
EggerMarc Mar 21, 2026
98c5ae5
chore: add next steps and issues
EggerMarc Mar 22, 2026
c280fdb
Merge pull request #13 from EggerMarc/optimization-example
EggerMarc Mar 22, 2026
7f2b1af
fix: add all build files to .gitignore
EggerMarc Mar 22, 2026
e6004db
fix: usage infrastructure now initialized once and not per request
EggerMarc Mar 22, 2026
9cb748c
Update package/endpoints/get-feature.ts
EggerMarc Mar 22, 2026
49efdde
Update package/endpoints/consume-feature.ts
EggerMarc Mar 22, 2026
f2a8880
fix: some docs alignments and better logging
EggerMarc Mar 22, 2026
9e6465e
Update package/adapters/cache.ts
EggerMarc Mar 22, 2026
90f6d58
Update examples/nextjs/middleware.ts
EggerMarc Mar 22, 2026
e228842
fix: coderabbit fixes
EggerMarc Mar 22, 2026
55e13fe
Merge branch 'optimization' of https://github.com/eggermarc/better-au…
EggerMarc Mar 22, 2026
d84d679
fix: coderabbit fixes
EggerMarc Mar 22, 2026
302acc4
Update package/resolvers/reset-usage.ts
EggerMarc Mar 22, 2026
319e4b9
fix: even more coderabbit fixes
EggerMarc Mar 22, 2026
f19a875
fix: singleton fixes for test cases, better e2e testing
EggerMarc Mar 22, 2026
bdcf6cd
chore: pass all tests
EggerMarc Mar 22, 2026
835ba2b
Merge pull request #14 from EggerMarc/test/optimization
EggerMarc Mar 22, 2026
95a8419
feat: optimizations
EggerMarc Mar 22, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,11 @@
node_modules
dist
.next

*.lock
package-lock.json
bun.lockb

**.DS_Store

**.claude
314 changes: 314 additions & 0 deletions AGENTS.md

Large diffs are not rendered by default.

164 changes: 120 additions & 44 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,93 +1,129 @@
# @eggermarc/better-auth-usage

**⚠️ Warning!** This package is a **work in progress**! Expect breaking changes and functionality changes.
**Warning!** This package is a **work in progress**! Expect breaking changes and functionality changes.

Feature and usage-based authorization plugin for [BetterAuth](https://www.better-auth.com/). Provides a way to define **features**, **track usage**, apply **per-plan limits**, and integrate with external systems (Stripe, custom hooks, etc).

## Roadmap
Below are the action items to fix known limitations of this plugin. Namely, customer management and consumption indepotency.
Below are the action items to fix known limitations of this plugin. Namely, customer management and consumption idempotency.
- [x] Customer table
- [ ] Customer to feature table (for customer limits)
- [ ] Feature
- [ ] Follow slowly changing dimensions model
- [x] Consumption adapter as transaction
- [ ] Non blocking syncUsage
- [x] Redis caching with Lua-based atomic increments
- [x] Real-time WebSocket updates (optional)
- [x] Integration test suite (DB-only + cached)
- [x] Comprehensive E2E and unit test coverage (284 tests)
- [ ] Customer provider (Optional - considering leaving this to dev)
- [ ] `useCustomer(referenceId)`
- [ ] Per-customer feature limits (`featureLimits`)
- [ ] Consumption idempotency keys


## Features

- Define features with maxLimit, minLimit, reset strategies, and metadata.
- Apply customer-specific overrides (e.g. different limits per customer).
- Apply plan-specific overrides (e.g. different limits per plan).
- Hook into usage events (before and after).
- Add custom authorization logic with authorizeReference.
- Optional Redis caching with atomic Lua-based increments.
- Optional real-time usage tracking via WebSocket (Socket.IO).

### Installation
```bash
npm add @eggermarc/better-auth-usage
# or
bun add @eggermarc/better-auth-usage
# or
pnpm add @eggermarc/better-auth-usage
```

### Usage
#### Server
```typescript
// server
import { betterAuth } from "better-auth";
import { usage } from "@eggermarc/better-auth-usage";

export const auth = betterAuth({
plugins: [usage({
features: {
"token-feature": {
key: "token-feature",
maxLimit: 1000,
reset: "monthly",
resetValue: 0,
details: ["Number of tokens per month"],
}
},
overrides: {
"starter-plan": {
"token-feature":{
maxLimit: 10_000,
hooks: {
after: async ({ usage, customer, feature }) => {
console.log(
`[AFTER HOOK] ${customer.referenceId} used ${usage.amount} of ${feature.key}`
);
features: {
"token-feature": {
maxLimit: 10_000,
stripeId: "price_xxx", // Can declare extra fields
hooks: {
after: async ({ usage, customer, feature }) => {
console.log(
`[AFTER HOOK] ${customer.referenceId} used ${usage.amount} of ${feature.key}`
);
},
},
},
stripeId: env.TOKEN_STARTER_ID // Can declare new fields
},
},
"pro-plan": {
"token-features": {
maxLimit: 1_000_000,
hooks: {
after: async ({ usage, customer, feature }) => {
console.log(
`[AFTER HOOK] ${customer.referenceId} used ${usage.amount} of ${feature.key}`
);
},
features: {
"token-feature": {
maxLimit: 1_000_000,
},
}
},
},
}
},
// Optional: enable Redis caching
// cacheOptions: {
// redisUrl: process.env.REDIS_URL!,
// enableRealtime: true, // optional WebSocket support
// port: 3001, // required if enableRealtime is true
// },
})]
})
```

#### Client
```ts
// client.ts
import { createAuthClient } from "better-auth/client";
import { usageClient } from "@eggermarc/better-auth-usage/client";

export const client = createAuthClient({
plugins: [usageClient()],
});
```

### Customer Registration

// Example: consume usage, in your app
**Important:** A customer must be registered before consuming usage. The consume endpoint requires the customer to exist.

```ts
// Register a customer first
await client.usage.upsertCustomer({
referenceId: "123",
referenceType: "user",
name: "John Doe",
email: "john@example.com",
});

// Then consume usage
await client.usage.consume({
featureKey: "token-feature",
overrideKey: "starter-plan",
referenceId: "123",
amount: 1,
});

// Check current usage and limits
const status = await client.usage.check({
featureKey: "token-feature",
referenceId: "123",
});
// => { status: "in-limit", currentAmount: 1, maxLimit: 1000, minLimit: undefined }
```

### Goals
Expand All @@ -98,17 +134,14 @@ Why customer registration and not per user / organization query?
#### Examples
##### Team based
```ts
const teamLimits = getTeamLimits(teamId, "token-feature")

const customer: Customer = {
referenceId: teamId, // Team ID,
const customer = {
referenceId: teamId,
referenceType: "team",
email: session.user.email, // User in team email
email: session.user.email,
name: `${session.user.name}@${teamName}`,
// WIP featureLimits: new Record("token-feature", teamLimits)
}

await client.usage.registerCustomer(customer)
await client.usage.upsertCustomer(customer)

await client.usage.consume({
featureKey: "token-feature",
Expand All @@ -119,19 +152,62 @@ await client.usage.consume({
```
##### Session based / IP based
```ts
const customer: Customer = {
referenceId: session.session.ipAddress ?? session.session.id,
referenceType: "session",
}

await client.usage.registerCustomer(customer)
const referenceId = session.session.ipAddress ?? session.session.id;

await client.usage.upsertCustomer({
referenceId,
referenceType: "session",
})

await client.usage.consume({
featureKey: "token-feature",
referenceId: session.session.ipAddress ?? session.session.id,
amount: 1
referenceId,
amount: 1,
})
```

In this current version, we apply to all customers the root limits, then overrides, and finally with customer specific overrides.
### API Endpoints

| Endpoint | Method | Auth | Description |
|----------|--------|------|-------------|
| `/usage/features` | GET | none | List all registered features |
| `/usage/features/{featureKey}` | GET | none | Get a single feature config (with optional override) |
| `/usage/consume` | POST | session | Consume/increment usage for a feature |
| `/usage/check` | POST | session | Check current usage vs limits (with optional preview amount) |
| `/usage/check-customer` | POST | session | Get customer details by referenceId |
| `/usage/upsert-customer` | POST | session | Create or update a customer |
| `/usage/sync` | POST | none | Manually trigger reset if due |

### Override Structure

Overrides require a nested `features` key:

```ts
overrides: {
"plan-name": {
features: { // <-- required
"feature-key": {
maxLimit: 10_000, // overrides the base feature's maxLimit
// any Feature field except `key` can be overridden
},
},
},
}
```

The `overrideKey` is passed per-request to `consume`, `check`, or `sync` endpoints to apply the override for that specific call.

### Reset Strategies

Features can specify a `reset` interval to automatically zero out usage:

| Reset | Boundary |
|-------|----------|
| `"hourly"` | Start of next hour |
| `"6-hourly"` | Next 6-hour block (00:00, 06:00, 12:00, 18:00) |
| `"daily"` | Tomorrow at 00:00 |
| `"weekly"` | Next Monday at 00:00 |
| `"monthly"` | 1st of next month at 00:00 |
| `"quarterly"` | 1st of next quarter at 00:00 |
| `"yearly"` | January 1st of next year at 00:00 |
| `"never"` | Never resets |
60 changes: 60 additions & 0 deletions TESTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Testing Guide

This document provides information about the test suite for the better-auth-usage plugin.

## Overview

The test suite uses **Bun's built-in test runner** and covers all major components including:

- Utility functions (date handling, limit checking, error handling)
- Cache adapter (Redis-based caching)
- Database query adapters (get, insert, reset operations)
- Real-time usage tracking (pub/sub)
- WebSocket server functionality

## Running Tests

### Run All Tests
```bash
bun test
```

### Run Tests in Watch Mode
```bash
bun test --watch
```

### Run Tests with Coverage
```bash
bun test --coverage
```

### Run Specific Test File
```bash
bun test package/__tests__/utils.test.ts
```

## Test Coverage Summary

- **8 test files** created covering all changed components
- **200+ individual test cases** across the codebase
- Tests for happy paths, edge cases, and error conditions

## Available Test Commands

- `bun test` - Run all tests once
- `bun test --watch` - Run tests in watch mode (re-run on file changes)
- `bun test --coverage` - Run tests with coverage report

## Test Files Created

1. **utils.test.ts** - Tests for utility functions (checkLimit, shouldReset, tryCatch)
2. **adapters/cache.test.ts** - Tests for Redis cache adapter
3. **adapters/index.test.ts** - Integration tests for main adapter
4. **adapters/queries/get-usage.test.ts** - Tests for get usage query
5. **adapters/queries/insert-usage.test.ts** - Tests for insert usage query
6. **adapters/queries/reset-usage.test.ts** - Tests for reset usage query
7. **realtime/usage-tracker.test.ts** - Tests for real-time usage tracker
8. **realtime/websocket-server.test.ts** - Tests for WebSocket server

All tests follow Bun's testing conventions and best practices.
Loading