agentsclimarketplace

Authorization rbac

Skill param087/saas-starter-skills/skills/authorization-rbac

Production-grade full-stack SaaS skills for AI coding agents — Next.js, Postgres/Drizzle, Auth, Stripe & Vercel patterns for Codex, Claude Code, Cursor & OpenCode.

Install
npx -y skills add param087/saas-starter-skills --skill authorization-rbac

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

What its author says it does

Copied from the file, not written here

Use when controlling what a user may do — implement role-based access control with a central permission map, server-side checks on every mutation, and a typed `can()` helper instead of scattered role string comparisons.

SKILL.md

2.9 KB, as published. Nobody here has run it

Authorization (RBAC)

Overview

Authentication says who you are; authorization says what you may do. Avoid if (role === "admin") sprinkled everywhere — it drifts and leaks. Define a single permission matrix mapping roles to actions, expose one typed can(role, action) helper, and call it on the server before every mutation. When rules outgrow roles, graduate to resource-level checks (ownership) without changing call sites.

When to use

  • Different roles (owner/admin/member) get different capabilities.
  • Any mutation needs a "is this user allowed?" check.
  • You're comparing role strings inline in handlers or components.

The pattern

// src/server/auth/permissions.ts
export type Role = "owner" | "admin" | "member";
export type Action =
  | "project:create" | "project:delete"
  | "member:invite" | "member:remove"
  | "billing:manage";

const MATRIX: Record<Role, Action[]> = {
  owner:  ["project:create", "project:delete", "member:invite", "member:remove", "billing:manage"],
  admin:  ["project:create", "project:delete", "member:invite"],
  member: ["project:create"],
};

export function can(role: Role, action: Action): boolean {
  return MATRIX[role]?.includes(action) ?? false;
}

export function authorize(role: Role, action: Action): void {
  if (!can(role, action)) {
    const e = new Error("Forbidden");
    (e as any).status = 403;
    throw e;
  }
}

Enforce in the action/handler, after resolving the tenant:

const { role, org } = await requireOrg(params.org);
authorize(role, "project:delete");      // throws 403 if not allowed
await deleteProject(org.id, projectId); // DAL still scopes by org_id

Beyond roles

  • Resource ownership: "only the creator or an admin can edit" → check row.createdBy === user.id || can(role, ...).
  • Keep the matrix data, not code paths — adding a permission is a one-line edit, fully typed by the Action union.
  • Mirror in the UI with the same can() to hide buttons — but the server check is the real gate.

Pitfalls

  • Authorizing in the client only — hiding a button isn't security; the endpoint must check.
  • Role checks scattered inline — they fall out of sync; centralize in the matrix.
  • Confusing tenancy with RBACmulti-tenancy ensures it's your org's data; RBAC ensures you're allowed to act on it. You need both.
  • Defaulting unknown actions to allow — default-deny (?? false).
  • Forgetting ownership rules — role alone can't express "your own drafts"; add resource checks.

Hand-off

A typed can() / authorize() enforced on the server. Pair with data-access-layer (which still scopes by org) and surface allowed actions to the UI.

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.