Skip to main content

Authorization

Terra uses fail-secure RBAC: when in doubt, deny access.

Role Hierarchy

RoleDashboardFormsSubmissionsSettingsUsers
super_adminFullAllAllAllManage
adminFullAllAllViewView
userLimitedAssignedAssigned
applicantSubmitOwn only

Permission Checks

Every server action checks permissions:
// src/app/actions/team.ts

export async function requireAdmin() {
  const session = await getSession();
  if (!session) throw new Error("Unauthorized");

  const { data: user } = await supabaseAdmin
    .from("user_profiles")
    .select("role")
    .eq("workos_user_id", session.user.id)
    .single();

  if (!user || !["admin", "super_admin"].includes(user.role)) {
    throw new Error("Forbidden");
  }

  return user;
}

Fail-Secure Pattern

export async function requireFormAccess(formId: string) {
  try {
    const session = await getSession();
    const user = await getUserWithRole(session.user.id);

    // Super admins see everything
    if (user.role === "super_admin") return user;

    // Check form-level access
    const hasAccess = await checkFormAccess(formId, user.id);
    if (!hasAccess) throw new Error("No access to form");

    return user;
  } catch (error) {
    // ANY error = no access
    throw new Error("Unauthorized");
  }
}
Key principle: If permission check fails for any reason (database error, missing user, etc.), deny access. Never fail open.

Folder-Based Access

Users can have access to specific folders (workspaces):
SELECT f.id
FROM forms f
JOIN folder_members fm ON fm.folder_id = f.folder_id
WHERE fm.user_id = $1 AND f.id = $2