Documentation

Plan limits

Server-side plan limit enforcement using the billing catalog and checkLimit().

Open inChatGPT (opens in a new tab)Claude (opens in a new tab)Cursor (opens in a new tab)Server-side plan limit enforcement using the billing catalog and checkLimit().

Motoko Base defines numeric plan limits in src/lib/billing/config/plans.ts (PLAN_CATALOG). The Usage settings page displays progress against those limits. Enforcement happens server-side in feature mutations — never in the UI alone.

Architecture

PLAN_CATALOG.limits
   ↓
getBillingSummary(userId) → summaryGetLimit()
   ↓
checkLimit(userId, limitId, currentUsage, delta)
   ↓
Server Action / route handler rejects or allows mutation

Limits are resolved from the user's billing summary. Users without paid access (including past_due) receive Free-tier limits. When Polar is not configured, checkLimit() allows mutations so minimal-config local development keeps working.

checkLimit API

import { checkLimit } from "@/lib/billing";

const result = await checkLimit(userId, "links", currentCount, 1);

if (result.status === "limit_reached") {
  // reject mutation with user-facing message
}

if (result.status === "unavailable") {
  // billing lookup failed — fail closed with safe error
}

Outcomes:

StatusMeaning
allowedMutation may proceed (limit: null = unlimited)
limit_reachedWould exceed the plan limit
unavailableCould not resolve billing summary

Shipped enforcement

FeatureBoundaryNotes
LinkscreateLinkActionUpdate/delete are not blocked at limit
StoragecreateUploadAction + confirmUploadActionPre-check at init; authoritative check at confirm (presigned upload lifecycle)

Storage usage counts files rows with status = "ready", summing sizeBytes. Pending uploads are included in the init pre-check only.

Usage metrics registry

Usage display (src/lib/billing/usage.ts) reads metrics from a registry. Each feature registers its measure:

// src/features/links/usage-metric.ts
registerUsageMetric({
  id: "links",
  limitId: "links",
  label: "Links",
  measure: countLinksForUser,
});

Import @/features/dashboard/lib/bootstrap-usage-metrics (or the individual usage-metric.ts modules) before calling getUsageSnapshot() so metrics are registered.

Removing a demo: delete the feature folder and its usage-metric.ts. No edit to src/lib/billing/usage.ts is required.

Adding limits to a new feature

  1. Add a LimitId and limit value to PLAN_CATALOG in config/plans.ts.
  2. Export a measure(userId) function from your feature queries.
  3. Register the metric in <feature>/usage-metric.ts.
  4. Call checkLimit() in the server mutation before persisting new quota-consuming state.
  5. Pass current usage as a parameter — do not hardcode plan limits in the feature.

Concurrency

Links and storage use best-effort enforcement: two concurrent creates near the limit may both succeed briefly. This is acceptable for the starter and documented honestly. Strict enforcement would require transactions or per-user locks — add that in your product if you need hard guarantees.

Concurrent storage uploads follow the same model: the authoritative check runs at confirm/finalization, but two confirms near the limit may both pass under race conditions.

  • Payments (Polar) — billing summary and entitlements
  • Storage — upload lifecycle
  • src/lib/security/README.md — security defaults