Skip to content

Integrations

Integrate Role-Based Access Control

RBAC gives you one place to say what a role can do and one way to check it. Permissions ride along inside the session, so an authorisation check is a local array lookup rather than another network call — which is what makes it cheap enough to put on every route.

Before you begin

  • An organization model. Roles are scoped to an organization, so the same person can be an admin in one tenant and a viewer in another.
  • A list of the actions in your product that are worth protecting. Permissions describe actions, not screens.
  • A default role for new members. Most products want the least privileged one.
  • A decision about session lifetime, because a permission change takes effect when the session next refreshes.

Install

Every example on this page uses the Node.js client. The same calls exist in the Python, Go, Ruby, PHP and Java SDKs, following the naming conventions of each language.

terminal
1npm i @paycux/node

Configure

RBAC needs no additional credentials. What it does need is a naming convention for permission slugs, chosen before you have fifty of them.

.env.local
1PAYCUX_API_KEY=sk_example_7f4c1b9ad2e84c6f
2PAYCUX_CLIENT_ID=client_01HQZX8N4T
3PAYCUX_COOKIE_PASSWORD=a-32-character-or-longer-secret

Then, in the dashboard:

  1. 1Open Roles and define your permission slugs first. A resource:action shape such as invoices:read reads well in code and sorts sensibly in a list.
  2. 2Create the roles your product actually has. Three is usually enough to start; a role per customer request is how this becomes unmanageable.
  3. 3Assign permissions to each role, and mark one role as the default for new members.
  4. 4Decide which roles an organization admin may hand out themselves, and which stay yours.
  5. 5Check the session settings, since a shorter access token lifetime means a revoked permission takes effect sooner.

Implement

There are two places to enforce a permission: in the session, for anything the user does in your product, and against the API, for background work where no session exists. Do both, and never rely on hiding a button.

Read permissions from the session

The authenticated session carries the role and the resolved permission list for the active organization. This is the check that belongs on every protected route.

lib/authz.ts
1import { Paycux } from "@paycux/node";
2import { cookies } from "next/headers";
3
4const paycux = new Paycux(process.env.PAYCUX_API_KEY);
5
6export async function requirePermission(permission: string) {
7 const jar = await cookies();
8 const sealed = jar.get("px_session")?.value;
9 if (!sealed) throw new Error("unauthenticated");
10
11 const session = paycux.userManagement.loadSealedSession({
12 sessionData: sealed,
13 cookiePassword: process.env.PAYCUX_COOKIE_PASSWORD,
14 });
15
16 const result = await session.authenticate();
17 if (!result.authenticated) throw new Error("unauthenticated");
18
19 if (!result.permissions.includes(permission)) {
20 throw new Error("forbidden");
21 }
22
23 return { user: result.user, organizationId: result.organizationId, role: result.role };
24}

Guard a route

Fail closed. An unknown permission slug, an expired session and a missing cookie should all end in the same refusal rather than in an unhandled exception.

app/api/invoices/route.ts
1import { requirePermission } from "@/lib/authz";
2
3export async function GET() {
4 try {
5 const { organizationId } = await requirePermission("invoices:read");
6 const invoices = await listInvoices(organizationId);
7 return Response.json(invoices);
8 } catch (error) {
9 const status = error.message === "forbidden" ? 403 : 401;
10 return Response.json({ error: error.message }, { status });
11 }
12}

Change someone's role

Role changes belong to your admin surface. Update the membership, then revoke the affected sessions so the new role applies immediately rather than at the next refresh.

app/api/members/role/route.ts
1import { Paycux } from "@paycux/node";
2import { requirePermission } from "@/lib/authz";
3
4const paycux = new Paycux(process.env.PAYCUX_API_KEY);
5
6export async function POST(request: Request) {
7 await requirePermission("members:manage");
8 const { membershipId, roleSlug } = await request.json();
9
10 const membership = await paycux.userManagement.updateOrganizationMembership({
11 organizationMembershipId: membershipId,
12 roleSlug,
13 });
14
15 await paycux.userManagement.revokeSessions({ userId: membership.userId });
16
17 return Response.json({ ok: true, role: membership.role.slug });
18}

Verify

A permissions test is only meaningful if it asserts a refusal. Verify what a viewer cannot do before you celebrate what an admin can.

  1. 1Sign in as a member with the default role and confirm a protected route returns 403 rather than an error page.
  2. 2Promote that member to admin, sign in again and confirm the same route now succeeds.
  3. 3Call the protected API directly with the member's session, bypassing your UI. It must still refuse.
  4. 4Remove a permission from a role while a session is open and confirm the change applies once the session refreshes.
  5. 5Open Roles in the dashboard and confirm the permission list matches the slugs your code checks — a typo in a slug fails open in most hand-rolled systems, and this is where you catch it.

Reference

Request and response shapes for each of these live in the API reference. Paths are relative to https://api.paycux.com.

MethodPathWhat it does
GET/rolesList the roles defined in the environment.
POST/rolesCreate a role with a slug, name and permission set.
PUT/roles/:idUpdate a role's name or its permissions.
DELETE/roles/:idDelete a role that no membership is using.
GET/permissionsList permission slugs and their descriptions.
PUT/user_management/organization_memberships/:idChange the role attached to a membership.
GET/user_management/organization_membershipsList memberships with their roles for one organization.

Next steps