Learn how to implement role-based access control for API endpoints using Sonamu’s Guards system.
Guards System Overview
Declarative Permissions @api guards option Simple permission setup
Guard Types admin, user, query Type definitions provided
Custom Guards Business logic Flexible extension
Hierarchical Permissions Role-based control RBAC implementation
Understanding Guards
What are Guards?
Guards are functions that verify permissions before an API method executes . They are used in the following situations:
APIs accessible only to logged-in users
APIs accessible only to administrators
APIs accessible only to users meeting specific conditions
Advantages of Guards :
Declarative : Simply declare like @api({ guards: ["admin"] })
Reusable : Use the same Guards across multiple APIs
Testable : Test Guards independently
Separation of Concerns : Permission logic separated from business logic
Guards Execution Flow
1. Client → API request
2. Sonamu → Create Context (including user info)
3. Guards → Permission verification
↓ Success
4. API method execution
↓
5. Return response
↓ Failure
3. 403 Forbidden response
Guard Types
Sonamu has three Guard types (user, admin, query) defined. These are type-safe keys, and the actual verification logic must be implemented in guardHandler in sonamu.config.ts .
user Guard
user Guard allows only logged-in users to access. It’s the most basic authentication Guard.
class PostModel extends BaseModelClass {
/**
* Get my posts list
*
* user Guard: Login required
*/
@ api ({ httpMethod: "GET" , guards: [ "user" ] })
async getMyPosts () : Promise <{
posts : Post [];
}> {
const context = Sonamu . getContext ();
// Thanks to user Guard, context.user is guaranteed to exist
const userId = context . user ! . id ;
const rdb = this . getPuri ( "r" );
const posts = await rdb
. table ( "posts" )
. where ( "user_id" , userId )
. orderBy ( "created_at" , "desc" )
. select ( "*" );
return { posts };
}
/**
* Create post
*/
@ api ({ httpMethod: "POST" , guards: [ "user" ] })
async create ( params : {
title : string ;
content : string ;
}) : Promise <{ postId : number }> {
const context = Sonamu . getContext ();
const { title , content } = params ;
const wdb = this . getPuri ( "w" );
const [ post ] = await wdb
. table ( "posts" )
. insert ({
user_id: context . user ! . id ,
title ,
content ,
created_at: new Date (),
})
. returning ({ id: "id" });
return { postId: post . id };
}
}
When to use?
All APIs that require login
APIs handling user-specific data (my profile, my orders, etc.)
Content creation/modification/deletion
admin Guard
admin Guard allows only administrators to access. Used for sensitive administrative functions.
class UserModel extends BaseModelClass {
/**
* All users list (admin only)
*/
@ api ({ httpMethod: "GET" , guards: [ "admin" ] })
async getAllUsers ( params : {
page ?: number ;
pageSize ?: number ;
}) : Promise <{
users : User [];
total : number ;
}> {
const { page = 1 , pageSize = 20 } = params ;
const rdb = this . getPuri ( "r" );
const users = await rdb
. table ( "users" )
. limit ( pageSize )
. offset (( page - 1 ) * pageSize )
. select ( "*" );
const [{ count }] = await rdb
. table ( "users" )
. count ( "* as count" );
return {
users ,
total: count ,
};
}
/**
* Force deactivate user (admin only)
*/
@ api ({ httpMethod: "POST" , guards: [ "admin" ] })
async deactivateUser ( userId : number ) : Promise <{ success : boolean }> {
const wdb = this . getPuri ( "w" );
await wdb
. table ( "users" )
. where ( "id" , userId )
. update ({
is_active: false ,
deactivated_at: new Date (),
});
return { success: true };
}
}
When to use?
System settings changes
Accessing all user data
User management (activation/deactivation, permission changes)
Statistics and analytics data
Sensitive business logic
query Guard
query Guard implements simple authentication via query string parameters. Mainly used for public APIs or temporary links.
class FileModel extends BaseModelClass {
/**
* File download (query token authentication)
*
* Usage: GET /api/file/download?fileId=123&token=abc...
*/
@ api ({ httpMethod: "GET" , guards: [ "query" ] })
async download ( params : {
fileId : number ;
token : string ;
}) : Promise <{
url : string ;
}> {
const { fileId , token } = params ;
// Verify token
const rdb = this . getPuri ( "r" );
const downloadToken = await rdb
. table ( "download_tokens" )
. where ( "file_id" , fileId )
. where ( "token" , token )
. where ( "expires_at" , ">" , new Date ())
. first ();
if ( ! downloadToken ) {
throw new Error ( "Invalid or expired download token" );
}
// Query file info
const file = await rdb
. table ( "files" )
. where ( "id" , fileId )
. first ();
if ( ! file ) {
throw new Error ( "File not found" );
}
// Generate Signed URL
const disk = Sonamu . storage . use ( file . disk_name );
const url = await disk . getSignedUrl ( file . key , 3600 );
return { url };
}
}
When to use?
Temporary download links sent via email
Shareable public APIs
Webhook callbacks (API key verification)
Cases requiring simple authentication
The specific implementation of query Guard may vary by project. It can be extended with various methods like API keys, tokens, signatures, etc.
Combining Multiple Guards
You can apply multiple Guards to a single API. All Guards must pass for the API to execute.
class ReportModel extends BaseModelClass {
/**
* Generate sensitive report (admin + additional verification)
*/
@ api ({ httpMethod: "POST" , guards: [ "admin" , "superAdmin" ] })
async generateSensitiveReport () : Promise <{ reportId : number }> {
// Executes only after passing both admin Guard and superAdmin Guard
// ...
}
}
Implementing Guards
Guards are implemented in guardHandler in sonamu.config.ts. Sonamu automatically executes guardHandler for each API call.
Implementing guardHandler
// sonamu.config.ts
import type { GuardKey } from "sonamu" ;
import type { FastifyRequest } from "fastify" ;
import { Sonamu } from "sonamu" ;
export default {
// ... other settings
api: {
route: {
prefix: "/api" ,
},
server: {
custom : ( server ) => {
// Fastify server customization
},
},
apiConfig: {
contextProvider : ( defaultContext , request ) => ({
... defaultContext ,
// Context extension
}),
/**
* Guards verification logic
*
* @param guard - Guard key ("user", "admin", "query", etc.)
* @param request - Fastify Request object
* @param api - API metadata
* @returns true to pass, false or throw exception to block
*/
guardHandler : ( guard : GuardKey , request : FastifyRequest , api ) => {
// Get user information from Sonamu Context
const context = Sonamu . getContext ();
switch ( guard ) {
case "user" :
// Only logged-in users can access
if ( ! context . user ) {
throw new Error ( "Authentication required" );
}
return true ;
case "admin" :
// Only administrators can access
if ( ! context . user || context . user . role !== "admin" ) {
throw new Error ( "Admin access required" );
}
return true ;
case "query" :
// Verify API key from query parameter
const apiKey = request . query . apiKey ;
if ( apiKey !== process . env . API_KEY ) {
throw new Error ( "Invalid API key" );
}
return true ;
default :
// Block unknown Guards
throw new Error ( `Unknown guard: ${ guard } ` );
}
},
},
} ,
} ;
Adding Custom Guard Types
To add project-specific Guards, extend the GuardKeys interface.
// types/guards.ts
declare module "sonamu" {
interface GuardKeys {
verified : true ; // Email verified users
premium : true ; // Premium members
moderator : true ; // Moderators
}
}
Now implement new Guards in guardHandler.
// sonamu.config.ts
guardHandler : ( guard , request , api ) => {
const context = Sonamu . getContext ();
switch ( guard ) {
case "user" :
if ( ! context . user ) {
throw new Error ( "Authentication required" );
}
return true ;
case "admin" :
if ( ! context . user || context . user . role !== "admin" ) {
throw new Error ( "Admin access required" );
}
return true ;
case "verified" :
// Email verified users only
if ( ! context . user || ! context . user . email_verified ) {
throw new Error ( "Email verification required" );
}
return true ;
case "premium" :
// Premium members only
if ( ! context . user || context . user . subscription_tier !== "premium" ) {
throw new Error ( "Premium subscription required" );
}
return true ;
case "moderator" :
// Moderators or administrators
if ( ! context . user ||
( context . user . role !== "moderator" && context . user . role !== "admin" )) {
throw new Error ( "Moderator access required" );
}
return true ;
default :
throw new Error ( `Unknown guard: ${ guard } ` );
}
},
Dynamic Verification
When you need to verify permissions dynamically based on API parameters, you can use the api parameter in guardHandler.
guardHandler : ( guard , request , api ) => {
const context = Sonamu . getContext ();
if ( guard === "owner" ) {
// Dynamic verification using API metadata
// Example: Check if post ID exists in parameters
const postId = request . query . postId || request . body ?. postId ;
if ( ! postId ) {
throw new Error ( "Resource ID required" );
}
// Only perform simple verification here
// Complex ownership verification should be done inside API methods
if ( ! context . user ) {
throw new Error ( "Authentication required" );
}
return true ;
}
// ... other Guards
},
Note : For permission verification requiring complex business logic or database queries, it’s better to handle it inside API methods rather than in Guards. Guards should only handle simple authentication/authorization checks.
Role-Based Access Control (RBAC)
Use RBAC (Role-Based Access Control) for more complex permission systems.
Defining Roles and Permissions
// types/permissions.ts
export enum Permission {
// User management
USER_READ = "user:read" ,
USER_WRITE = "user:write" ,
USER_DELETE = "user:delete" ,
// Post management
POST_READ = "post:read" ,
POST_WRITE = "post:write" ,
POST_DELETE = "post:delete" ,
// Administration
ADMIN_DASHBOARD = "admin:dashboard" ,
ADMIN_SETTINGS = "admin:settings" ,
}
export const rolePermissions : Record < string , Permission []> = {
admin: [
Permission . USER_READ ,
Permission . USER_WRITE ,
Permission . USER_DELETE ,
Permission . POST_READ ,
Permission . POST_WRITE ,
Permission . POST_DELETE ,
Permission . ADMIN_DASHBOARD ,
Permission . ADMIN_SETTINGS ,
],
moderator: [
Permission . POST_READ ,
Permission . POST_WRITE ,
Permission . POST_DELETE ,
],
user: [
Permission . POST_READ ,
Permission . POST_WRITE ,
],
guest: [
Permission . POST_READ ,
],
};
/**
* Check if user has specific permission
*/
export function hasPermission (
user : User | null ,
permission : Permission
) : boolean {
if ( ! user ) return false ;
const permissions = rolePermissions [ user . role ] || [];
return permissions . includes ( permission );
}
Implementing Permission-Based Guards
To integrate the RBAC system with Guards, verify permissions in guardHandler.
// sonamu.config.ts
import { Permission , hasPermission } from "./types/permissions" ;
// Add permission-based Guards to GuardKeys
declare module "sonamu" {
interface GuardKeys {
"user:write" : true ;
"user:delete" : true ;
"post:delete" : true ;
"admin:settings" : true ;
}
}
export default {
// ... settings
api: {
apiConfig: {
guardHandler : ( guard , request , api ) => {
const context = Sonamu . getContext ();
// Basic Guards
switch ( guard ) {
case "user" :
if ( ! context . user ) {
throw new Error ( "Authentication required" );
}
return true ;
case "admin" :
if ( ! context . user || context . user . role !== "admin" ) {
throw new Error ( "Admin access required" );
}
return true ;
// Permission-based Guards
case "user:write" :
if ( ! hasPermission ( context . user , Permission . USER_WRITE )) {
throw new Error ( "USER_WRITE permission required" );
}
return true ;
case "user:delete" :
if ( ! hasPermission ( context . user , Permission . USER_DELETE )) {
throw new Error ( "USER_DELETE permission required" );
}
return true ;
case "post:delete" :
if ( ! hasPermission ( context . user , Permission . POST_DELETE )) {
throw new Error ( "POST_DELETE permission required" );
}
return true ;
case "admin:settings" :
if ( ! hasPermission ( context . user , Permission . ADMIN_SETTINGS )) {
throw new Error ( "ADMIN_SETTINGS permission required" );
}
return true ;
default :
throw new Error ( `Unknown guard: ${ guard } ` );
}
},
},
} ,
} ;
Using Permission Guard
class UserModel extends BaseModelClass {
/**
* Update user info (admin or self)
*/
@ api ({ httpMethod: "PUT" , guards: [ "user:write" ] })
async updateUser ( params : {
userId : number ;
username ?: string ;
email ?: string ;
}) : Promise <{ user : User }> {
const context = Sonamu . getContext ();
const { userId , username , email } = params ;
// Deny if not self and not admin
if (
context . user ! . id !== userId &&
! hasPermission ( context . user , Permission . USER_WRITE )
) {
throw new Error ( "Permission denied" );
}
const wdb = this . getPuri ( "w" );
await wdb
. table ( "users" )
. where ( "id" , userId )
. update ({
username ,
email ,
updated_at: new Date (),
});
const rdb = this . getPuri ( "r" );
const user = await rdb
. table ( "users" )
. where ( "id" , userId )
. first ();
return { user };
}
}
Resource-Based Permission Control
Pattern for verifying ownership of specific resources.
class PostModel extends BaseModelClass {
/**
* Update post (author or admin only)
*/
@ api ({ httpMethod: "PUT" , guards: [ "user" ] })
async update ( params : {
postId : number ;
title ?: string ;
content ?: string ;
}) : Promise <{ post : Post }> {
const context = Sonamu . getContext ();
const { postId , title , content } = params ;
// Query post
const rdb = this . getPuri ( "r" );
const post = await rdb
. table ( "posts" )
. where ( "id" , postId )
. first ();
if ( ! post ) {
throw new Error ( "Post not found" );
}
// Verify ownership
const isOwner = post . user_id === context . user ! . id ;
const isAdmin = context . user ! . role === "admin" ;
if ( ! isOwner && ! isAdmin ) {
throw new Error ( "You can only edit your own posts" );
}
// Update
const wdb = this . getPuri ( "w" );
await wdb
. table ( "posts" )
. where ( "id" , postId )
. update ({
title ,
content ,
updated_at: new Date (),
});
const updatedPost = await rdb
. table ( "posts" )
. where ( "id" , postId )
. first ();
return { post: updatedPost };
}
/**
* Delete post (author or admin only)
*/
@ api ({ httpMethod: "DELETE" , guards: [ "user" ] })
async remove ( postId : number ) : Promise <{ success : boolean }> {
const context = Sonamu . getContext ();
const rdb = this . getPuri ( "r" );
const post = await rdb
. table ( "posts" )
. where ( "id" , postId )
. first ();
if ( ! post ) {
throw new Error ( "Post not found" );
}
// Verify ownership
if (
post . user_id !== context . user ! . id &&
context . user ! . role !== "admin"
) {
throw new Error ( "Permission denied" );
}
const wdb = this . getPuri ( "w" );
await wdb . table ( "posts" ). where ( "id" , postId ). delete ();
return { success: true };
}
}
Testing Guards
You can test Guards independently.
// guards/__tests__/guards.test.ts
import { describe , it , expect } from "vitest" ;
import { guards } from "../index" ;
import type { AppContext } from "../../types/context" ;
describe ( "Guards" , () => {
describe ( "user guard" , () => {
it ( "passes logged in user" , () => {
const context : Partial < AppContext > = {
user: {
id: 1 ,
email: "test@example.com" ,
role: "user" ,
} as any ,
};
expect ( guards . user ( context as AppContext )). toBe ( true );
});
it ( "blocks non-logged in user" , () => {
const context : Partial < AppContext > = {
user: null ,
};
expect ( guards . user ( context as AppContext )). toBe ( false );
});
});
describe ( "admin guard" , () => {
it ( "passes admin" , () => {
const context : Partial < AppContext > = {
user: {
id: 1 ,
email: "admin@example.com" ,
role: "admin" ,
} as any ,
};
expect ( guards . admin ( context as AppContext )). toBe ( true );
});
it ( "blocks regular user" , () => {
const context : Partial < AppContext > = {
user: {
id: 2 ,
email: "user@example.com" ,
role: "user" ,
} as any ,
};
expect ( guards . admin ( context as AppContext )). toBe ( false );
});
});
});
Cautions
Cautions when using Guards :
Guards only handle Authentication, Authorization should be handled in business logic
Additional verification required for sensitive data even after passing Guards
Return 403 Forbidden on Guard failure (not 401 Unauthorized)
Consider order when using multiple Guards (general to specific)
Avoid complex business logic in Guards (simple permission checks only)
Resource ownership verification should be handled inside API methods
Next Steps