| name | server-fn-auth |
| description | Authentication and authorization patterns for TanStack Start server functions.
Use when: adding auth to a new server function, fixing missing authorization,
checking user permissions, protecting endpoints, or implementing organizer/volunteer
access control for competitions.
|
Server Function Authentication
Quick Reference
Pattern 1: Permission-Based (Most Common)
For operations requiring a specific team permission:
import { TEAM_PERMISSIONS } from "@/db/schemas/teams"
import { requireTeamPermission } from "@/utils/team-auth"
export const myServerFn = createServerFn({ method: "POST" })
.inputValidator((data: unknown) =>
z.object({
teamId: z.string().startsWith("team_"),
}).parse(data),
)
.handler(async ({ data }) => {
await requireTeamPermission(data.teamId, TEAM_PERMISSIONS.MANAGE_COMPETITIONS)
})
Pattern 2: Competition Access (Organizer OR Volunteer)
For read operations accessible to competition organizers and volunteers:
import { and, eq } from "drizzle-orm"
import { SYSTEM_ROLES_ENUM } from "@/db/schemas/teams"
import { ROLES_ENUM } from "@/db/schemas/users"
import { getSessionFromCookie } from "@/utils/auth"
import { teamMembershipTable } from "@/db/schema"
export const myServerFn = createServerFn({ method: "GET" })
.inputValidator((data: unknown) =>
z.object({
competitionId: z.string().startsWith("comp_"),
organizingTeamId: z.string().startsWith("team_"),
competitionTeamId: z.string().startsWith("team_").nullable(),
}).parse(data),
)
.handler(async ({ data }) => {
const db = getDb()
const session = await getSessionFromCookie()
if (!session?.userId) {
throw new Error("Unauthorized: Must be logged in")
}
if (session.user?.role === ROLES_ENUM.ADMIN) {
} else {
let canAccess = false
const orgMembership = await db
.select({ roleId: teamMembershipTable.roleId })
.from(teamMembershipTable)
.where(
and(
eq(teamMembershipTable.teamId, data.organizingTeamId),
eq(teamMembershipTable.userId, session.userId),
eq(teamMembershipTable.isActive, 1),
),
)
.limit(1)
if (
orgMembership[0]?.roleId === SYSTEM_ROLES_ENUM.ADMIN ||
orgMembership[0]?.roleId === SYSTEM_ROLES_ENUM.OWNER
) {
canAccess = true
}
if (!canAccess && data.competitionTeamId) {
const volMembership = await db
.select({ id: teamMembershipTable.id })
.from(teamMembershipTable)
.where(
and(
eq(teamMembershipTable.teamId, data.competitionTeamId),
eq(teamMembershipTable.userId, session.userId),
eq(teamMembershipTable.roleId, SYSTEM_ROLES_ENUM.VOLUNTEER),
eq(teamMembershipTable.isSystemRole, 1),
eq(teamMembershipTable.isActive, 1),
),
)
.limit(1)
if (volMembership[0]) {
canAccess = true
}
}
if (!canAccess) {
throw new Error("Unauthorized: Must be an organizer or volunteer")
}
}
})
Available Utilities
From @/utils/team-auth
| Function | Use Case |
|---|
requireTeamPermission(teamId, permission) | Throws if user lacks permission |
hasTeamPermission(teamId, permission) | Returns boolean, doesn't throw |
requireTeamMembership(teamId) | Throws if not a team member |
isTeamMember(teamId) | Returns boolean |
From @/utils/auth
| Function | Use Case |
|---|
getSessionFromCookie() | Get current user session |
Constants
TEAM_PERMISSIONS (from @/db/schemas/teams)
MANAGE_COMPETITIONS - Create/edit competitions
MANAGE_PROGRAMMING - Manage programming tracks
MANAGE_SCALING_GROUPS - Manage scaling options
INVITE_MEMBERS, REMOVE_MEMBERS - Member management
EDIT_TEAM_SETTINGS, DELETE_TEAM - Team settings
ACCESS_DASHBOARD, ACCESS_BILLING - Resource access
SYSTEM_ROLES_ENUM (from @/db/schemas/teams)
OWNER - Team owner (full access)
ADMIN - Team admin (full access)
CAPTAIN - Competition team captain
MEMBER - Regular member
VOLUNTEER - Competition volunteer
GUEST - Limited access
ROLES_ENUM (from @/db/schemas/users)
ADMIN - Site admin (bypasses all team checks)
USER - Regular user
Key Principles
- Always add auth to server functions - Route-level guards are not enough; server functions can be called directly
- Site admins bypass checks - Check
session.user?.role === ROLES_ENUM.ADMIN first
- Include required IDs in input schema - Add
teamId, organizingTeamId, etc. to enable auth checks
- Update callers when adding auth params - Routes/components must pass the new required fields