Authorization
How authorization works in the RawStack API.
The RawStack API uses authorization to decide what an authenticated user is allowed to do once their identity has been established. In practice, authorization combines broad role-based access with more targeted guard logic for resource-specific checks.
Authentication establishes who the user is. Authorization establishes what that user can do. This page focuses on the authorization side of that flow.
Overview
At a high level, authorization in the API happens in two layers:
- role-based checks for broad access rules such as admin-only actions
- custom guards for finer-grained decisions such as whether a user can act on a specific resource
This keeps the common cases simple while still allowing more detailed rules where needed.
The logged-in user
Once a request has been authenticated, the API creates a LoggedInUser value and makes it available throughout the request lifecycle. That object contains the minimum identity information needed for downstream authorization decisions, typically the user ID and roles.
export class LoggedInUser {
constructor(
public id: string,
public roles: string[] = [],
) {}
}This logged-in user can then be accessed in a few different places:
- directly in controllers through a user decorator
- from
request.userinside guards - through a
LoggedInUserProviderin application or domain-facing orchestration code
That shared actor context is what allows both guards and services to reason about who is performing an action.
Roles and broad access rules
Out of the box, RawStack includes two core roles:
VERIFIED_USERADMIN
These roles provide a simple starting point for broad authorization rules. For example, an ADMIN may be allowed to perform management actions across many resources, while a VERIFIED_USER may only have access to capabilities intended for normal signed-in users.
Role checks are useful when the rule is global and easy to express. They are less useful when access depends on the specific record being accessed. That is where custom guards become more helpful.
Custom guards
For more detailed authorization rules, the API uses custom guards. A guard can inspect the current request, look at the authenticated user, and decide whether the current action should be allowed.
This is especially useful for cases such as:
- allowing admins to manage any user
- allowing normal users to update only their own record
- denying access when the request does not include enough context to make a safe decision
Here is a simplified example of that pattern:
type GuardUser = {
id: string;
roles: string[];
};
type UserUserGuardOptions = {
action: string;
userIdFrom?: (request: { params?: { userId?: string } }) => string | undefined;
};
export function canAccessUserAction(
user: GuardUser | undefined,
request: { params?: { userId?: string } },
options: UserUserGuardOptions,
): boolean {
const { action, userIdFrom = (request) => request.params?.userId } = options;
const userId = userIdFrom(request);
if (!user || !userId) {
return false;
}
if (user.roles.indexOf('ADMIN') !== -1) {
return true;
}
switch (action) {
case 'UPDATE_USER':
case 'DELETE_USER':
return user.id === userId;
default:
return false;
}
}In this example, the guard logic grants access immediately if the user has the ADMIN role. Otherwise, it falls back to action-specific logic and only allows certain operations when the authenticated user is acting on their own user ID.
That gives you a useful blend of broad administrative access and fine-grained ownership checks.
Where guards fit in the request flow
Guards usually run at the HTTP boundary, before the main application logic executes. That makes them a good fit for request-level authorization decisions.
For example, a route may apply both authentication and authorization before it reaches the main use-case logic:
authenticated request
-> JwtGuard confirms identity
-> UserUserGuard confirms the user can perform UPDATE_USER on :userId
-> controller/action executes
-> command or query layer runsIn that flow:
JwtGuardensures the request is authenticated.UserUserGuarddecides whether the authenticated user is allowed to perform the requested action.- Only then does the controller hand off to the command or query layer.
This keeps controllers thin and prevents unauthorized requests from reaching the main use-case logic.
Guards are not the whole story
Even when a guard allows a request through, important authorization rules may still exist deeper in the application. This is especially true for business rules that belong in the domain layer rather than at the HTTP edge.
For example, a service may still enforce rules such as:
- only admins can update roles
- users cannot modify their own elevated roles
- certain state transitions are forbidden even for otherwise authenticated users
That separation is useful. Guards answer, "should this request be allowed to proceed at all?" Domain services answer, "is this business operation valid?"
Why this approach works well
This authorization model stays practical without becoming overly complicated:
- roles handle simple, broad access rules
- custom guards handle request-specific checks
LoggedInUsergives the system a consistent actor model- domain services remain free to enforce deeper business rules
Taken together, this gives the API a clear and extensible authorization model that works well for both simple role checks and more nuanced ownership-based access control.