Skip to content

React

@vperms/react re-exports the client factory plus React bindings. Importing @vperms/react resolves to the server entry under the react-server condition and to the client entry otherwise, so the same import works in both environments.

permissions.ts
import { createVPerms } from "@vperms/react";
export const vperms = createVPerms("https://api.example.com", {
subjectResolver: async () => currentUser,
});

createVPerms(origin, config) returns a ServerVPerms whose getResolvedSubject and getAbility are wrapped in a request/render-scoped cache (React.cache by default). Every Server Component in the same render reuses one resolved subject.

import { vperms } from "@/permissions";
export default async function Page() {
const ability = await vperms.getAbility();
if (!ability.can("dashboard.read")) return null;
return (
<vperms.Provider>
<vperms.Ability permission="admin.read" fallback={<NoAccess />}>
<Dashboard />
</vperms.Ability>
</vperms.Provider>
);
}
interface ServerVPerms {
client: VPermsClient;
getResolvedSubject(subjectId?): Promise<ResolvedSubject>;
getAbility(subjectId?): Promise<PermissionAbility>;
Provider: (props: { children?: ReactNode }) => Promise<ReactNode>;
Ability: (props: AbilityProps) => Promise<ReactNode>;
}

The most recently created instance also becomes the module default used by the standalone exports getAbility, getResolvedSubject, Provider and ServerAbility (alias Ability). Calling them without an instance throws MISSING_INSTANCE_MESSAGE.

The server Provider resolves the subject once and hands the JSON-safe snapshot to the client AbilityProvider — the class instance never crosses the RSC boundary.

"use client";
import { vperms } from "@/permissions";
function CreateButton() {
const ability = vperms.useAbility();
return ability.can("projects.create") ? <button>Create</button> : null;
}

createVPerms on the client returns a ClientVPerms:

interface ClientVPerms {
client: VPermsClient;
getResolvedSubject: VPermsClient["getResolvedSubject"];
getAbility: VPermsClient["getAbility"];
Provider: typeof AbilityProvider;
Ability: typeof ClientAbility;
useAbility: typeof useAbility;
}

useAbility() throws MISSING_PROVIDER_MESSAGE outside a provider. AbilityProvider takes a ResolvedSubject snapshot and hydrates the ability synchronously for the subtree.

interface AbilityProps {
permission?: string;
permissions?: string[];
any?: boolean;
fallback?: ReactNode;
children?: ReactNode;
}

permission and permissions require every permission by default; set any to require at least one. Both short-circuit, and fallback renders when access is denied. abilityAllows(ability, props) is exported if you need the same logic without rendering.

<vperms.Ability permissions={["a.read", "b.read"]} any fallback={<NoAccess />}>
<Content />
</vperms.Ability>
  • Server: createVPerms, getAbility, getResolvedSubject, Provider, ServerAbility (alias Ability), abilityAllows, MISSING_INSTANCE_MESSAGE, plus the client’s createAbility, fetchResolvedSubject, PermissionAbility, VPermsHttpError and types.
  • Client: createVPerms, AbilityProvider, ClientAbility (alias Ability), getAbilityContext, useAbility, MISSING_PROVIDER_MESSAGE, plus the same client re-exports.

ServerAbility and ClientAbility are exported explicitly for advanced use.

Client-side permissions are for conditional rendering and UX only. They are not authoritative authorization and client state must never be trusted. Every sensitive operation must still be authorized on the server with VeguiPerms.