BlogMetering

The usage bar that sells the upgrade (build it with a public key in an afternoon)

An invisible limit feels like a trap. A visible one feels like a fuel gauge. Same quota, same plan, same user - and a measurably different reaction when the wall finally arrives.

Surprise limits punish your best users

Think about who actually hits your free-tier cap: your most engaged free users - the exact people most likely to pay. If the first time they learn the limit exists is the moment it blocks them, the product just punished enthusiasm. The same enforcement lands completely differently when the user watched the meter fill all week: the limit becomes a known boundary they chose to approach, the wall becomes an expected event, and the upgrade becomes the obvious continuation of something going well rather than a toll booth that appeared out of nowhere.

The public key exists for exactly this

Each Vevee app ships a pk_live key alongside the secret one, and the public key can do precisely one thing: read a user’s own usage. Safe to embed in client code, useless for tracking or admin operations even if extracted. That scoping means the usage bar needs no backend route, no proxy endpoint, no session plumbing - the component fetches the numbers directly. The response carries every counter for the user’s current period: quota, count, remaining, the human label you set in the plan builder, and the period window itself.

// Client-side - pk key, reads this user's usage only
import { createClient } from '@vevee/sdk';

const vevee = createClient({ apiKey: process.env.NEXT_PUBLIC_VEVEE_PK! });

export async function UsageBar({ userId }: { userId: string }) {
  const { counters, period } = await vevee.usage(userId);
  return counters.map(c => (
    <Meter key={c.groupId} label={c.label}
      value={c.count} max={c.quota}
      hint={`resets ${formatDate(period?.end)}`} />
  ));
}

Render the reset date, not just the fraction

The single highest-leverage detail in a usage UI is the reset date. "38 of 50" creates anxiety; "38 of 50, resets June 14" creates a decision: wait six days or upgrade now. The usage response includes the actual period window - computed from the plan’s period and the subscription’s anchor - so the date you render is the date the counter genuinely rolls over, even for users whose cycle is pinned to a Stripe renewal mid-month. Never hardcode "the 1st"; for most of your users that date is wrong.

The 80% color change is your softest paywall

A meter that turns amber at 80% is an upgrade prompt that no one resents, because it is information, not interruption. Pair the threshold with a quiet inline link - "Running hot this month? Pro removes the cap" - and you have built the top of an upgrade funnel inside a status display. This composes with server-side nudges: the bar makes the situation visible, the personalized nudge makes the pitch, the paywall enforces the boundary. Three touches, escalating, all driven by the same counters.

Transparency is a conversion strategy

There is a fear that showing limits prominently scares users away, and the indie-SaaS experience keeps contradicting it: users trust products that show the meter, and trusted products get the upgrade. The bar also quietly kills a whole category of support ticket - "why was I blocked?" answers itself when the user watched themselves approach the line. Total cost: one component, one public key, one read endpoint you didn’t have to build. It is the cheapest conversion surface in your entire product, and most apps still don’t have one.

See your own numbers in two minutes.Add the SDK and your users, costs and revenue fill in on the next request.
Join waitlist

Keep reading

ShareCopy a link to this post.

Set your first limit in about five minutes.

50k operations free every month. No card required.
Join waitlist