Scope types vs. scope instances
The catalog declares scope types, which are the shapes. Your application data holds scope instances, which are the individual resources. The distinction matters:
A scope instance id is always
<scopeType>:<instanceId>. The instance id (42) is an opaque string. It should be a primary key or UUID, and never a path. The hierarchy is stored in your database as parent pointers, not encoded in the id:
The global scope
The global scope* is built into every catalog. A permission grantable only at *, meaning one with no scope types declared, applies across your entire application, regardless of which resource the check names. You do not declare * in scopeTypes.
For flat applications with no resource hierarchy, you do not need to declare any scope types at all. Every permission defaults to global-only grantability, and checks pass or fail based solely on whether the actor holds the permission globally.
Declaring scope types: ScopeTypeInput
Add scope types to the scopeTypes record in your CatalogInput. Keys follow the same dotted-segment format as permission keys (docs.folder, docs.doc), and must fall within a declared namespace.
string
Human-facing description of what this scope type represents, shown in admin surfaces and permission pickers.
ScopeType | null
The expected parent scope type for instances of this type. Use
null for top-level types whose instances parent directly to *. This is a commitment and not a hint. Declaring parent: null means the ancestor chain for every instance of this type is exactly [instance, "*"], which lets the runtime check these synchronously without an async ancestry lookup.For recursive hierarchies (folders inside folders), set the parent to the same type: { parent: "docs.folder" }. There is no “undeclared” state: omitting parent is identical to writing parent: null, and so carries the same flat-instance commitment. Declare it explicitly.boolean
default:"false"
Enables multi-parent instances for this scope type. With multi-parent on, an instance’s effective access is the union of all its parents’ access, so a user with access to any one parent of a shared document has access to the document.Multi-parent is off by default because it is a meaningful widening: in most products, a file having two parent folders should not mean that access granted in either folder propagates to the file. Use multi-parent explicitly for products where that union is the correct behavior, such as shortcuts, labels-as-folders, or cross-project sharing models.
object
Declares that grants at instances of this scope type may be requested by end users. Nothing is requestable by default; you opt in here.When set, the request form for this scope type shows the configured
prompts (justification fields), respects maxDurationMs as a ceiling on how long a request may propose to run, and requires an expiry when requireExpiry is true. The policy field is required and must declare at least one approval stage.Folder and document hierarchy: a complete example
Per-scope-type grantability
Thescopes field on PermissionLeafInput (and GroupInput) controls which scope types a permission can be granted at. Alfiz validates this at the grant write path. Attempting to grant a permission at a scope type it never declared is a hard error and not a silent no-op.
Single-parent vs. multi-parent
- Single-parent (default)
- Multi-parent (explicit opt-in)
Each instance has exactly one parent. Access flows strictly up the chain, so a grant on a folder covers everything inside it, and a grant on a document covers only that document.This is the right model for most hierarchical products. It is the default, so you do not need to say anything.
Scope type keys must fall within a declared namespace.
docs.folder is valid when docs appears in namespaces. A scope type referencing an undeclared namespace is a boot-time error from defineCatalog.