Capabilities
While Roles dictate what a user is allowed to do within an organization, Capabilities dictate what the organization itself is allowed to do based on its billing plan.
While Roles dictate what a user is allowed to do within an organization, Capabilities dictate what the organization itself is allowed to do based on its billing plan.
A user cannot perform an action unless their role permits it AND their organization has the required capability.
Defined Capabilities
Capabilities are defined in
packages/backend/convex/permissions/capabilities.ts:
| Capability Key | Purpose |
|---|---|
feature.pro | Grants access to Pro-tier features |
workspace.members.invite | Allows inviting additional members |
workspace.members.limit.10 | Imposes a 10-member maximum |
workspace.members.limit.unlimited | Allows unlimited members |
billing.portal | Allows opening the billing management portal |
How Capabilities are Granted
Capabilities are granted by the billing system. When a subscription starts or
a manual grant is created, rows are inserted into the billing_grants table.
For example, the pro_monthly plan in billing/plans.config.ts grants:
["feature.pro", "workspace.members.invite", "workspace.members.limit.10", "billing.portal"]
Checking Capabilities
You rarely check capabilities directly. Instead, you define an App Permission
that requires a capability, and use requireAppPermission().
1. Define the Permission
In permissions/appPermissions.ts:
"feature.pro.use": {
// Requires the user to have the "pro.use" statement (all roles except viewer)
betterAuth: { feature: ["pro.use"] },
// AND requires the organization to have the "feature.pro" capability
capabilities: ["feature.pro"],
},2. Enforce on the Backend
export const doProAction = mutation({
handler: async (ctx) => {
const actor = await requireAppPermission(ctx, {
permission: "feature.pro.use",
});
// Action proceeds only if the workspace has the feature.pro capability
}
});3. Check on the Frontend
import { useAppPermission } from "@/lib/auth/use-app-permission";
export function ProFeature() {
const canUsePro = useAppPermission("feature.pro.use");
if (!canUsePro) {
return <UpgradePrompt />;
}
return <ProComponent />;
}Direct Capability Checks
Sometimes you need to check capabilities directly, such as when evaluating limits rather than simple booleans.
const limit = getMemberLimitFromCapabilities(actor.capabilities);Or when returning the capability list to the client (which the
resolveOrganizationCapabilities internal query does during session hydration).
Next Reads
- Entitlements and Grants — how the billing system assigns capabilities.
- Roles and Permissions — role-based access.
Roles and Permissions
Leave Localhost implements Role-Based Access Control (RBAC) backed by Better Auth. Every member of an organization has a specific role that dictates what actions they can perform.
Workspace Demo
The starter includes a "Workspace Records" demo feature to illustrate how tenant-scoped data works in practice. It consists of a simple CRUD application for managing "Records" (like tasks or documents).