@usequota/nextjs
v4.0.0
Published
Next.js SDK for Quota — AI credit billing middleware, hooks, and components
Downloads
950
Readme
@usequota/nextjs
Next.js SDK for Quota — AI credit wallet and multi-provider inference API.
Full docs: usequota.ai/docs
The three integration paths
Pick one. This SDK supports all three.
| You want… | Use | Who pays |
| --- | --- | --- |
| To try the API, or to absorb AI cost yourself | API key (sk-quota-…) | Your developer wallet |
| A new app, no auth yet — Quota provides identity and a wallet per user | <QuotaSignInButton /> (recipe) | The signed-in user's wallet |
| An existing app with its own login — attach Quota credits to your users | <QuotaConnectButton /> (recipe) | The connected user's wallet |
This README covers the SDK surface (createQuotaRouteHandlers, hooks,
components, server helpers). For end-to-end walkthroughs, go to the docs.
Installation
npm install @usequota/nextjsQuick Start
1. Set up environment variables
# .env.local
QUOTA_CLIENT_ID=your_client_id
QUOTA_CLIENT_SECRET=your_client_secret
NEXT_PUBLIC_QUOTA_CLIENT_ID=your_client_id2. Mount the route handlers
Create lib/quota.ts and re-export from each route file. createQuotaRouteHandlers
mints handlers for authorize, callback, me, logout, status, packages,
checkout, and disconnect in one call — supports PKCE, HMAC-signed state,
and OIDC id_token verification out of the box.
// lib/quota.ts
import { createQuotaRouteHandlers } from "@usequota/nextjs/server";
export const {
authorize,
callback,
me,
logout,
status,
packages,
checkout,
disconnect,
} = createQuotaRouteHandlers({
clientId: process.env.QUOTA_CLIENT_ID!,
clientSecret: process.env.QUOTA_CLIENT_SECRET!,
});// app/api/quota/callback/route.ts
export { callback as GET } from "@/lib/quota";
// app/api/quota/authorize/route.ts
export { authorize as GET } from "@/lib/quota";
// (repeat for me, logout, status, packages, checkout, disconnect)3. Add QuotaProvider
Wrap your app with the provider in app/layout.tsx:
import { QuotaProvider } from "@usequota/nextjs";
import "@usequota/nextjs/styles.css"; // Required for pre-built UI components
export default function RootLayout({ children }) {
return (
<html>
<body>
<QuotaProvider clientId={process.env.NEXT_PUBLIC_QUOTA_CLIENT_ID!}>
{children}
</QuotaProvider>
</body>
</html>
);
}4. Create API routes
Create the following API routes:
app/api/quota/me/route.ts - Fetch user data:
import { getQuotaUser } from "@usequota/nextjs/server";
export async function GET() {
const user = await getQuotaUser({
clientId: process.env.QUOTA_CLIENT_ID!,
clientSecret: process.env.QUOTA_CLIENT_SECRET!,
});
if (!user) {
return new Response("Unauthorized", { status: 401 });
}
return Response.json(user);
}app/api/quota/logout/route.ts - Handle logout:
import { clearQuotaAuth } from "@usequota/nextjs/server";
export async function POST() {
await clearQuotaAuth();
return Response.json({ success: true });
}app/api/quota/packages/route.ts - List available credit packages
(required for useQuotaPackages and <QuotaBuyCredits />; proxies the
public /v1/packages endpoint server-side so the browser request stays
same-origin):
// Re-export the packages handler from your createQuotaRouteHandlers() setup.
// See the "Route Handlers" section below for the createQuotaRouteHandlers call.
export { packages as GET } from "@/lib/quota";5. Use in components
"use client";
import { useQuota } from "@usequota/nextjs";
export function MyComponent() {
const { user, isLoading, login } = useQuota();
if (isLoading) return <div>Loading...</div>;
if (!user) return <button onClick={login}>Sign in with Quota</button>;
return (
<div>
<p>Welcome, {user.email}!</p>
<p>Balance: ${(user.balance / 1_000_000).toFixed(2)}</p>
</div>
);
}Features
- OAuth 2.0 Authentication - Secure user authentication flow
- Token Management - Automatic token refresh and cookie management
- React Hooks - Convenient hooks for auth state and user data
- Server-Side Utilities - Functions for API routes and server components
- TypeScript Support - Full type safety with TypeScript
- Hosted Mode - Optional server-side token storage for enhanced security
Choosing your sign-in component
The SDK ships two pre-built buttons. They look similar but solve different problems — pick by what your app needs from Quota.
<QuotaSignInButton /> — IdP pattern
Use when Quota IS your auth. One click signs the user in to your app
and gives them a spendable AI balance. Default scopes:
openid email profile credits.spend. Your app gets a verified id_token
and never has to build a user table, password reset flow, or payment flow.
"use client";
import { QuotaSignInButton, useQuota } from "@usequota/nextjs";
export default function Home() {
const { user } = useQuota();
return user ? <p>Hi, {user.email}</p> : <QuotaSignInButton />;
}<QuotaConnectButton /> — wallet-linking pattern
Use when your app already has its own auth (NextAuth / Auth0 / Clerk /
custom) and you just want to attach Quota credits to existing users. The
button assumes the user is already signed in — it does not authenticate
them. The scopes carried on the resulting access token are controlled by
your createQuotaRouteHandlers({ scopes }) config — pass
["credits.spend"] for a wallet-only token (no OIDC, no id_token).
"use client";
import { QuotaConnectButton } from "@usequota/nextjs";
export function BillingSettings() {
// Render this on your settings page after your own auth is satisfied.
return <QuotaConnectButton />;
}Decision shortcut
| You're building… | Use |
| -------------------------------------------------------------------- | ------------------------- |
| A brand-new app, no existing user table | <QuotaSignInButton /> |
| An existing app with its own auth, wants Quota credits on top | <QuotaConnectButton /> |
| You'll pay for AI usage yourself (not your users) | No button — use a server-side API key |
Both buttons are first-class and supported. <QuotaConnectButton /> is
not deprecated — it's the right tool for the wallet-linking pattern. Full
decision tree and rationale: usequota.ai/docs.
API Reference
Client-Side
Hooks
useQuota()- Access full Quota contextuseQuotaUser()- Get current useruseQuotaAuth()- Auth state and actionsuseQuotaBalance()- User credit balanceuseQuotaPackages()- List available credit packages (requiresapp/api/quota/packages/route.ts)
Components
<QuotaProvider>- React context provider<MockQuotaProvider>- Drop-in replacement that injects a fixed auth state (see Mock provider)
Server-Side
Import from @usequota/nextjs/server:
getQuotaUser(config)- Get authenticated userrequireQuotaAuth(config)- Require auth (redirects if not logged in)getQuotaPackages(config?)- Fetch available credit packagescreateQuotaCheckout(config)- Create Stripe checkout sessionclearQuotaAuth(config?)- Clear auth cookiescreateQuotaRouteHandlers(config)- Generate all 6 API route handlers in one callwithQuotaAuth(config, handler)- Wrap a route handler with automatic Quota auth
Typed Errors
QuotaError- Base error class (code,statusCode,hint)QuotaInsufficientCreditsError- 402, includesbalanceandrequiredQuotaNotConnectedError- 401, user has no Quota account connectedQuotaTokenExpiredError- 401, token expired and refresh failedQuotaRateLimitError- 429, includesretryAfter(seconds)
Route-level sign-in gating
withQuotaIdentity(options)- Next.js middleware preset that gates routes behind the auth cookies set bycreateQuotaRouteHandlers. Reads cookies only — it does NOT handle OAuth itself.
Advanced Usage
Custom OAuth Flow
const { login } = useQuota();
// Trigger OAuth flow
login();Server Components
import { getQuotaUser } from "@usequota/nextjs/server";
export default async function DashboardPage() {
const user = await getQuotaUser({
clientId: process.env.QUOTA_CLIENT_ID!,
clientSecret: process.env.QUOTA_CLIENT_SECRET!,
});
if (!user) {
redirect("/");
}
return <div>Welcome, {user.email}!</div>;
}Protected Routes
import { requireQuotaAuth } from "@usequota/nextjs/server";
export default async function ProtectedPage() {
// Automatically redirects to login if not authenticated
const user = await requireQuotaAuth({
clientId: process.env.QUOTA_CLIENT_ID!,
clientSecret: process.env.QUOTA_CLIENT_SECRET!,
});
return <div>Protected content for {user.email}</div>;
}Credit Packages
"use client";
import { useState, useEffect } from "react";
import type { CreditPackage } from "@usequota/nextjs";
export function BuyCredits() {
const [packages, setPackages] = useState<CreditPackage[]>([]);
useEffect(() => {
fetch("/api/packages")
.then((res) => res.json())
.then(setPackages);
}, []);
const handleBuy = async (packageId: string) => {
const res = await fetch("/api/checkout", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ packageId }),
});
const { url } = await res.json();
window.location.href = url;
};
return (
<div>
{packages.map((pkg) => (
<button key={pkg.id} onClick={() => handleBuy(pkg.id)}>
Buy {pkg.credits} credits for {pkg.price_display}
</button>
))}
</div>
);
}Mock provider
<MockQuotaProvider> is a drop-in replacement for <QuotaProvider> that injects a fixed auth state instead of fetching from your API routes. Use it in docs sites, Storybook decorators, customer playgrounds, and unit tests — anywhere you want to render real SDK components against a fake user without standing up an OAuth client or a backend.
import { MockQuotaProvider, QuotaBalance, QuotaUserMenu } from "@usequota/nextjs";
const fakeUser = {
id: "usr_demo",
email: "[email protected]",
balance: 5_000_000, // $5.00
};
export function HeaderPreview() {
return (
<MockQuotaProvider user={fakeUser}>
<QuotaBalance format="dollars" />
<QuotaUserMenu />
</MockQuotaProvider>
);
}Pass user={null} to render the signed-out branch. login(), logout(), and refetch() are no-ops in mock mode, so click handlers won't navigate or fetch — perfect for demos and snapshot tests.
Props
| Prop | Type | Default | Purpose |
| ----------- | ------------------------------------------ | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| user | QuotaUser \| null | seeded mock user | Override the authenticated user. null renders the signed-out branch. |
| claims | IdTokenClaims \| null | null | Verified OIDC id_token claims. Set this to render IdP-mode components (anything reading useQuota().claims) against the mock. |
| idToken | string \| null | null | Raw id_token JWT for components that read useQuota().idToken directly (e.g. forwarding identity to your own backend). |
| isLoading | boolean | false | Force the loading branch. |
| error | Error \| null | null | Force the error branch. |
| baseUrl | string | https://api.usequota.ai | Base URL — also used to decide which /v1/packages fetch to intercept (see packages below). |
| packages | CreditPackage[] \| Promise<...> \| undefined | undefined | When set, intercepts ${baseUrl}/v1/packages fetches and returns this list. Lets <QuotaBuyCredits />'s zero-config picker render without a live API. |
The default mock user is { id: "usr_mock_001", email: "[email protected]", balance: 5_000_000 } (5,000,000 credits = $5.00). DEFAULT_MOCK_PACKAGES is also exported — the canonical 4-package set (starter / basic / plus / pro) matching what production /v1/packages returns.
Storybook decorator pattern (parameters.quota)
Wire the mock provider into every story via a global decorator, then let each story configure its own auth and packages state through parameters.quota. This is how @usequota/nextjs's own Storybook is set up.
// .storybook/preview.tsx
import type { Preview } from "@storybook/react";
import { MockQuotaProvider } from "@usequota/nextjs";
const preview: Preview = {
decorators: [
(Story, context) => (
<MockQuotaProvider {...(context.parameters.quota || {})}>
<Story />
</MockQuotaProvider>
),
],
};
export default preview;Each story sets its own parameters.quota:
// MyHeader.stories.tsx
import type { Meta, StoryObj } from "@storybook/react";
import { DEFAULT_MOCK_PACKAGES } from "@usequota/nextjs";
import { MyHeader } from "./MyHeader";
const meta: Meta<typeof MyHeader> = { component: MyHeader };
export default meta;
export const SignedIn: StoryObj<typeof MyHeader> = {
parameters: {
quota: {
user: { id: "u_1", email: "[email protected]", balance: 5_000_000 },
packages: DEFAULT_MOCK_PACKAGES, // so <QuotaBuyCredits /> picker renders
},
},
};
export const SignedOut: StoryObj<typeof MyHeader> = {
parameters: { quota: { user: null } },
};The packages parameter accepts an array, a Promise<CreditPackage[]> (loading simulation), or a rejected promise (error simulation) — see the PickerOpen / PickerLoading / PickerError stories on <QuotaBuyCredits /> for the full pattern.
Webhook Verification
Quota can send webhooks to notify your application about events. The SDK provides utilities to verify webhook signatures and handle events securely.
Set up webhook endpoint
Add your webhook secret to .env.local:
QUOTA_WEBHOOK_SECRET=your_webhook_secretCreate webhook handler
app/api/quota/webhook/route.ts:
import { createWebhookHandler } from "@usequota/nextjs";
export const POST = createWebhookHandler(process.env.QUOTA_WEBHOOK_SECRET!, {
"balance.low": async (event) => {
// Send email notification when user balance is low
await sendLowBalanceEmail(event.data.user_id, event.data.current_balance);
},
"user.connected": async (event) => {
// Track new user connection
await analytics.track("user_connected", event.data);
},
"usage.completed": async (event) => {
// Log usage for analytics
await logUsage(event.data);
},
});Webhook event types
Quota sends the following webhook events:
user.connected- User completed OAuth connectionuser.disconnected- User disconnected their accountbalance.updated- User's credit balance changedbalance.low- User's balance fell below thresholdusage.completed- API request completed and billed
Manual signature verification
For custom implementations:
import { verifyWebhookSignature, parseWebhook } from "@usequota/nextjs";
export async function POST(req: Request) {
try {
// Option 1: Parse and verify in one step
const event = await parseWebhook(req, process.env.QUOTA_WEBHOOK_SECRET!);
// Option 2: Manual verification
const payload = await req.text();
const signature = req.headers.get("x-quota-signature")!;
const isValid = verifyWebhookSignature({
payload,
signature,
secret: process.env.QUOTA_WEBHOOK_SECRET!,
});
if (!isValid) {
return Response.json({ error: "Invalid signature" }, { status: 400 });
}
const event = JSON.parse(payload);
// Handle event...
return Response.json({ received: true });
} catch (error) {
return Response.json({ error: error.message }, { status: 400 });
}
}Route Handler Factory
createQuotaRouteHandlers generates all API route handlers for Quota integration in one call, replacing 6 separate route files with boilerplate.
// lib/quota.ts
import { createQuotaRouteHandlers } from "@usequota/nextjs/server";
export const {
authorize, // GET - initiates OAuth flow
callback, // GET - handles OAuth callback
status, // GET - returns connection status + balance
packages, // GET - returns credit packages
checkout, // POST - creates Stripe checkout session
disconnect, // POST - disconnects user's Quota account
} = createQuotaRouteHandlers({
clientId: process.env.QUOTA_CLIENT_ID!,
clientSecret: process.env.QUOTA_CLIENT_SECRET!,
});Then create thin route files:
// app/api/quota/authorize/route.ts
export { authorize as GET } from "@/lib/quota";
// app/api/quota/callback/route.ts
export { callback as GET } from "@/lib/quota";
// app/api/quota/status/route.ts
export { status as GET } from "@/lib/quota";
// app/api/quota/packages/route.ts
// Required for useQuotaPackages and <QuotaBuyCredits />. The handler
// proxies the public /v1/packages endpoint server-side so the browser
// request stays same-origin (no CORS config needed on api.usequota.ai).
export { packages as GET } from "@/lib/quota";
// app/api/quota/checkout/route.ts
export { checkout as POST } from "@/lib/quota";withQuotaAuth
Wraps a route handler with automatic Quota authentication, token refresh, and error handling:
// app/api/summarize/route.ts
import { withQuotaAuth } from "@usequota/nextjs/server";
export const POST = withQuotaAuth(
{
clientId: process.env.QUOTA_CLIENT_ID!,
clientSecret: process.env.QUOTA_CLIENT_SECRET!,
},
async (request, { user, accessToken }) => {
// user is guaranteed to exist
// accessToken can proxy requests through Quota
const response = await fetch(
"https://api.usequota.ai/v1/chat/completions",
{
method: "POST",
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model: "gpt-4o-mini",
messages: [{ role: "user", content: "Hello" }],
}),
},
);
return Response.json(await response.json());
},
);Typed Errors
Handle specific failure modes with instanceof checks:
import {
QuotaInsufficientCreditsError,
QuotaNotConnectedError,
QuotaRateLimitError,
QuotaError,
} from "@usequota/nextjs/server";
try {
await callQuotaAPI();
} catch (e) {
if (e instanceof QuotaInsufficientCreditsError) {
console.log(e.balance, e.required); // current balance, credits needed
}
if (e instanceof QuotaNotConnectedError) {
// redirect to login
}
if (e instanceof QuotaRateLimitError) {
await delay(e.retryAfter * 1000); // seconds until retry
}
if (e instanceof QuotaError) {
console.log(e.code, e.statusCode, e.hint);
}
}Contributing
Any change under src/ (except tests) requires a version bump in
package.json. See agent/WORKFLOWS.md → SDK Versioning
for semver guidance and the bump+publish sequence. A CI check fails any
PR that touches src without bumping. This package depends on
@usequota/core and @usequota/types; when those see a major bump,
update the ^X.Y.Z ranges here in lockstep.
License
MIT
