Skip to main content

Access Control

The access control surface lives in src/services/accessCheck/**. There are three exports:

HelperWhere to useWhat it does
getRoutePermissionsLayouts and pages that need the raw snapshotReturns { teamAccess, campaignAccess?, permissionKeys? } (cached)
accessCheckAnywhere a service needs the user's permission keys + a QueryResponse envelopeReturns { message, error, data: permissionKeys[] }
requireAccessTop of a server component / layoutCalls 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_cache keyed ["permissions-snapshot", userId, teamId ?? "", campaignId ?? ""] with revalidate: 3600 and tags: [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:

ConditionResult
teamId set + !teamAccessredirect("/no-access")
campaignId set + !campaignAccess + not super-admin teamredirect("/${teamId}/campaign/no-access")
key provided + user lacks ALL of them + team scoperedirect("/no-access")
key provided + user lacks ALL of them + campaign scoperedirect("/${teamId}/campaign/no-access")
All checks passreturns

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.