Permissions that ship with the feature.

Your app changes every week. Your permissions shouldn't be left catching up.

$ npm i @alfiz/core @alfiz/application
Declare it
src/authz/catalog.tsts
import { defineCatalog } from "@alfiz/core"; export const catalog = defineCatalog({  namespaces: ["docs"],  permissions: {    "docs.files.read":   { scopes: ["docs.folder", "docs.doc"] },    "docs.files.share":  { scopes: ["docs.folder", "docs.doc"] },    "docs.files.delete": { scopes: ["docs.folder"] },  },  scopeTypes: {    "docs.folder": { parent: null },    "docs.doc":    { parent: "docs.folder" },  },});
Check it
app/files/[id]/page.tsxts
// A grant on the folder covers every doc inside it.const ok = await alfiz.can(  { userId }, "docs.files.share", "docs.doc:123",);

Every key is a compile-time type, and alfiz-verify fails the build on a surface you left ungated.

Your apps declare what access existsin the code that enforces it
docs@1.4.0 · published from CIdocs.files.readdocs.files.share
Your team decides who gets itno engineering ticket
Jane Okafor → Project Leadscope · docs.folder:apolloapproved by M. Reyes, 12 Mar
Your auditor can see whyon any grant, on demand
explain() → allowedvia role Project Leadrequest #4182 · manager approval
The problem

Permissions kept outside the code inevitably drift away from it.

A catalog that lives in a console falls behind every time the application changes. Alfiz keeps permissions in the codebase that actually uses it.

Your applications, where access is defined
01

Declare

Your permission catalog lives inside the application it governs.

namespace: docsdocs.files.readdocs.files.sharedocs.files.delete
02

Publish

CI publishes the catalog to your provider as a versioned contract.

publish docs@1.4.0
publishes
Your team, where it is granted
03

Compose

Your team builds roles from published keys and assigns them to people, groups, or a place in the org chart.

role: Project Leaddocs.files.*billing.invoices.read

No field here for a permission your code doesn’t enforce.

04 the grant lands in your own database, and can() answers there, in-process
The arrow runs outward from the code, not inward from a console.
Topologies

Alfiz fits the organization you already have.

One application or a dozen, one database or one each, permissions crossing between them or strictly separate. Every combination below is a supported deployment, and a place deployments stay.

In all four, can() answers in your process, from your own tables.

Coding agents

Built to support Agent-driven development.

An agent has two lines to get right when it adds a permission, one search to find every place one is spent, and a verifier that fails the build when it misses a gate.

Adding one

Expanding permissions requires minimal context.

Two lines add a permission: one in the catalog, one at the call site. There is nothing else an agent has to find first.

ts
// ...catalog.ts  "docs.files.export": { scopes: ["docs.folder"] }, // app/actions/export.tsawait alfiz.requirePermission(  { userId }, "docs.files.export", scope,);
Changing one

One search reveals the full blast radius.

One search returns the declaration and every gate that depends on it. The same property lets alfiz-verify work without a type-checker.

bash
$ grep -rn "docs.files.share" src/ authz/catalog.ts:14      "docs.files.share": { scopes: [...] }docs/[id]/page.tsx:22    snap.can("docs.files.share", scope)actions/share.ts:9       requirePermission(p, "docs.files.share")components/nav.ts:31     { permission: "docs.files.share" }

Trust, but verify.

An agent works on the task in front of it and misses the surface next door. alfiz-verify fails the build when a route ships without a gate.

A surface is not done until all four points hold:

1 · The page

Gated before a row is fetched.

2 · Navigation

Only items the principal matches are rendered at all.

3 · The server action

Gated before any work. The step that gets skipped, so it errors.

4 · Conditional UI

Gated at the scope of the thing it acts on.

ci · every commitbash
$ alfiz-verify  [ungated-action] app/actions/export.ts:14  exported server action "exportDocument" contains no gate  [visibility-as-gate] app/actions/share.ts:15  canAny() is a visibility affordance, never a gate  [unreferenced-leaf] "docs.files.read_history"  declared, but referenced by no gate or nav item

Your first check, in under ten minutes.

Install two packages, declare a catalog, wire the driver you already have, and call can().