Skip to content

Quick Start

Time: ~30 minutes

This tutorial walks you through setting up Kessel from scratch. By the end, you will have:

  1. Run Kessel locally with Docker Compose
  2. Defined a resource type with permissions using Kessel’s schema language
  3. Reported a resource to Kessel’s inventory
  4. 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):

Let’s get some prerequisites out of the way. These are temporary solutions, so don’t worry about them too much.

  1. 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@latest

    Ensure ksl is in your $PATH, or adjust the path in the scripts below.

  2. 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@latest
  3. 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-started
    cd kessel-getting-started
  4. 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.1
    namespace kessel
    internal type lock {
    relation #version: [ExactlyOne lockversion]
    }
    internal type lockversion {}
    EOF
    Terminal window
    cat > rbac.ksl << 'EOF'
    version 0.1
    namespace rbac
    public 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

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.

  1. 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 type
    mkdir -p document/

    The document resource type has a single “drive” reporter.

    Terminal window
    cat > document/config.yaml << 'EOF'
    resource_type: document
    resource_reporters:
    - drive
    EOF

    Documents 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_id attribute.

    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"
    ]
    }
    EOF

    Reporters 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/drive
    Terminal window
    cat > document/reporters/drive/config.yaml << 'EOF'
    resource_type: document
    reporter_name: drive
    namespace: drive
    EOF
    Terminal 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"
    ]
    }
    EOF

    And finally we configure permissions. Permissions are defined in a KSL schema file.

    Terminal window
    cat > drive.ksl << 'EOF'
    version 0.1
    namespace drive
    import 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_document
    relation edit: workspace.edit_document
    relation delete: workspace.delete_document
    }
    EOF
  2. Compile to SpiceDB schema.

    We installed the ksl schema compiler earlier. Use it now to compile the schemas:

    Terminal window
    ksl kessel.ksl rbac.ksl drive.ksl

    This will generate a schema.zed file in the current directory.

    Example generated schema.zed file
    schema.zed
    definition drive/document {
    permission edit = t_workspace->edit_document
    permission delete = t_workspace->delete_document
    permission view = t_workspace->view_document
    permission workspace = t_workspace
    relation t_workspace: rbac/workspace
    }
    definition kessel/lock {
    permission version = t_version
    relation t_version: kessel/lockversion
    }
    definition kessel/lockversion {}
    definition rbac/group {
    permission member = t_member
    relation t_member: rbac/principal | rbac/group#member
    }
    definition rbac/principal {}
    definition rbac/role {
    permission delete_document = t_delete_document
    relation t_delete_document: rbac/principal:*
    permission edit_document = t_edit_document
    relation t_edit_document: rbac/principal:*
    permission view_document = t_view_document
    relation 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_granted
    relation t_granted: rbac/role
    permission subject = t_subject
    relation t_subject: rbac/principal | rbac/group#member
    permission view_document = (subject & t_granted->view_document)
    }
    definition rbac/workspace {
    permission delete_document = t_user_grant->delete_document + t_parent->delete_document
    permission edit_document = t_user_grant->edit_document + t_parent->edit_document
    permission parent = t_parent
    relation t_parent: rbac/workspace
    permission user_grant = t_user_grant
    relation t_user_grant: rbac/role_binding
    permission view_document = t_user_grant->view_document + t_parent->view_document
    }
  1. Clone the Kessel Inventory API repository and load your custom schema.

    Terminal window
    git clone git@github.com:project-kessel/inventory-api.git
    cd inventory-api
    cp -r ../document data/schema/resources/
    go run main.go preload-schema
  2. Start the full Kessel stack with your custom SpiceDB schema.

    Terminal window
    SCHEMA_ZED_FILE=../schema.zed make kessel-up

    This starts all services in a single command. The SCHEMA_ZED_FILE variable 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.

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:

  1. Permissions, grouped into a role.
  2. Resources, which can be grouped into a workspace.
  3. 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.

  1. Create a role

    A role defines a set of permissions. Edit the ROLE_NAME and PERMISSIONS variables 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 case
    ROLE_NAME="drive-admin-role"
    PERMISSIONS=("view_document" "edit_document" "delete_document")
    RELATIONS_PORT=9000
    # =======================================
    # Build the tuples array for all permissions
    TUPLES=""
    for i in "${!PERMISSIONS[@]}"; do
    PERM="${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 ]; then
    TUPLES="${TUPLES},"
    fi
    TUPLES="${TUPLES}${TUPLE}"
    done
    MESSAGE="{\"tuples\":[${TUPLES}]}"
    echo "Creating role '${ROLE_NAME}' with permissions: ${PERMISSIONS[*]}"
    grpcurl -plaintext -d "${MESSAGE}" \
    "localhost:${RELATIONS_PORT}" \
    kessel.relations.v1beta1.KesselTupleService.CreateTuples
  2. 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, and RESOURCE_ID variables, 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 case
    ROLE_NAME="drive-admin-role"
    USER_ID="sarah"
    RESOURCE_ID="workspace-1"
    RELATIONS_PORT=9000
    # =======================================
    # Auto-generate a deterministic binding ID from the inputs
    BINDING_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_binding
    MESSAGE="{\"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

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.

  1. Install dependencies.

    If you followed along with Prerequisites, you’re good to go.

  2. Configure a client.

    Terminal window
    # Set your Kessel gRPC endpoint
    KESSEL_GRPC_ENDPOINT="localhost:9081"
    # For insecure local development:
    GRPC_OPTS="-plaintext"

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.

Terminal window
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

Once Kessel knows about a resource and its relationships, you can check whether a user has a specific permission on it.

Terminal window
# 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
#!/bin/bash
# Set your Kessel gRPC endpoint
KESSEL_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.Check

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 ksl compiler
  • 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.

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?

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.

Several parts of this workflow are being simplified, including schema configuration, RBAC management, and local development setup.

View the roadmap

Have a use case or gap you’d like to see addressed? Found a bug?

Request a feature (RFE)

Report a bug