HelloJohn / docs
Organizations

Permissions

Role-based access control within organizations — built-in roles, custom roles, and permission checks.

Permissions

HelloJohn uses role-based access control (RBAC) within organizations. Every member has a role that determines what they can do.

Built-In Roles

RoleDescription
ownerFull control — manage members, settings, billing, delete org
adminManage members and settings, cannot delete org
memberStandard member access

These roles are available on all plans and cannot be renamed or removed.


Permissions by Role

Permissionmemberadminowner
Read org data
Read member list
Invite members
Remove members
Promote to admin
Update org settings
Update org metadata
Delete organization
Transfer ownership

Custom Roles (Pro+)

On the Pro plan and above, you can define custom roles with fine-grained permissions.

Creating a Custom Role

curl -X POST "https://api.hellojohn.dev/v1/organizations/org_01HABCDEF777666/roles" \
  -H "Authorization: Bearer sk_live_abc123" \
  -H "X-Tenant-ID: tnt_01HABCDEF654321" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Billing Manager",
    "slug": "billing-manager",
    "permissions": [
      "billing:read",
      "billing:write",
      "members:read"
    ]
  }'

Response: 201 Created

{
  "id": "role_01HABCDEF222111",
  "name": "Billing Manager",
  "slug": "billing-manager",
  "permissions": ["billing:read", "billing:write", "members:read"],
  "created_at": "2024-01-15T10:00:00Z"
}

Assigning a Custom Role

curl -X PATCH "https://api.hellojohn.dev/v1/organizations/org_01HABCDEF777666/members/usr_01HABCDEF789012" \
  -H "Authorization: Bearer sk_live_abc123" \
  -H "X-Tenant-ID: tnt_01HABCDEF654321" \
  -H "Content-Type: application/json" \
  -d '{"role": "billing-manager"}'

Role in JWT

The user's organization role is included in the JWT when an active organization is selected:

{
  "sub": "usr_01HABCDEF123456",
  "org_id": "org_01HABCDEF777666",
  "role": "admin",
  "permissions": ["billing:read", "members:read"]
}

Custom roles include a permissions array. Built-in roles (owner, admin, member) do not include a permissions array — use the role name directly.


Enforcing Permissions in Your Backend

Using Role

function requireRole(...roles: string[]) {
  return (req: Request, res: Response, next: NextFunction) => {
    if (!roles.includes(req.token.role)) {
      return res.status(403).json({
        error: "forbidden",
        message: `Requires role: ${roles.join(" or ")}`,
      });
    }
    next();
  };
}

// Usage
app.delete("/org/members/:id", requireRole("admin", "owner"), removeMember);
app.delete("/org", requireRole("owner"), deleteOrg);

Using Custom Permissions

function requirePermission(permission: string) {
  return (req: Request, res: Response, next: NextFunction) => {
    const { permissions = [], role } = req.token;

    // Owners and admins always have access
    if (["owner", "admin"].includes(role)) return next();

    if (!permissions.includes(permission)) {
      return res.status(403).json({
        error: "forbidden",
        message: `Missing permission: ${permission}`,
      });
    }
    next();
  };
}

// Usage
app.get("/billing", requirePermission("billing:read"), getBilling);
app.post("/billing", requirePermission("billing:write"), updateBilling);

Checking Permissions in the UI

import { useOrganization } from "@hellojohn/react";

function OrgSettings() {
  const { currentUserRole, currentUserPermissions } = useOrganization();

  const canManageMembers = ["admin", "owner"].includes(currentUserRole);
  const canBilling = currentUserPermissions?.includes("billing:read");

  return (
    <nav>
      {canManageMembers && <a href="/settings/members">Members</a>}
      {canBilling && <a href="/settings/billing">Billing</a>}
    </nav>
  );
}

Permission Scoping

Organization permissions are always scoped to the active organization. They do not affect other organizations the user belongs to, and they do not grant system-level access.


On this page