Advanced Patterns
Authorization Patterns
Secure your data with proper authorization checks
Authorization Patterns
Authorization ensures users can only access and modify data they're allowed to. This template uses multiple layers of authorization.
Authorization Layers
1. Authentication (Better Auth)
└── Is the user logged in?
2. Organization Membership
└── Does the user belong to this org?
3. Role-Based Access
└── Does the user's role permit this action?
4. Resource Ownership
└── Does the user own this specific resource?Automatic Organization Scoping
The template automatically injects org context into every query:
// In functions.ts
export const zQuery = zCustomQuery(baseQuery, {
input: async (ctx) => {
const identity = await ctx.auth.getUserIdentity();
if (!identity) {
throw new Error("Unauthorized");
}
const parsed = parseIdentity(identity);
return {
ctx: {
...ctx,
identity: parsed, // Includes activeOrganizationId
},
};
},
});Query-Level Authorization
Organization-Scoped Queries
Always filter by organization:
export const list = zQuery({
args: z.object({}),
handler: async (ctx) => {
// Users only see their organization's data
return await ctx
.table("todos", "organizationId", (q) =>
q.eq("organizationId", ctx.identity.activeOrganizationId)
)
.collect();
},
});Validate Before Returning
export const getById = zQuery({
args: z.object({ id: z.string() }),
handler: async (ctx, args) => {
const todo = await ctx.table("todos").get(args.id);
// Don't reveal existence of other orgs' data
if (!todo || todo.organizationId !== ctx.identity.activeOrganizationId) {
return null; // Or throw "Not found"
}
return todo;
},
});Mutation-Level Authorization
Ownership Check
export const update = zMutation({
args: z.object({
id: z.string(),
title: z.string(),
}),
handler: async (ctx, args) => {
const todo = await ctx.table("todos").get(args.id);
// Check existence
if (!todo) {
throw new Error("Todo not found");
}
// Check org membership
if (todo.organizationId !== ctx.identity.activeOrganizationId) {
throw new Error("Not found"); // Don't reveal it exists
}
// Optionally check ownership for non-admins
const isAdmin = ["owner", "admin"].includes(ctx.identity.organizationRole);
if (!isAdmin && todo.userId !== ctx.identity.userId) {
throw new Error("You can only edit your own todos");
}
return await ctx.table("todos").patch(args.id, { title: args.title });
},
});Role-Based Mutations
export const deleteAll = zMutation({
args: z.object({}),
handler: async (ctx) => {
// Only owners can delete all
if (ctx.identity.organizationRole !== "owner") {
throw new Error("Only organization owners can delete all todos");
}
const todos = await ctx
.table("todos", "organizationId", (q) =>
q.eq("organizationId", ctx.identity.activeOrganizationId)
)
.collect();
await Promise.all(
todos.map((todo) => ctx.table("todos").delete(todo._id))
);
},
});Authorization Helper Functions
Create reusable authorization helpers:
// shared/auth_shared.ts
export function assertOrgMember(
doc: { organizationId: string },
identity: Identity
): void {
if (doc.organizationId !== identity.activeOrganizationId) {
throw new Error("Resource not found");
}
}
export function assertOwnerOrAdmin(identity: Identity): void {
if (!["owner", "admin"].includes(identity.organizationRole)) {
throw new Error("Admin access required");
}
}
export function assertResourceOwner(
doc: { userId: string },
identity: Identity
): void {
if (doc.userId !== identity.userId) {
throw new Error("You don't have permission to modify this resource");
}
}
export function canEditResource(
doc: { userId: string; organizationId: string },
identity: Identity
): boolean {
// Must be in same org
if (doc.organizationId !== identity.activeOrganizationId) {
return false;
}
// Admins can edit anything
if (["owner", "admin"].includes(identity.organizationRole)) {
return true;
}
// Others can only edit their own
return doc.userId === identity.userId;
}Usage
export const update = zMutation({
args: UpdateArgs,
handler: async (ctx, args) => {
const todo = await ctx.table("todos").getX(args.id);
assertOrgMember(todo, ctx.identity);
if (!canEditResource(todo, ctx.identity)) {
throw new Error("Permission denied");
}
return await ctx.table("todos").patch(args.id, args.updates);
},
});Team-Level Authorization
export function assertTeamMember(
doc: { teamId?: string },
identity: Identity
): void {
// If doc has a team and user has an active team, they must match
if (doc.teamId && identity.activeTeamId && doc.teamId !== identity.activeTeamId) {
throw new Error("Resource not found");
}
}Authorization in Edges
When loading related data, verify access:
export const getTodoWithAssignees = zQuery({
args: z.object({ id: z.string() }),
handler: async (ctx, args) => {
const todo = await ctx.table("todos").get(args.id);
// Authorize the parent
if (!todo || todo.organizationId !== ctx.identity.activeOrganizationId) {
return null;
}
// Child data inherits authorization from parent
const assignees = await todo.edge("todoAssignedUsers");
return {
...todo,
assignees,
};
},
});Hiding Unauthorized Actions in UI
function TodoActions({ todo }: { todo: Todo }) {
const { data: session } = useSession();
const identity = session?.session;
const isOwner = todo.userId === session?.user.id;
const isAdmin = ["owner", "admin"].includes(identity?.organizationRole ?? "");
const canEdit = isOwner || isAdmin;
const canDelete = isOwner || isAdmin;
return (
<div>
{canEdit && <Button onClick={() => editTodo(todo.id)}>Edit</Button>}
{canDelete && (
<Button variant="destructive" onClick={() => deleteTodo(todo.id)}>
Delete
</Button>
)}
</div>
);
}Security Best Practices
1. Never Trust Client Data
// Bad: Using client-provided orgId
await ctx.table("todos").insert({
organizationId: args.organizationId, // Client can fake this!
});
// Good: Use server-side identity
await ctx.table("todos").insert({
organizationId: ctx.identity.activeOrganizationId,
});2. Check Before Every Operation
// Always verify access, even for reads
const todo = await ctx.table("todos").get(args.id);
assertOrgMember(todo, ctx.identity);3. Return Generic Errors
// Bad: Reveals information
throw new Error("Todo belongs to org_abc123");
// Good: Generic error
throw new Error("Resource not found");4. Log Suspicious Activity
if (todo.organizationId !== ctx.identity.activeOrganizationId) {
console.warn(`Unauthorized access attempt: user=${ctx.identity.userId}`);
throw new Error("Not found");
}Next, learn about Frontend Integration.