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
| Role | Description |
|---|---|
owner | Full control — manage members, settings, billing, delete org |
admin | Manage members and settings, cannot delete org |
member | Standard member access |
These roles are available on all plans and cannot be renamed or removed.
Permissions by Role
| Permission | member | admin | owner |
|---|---|---|---|
| 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.