Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.kynesys.xyz/llms.txt

Use this file to discover all available pages before exploring further.

Access Control

Every Storage Program carries a StorageProgramACL that the node consults on every read, write, and delete. This guide covers the ACL data model, the resolution priority the node uses to grant or deny a request, each of the three modes, and how to mutate the ACL after deployment. This page assumes you already have a connected demos instance and an ownerAddress. If not, start with Getting Started.

The ACL data model

The ACL lives on the program payload as acl and is fully typed in @kynesyslabs/demosdk:
import type {
    StorageProgramACL,
    StorageACLMode,
    StorageGroupPermissions,
} from "@kynesyslabs/demosdk/storage"

// type StorageACLMode = "owner" | "public" | "restricted"

interface StorageProgramACL {
    /** Default behavior when no allowed/blacklist/group rule matches. */
    mode: StorageACLMode
    /** Addresses explicitly granted access. */
    allowed?: string[]
    /** Addresses explicitly denied. Takes precedence over allowed and groups. */
    blacklisted?: string[]
    /** Named groups, each with members and a permission set. */
    groups?: Record<string, StorageGroupPermissions>
}

interface StorageGroupPermissions {
    members: string[]
    permissions: ("read" | "write" | "delete")[]
}
A few invariants that come straight from the SDK source:
  • The owner of a program (the deployerAddress passed to createStorageProgram) always has full access. They are not listed in allowed and cannot be put on blacklisted.
  • mode is the only required field. The other three (allowed, blacklisted, groups) are independent and can be combined freely.
  • If you omit acl entirely on createStorageProgram, the SDK fills in { mode: "owner" }.

Resolution priority

The node walks the ACL in this exact order on every operation. The first rule that matches wins.
  1. Owner — the program’s deployer always has FULL ACCESS (read, write, delete).
  2. Blacklisted — if the requester is in blacklisted, the request is DENIED, even if they also appear in allowed or in a group.
  3. Allowed — if the requester is in allowed, the requested permission is granted.
  4. Groups — each group is checked; if the requester is a member of a group whose permissions include the requested action, it’s granted.
  5. Mode fallback — when nothing above matches:
    • owner → DENIED
    • public → READ-only (write and delete are denied)
    • restricted → DENIED
That ordering is what makes blacklisted strictly stronger than allowed and groups, and what makes public a true “anyone can read” default without needing to list every reader.

Mode reference

All three modes use the same payload-builder flow described in Getting Started: build the payload, demos.storagePrograms.sign(payload), demos.confirm(tx), then demos.broadcast(validityData).

Owner mode

Default mode. Only the program’s owner can read, write, or delete. Use it for personal data you never intend to share.
import { StorageProgram } from "@kynesyslabs/demosdk/storage"

const nonce = await demos.getAddressNonce(ownerAddress)

const payload = StorageProgram.createStorageProgram(
    ownerAddress,
    "personalNotes",
    { todo: ["draft proposal", "review PR"] },
    "json",
    { mode: "owner" },
    { nonce },
)

const tx = await demos.storagePrograms.sign(payload)
await demos.broadcast(await demos.confirm(tx))
Any non-owner read, write, or delete falls through to step 5 and is denied.

Public mode

Anyone can read. Only the owner can write or delete. Use it for announcements, profiles, indexes, or anything you want to publish openly.
const payload = StorageProgram.createStorageProgram(
    ownerAddress,
    "publicProfile",
    { displayName: "Alice", bio: "Web3 builder" },
    "json",
    { mode: "public" },
    { nonce },
)
Non-owner writes and deletes still hit the mode fallback and are denied — public only relaxes reads.

Restricted mode

Reads, writes, and deletes are denied by default. Access is granted only through explicit allowed entries or groups. Use it for team workspaces, multi-tenant data, or any non-public dataset with a known set of collaborators.
const payload = StorageProgram.createStorageProgram(
    ownerAddress,
    "teamRoster",
    { members: [], pendingInvites: [] },
    "json",
    {
        mode: "restricted",
        allowed: ["demos1alice...", "demos1bob..."],
    },
    { nonce },
)
A pure { mode: "restricted" } with no allowed and no groups is identical to owner mode — only the deployer gets through.

allowed and blacklisted

These two lists are independent of mode and can be combined with any mode.
  • allowed grants the listed addresses all permissions (read, write, delete).
  • blacklisted denies the listed addresses every permission, no matter what mode, allowed, or groups say. It is checked before allowed and groups, so blacklisting always wins.
A typical pattern: a public dataset where you’ve identified a few abusive addresses you want to lock out.
const payload = StorageProgram.createStorageProgram(
    ownerAddress,
    "publicFeed",
    { posts: [] },
    "json",
    {
        mode: "public",
        blacklisted: ["demos1spam...", "demos1abuser..."],
    },
    { nonce },
)
Or a restricted program where one specific address gets full access while the rest of the world is denied:
const payload = StorageProgram.createStorageProgram(
    ownerAddress,
    "auditLog",
    { entries: [] },
    "json",
    {
        mode: "restricted",
        allowed: ["demos1auditor..."],
    },
    { nonce },
)
The owner cannot be denied. Putting the owner’s address in blacklisted has no effect — rule 1 (owner = full access) is checked before rule 2.

Groups

