Documentation
Plan limits
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 mutationLimits 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:
| Status | Meaning |
|---|---|
allowed | Mutation may proceed (limit: null = unlimited) |
limit_reached | Would exceed the plan limit |
unavailable | Could not resolve billing summary |
Shipped enforcement
| Feature | Boundary | Notes |
|---|---|---|
| Links | createLinkAction | Update/delete are not blocked at limit |
| Storage | createUploadAction + confirmUploadAction | Pre-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
- Add a
LimitIdand limit value toPLAN_CATALOGinconfig/plans.ts. - Export a
measure(userId)function from your feature queries. - Register the metric in
<feature>/usage-metric.ts. - Call
checkLimit()in the server mutation before persisting new quota-consuming state. - Pass
currentusage 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.
Related docs
- Payments (Polar) — billing summary and entitlements
- Storage — upload lifecycle
src/lib/security/README.md— security defaults