Quick Start
Time: ~30 minutes
This tutorial walks you through setting up Kessel from scratch. By the end, you will have:
- Run Kessel locally with Docker Compose
- Defined a resource type with permissions using Kessel’s schema language
- Reported a resource to Kessel’s inventory
- Checked permissions to verify access control is working
The example uses a simple document management scenario, similar to Google Drive: users create documents, organize them into workspaces, and share access through roles.
Along the way, you’ll touch on these concepts (each links to a deeper explanation):
- Resources and representations - the building blocks of Kessel’s inventory
- Schema - how you describe resource types and their relationships
- RBAC - roles, role bindings, and permission checks
- Reporting resources - telling Kessel about your resources
- Protecting resource access - checking permissions before serving a request
Prerequisites
Section titled “Prerequisites”Let’s get some prerequisites out of the way. These are temporary solutions, so don’t worry about them too much.
-
We need a schema compiler.
Install the KSL compiler directly from GitHub:
Terminal window go install github.com/project-kessel/ksl-schema-language/cmd/ksl@latestAlternatively, you can clone the repository and build locally:
Terminal window git clone https://github.com/project-kessel/ksl-schema-language.gitcd ksl-schema-languagemake buildEnsure
kslis in your$PATH, or adjust the path in the scripts below. -
We need gRPCurl to perform some setup operations. gRPCurl is like curl, but for gRPC.
Terminal window go install github.com/fullstorydev/grpcurl/cmd/grpcurl@latestTerminal window brew install grpcurlSee the grpcurl docs for additional installation methods including Snap, Docker, and prebuilt binaries.
-
We need a clean working directory for some config files. If you don’t have one already, create one and change to it now.
Terminal window mkdir -p kessel-getting-startedcd kessel-getting-started -
Some built-in schemas need to be configured manually for now. We’ll talk more about schemas in the next section.
Terminal window cat > kessel.ksl << 'EOF'version 0.1namespace kesselinternal type lock {relation #version: [ExactlyOne lockversion]}internal type lockversion {}EOFTerminal window cat > rbac.ksl << 'EOF'version 0.1namespace rbacpublic type principal {}public type group {relation member: [Any principal or group.member]}public type role {}public type role_binding {relation subject: [Any principal or group.member]relation granted: [AtLeastOne role]}public type workspace {relation parent: [AtMostOne workspace]relation user_grant: [Any role_binding]}public extension add_permission(name) {type role {private relation `${name}`: [bool]}type role_binding {internal relation `${name}`: subject and granted.`${name}`}type workspace {internal relation `${name}`: user_grant.`${name}` or parent.`${name}`}}EOF
Configure resources
Section titled “Configure resources”Now for the interesting part. To protect and share your application’s resources, you first need to configure a resource schema.
The example below defines a simple “document” resource type, similar to the scenario described in Understanding Kessel. Documents are organized into workspaces and have assignable permissions through Kessel’s RBAC model.
-
Tell Kessel about the resource type. Just like a database needs table definitions before you can insert rows, Kessel needs to know about your resource types before you can report resources. A resource type defines the attributes a resource can have and the relationships it participates in. Relationships are what power access control.
A single resource can have multiple representations from different reporters (services that contribute data about the resource). For this tutorial, we’ll define one resource type called “document” with one reporter called “drive”.
The metadata for resources and reporters follows a directory structure.
Terminal window # Create a folder for the document resource typemkdir -p document/The document resource type has a single “drive” reporter.
Terminal window cat > document/config.yaml << 'EOF'resource_type: documentresource_reporters:- driveEOFDocuments have a common representation (shared JSON data that is the same regardless of which reporter submits it). Here we configure the common representation schema, which includes a required
workspace_idattribute.A workspace is a built-in Kessel resource that groups other resources together, similar to a folder. You can nest workspaces to create a resource hierarchy. In this tutorial, workspaces take the place of Drive folders.
The workspace relationship is part of the common representation because a document always belongs to exactly one workspace, no matter how many reporters contribute data about it.
Terminal window cat > document/common_representation.json << 'EOF'{"$schema": "http://json-schema.org/draft-07/schema#","type": "object","properties": {"workspace_id": { "type": "string" }},"required": ["workspace_id"]}EOFReporters also have representations specific to their own state. Here we configure the “drive” reporter and its document representation schema.
Terminal window # Create a folder for the drive reporter's representation of a document.mkdir -p document/reporters/driveTerminal window cat > document/reporters/drive/config.yaml << 'EOF'resource_type: documentreporter_name: drivenamespace: driveEOFTerminal window cat > document/reporters/drive/document.json << 'EOF'{"$schema": "http://json-schema.org/draft-07/schema#","type": "object","properties": {"document_id": { "type": "string" },"document_name": { "type": "string" },"document_type": {"type": "string","enum": ["document"]},"created_at": {"type": "string","format": "date-time"},"file_size": {"type": "integer","minimum": 0},"owner_id": { "type": "string" }},"required": ["document_id","document_name","document_type"]}EOFAnd finally we configure permissions. Permissions are defined in a KSL schema file.
Terminal window cat > drive.ksl << 'EOF'version 0.1namespace driveimport rbac// Extend RBAC model with relevant document permissions@rbac.add_permission(name:'view_document')@rbac.add_permission(name:'edit_document')@rbac.add_permission(name:'delete_document')public type document {relation workspace: [ExactlyOne rbac.workspace]// Define document permissions,// which are inherited from the workspace-level permissions// that we defined above.relation view: workspace.view_documentrelation edit: workspace.edit_documentrelation delete: workspace.delete_document}EOF -
Compile to SpiceDB schema.
We installed the
kslschema compiler earlier. Use it now to compile the schemas:Terminal window ksl kessel.ksl rbac.ksl drive.kslThis will generate a
schema.zedfile in the current directory.Example generated
schema.zedfileschema.zed definition drive/document {permission edit = t_workspace->edit_documentpermission delete = t_workspace->delete_documentpermission view = t_workspace->view_documentpermission workspace = t_workspacerelation t_workspace: rbac/workspace}definition kessel/lock {permission version = t_versionrelation t_version: kessel/lockversion}definition kessel/lockversion {}definition rbac/group {permission member = t_memberrelation t_member: rbac/principal | rbac/group#member}definition rbac/principal {}definition rbac/role {permission delete_document = t_delete_documentrelation t_delete_document: rbac/principal:*permission edit_document = t_edit_documentrelation t_edit_document: rbac/principal:*permission view_document = t_view_documentrelation t_view_document: rbac/principal:*}definition rbac/role_binding {permission delete_document = (subject & t_granted->delete_document)permission edit_document = (subject & t_granted->edit_document)permission granted = t_grantedrelation t_granted: rbac/rolepermission subject = t_subjectrelation t_subject: rbac/principal | rbac/group#memberpermission view_document = (subject & t_granted->view_document)}definition rbac/workspace {permission delete_document = t_user_grant->delete_document + t_parent->delete_documentpermission edit_document = t_user_grant->edit_document + t_parent->edit_documentpermission parent = t_parentrelation t_parent: rbac/workspacepermission user_grant = t_user_grantrelation t_user_grant: rbac/role_bindingpermission view_document = t_user_grant->view_document + t_parent->view_document}
Install and run Kessel
Section titled “Install and run Kessel”-
Clone the Kessel Inventory API repository and load your custom schema.
Terminal window git clone git@github.com:project-kessel/inventory-api.gitcd inventory-apicp -r ../document data/schema/resources/go run main.go preload-schema -
Start the full Kessel stack with your custom SpiceDB schema.
Terminal window SCHEMA_ZED_FILE=../schema.zed make kessel-upThis starts all services in a single command. The
SCHEMA_ZED_FILEvariable tells the startup script to use your compiled schema instead of downloading the default from GitHub.For more details on the full stack, configuration options, and troubleshooting, see Run locally with Docker Compose.
Set up access with RBAC
Section titled “Set up access with RBAC”Kessel is running. You’ve configured the built-in RBAC schemas and defined a document resource type with permissions. Now you need to grant a user access.
For a user to get access, three things come together:
- Permissions, grouped into a role.
- Resources, which can be grouped into a workspace.
- A subject (for example, a user), which can belong to a group.
A role binding ties these together: it grants a role to a subject on a specific resource or workspace. In the next steps, you’ll create a role with some permissions, then bind that role to a user on a workspace. This gives the user access to that workspace and any documents inside it.
-
Create a role
A role defines a set of permissions. Edit the
ROLE_NAMEandPERMISSIONSvariables to customize, then run the script.Terminal window #!/bin/bash# Create a role with specified permissions## Usage: Edit the configuration below.## The script can be copied directly into a terminal or saved as a file.# ============ CONFIGURATION ============# Modify these values for your use caseROLE_NAME="drive-admin-role"PERMISSIONS=("view_document" "edit_document" "delete_document")RELATIONS_PORT=9000# =======================================# Build the tuples array for all permissionsTUPLES=""for i in "${!PERMISSIONS[@]}"; doPERM="${PERMISSIONS[$i]}"TUPLE="{\"resource\":{\"id\":\"${ROLE_NAME}\",\"type\":{\"name\":\"role\",\"namespace\":\"rbac\"}},\"relation\":\"${PERM}\",\"subject\":{\"subject\":{\"id\":\"*\",\"type\":{\"name\":\"principal\",\"namespace\":\"rbac\"}}}}"if [ $i -gt 0 ]; thenTUPLES="${TUPLES},"fiTUPLES="${TUPLES}${TUPLE}"doneMESSAGE="{\"tuples\":[${TUPLES}]}"echo "Creating role '${ROLE_NAME}' with permissions: ${PERMISSIONS[*]}"grpcurl -plaintext -d "${MESSAGE}" \"localhost:${RELATIONS_PORT}" \kessel.relations.v1beta1.KesselTupleService.CreateTuples -
Create a role binding
A role binding grants a user access to a role for a specific resource (e.g., a workspace). Edit the
ROLE_NAME,USER_ID, andRESOURCE_IDvariables, then run the script. The binding ID is auto-generated from these inputs.Terminal window #!/bin/bash# Create a role binding that grants a user access to a role for a specific resource## Usage: Edit the configuration below.## The script can be copied directly into a terminal or saved as a file.# ============ CONFIGURATION ============# Modify these values for your use caseROLE_NAME="drive-admin-role"USER_ID="sarah"RESOURCE_ID="workspace-1"RELATIONS_PORT=9000# =======================================# Auto-generate a deterministic binding ID from the inputsBINDING_ID="${ROLE_NAME}--${USER_ID}--${RESOURCE_ID}"echo "Creating role binding '${BINDING_ID}'"echo " Role: ${ROLE_NAME}"echo " User: ${USER_ID}"echo " Resource: ${RESOURCE_ID}"# Create all three relationships in a single call:# 1. role_binding -> granted -> role# 2. role_binding -> subject -> user# 3. workspace -> user_grant -> role_bindingMESSAGE="{\"tuples\":[{\"resource\":{\"id\":\"${BINDING_ID}\",\"type\":{\"name\":\"role_binding\",\"namespace\":\"rbac\"}},\"relation\":\"granted\",\"subject\":{\"subject\":{\"id\":\"${ROLE_NAME}\",\"type\":{\"name\":\"role\",\"namespace\":\"rbac\"}}}},{\"resource\":{\"id\":\"${BINDING_ID}\",\"type\":{\"name\":\"role_binding\",\"namespace\":\"rbac\"}},\"relation\":\"subject\",\"subject\":{\"subject\":{\"id\":\"${USER_ID}\",\"type\":{\"name\":\"principal\",\"namespace\":\"rbac\"}}}},{\"resource\":{\"id\":\"${RESOURCE_ID}\",\"type\":{\"name\":\"workspace\",\"namespace\":\"rbac\"}},\"relation\":\"user_grant\",\"subject\":{\"subject\":{\"id\":\"${BINDING_ID}\",\"type\":{\"name\":\"role_binding\",\"namespace\":\"rbac\"}}}}]}"grpcurl -plaintext -d "${MESSAGE}" \"localhost:${RELATIONS_PORT}" \kessel.relations.v1beta1.KesselTupleService.CreateTuples
Set up your environment
Section titled “Set up your environment”Kessel is configured and access is granted at the workspace level. Now it’s time to write some code.
Start by setting up a client to interact with Kessel. SDKs are available for many popular languages, or you can use the gRPC API directly.
-
Install dependencies.
If you followed along with Prerequisites, you’re good to go.
Terminal window pip install kessel-sdkTerminal window go get github.com/project-kessel/kessel-sdk-goTerminal window npm install @project-kessel/kessel-sdkTerminal window gem install kessel-sdk<dependency><groupId>org.project-kessel</groupId><artifactId>kessel-sdk</artifactId><version>1.7.0</version></dependency><dependency><groupId>io.grpc</groupId><artifactId>grpc-netty-shaded</artifactId><version>1.77.0</version><scope>runtime</scope></dependency><dependency><groupId>com.nimbusds</groupId><artifactId>oauth2-oidc-sdk</artifactId><version>11.30.1</version></dependency>dependencies {implementation 'org.project-kessel:kessel-sdk:1.6.0'runtimeOnly 'io.grpc:grpc-netty-shaded:1.77.0'implementation 'com.nimbusds:oauth2-oidc-sdk:11.30.1'} -
Configure a client.
Terminal window # Set your Kessel gRPC endpointKESSEL_GRPC_ENDPOINT="localhost:9081"# For insecure local development:GRPC_OPTS="-plaintext"# For insecure local development:stub, channel = ClientBuilder(KESSEL_ENDPOINT).insecure().build()inventoryClient, conn, err := v1beta2.NewClientBuilder(KESSEL_ENDPOINT).Insecure().Build()if err != nil {log.Fatal("Failed to create gRPC client:", err)}defer func() {if closeErr := conn.Close(); closeErr != nil {log.Printf("Failed to close gRPC client: %v", closeErr)}}()// For insecure local development:const client = new ClientBuilder(process.env.KESSEL_ENDPOINT).insecure().buildAsync();# For insecure local development:client = KesselInventoryService::ClientBuilder.new(ENV.fetch('KESSEL_ENDPOINT', 'localhost:9081')).insecure.buildString kesselEndpoint = System.getenv().getOrDefault("KESSEL_ENDPOINT", "localhost:9081");Pair<KesselInventoryServiceBlockingStub, ManagedChannel> clientAndChannel =new ClientBuilder(kesselEndpoint).insecure().build();KesselInventoryServiceBlockingStub client = clientAndChannel.getLeft();
Report a resource
Section titled “Report a resource”Before you can check access on a resource, Kessel needs to know about it. You tell Kessel about resources by reporting them.
In this example, a document is reported along with its relationship to a workspace. Because the document belongs to a workspace, it inherits the access you granted at the workspace level.
REPORT_MESSAGE='{"type": "document", "reporterType": "drive", "reporterInstanceId": "drive-1","representations": {"metadata": {"localResourceId": "doc-123","apiHref": "https://drive.example.com/document/123","consoleHref": "https://www.console.com/drive/documents","reporterVersion": "2.7.16"},"common": {"workspace_id": "workspace-1"},"reporter": {"document_id": "doc-123","document_name": "My Important Document","document_type": "document","created_at": "2025-08-31T10:30:00Z","file_size": 2048576,"owner_id": "user-1"}}}'grpcurl $GRPC_OPTS \ -d "$REPORT_MESSAGE" \ "$KESSEL_GRPC_ENDPOINT" \ kessel.inventory.v1beta2.KesselInventoryService.ReportResourcecommon_struct = struct_pb2.Struct()common_struct.update({"workspace_id": "workspace-1"})
reporter_struct = struct_pb2.Struct()reporter_struct.update({ "document_id": "doc-123", "document_name": "My Important Document", "document_type": "document", "created_at": "2025-08-31T10:30:00Z", "file_size": 2048576, "owner_id": "user-1",})
metadata = representation_metadata_pb2.RepresentationMetadata( local_resource_id="doc-123", api_href="https://drive.example.com/document/123", console_href="https://www.console.com/drive/documents", reporter_version="2.7.16",)
representations = resource_representations_pb2.ResourceRepresentations( metadata=metadata, common=common_struct, reporter=reporter_struct,)
report_req = report_resource_request_pb2.ReportResourceRequest( type="document", reporter_type="drive", reporter_instance_id="drive-1", representations=representations,)stub.ReportResource(report_req)reportResourceRequest := &v1beta2.ReportResourceRequest{ Type: "document", ReporterType: "drive", ReporterInstanceId: "drive-1", Representations: &v1beta2.ResourceRepresentations{ Metadata: &v1beta2.RepresentationMetadata{ LocalResourceId: "doc-123", ApiHref: "https://drive.example.com/document/123", ConsoleHref: addr("https://www.console.com/drive/documents"), ReporterVersion: addr("2.7.16"), }, Common: &structpb.Struct{ Fields: map[string]*structpb.Value{ "workspace_id": structpb.NewStringValue("workspace-1"), }, }, Reporter: &structpb.Struct{ Fields: map[string]*structpb.Value{ "document_id": structpb.NewStringValue("doc-123"), "document_name": structpb.NewStringValue("My Important Document"), "document_type": structpb.NewStringValue("document"), "created_at": structpb.NewStringValue("2025-08-31T10:30:00Z"), "file_size": structpb.NewNumberValue(2048576), "owner_id": structpb.NewStringValue("user-1"), }, }, },}
_, err = inventoryClient.ReportResource(ctx, reportResourceRequest)if err != nil { log.Fatalf("ReportResource failed: %v", err)}const common = { workspace_id: "workspace-1" };const reporter = { document_id: "doc-123", document_name: "My Important Document", document_type: "document", created_at: "2025-08-31T10:30:00Z", file_size: 2048576, owner_id: "user-1",};const metadata: RepresentationMetadata = { localResourceId: "doc-123", apiHref: "https://drive.example.com/document/123", consoleHref: "https://www.console.com/drive/documents", reporterVersion: "2.7.16",};const representations: ResourceRepresentations = { metadata, common, reporter,};const reportResourceRequest: ReportResourceRequest = { type: "document", reporterType: "drive", reporterInstanceId: "drive-1", representations,};await client.reportResource(reportResourceRequest);common = Google::Protobuf::Struct.decode_json({ 'workspace_id' => 'workspace-1' }.to_json)reporter = Google::Protobuf::Struct.decode_json({ 'document_id' => 'doc-123', 'document_name' => 'My Important Document', 'document_type' => 'document', 'created_at' => '2025-08-31T10:30:00Z', 'file_size' => 2048576, 'owner_id' => 'user-1'}.to_json)metadata = RepresentationMetadata.new( local_resource_id: 'doc-123', api_href: 'https://drive.example.com/document/123', console_href: 'https://www.console.com/drive/documents', reporter_version: '2.7.16')representations = ResourceRepresentations.new( metadata: metadata, common: common, reporter: reporter)client.report_resource( ReportResourceRequest.new( type: 'document', reporter_type: 'drive', reporter_instance_id: 'drive-1', representations: representations ))ReportResourceRequest reportResourceRequest = ReportResourceRequest .newBuilder() .setType("document") .setReporterType("drive") .setReporterInstanceId("drive-1") .setRepresentations( ResourceRepresentations .newBuilder() .setMetadata( RepresentationMetadata .newBuilder() .setLocalResourceId("doc-123") .setApiHref("https://drive.example.com/document/123") .setConsoleHref("https://www.console.com/drive/documents") .setReporterVersion("2.7.16") .build() ) .setCommon( Struct .newBuilder() .putFields("workspace_id", Value.newBuilder().setStringValue("workspace-1").build()) .build() ) .setReporter( Struct .newBuilder() .putFields("document_id", Value.newBuilder().setStringValue("doc-123").build()) .putFields("document_name", Value.newBuilder().setStringValue("My Important Document").build()) .putFields("document_type", Value.newBuilder().setStringValue("document").build()) .putFields("created_at", Value.newBuilder().setStringValue("2025-08-31T10:30:00Z").build()) .putFields("file_size", Value.newBuilder().setNumberValue(2048576).build()) .putFields("owner_id", Value.newBuilder().setStringValue("user-1").build()) .build() ) .build() ) .build();
client.reportResource(reportResourceRequest);Check permissions
Section titled “Check permissions”Once Kessel knows about a resource and its relationships, you can check whether a user has a specific permission on it.
# NOTE: You may need to wait for replication and caches to update.CHECK_MESSAGE='{"object": {"resource_type": "document", "resource_id": "doc-123", "reporter": {"type": "drive"}}, "relation": "view", "subject": {"resource": {"resource_type": "principal", "resource_id": "sarah", "reporter": {"type": "rbac"}}}}'grpcurl $GRPC_OPTS \ -d "$CHECK_MESSAGE" \ "$KESSEL_GRPC_ENDPOINT" \ kessel.inventory.v1beta2.KesselInventoryService.Check# NOTE: You may need to wait for replication and caches to update.subject = subject_reference_pb2.SubjectReference( resource=resource_reference_pb2.ResourceReference( reporter=reporter_reference_pb2.ReporterReference(type="rbac"), resource_id="sarah", resource_type="principal", ))
resource_ref = resource_reference_pb2.ResourceReference( resource_id="doc-123", resource_type="document", reporter=reporter_reference_pb2.ReporterReference(type="drive"),)
check_request = check_request_pb2.CheckRequest( subject=subject, relation="view", object=resource_ref,)
try: check_response = stub.Check(check_request) print("Check response received successfully") print(check_response)except grpc.RpcError as e: print("gRPC error occurred during Check:") print(f"Code: {e.code()}") print(f"Details: {e.details()}")// NOTE: You may need to wait for replication and caches to update.checkRequest := &v1beta2.CheckRequest{ Object: &v1beta2.ResourceReference{ ResourceType: "document", ResourceId: "doc-123", Reporter: &v1beta2.ReporterReference{Type: "drive"}, }, Relation: "view", Subject: &v1beta2.SubjectReference{ Resource: &v1beta2.ResourceReference{ ResourceType: "principal", ResourceId: "sarah", Reporter: &v1beta2.ReporterReference{Type: "rbac"}, }, },}
fmt.Println("Making check request:")response, err := inventoryClient.Check(ctx, checkRequest)if err != nil { if st, ok := status.FromError(err); ok { switch st.Code() { case codes.Unavailable: log.Fatal("Service unavailable: ", err) case codes.PermissionDenied: log.Fatal("Permission denied: ", err) default: log.Fatal("gRPC connection error: ", err) } } else { log.Fatal("Unknown error: ", err) }}fmt.Printf("Check response: %+v\n", response)// NOTE: You may need to wait for replication and caches to update.const subjectReference: SubjectReference = { resource: { reporter: { type: "rbac" }, resourceId: "sarah", resourceType: "principal", },};const resource: ResourceReference = { reporter: { type: "drive" }, resourceId: "doc-123", resourceType: "document",};const check_request: CheckRequest = { object: resource, relation: "view", subject: subjectReference,};const response = await client.check(check_request);console.log("Check response received successfully:");console.log(response);# NOTE: You may need to wait for replication and caches to update.subject_reference = SubjectReference.new( resource: ResourceReference.new( reporter: ReporterReference.new(type: 'rbac'), resource_id: 'sarah', resource_type: 'principal' ))resource = ResourceReference.new( reporter: ReporterReference.new(type: 'drive'), resource_id: 'doc-123', resource_type: 'document')
begin response = client.check( CheckRequest.new( object: resource, relation: 'view', subject: subject_reference ) ) p 'check response received successfully:' p responserescue => e p 'gRPC error occurred during check:' p "Exception: #{e}"end// NOTE: You may need to wait for replication and caches to update.CheckRequest checkRequest = CheckRequest .newBuilder() .setObject( ResourceReference .newBuilder() .setReporter(ReporterReference.newBuilder().setType("drive").build()) .setResourceId("doc-123") .setResourceType("document") .build() ) .setRelation("view") .setSubject( SubjectReference .newBuilder() .setResource( ResourceReference .newBuilder() .setReporter(ReporterReference.newBuilder().setType("rbac").build()) .setResourceId("sarah") .setResourceType("principal") .build() ) .build() ) .build();
CheckResponse response = client.check(checkRequest);System.out.println("Check response received successfully:");System.out.println(response);Complete example
Section titled “Complete example”#!/bin/bash
# Set your Kessel gRPC endpointKESSEL_GRPC_ENDPOINT="localhost:9081"
# For insecure local development:GRPC_OPTS="-plaintext"
REPORT_MESSAGE='{"type": "document", "reporterType": "drive", "reporterInstanceId": "drive-1","representations": {"metadata": {"localResourceId": "doc-123","apiHref": "https://drive.example.com/document/123","consoleHref": "https://www.console.com/drive/documents","reporterVersion": "2.7.16"},"common": {"workspace_id": "workspace-1"},"reporter": {"document_id": "doc-123","document_name": "My Important Document","document_type": "document","created_at": "2025-08-31T10:30:00Z","file_size": 2048576,"owner_id": "user-1"}}}'grpcurl $GRPC_OPTS \ -d "$REPORT_MESSAGE" \ "$KESSEL_GRPC_ENDPOINT" \ kessel.inventory.v1beta2.KesselInventoryService.ReportResource
# NOTE: You may need to wait for replication and caches to update.CHECK_MESSAGE='{"object": {"resource_type": "document", "resource_id": "doc-123", "reporter": {"type": "drive"}}, "relation": "view", "subject": {"resource": {"resource_type": "principal", "resource_id": "sarah", "reporter": {"type": "rbac"}}}}'grpcurl $GRPC_OPTS \ -d "$CHECK_MESSAGE" \ "$KESSEL_GRPC_ENDPOINT" \ kessel.inventory.v1beta2.KesselInventoryService.Checkimport grpcfrom google.protobuf import struct_pb2from kessel.inventory.v1beta2 import ( ClientBuilder, subject_reference_pb2, resource_reference_pb2, reporter_reference_pb2, check_request_pb2, report_resource_request_pb2, resource_representations_pb2, representation_metadata_pb2,)
KESSEL_ENDPOINT = "localhost:9081"
def main(): # For insecure local development: stub, channel = ClientBuilder(KESSEL_ENDPOINT).insecure().build()
with channel: common_struct = struct_pb2.Struct() common_struct.update({"workspace_id": "workspace-1"})
reporter_struct = struct_pb2.Struct() reporter_struct.update({ "document_id": "doc-123", "document_name": "My Important Document", "document_type": "document", "created_at": "2025-08-31T10:30:00Z", "file_size": 2048576, "owner_id": "user-1", })
metadata = representation_metadata_pb2.RepresentationMetadata( local_resource_id="doc-123", api_href="https://drive.example.com/document/123", console_href="https://www.console.com/drive/documents", reporter_version="2.7.16", )
representations = resource_representations_pb2.ResourceRepresentations( metadata=metadata, common=common_struct, reporter=reporter_struct, )
report_req = report_resource_request_pb2.ReportResourceRequest( type="document", reporter_type="drive", reporter_instance_id="drive-1", representations=representations, ) stub.ReportResource(report_req)
# NOTE: You may need to wait for replication and caches to update. subject = subject_reference_pb2.SubjectReference( resource=resource_reference_pb2.ResourceReference( reporter=reporter_reference_pb2.ReporterReference(type="rbac"), resource_id="sarah", resource_type="principal", ) )
resource_ref = resource_reference_pb2.ResourceReference( resource_id="doc-123", resource_type="document", reporter=reporter_reference_pb2.ReporterReference(type="drive"), )
check_request = check_request_pb2.CheckRequest( subject=subject, relation="view", object=resource_ref, )
try: check_response = stub.Check(check_request) print("Check response received successfully") print(check_response) except grpc.RpcError as e: print("gRPC error occurred during Check:") print(f"Code: {e.code()}") print(f"Details: {e.details()}")
if __name__ == "__main__": main()package main
import ( "context" "fmt" "log"
_ "github.com/joho/godotenv/autoload"
"google.golang.org/protobuf/types/known/structpb"
"github.com/project-kessel/kessel-sdk-go/kessel/inventory/v1beta2" "google.golang.org/grpc/codes" "google.golang.org/grpc/status")
const KESSEL_ENDPOINT = "localhost:9081"
func main() { ctx := context.Background()
inventoryClient, conn, err := v1beta2.NewClientBuilder(KESSEL_ENDPOINT). Insecure(). Build() if err != nil { log.Fatal("Failed to create gRPC client:", err) } defer func() { if closeErr := conn.Close(); closeErr != nil { log.Printf("Failed to close gRPC client: %v", closeErr) } }()
reportResourceRequest := &v1beta2.ReportResourceRequest{ Type: "document", ReporterType: "drive", ReporterInstanceId: "drive-1", Representations: &v1beta2.ResourceRepresentations{ Metadata: &v1beta2.RepresentationMetadata{ LocalResourceId: "doc-123", ApiHref: "https://drive.example.com/document/123", ConsoleHref: addr("https://www.console.com/drive/documents"), ReporterVersion: addr("2.7.16"), }, Common: &structpb.Struct{ Fields: map[string]*structpb.Value{ "workspace_id": structpb.NewStringValue("workspace-1"), }, }, Reporter: &structpb.Struct{ Fields: map[string]*structpb.Value{ "document_id": structpb.NewStringValue("doc-123"), "document_name": structpb.NewStringValue("My Important Document"), "document_type": structpb.NewStringValue("document"), "created_at": structpb.NewStringValue("2025-08-31T10:30:00Z"), "file_size": structpb.NewNumberValue(2048576), "owner_id": structpb.NewStringValue("user-1"), }, }, }, }
_, err = inventoryClient.ReportResource(ctx, reportResourceRequest) if err != nil { log.Fatalf("ReportResource failed: %v", err) }
// NOTE: You may need to wait for replication and caches to update. checkRequest := &v1beta2.CheckRequest{ Object: &v1beta2.ResourceReference{ ResourceType: "document", ResourceId: "doc-123", Reporter: &v1beta2.ReporterReference{Type: "drive"}, }, Relation: "view", Subject: &v1beta2.SubjectReference{ Resource: &v1beta2.ResourceReference{ ResourceType: "principal", ResourceId: "sarah", Reporter: &v1beta2.ReporterReference{Type: "rbac"}, }, }, }
fmt.Println("Making check request:") response, err := inventoryClient.Check(ctx, checkRequest) if err != nil { if st, ok := status.FromError(err); ok { switch st.Code() { case codes.Unavailable: log.Fatal("Service unavailable: ", err) case codes.PermissionDenied: log.Fatal("Permission denied: ", err) default: log.Fatal("gRPC connection error: ", err) } } else { log.Fatal("Unknown error: ", err) } } fmt.Printf("Check response: %+v\n", response)}
func addr[T any](t T) *T { return &t }// example.ts - Runnable JavaScript/TypeScript SDK exampleimport { ResourceReference } from "@project-kessel/kessel-sdk/kessel/inventory/v1beta2/resource_reference";import { SubjectReference } from "@project-kessel/kessel-sdk/kessel/inventory/v1beta2/subject_reference";import { CheckRequest } from "@project-kessel/kessel-sdk/kessel/inventory/v1beta2/check_request";import { ReportResourceRequest } from "@project-kessel/kessel-sdk/kessel/inventory/v1beta2/report_resource_request";import { ResourceRepresentations } from "@project-kessel/kessel-sdk/kessel/inventory/v1beta2/resource_representations";import { RepresentationMetadata } from "@project-kessel/kessel-sdk/kessel/inventory/v1beta2/representation_metadata";import { ClientBuilder } from "@project-kessel/kessel-sdk/kessel/inventory/v1beta2";import "dotenv/config";
async function run() { // For insecure local development: const client = new ClientBuilder(process.env.KESSEL_ENDPOINT).insecure().buildAsync();
try { const common = { workspace_id: "workspace-1" }; const reporter = { document_id: "doc-123", document_name: "My Important Document", document_type: "document", created_at: "2025-08-31T10:30:00Z", file_size: 2048576, owner_id: "user-1", }; const metadata: RepresentationMetadata = { localResourceId: "doc-123", apiHref: "https://drive.example.com/document/123", consoleHref: "https://www.console.com/drive/documents", reporterVersion: "2.7.16", }; const representations: ResourceRepresentations = { metadata, common, reporter, }; const reportResourceRequest: ReportResourceRequest = { type: "document", reporterType: "drive", reporterInstanceId: "drive-1", representations, }; await client.reportResource(reportResourceRequest);
// NOTE: You may need to wait for replication and caches to update. const subjectReference: SubjectReference = { resource: { reporter: { type: "rbac" }, resourceId: "sarah", resourceType: "principal", }, }; const resource: ResourceReference = { reporter: { type: "drive" }, resourceId: "doc-123", resourceType: "document", }; const check_request: CheckRequest = { object: resource, relation: "view", subject: subjectReference, }; const response = await client.check(check_request); console.log("Check response received successfully:"); console.log(response); } catch (error) { console.log("Error during report or check:"); console.log("Exception:", error); }}
run();#!/usr/bin/env ruby# frozen_string_literal: true# example.rb - Runnable Ruby SDK example
require 'dotenv/load'require 'json'require 'kessel-sdk'
include Kessel::Inventory::V1beta2include Kessel::GRPCinclude Kessel::Auth
# For insecure local development:client = KesselInventoryService::ClientBuilder.new(ENV.fetch('KESSEL_ENDPOINT', 'localhost:9081')) .insecure .build
common = Google::Protobuf::Struct.decode_json({ 'workspace_id' => 'workspace-1' }.to_json)reporter = Google::Protobuf::Struct.decode_json({ 'document_id' => 'doc-123', 'document_name' => 'My Important Document', 'document_type' => 'document', 'created_at' => '2025-08-31T10:30:00Z', 'file_size' => 2048576, 'owner_id' => 'user-1'}.to_json)metadata = RepresentationMetadata.new( local_resource_id: 'doc-123', api_href: 'https://drive.example.com/document/123', console_href: 'https://www.console.com/drive/documents', reporter_version: '2.7.16')representations = ResourceRepresentations.new( metadata: metadata, common: common, reporter: reporter)client.report_resource( ReportResourceRequest.new( type: 'document', reporter_type: 'drive', reporter_instance_id: 'drive-1', representations: representations ))
# NOTE: You may need to wait for replication and caches to update.subject_reference = SubjectReference.new( resource: ResourceReference.new( reporter: ReporterReference.new(type: 'rbac'), resource_id: 'sarah', resource_type: 'principal' ))resource = ResourceReference.new( reporter: ReporterReference.new(type: 'drive'), resource_id: 'doc-123', resource_type: 'document')
begin response = client.check( CheckRequest.new( object: resource, relation: 'view', subject: subject_reference ) ) p 'check response received successfully:' p responserescue => e p 'gRPC error occurred during check:' p "Exception: #{e}"endimport com.google.protobuf.Struct;import com.google.protobuf.Value;import com.nimbusds.jose.util.Pair;import io.grpc.ManagedChannel;import io.grpc.StatusRuntimeException;import static org.project_kessel.api.inventory.v1beta2.KesselInventoryServiceGrpc.KesselInventoryServiceBlockingStub;import org.project_kessel.api.inventory.v1beta2.*;
public class Example { public static void main(String[] args) { String kesselEndpoint = System.getenv().getOrDefault("KESSEL_ENDPOINT", "localhost:9081");
Pair<KesselInventoryServiceBlockingStub, ManagedChannel> clientAndChannel = new ClientBuilder(kesselEndpoint) .insecure() .build(); KesselInventoryServiceBlockingStub client = clientAndChannel.getLeft();
try { ReportResourceRequest reportResourceRequest = ReportResourceRequest .newBuilder() .setType("document") .setReporterType("drive") .setReporterInstanceId("drive-1") .setRepresentations( ResourceRepresentations .newBuilder() .setMetadata( RepresentationMetadata .newBuilder() .setLocalResourceId("doc-123") .setApiHref("https://drive.example.com/document/123") .setConsoleHref("https://www.console.com/drive/documents") .setReporterVersion("2.7.16") .build() ) .setCommon( Struct .newBuilder() .putFields("workspace_id", Value.newBuilder().setStringValue("workspace-1").build()) .build() ) .setReporter( Struct .newBuilder() .putFields("document_id", Value.newBuilder().setStringValue("doc-123").build()) .putFields("document_name", Value.newBuilder().setStringValue("My Important Document").build()) .putFields("document_type", Value.newBuilder().setStringValue("document").build()) .putFields("created_at", Value.newBuilder().setStringValue("2025-08-31T10:30:00Z").build()) .putFields("file_size", Value.newBuilder().setNumberValue(2048576).build()) .putFields("owner_id", Value.newBuilder().setStringValue("user-1").build()) .build() ) .build() ) .build();
client.reportResource(reportResourceRequest);
// NOTE: You may need to wait for replication and caches to update. CheckRequest checkRequest = CheckRequest .newBuilder() .setObject( ResourceReference .newBuilder() .setReporter(ReporterReference.newBuilder().setType("drive").build()) .setResourceId("doc-123") .setResourceType("document") .build() ) .setRelation("view") .setSubject( SubjectReference .newBuilder() .setResource( ResourceReference .newBuilder() .setReporter(ReporterReference.newBuilder().setType("rbac").build()) .setResourceId("sarah") .setResourceType("principal") .build() ) .build() ) .build();
CheckResponse response = client.check(checkRequest); System.out.println("Check response received successfully:"); System.out.println(response); } catch (StatusRuntimeException statusException) { System.out.println("gRPC error occurred:"); statusException.printStackTrace(); } finally { clientAndChannel.getRight().shutdown(); } }}What you learned
Section titled “What you learned”In this tutorial, you:
- Defined a resource type using KSL schema files and JSON Schema for resource attributes
- Compiled the schema to SpiceDB format using the
kslcompiler - Ran the full Kessel stack locally with Docker Compose
- Set up RBAC by creating a role with permissions and binding it to a user on a workspace
- Reported a resource to Kessel’s inventory, including its relationship to a workspace
- Checked permissions to verify that access control works through the relationship graph
These are the same building blocks you’ll use when integrating Kessel into a real application. The how-to guides go deeper into each step.
Try it yourself
Section titled “Try it yourself”Experiment with the schema and relationships. Can you:
- Create a new role with different permissions?
- Assign that role to another user?
- Add a new permission to the document resource type?
Next steps
Section titled “Next steps”Learn the concepts
Section titled “Learn the concepts”If you haven’t already, read Understanding Kessel to learn how Kessel Inventory and Kessel RBAC work together and why the system is designed this way.
Go deeper with guides
Section titled “Go deeper with guides”What’s changing
Section titled “What’s changing”Several parts of this workflow are being simplified, including schema configuration, RBAC management, and local development setup.
View the roadmapFeedback
Section titled “Feedback”Have a use case or gap you’d like to see addressed? Found a bug?
Request a feature (RFE)
Report a bug