Show users their AI usage: a Next.js dashboard that warns before the 402
A server-rendered usage page that shows requests against the plan cap, tokens, and sub-cent costs, so users see the limit before a 402 tells them.
A quota that users cannot see is a quota they discover by hitting it. They are mid-task, the request fails with a 402, and the first time they learn the plan has a limit is the moment it stops them. A usage page fixes that cheaply: if a user can watch the bar fill up, hitting the cap stops being a surprise and the upgrade button is right there beside it. Here is the dashboard the kit ships, and the few decisions that make it trustworthy.
Do the real session check in the page
The App Router proxy only does an optimistic cookie check — it bounces users with no session cookie, but it cannot validate the session. So the dashboard, a server component, does the real check itself before reading any data, then loads usage and plan in parallel:
export default async function DashboardPage() {
// The real, DB-backed session check (proxy.ts only did the cookie check).
const session = await auth.api.getSession({ headers: await headers() });
if (!session) redirect("/sign-in?redirect=/dashboard");
let usage: MonthlyUsage | null = null;
let plan: Plan = FREE_PLAN;
if (hasDatabase()) {
[usage, plan] = await Promise.all([
getMonthlyUsage(session.user.id),
getUserPlan(session.user.id),
]);
}
const usedPct = usage
? Math.min(100, Math.round((usage.requests / plan.monthlyRequestLimit) * 100))
: 0;
// ...render plan, the usage bar, and the stats
}
Why the cookie check is not enough on its own is covered in the Better Auth setup gotchas. Because this is a server component, the numbers are fetched on the server per request — there is no usage API route to secure separately, and nothing is cached across users.
Show the number the quota actually enforces
The bar compares this month's request count against the plan's monthlyRequestLimit, and that is deliberate: it is the exact number the chat route checks before returning 402. The dashboard and the quota both call the same rollup over the usage table, so they cannot disagree — the user never sees "40 of 100" while the route thinks they are at the limit. That shared query, and the index that keeps it fast, are in tracking LLM costs per user. Beside the bar sits the call to action that matches the plan: free users get "Upgrade," paid users get "Manage billing," which opens the Stripe portal.
Format money for AI-sized amounts
AI requests cost fractions of a cent, so a currency formatter with the usual two decimals turns a light month into "$0.00" and makes the cost readout look broken. The kit stores costs as integer millionths of a dollar and formats with up to four decimal places:
const usd = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
minimumFractionDigits: 2,
maximumFractionDigits: 4,
});
A maxed-out free month on Haiku — about 25 short requests — reads as "$0.0875" instead of "$0.09" or "$0.00" — small, but it is the difference between a number users trust and one they ignore.
Things that bite
- Calendar month, not billing period. The rollup counts from the first of the month in UTC, so every user's quota resets on the 1st — not on the anniversary of their subscription, even though the webhook stores
current_period_end. That is simple and predictable, but say it on the page, or a user who subscribed on the 28th will feel short-changed. - Show tokens and cost, but cap on requests. Users understand "requests"; tokens and dollars are context. Keep the enforced unit front and center so the bar means what the 402 means.
- Handle the unconfigured case. Without a database there is no usage to show, so the page renders a short setup notice instead of a broken chart — the same deliberate fallback as the rest of the kit's lazy, guarded clients.
- Do not cache this page. Usage changes with every request; a statically cached dashboard shows yesterday's bar to the person about to hit today's limit.
Shipwright — the Next.js and Claude starter kit this blog documents — ships this dashboard wired to the same metering and quota as the chat route, so what users see is exactly what is enforced. Start from the live demo.