Access Control
The access control surface lives in src/services/accessCheck/**. There are three exports:
| Helper | Where to use | What it does |
|---|---|---|
getRoutePermissions | Layouts and pages that need the raw snapshot | Returns { teamAccess, campaignAccess?, permissionKeys? } (cached) |
accessCheck | Anywhere a service needs the user's permission keys + a QueryResponse envelope | Returns { message, error, data: permissionKeys[] } |
requireAccess | Top of a server component / layout | Calls getRoutePermissions and redirect()s when access is denied |
The snapshot itself is produced by the get-permissions-snapshot Supabase Edge Function. See Permissions Snapshot for details on caching and invalidation.
7.1 getRoutePermissions
Location: src/services/accessCheck/getRoutePermissions.ts
export async function getRoutePermissions({
teamId,
campaignId,
}: {
teamId?: string;
campaignId?: string;
}): Promise<PermissionsSnapshot>;
- Fetches via
supabase.functions.invoke("get-permissions-snapshot", ...). - Wrapped in
unstable_cachekeyed["permissions-snapshot", userId, teamId ?? "", campaignId ?? ""]withrevalidate: 3600andtags: [permissions-${userId}]. - Throws if the user is not authenticated or if the edge function returns no data.
PermissionsSnapshot (defined in src/lib/permissions/fetchPermissionsSnapshot.ts):
type PermissionsSnapshot = {
teamAccess: boolean;
campaignAccess?: boolean; // present when campaign_id was queried
permissionKeys?: string[]; // union of team-level + campaign-level perm keys
};
7.2 accessCheck
Location: src/services/accessCheck/accessCheck.ts
export const accessCheck = async ({
teamId,
campaignId,
}: {
teamId?: string;
campaignId?: string;
}): Promise<QueryResponse>;
A thin wrapper that returns the permissionKeys array inside the standard QueryResponse shape. Returns { message: "Something went wrong.", error: true, data: [] } when the user has no team access, otherwise { message: "Success", error: false, data: permissionKeys }. Useful when a service needs the user's keys.
7.3 requireAccess
Location: src/services/accessCheck/requireAccess.ts
export async function requireAccess({
teamId,
campaignId,
key,
}: {
teamId?: string;
campaignId?: string;
key?: string | string[];
}): Promise<void>;
Server-side guard intended to be the first thing a server component / layout does. Behavior:
| Condition | Result |
|---|---|
teamId set + !teamAccess | redirect("/no-access") |
campaignId set + !campaignAccess + not super-admin team | redirect("/${teamId}/campaign/no-access") |
key provided + user lacks ALL of them + team scope | redirect("/no-access") |
key provided + user lacks ALL of them + campaign scope | redirect("/${teamId}/campaign/no-access") |
| All checks pass | returns |
key accepts a single string or an array; the user only needs one matching key (OR semantics). The super-admin team (SUPER_ADMIN_TEAM_ID) is exempt from key checks.
Example:
import { requireAccess } from "@/services/accessCheck/requireAccess";
export default async function PetitionsPage({
params,
}: {
params: { primaryTeam: string; slug: string };
}) {
await requireAccess({
teamId: params.primaryTeam,
campaignId: params.slug,
key: ["campaign-petitions-page"],
});
// ...
}
7.4 Permission keys
Permission keys are slugs stored in the perm_keys table and bundled into "Responsibilities" (permission_set_keys). The current mainline keys break into:
- Team-level:
team-*(e.g.team-campaigns-page,team-voter-search,team-admin-voter-search,team-members-page,team-roles-page,team-permission-keys-page). - Campaign-level:
campaign-*(e.g.campaign-petitions-page,campaign-validators-page,campaign-circulators-page,campaign-signatures-page,campaign-households-page,campaign-dashboard-page,campaign-rates-page,campaign-transactions-page,campaign-turn-in-page). - Admin keys:
admin-*(e.g.admin-credentials-page).
The canonical list lives in App/src/lib/data/permissions/standard-permissions.ts and is grouped in the UI via primary-groups.ts and campaign-sub-groups.ts.
7.5 Role hierarchy
In addition to permission keys, every role has a numeric hierarchy_level. Member-management actions check this level via helpers in App/src/lib/team-member-hierarchy.ts — see Role Hierarchy.