# Alfiz — Authorization for TypeScript apps, declared in code

> Open-source authorization for TypeScript. Your apps declare their permissions in code; your team grants them without an engineering ticket.

MIT core · scoped RBAC · your own database

Your applications declare their permissions in the code that enforces them. Your team grants them without an engineering ticket. Every check runs in your own database.

- Get started: <https://docs.alfiz.dev/quickstart>
- Source: <https://github.com/alfiz-auth>
- Documentation: <https://docs.alfiz.dev>

```bash
npm i @alfiz/core @alfiz/application
```

## Declare it

`src/authz/catalog.ts`

```ts
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.tsx`

```ts
// 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.

## The three clauses

| Clause | | What it looks like |
| --- | --- | --- |
| Your apps declare what access exists | in the code that enforces it | `docs@1.4.0 · published from CI`, `docs.files.read`, `docs.files.share` |
| Your team decides who gets it | no engineering ticket | `Jane Okafor → Project Lead`, `scope · docs.folder:apollo`, `approved by M. Reyes, 12 Mar` |
| Your auditor can see why | on any grant, on demand | `explain() → allowed`, `via role Project Lead`, `request #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.

### The declare / compose loop

*Your applications — where access is defined*

1. **Declare.** Your permission catalog lives inside the application it governs. `namespace: docs` · `docs.files.read` · `docs.files.share` · `docs.files.delete`
2. **Publish.** CI publishes the catalog to your provider as a versioned contract. `publish docs@1.4.0`

*Your team — where it is granted*

3. **Compose.** Your team builds roles from published keys and assigns them to people, groups, or a place in the org chart. `role: Project Lead` · `docs.files.*` · `billing.invoices.read`

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

4. the grant lands in your own database — `can()` answers there, in-process

The arrow runs outward from the code, not inward from a console.

## Coding agents

**Built to support Agent-driven development.**

Alfiz is designed from the ground up to be as easy as possible for Agents to develop secure applications without compromising on development speed.

### Adding one — Expanding permissions requires minimal context.

Agents only need to edit two lines to add a new permission: once at the catalog, once at the call site. Everything else builds on this foundation.

```ts
// ...catalog.ts
  "docs.files.export": { scopes: ["docs.folder"] },

// app/actions/export.ts
await 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.

Agents respond to the immediate task at hand, often losing the bigger picture. `alfiz-verify` ensures that your agent never forgets to protect new routes.

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 commit`

```bash
$ 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()`.

- Read the quickstart: <https://docs.alfiz.dev/quickstart>
- View on GitHub: <https://github.com/alfiz-auth>

## About

Open-source authorization for TypeScript. Declared in the code that enforces it, administered by your team, never on your request path.

AL-feez · from Andalusi Arabic al-ḥayyiz, “the bounded space”

- Quickstart: <https://docs.alfiz.dev/quickstart>
- Documentation: <https://docs.alfiz.dev>
- GitHub: <https://github.com/alfiz-auth>
- `@alfiz/core` on npm: <https://www.npmjs.com/package/@alfiz/core>
- Contact: <hello@alfiz.dev>

© 2026 Alfiz. Every opening carries its own frame.
