| name | ksl-new-resource |
| description | Author a new KSL resource type in the schema directory. Use when adding a new resource, creating a new schema file, defining a new namespace with resources, relations, and fields, or onboarding a new service to Kessel authorization. |
Author a New KSL Resource Type
Quick Reference
For DSL primitives (DataType, Cardinality, relation combinators, runtime helpers), see dsl-reference.md.
When to Create a New File vs Add to Existing
- New file when the resource belongs to a new application/service namespace (e.g.,
compliance, patch).
- Add to existing when the resource belongs to a namespace that already has a file (e.g., adding a second resource type inside
hbi).
Each file in schema/ is a TypeScript namespace. One namespace per file.
Resource Skeleton
namespace myapp {
export class my_resource extends Resource {
private workspace = new Relation<rbac.workspace>(
() => assignable(Cardinality.ExactlyOne, rbac.workspace, uuid())
);
some_id = new Field(false, uuid());
private myapp_resource_view!: rbac.workspace_permission;
private myapp_resource_update!: rbac.workspace_permission;
view = new Relation(
() => rbac.has_permission_on_workspace(this.workspace, this.myapp_resource_view)
);
update = new Relation(
() => rbac.has_permission_on_workspace(this.workspace, this.myapp_resource_update)
);
override applyExtensions(): void {
this.myapp_resource_view = rbac.create_v1_based_workspace_permission(
"myapp", "my_resources", "read", "myapp_resource_view"
);
this.myapp_resource_update = rbac.create_v1_based_workspace_permission(
"myapp", "my_resources", "write", "myapp_resource_update"
);
}
}
}
Key Rules
- Namespace wrapping is mandatory. The namespace name becomes the SpiceDB namespace prefix (e.g.,
myapp/my_resource).
extends Resource -- every resource type must extend Resource.
export the class if other namespaces need to reference it. Non-exported types require the @resource_type decorator (see rbac.ts for the pattern).
- Workspace relation -- workspace-scoped resources need an assignable
Relation<rbac.workspace> with Cardinality.ExactlyOne.
- Permission fields use
!: (definite assignment assertion) because they are assigned in applyExtensions(), not in the constructor.
- Cross-namespace calls go in
applyExtensions(), never in field initializers. Other namespaces may not be loaded when initializers run.
- Relation bodies are lazy -- the
() => ... factory is not evaluated until visit time, so forward references are safe inside the factory.
Non-Exported Types
For types that should not be accessible from other namespaces (like rbac.principal):
namespace myapp {
const resource_type = resource_type_for_namespace(myapp);
@resource_type("internal_thing")
class internal_thing extends Resource {
}
}
This requires experimentalDecorators in schema/tsconfig.json (already enabled).
Checklist
- File created in
schema/ with a namespace wrapper
- Class extends
Resource and is exported (or uses @resource_type)
- Workspace relation defined with proper cardinality
- Data fields added with appropriate types
- Permission fields declared with
!: type annotation
view/update (and any other) relations use rbac.has_permission_on_workspace
applyExtensions() calls rbac.create_v1_based_workspace_permission for each permission
- Run
make run and verify SpiceDB schema, JSON Schema, and V1 Permissions output