Permissions and Roles
The SaaS Boilerplate implements a comprehensive role-based access control (RBAC) system integrated with user flags for granular permission management.
User Roles
Role Hierarchy
admin (Level 4)
└─ colaborator (Level 3)
└─ member (Level 2)
└─ user (Level 1)
Role Definitions
| Role | Level | Description | Typical Use |
|---|---|---|---|
user |
1 | Basic access | Limited features, restricted data |
member |
2 | Standard user | Most features, own data |
colaborator |
3 | Extended access | Content management, team collaboration |
admin |
4 | Full control | System administration, all features |
Role Assignment
Default Role:
// core/lib/config.ts
export const USER_ROLES_CONFIG = {
defaultRole: 'member' // New users get 'member' role
}
On Signup:
// Automatic via Better Auth additionalFields
role: {
type: "string",
required: false,
input: false, // Users cannot set their own role
defaultValue: USER_ROLES_CONFIG.defaultRole
}
Manual Assignment:
// Only admins can change roles
await updateUser(userId, {
role: 'colaborator'
})
User Flags
Overview
User flags provide granular feature access beyond role-based permissions.
type UserFlag =
| 'beta_tester' // Access to beta features
| 'early_adopter' // Early access to new features
| 'limited_access' // Restricted feature set
| 'vip' // Premium features
| 'restricted' // Limited permissions
| 'experimental' // Experimental feature access
Storage
Flags are stored in the user_metas table:
CREATE TABLE "user_metas" (
"id" TEXT PRIMARY KEY,
"userId" TEXT NOT NULL REFERENCES "users"("id") ON DELETE CASCADE,
"metaKey" TEXT NOT NULL,
"metaValue" TEXT NOT NULL,
"createdAt" TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE("userId", "metaKey")
);
-- User flags stored as:
-- metaKey: 'user_flags'
-- metaValue: '["beta_tester", "vip"]' (JSON array)
Managing Flags
// Get user flags
const flags = await getUserFlags(userId)
// Returns: ['beta_tester', 'vip']
// Set user flags
await updateUserFlags(userId, ['beta_tester', 'vip', 'early_adopter'])
// Add flag
await addUserFlag(userId, 'experimental')
// Remove flag
await removeUserFlag(userId, 'limited_access')
Flags in Session
Flags are automatically loaded into the session:
// Available in session
session.user.flags // ['beta_tester', 'vip']
Checking Permissions
In Server Components
import { auth } from '@/core/lib/auth'
import { headers } from 'next/headers'
export default async function AdminPage() {
const session = await auth.api.getSession({
headers: await headers()
})
if (!session) {
redirect('/login')
}
// Check role
if (session.user.role !== 'admin') {
redirect('/403')
}
return <AdminDashboard />
}
In Client Components
'use client'
import { useSession } from '@/core/lib/auth-client'
export function FeatureComponent() {
const { data: session } = useSession()
// Check role
const isAdmin = session?.user.role === 'admin'
const isCollaborator = ['admin', 'colaborator'].includes(session?.user.role || '')
// Check flags
const hasBetaAccess = session?.user.flags?.includes('beta_tester')
return (
<div>
{isAdmin && <AdminPanel />}
{isCollaborator && <CollaboratorTools />}
{hasBetaAccess && <BetaFeature />}
</div>
)
}
In API Routes
// app/api/admin/route.ts
import { auth } from '@/core/lib/auth'
import { NextRequest, NextResponse } from 'next/server'
export async function GET(request: NextRequest) {
const session = await auth.api.getSession({
headers: request.headers
})
if (!session) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
// Check role
if (session.user.role !== 'admin') {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
}
// Admin-only logic
return NextResponse.json({ data: 'Admin data' })
}
Protecting Routes
Middleware Protection
// middleware.ts
import { auth } from '@/core/lib/auth'
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
const roleProtectedRoutes: Record<string, string[]> = {
'/admin': ['admin'],
'/dashboard/settings': ['admin', 'colaborator'],
'/dashboard': ['admin', 'colaborator', 'member']
}
export async function middleware(request: NextRequest) {
const session = await auth.api.getSession({
headers: request.headers
})
if (!session) {
return NextResponse.redirect(new URL('/login', request.url))
}
// Check role-based access
for (const [path, allowedRoles] of Object.entries(roleProtectedRoutes)) {
if (request.nextUrl.pathname.startsWith(path)) {
if (!allowedRoles.includes(session.user.role)) {
return NextResponse.redirect(new URL('/403', request.url))
}
}
}
return NextResponse.next()
}
Entity-Level Permissions
Integration with Entity System
The entity system automatically checks permissions:
// Entity configuration
permissions: {
read: ['admin', 'colaborator', 'member'],
create: ['admin', 'colaborator', 'member'],
update: ['admin', 'colaborator'],
delete: ['admin']
}
Permission Check Flow
1. Request to entity endpoint
↓
2. Authenticate user (session or API key)
↓
3. Load user role + flags
↓
4. Check entity permissions
↓
5. Verify RLS policies
↓
6. Check flag access (if configured)
↓
7. Grant/Deny access
API Key Scopes
Scope-Based Permissions
API keys use scope-based permissions:
// API key scopes
{
scopes: [
'tasks:read', // Read tasks
'tasks:write', // Create and update tasks
'users:read', // Read user information
'admin:api-keys' // Manage API keys
]
}
Checking Scopes
import { hasScope } from '@/core/lib/api/auth'
export async function GET(request: NextRequest) {
const auth = await validateApiKey(request)
if (!auth) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
// Check specific scope
if (!hasScope(auth, 'tasks:read')) {
return NextResponse.json({ error: 'Insufficient permissions' }, { status: 403 })
}
// Continue with logic
}
Row-Level Security (RLS)
Database-Level Isolation
RLS policies enforce data isolation at the PostgreSQL level:
-- Users can only see their own tasks
CREATE POLICY "tasks_select_own" ON "tasks"
FOR SELECT
USING ("userId" = auth.uid());
-- Users can only create tasks for themselves
CREATE POLICY "tasks_insert_own" ON "tasks"
FOR INSERT
WITH CHECK ("userId" = auth.uid());
-- Users can only update their own tasks
CREATE POLICY "tasks_update_own" ON "tasks"
FOR UPDATE
USING ("userId" = auth.uid())
WITH CHECK ("userId" = auth.uid());
-- Only admin can delete any task
CREATE POLICY "tasks_delete_admin" ON "tasks"
FOR DELETE
USING (
auth.user_role() = 'admin'
);
Helper Functions
-- Get current user's role
CREATE OR REPLACE FUNCTION auth.user_role()
RETURNS TEXT AS $$
SELECT "role" FROM "users" WHERE "id" = auth.uid()
$$ LANGUAGE SQL STABLE;
Permission Utilities
Helper Functions
// core/lib/permissions.ts
export function hasRole(user: SessionUser, ...roles: string[]): boolean {
return roles.includes(user.role)
}
export function hasFlag(user: SessionUser, flag: UserFlag): boolean {
return user.flags?.includes(flag) || false
}
export function hasAnyFlag(user: SessionUser, ...flags: UserFlag[]): boolean {
return flags.some(flag => user.flags?.includes(flag))
}
export function hasAllFlags(user: SessionUser, ...flags: UserFlag[]): boolean {
return flags.every(flag => user.flags?.includes(flag))
}
Usage
import { hasRole, hasFlag } from '@/core/lib/permissions'
const canManageUsers = hasRole(session.user, 'admin', 'colaborator')
const hasBetaAccess = hasFlag(session.user, 'beta_tester')
Custom Permission Logic
Hook-Based Permissions
// Entity hooks for custom permission logic
hooks: {
beforeCreate: [
async (context) => {
const { data, userId } = context
// Custom permission check
if (data.priority === 'critical' && !hasRole(user, 'admin')) {
return {
continue: false,
error: 'Only admins can create critical tasks'
}
}
return { continue: true }
}
]
}
Best Practices
Principle of Least Privilege
// ✅ Good: Minimal permissions
permissions: {
read: ['admin', 'colaborator', 'member'],
create: ['admin', 'colaborator'],
update: ['admin'],
delete: ['admin']
}
// ❌ Bad: Too permissive
permissions: {
read: ['admin', 'colaborator', 'member', 'user'],
create: ['admin', 'colaborator', 'member', 'user'],
update: ['admin', 'colaborator', 'member', 'user'],
delete: ['admin', 'colaborator', 'member', 'user']
}
Defense in Depth
Always implement multiple security layers:
- Application Layer: Role/flag checks in code
- API Layer: Permission validation in endpoints
- Database Layer: RLS policies
Next Steps
- Security Best Practices - Security implementation
- Testing Authentication - Testing permissions
💡 Tip: Use roles for broad access levels and flags for specific feature gates. Combine both for fine-grained control.