Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Skip to content

Access Control Reference

Technical reference for the Gateway ACL model — principals, roles, group resolution, and D1 schema.

For the non-technical guide, see Sharing & Permissions.

Two-Role Model

Gateway uses a flat two-role model. No hierarchies, no granular permissions.

RoleProceduresRuns
AdminFull read/write. Edit document, manage access, view all runs, trigger exports.See all runs for their procedures. Full data.
UserDiscover (if public or shared). Invoke (if public or shared). Cannot read procedure internals.See own runs (creator). See assigned runs (from assignment step onward).

Principals

A principal is an identity that can be granted access. Three formats:

FormatExampleResolves to
slack:#channelslack:#finance-teamAll members of the Slack channel
email:addremail:alice@co.comSingle user, matched by email across providers
user:iduser:abc123Single user by Gateway ID

Slack channels as groups

Slack channels are the primary group primitive. Membership is resolved dynamically via the Slack API and cached in D1 with a 1-hour TTL.

-- group_cache table
CREATE TABLE group_cache (
  group_ref TEXT NOT NULL,      -- e.g. "slack:#finance-team"
  user_email TEXT NOT NULL,
  resolved_at TEXT NOT NULL,
  PRIMARY KEY (group_ref, user_email)
);

Resolution flow:

  1. ACL check needs to evaluate slack:#finance-team
  2. Check group_cache for entries where resolved_at is within 1 hour
  3. If cache miss or stale → call Slack API conversations.members + users.info to resolve emails
  4. Upsert cache entries
  5. Check if the user's email is in the resolved set

Cross-provider identity

Email is the join key. A user connects Notion, Linear, and Slack with potentially different usernames, but the same email links them. Gateway stores provider_user_email in linked_accounts for this mapping.

Procedure Access

Stored in D1 procedure_access table:

CREATE TABLE procedure_access (
  procedure_id TEXT NOT NULL,
  principal TEXT NOT NULL,        -- "slack:#finance", "email:a@b.com", "user:xyz"
  principal_type TEXT NOT NULL,   -- "slack_channel", "email", "user"
  role TEXT NOT NULL,             -- "admin" or "user"
  is_creator INTEGER DEFAULT 0,
  created_at TEXT DEFAULT (datetime('now')),
  PRIMARY KEY (procedure_id, principal)
);

Sync from frontmatter

When a procedure document is saved, syncProcedureAccess reads the frontmatter and upserts the procedure_access table:

frontmatter.admins  → role: "admin"
frontmatter.share   → role: "user"
procedure creator   → role: "admin", is_creator: 1

Check flow

canDiscover(userId, procedureId)
  → procedure public? yes → true
  → user is admin? yes → true
  → user in share list (direct or via group)? yes → true
  → false
 
canReadProcedure(userId, procedureId)
  → user is admin? yes → true
  → false (users can discover/invoke but not read internals)

Run Access

Stored in D1 run_access table:

CREATE TABLE run_access (
  run_id TEXT NOT NULL,
  user_id TEXT NOT NULL,
  role TEXT NOT NULL,              -- "creator" or "assigned"
  assigned_at_step TEXT,           -- which step triggered assignment
  created_at TEXT DEFAULT (datetime('now')),
  PRIMARY KEY (run_id, user_id)
);

Lifecycle

  1. Run created → creator gets role: "creator" in run_access
  2. Step with assignment → assigned user gets role: "assigned" with assigned_at_step
  3. ACL checkcanAccessRun checks run_access + procedure admin status
canAccessRun(userId, runId)
  → user in run_access? yes → true
  → user is admin of the run's procedure? yes → true
  → false
 
canCancelRun(userId, runId)
  → user is run creator? yes → true
  → user is admin of the run's procedure? yes → true
  → false

D1 Tables Summary

TablePurpose
procedure_accessWho can discover/invoke/admin a procedure
run_accessWho can see a specific run (creator + assigned)
group_cacheCached Slack channel membership (1h TTL)
runsRun state, procedure snapshot, wait metadata
run_submissionsData collected during execution steps
run_logExecution log entries
slack_workspaceBot token per Slack workspace

API Functions

acl.ts

FunctionPurpose
hasRole(userId, procedureId, role, db)Check if user has a specific role on a procedure
canDiscover(userId, procedureId, db)Can user find this procedure?
canReadProcedure(userId, procedureId, db)Can user read procedure internals? (admin only)
canAccessRun(userId, runId, db)Can user see this run?
canCancelRun(userId, runId, db)Can user cancel this run?
grantRunAccess(runId, userId, role, step, db)Add user to run's access list
resolveGroup(groupRef, resolver, db)Resolve Slack channel → emails (with cache)
syncProcedureAccess(procedureId, frontmatter, creatorId, db)Sync frontmatter → procedure_access table

runs.ts

FunctionPurpose
createRun(...)Create run + grant creator access
loadRun(runId, userId, db)Load run (ACL-gated)
submitData(runId, step, data, userId, db)Record step submission (ACL-gated)
logStep(runId, step, entry, autonomous, db)Append to execution log
parkRun(runId, waitUntil, waitContext, db)Set status to waiting
completeRun(runId, db)Set status to completed
cancelRun(runId, userId, db)Cancel run (ACL-gated)
listRuns(procedureId, userId, db)List runs user can see