Groups let you express role-based access without flattening every member into a single allowed list. Each group has a members array and a permissions array drawn from "read", "write", and "delete".
const payload = StorageProgram.createStorageProgram(
    ownerAddress,
    "teamDocument",
    { title: "Q1 Plan", body: "" },
    "json",
    {
        mode: "restricted",
        groups: {
            editors: {
                members: ["demos1alice...", "demos1bob..."],
                permissions: ["read", "write"],
            },
            viewers: {
                members: ["demos1carol...", "demos1dave..."],
                permissions: ["read"],
            },
        },
    },
    { nonce },
)
In that example:
  • The owner can do anything (rule 1).
  • Alice and Bob can read and write but not delete.
  • Carol and Dave can read.
  • Anyone else falls through to the restricted mode fallback and is denied.
A single address in multiple groups gets the union of those groups’ permissions. blacklisted still overrides every group membership.

Mutating the ACL after creation

Use StorageProgram.updateAccessControl(storageAddress, acl) to replace the ACL on an existing program. Only the program’s owner can submit this transaction — any other signer is rejected at the node.
// Open up an internal program to the public for read-only access.
const aclPayload = StorageProgram.updateAccessControl(
    storageAddress,
    { mode: "public" },
)

const tx = await demos.storagePrograms.sign(aclPayload)
await demos.broadcast(await demos.confirm(tx))
The acl argument is the new ACL in full — the node replaces the stored ACL with what you send. To add a member to a group, read the current ACL, mutate the groups map, and submit the whole object back:
import type { StorageProgramACL } from "@kynesyslabs/demosdk/storage"

const program = await StorageProgram.getByAddress(rpcUrl, storageAddress)
if (!program) throw new Error("Program not found")

// `program` does not return the ACL directly today — keep the ACL you set
// on creation in your own state, or re-derive it from your application logic
// before calling updateAccessControl.

const nextAcl: StorageProgramACL = {
    mode: "restricted",
    groups: {
        editors: {
            members: ["demos1alice...", "demos1bob...", "demos1eve..."],
            permissions: ["read", "write"],
        },
    },
}

const aclPayload = StorageProgram.updateAccessControl(storageAddress, nextAcl)
const tx = await demos.storagePrograms.sign(aclPayload)
await demos.broadcast(await demos.confirm(tx))
A few constraints worth remembering:
  • The owner of a program is set at creation time and cannot be changed via updateAccessControl. Ownership is permanent.
  • updateAccessControl is a regular Storage Program transaction — it incurs the same per-chunk fee as any other write. See Getting Started → Fees.
  • The new ACL takes effect once the transaction is included in a block. In-flight reads against the previous ACL may still succeed until the new state is committed.

Quick decision guide

You want…Use
Personal data only you can touchmode: "owner"
Open read, owner-only writesmode: "public"
A small fixed set of collaboratorsmode: "restricted" + allowed
Different roles with different permissionsmode: "restricted" + groups
Block known-bad addressesany mode + blacklisted
Open to all readers but tightened for writesmode: "public" (writes are owner-only by definition)

Convenience helpers

The SDK exposes a few static helpers on StorageProgram that return prebuilt ACL objects so you don’t have to spell them out:
StorageProgram.privateACL()
// { mode: "owner" }

StorageProgram.publicACL()
// { mode: "public" }

StorageProgram.restrictedACL(["demos1alice...", "demos1bob..."])
// { mode: "restricted", allowed: [...] }

StorageProgram.groupACL({
    editors: { members: ["demos1alice..."], permissions: ["read", "write"] },
})
// { mode: "restricted", groups: {...} }

StorageProgram.blacklistACL("public", ["demos1spam..."])
// { mode: "public", blacklisted: ["demos1spam..."] }
All of these return a StorageProgramACL you can pass straight into createStorageProgram or updateAccessControl.

Programmatic permission checks

If you want to predict whether a given address would be granted a permission against a given ACL — for example, to gate UI elements before submitting a transaction — use StorageProgram.checkPermission:
import { StorageProgram } from "@kynesyslabs/demosdk/storage"

const acl: StorageProgramACL = {
    mode: "restricted",
    groups: {
        editors: {
            members: ["demos1alice..."],
            permissions: ["read", "write"],
        },
    },
    blacklisted: ["demos1spam..."],
}

StorageProgram.checkPermission(acl, ownerAddress, ownerAddress, "delete")
// true  — owner always wins

StorageProgram.checkPermission(acl, ownerAddress, "demos1alice...", "write")
// true  — granted via the editors group

StorageProgram.checkPermission(acl, ownerAddress, "demos1alice...", "delete")
// false — editors do not include "delete"

StorageProgram.checkPermission(acl, ownerAddress, "demos1spam...", "read")
// false — blacklisted, even with no other rule
This helper runs the exact resolution priority described above, so it’s a faithful preview of what the node will do.

Legacy access control (deprecated)

Before v3.1.0, programs used a single string field accessControl: "private" | "public" | "restricted" | "deployer-only" plus a top-level allowedAddresses array. That shape is deprecated and only kept on StorageProgramPayload for backward compatibility with old clients. New code should always use acl. The mapping if you’re migrating older payloads:
Legacy accessControlNew acl.mode
"private""owner"
"deployer-only""owner"
"public""public"
"restricted""restricted" (move allowedAddresses into acl.allowed)
The SDK still exposes StorageProgram.createStorageProgramLegacy and StorageProgram.updateAccessControlLegacy for that legacy shape, both marked @deprecated. Don’t call them in new code — the new acl form is strictly more expressive and is the only one that supports blacklisted and groups.

Where to go next

  • Operations — full lifecycle of a Storage Program (create, write, granular updates, delete) and how the ACL applies at each step.
  • RPC Queries — passing a requester identity to ACL-gated reads and listing programs by owner.
  • API Reference — every StorageProgram static helper, payload shape, and response type.
  • Cookbook — end-to-end recipes including team workspaces and public profiles.