Leave Localhost logoLeave LocalhostDocs
Multi-tenancy

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 KeyPurpose
feature.proGrants access to Pro-tier features
workspace.members.inviteAllows inviting additional members
workspace.members.limit.10Imposes a 10-member maximum
workspace.members.limit.unlimitedAllows unlimited members
billing.portalAllows 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

On this page