Features
RBAC
Role-based access control via Better Auth's access control plugin.
TheShipStack uses Better Auth's access control plugin to enforce permissions across the entire app — in server actions, API routes, and the UI.
Roles
| Role | Who | Permissions |
|---|---|---|
owner | Workspace creator | Full access — update, delete workspace; manage billing; manage members and invitations; all project actions |
admin | Invited as admin | Update workspace; manage members and invitations; all project actions; read billing |
member | Invited as member | Create and update projects only |
Permissions by resource
| Resource | Actions | owner | admin | member |
|---|---|---|---|---|
organization | update, delete | ✓ / ✓ | ✓ / — | — / — |
invitation | create, cancel | ✓ | ✓ | — |
member | update, delete | ✓ | ✓ | — |
project | create, update, delete | ✓ | ✓ | create / update |
billing | read, manage | ✓ | read | — |
Checking permissions in server actions
Use requirePermission from lib/require-org.ts. It throws 'Forbidden' if the current user lacks the permission:
import { requirePermission } from '@/lib/require-org'
export async function deleteProject(id: string) {
await requirePermission('project', 'delete')
// ... safe to proceed
}Checking permissions in server components
Use auth.api.hasPermission for conditional UI rendering:
import { auth } from '@/lib/auth'
import { headers } from 'next/headers'
const canManageBilling = await auth.api
.hasPermission({
headers: await headers(),
body: { permissions: { billing: ['manage'] } },
})
.then((r) => r.success)UI enforcement
The UI shows all actions but disables them with a tooltip for unauthorized users — except the danger zone tab, which is hidden entirely from non-owners. This gives every user a clear picture of what's possible without exposing destructive controls.
Customizing roles
The role definitions live in lib/permissions.ts. To add a new resource or action:
- Add it to the
statementobject - Grant it to the appropriate roles in
memberRole,adminRole, orownerRole - Register the updated
acinlib/auth.tsandlib/auth-client.ts
// lib/permissions.ts
export const statement = {
organization: ['update', 'delete'],
invitation: ['create', 'cancel'],
member: ['update', 'delete'],
project: ['create', 'update', 'delete'],
billing: ['read', 'manage'],
// add your resource here
report: ['create', 'view', 'delete'],
} as const