Docs

Authorization

Authorization decides what each signed-in principal may see and do. Reach for it when "signed in or not" is not enough — finance admins run sensitive workflows, team members read invoices but can't refund them, and new teammates land in the right group.

Authentication decides who a principal is; authorization decides what they may do. Sixb builds it from four small layers:

LayerRole
GroupsNamed buckets that principals belong to
RolesBundles of grants attached to groups
GrantsThe capabilities a role gives — access, view, apply, run
Membership policiesWho can administer membership for which groups

At request time these resolve into one set of grants per principal, and a scoped runtime enforces them. You describe access next to the ontology, datasets, actions, workflows, syncs, and pipelines it protects, and the runtime applies it the same way everywhere.

Define a group

A group is a named bucket. Principals belong to groups; roles and membership policies are written against groups, never against individual users.

TS
// security/groups/team-members.ts
import { defineGroup } from "@sixb/core"

export const teamMembers = defineGroup("team-members", {
  label: "Team members",
})
TS
// security/groups/finance-admins.ts
import { defineGroup } from "@sixb/core"

export const financeAdmins = defineGroup("finance-admins", {
  label: "Finance admins",
})

Define a role

A role bundles grants and attaches them to one or more groups. Every member of a grantedTo group receives the role's grants.

TS
// security/roles/billing-access.ts
import { applications, can, defineRole } from "@sixb/core"
import { sendReminder } from "../../actions/sendReminder"
import { Customer } from "../../ontology/customer"
import { Invoice } from "../../ontology/invoice"
import { teamMembers } from "../groups/team-members"

export const teamMemberBillingAccess = defineRole("team-member.billing-access", {
  grantedTo: [teamMembers],
  grants: [
    can.access(applications.app),
    can.view([Customer, Invoice]),
    can.apply(sendReminder),
  ],
})

Every member of team-members can now view Customer and Invoice objects and apply the sendReminder action — nothing else.

PartMeaning
defineRole("team-member.billing-access")Names the role
grantedTo: [teamMembers]Groups whose members receive the role
grants: [...]The capabilities the role gives
can.access(applications.app)Allow access to the custom app
can.view([Customer, Invoice])Allow viewing those object types
can.apply(sendReminder)Allow applying the sendReminder action

A role needs at least one grantedTo group and at least one grant. A principal's effective grants are the union of every role whose grantedTo group it belongs to.

Grants

A grant pairs a capability with the definitions it covers. Five capability builders — can.access, can.view, can.apply, can.run, and can.observe — resolve to nine grant kinds, one per protected target family.

Grant kindBuilderAllowsTargets
access:applicationcan.access(...)Open a grant-controlled browser applicationapplications.atlas, applications.app
view:objectcan.view(...)Read objects: get, list, query, telemetry, related eventsObject types
view:datasetcan.view(...)Read datasets and their versionsDatasets
apply:actioncan.apply(...)Request actionsActions
run:workflowcan.run(...)Start workflowsWorkflows
run:synccan.run(...)Run syncsSyncs
run:pipelinecan.run(...)Run pipelinesPipelines
run:agentcan.run(...)Run agents and read their threadsAgents
observe:logscan.observe("logs")Read captured run logsLogging

can.access accepts the built-in applications.atlas and applications.app definitions. can.view resolves to view:object or view:dataset from the definition you pass; can.run picks between run:workflow, run:sync, run:pipeline, and run:agent the same way. Each is type-checked, so mixing target families in one call does not compile. can.view(Type) also covers the type's subtypes. can.observe takes the "logs" target literal and grants observe:logs, which gates reading captured logs.

Selecting definitions

Each builder takes one definition, a list, or a breadth selector.

WantWrite
One applicationcan.access(applications.atlas)
One definitioncan.view(Invoice)
Several definitionscan.view([Customer, Invoice])
Every object typecan.view(ontology.objects())
Every datasetcan.view(datasets())
Every actioncan.apply(actions())
Every workflowcan.run(workflows())
Every synccan.run(syncs())
Every pipelinecan.run(pipelines())
Every agentcan.run(agents())
Everything but a fewcan.view(ontology.objects().except([Customer]))

The breadth selectors are exported from @sixb/core: ontology.objects(), datasets(), actions(), workflows(), syncs(), pipelines(), and agents(). Each picks its target family's whole registered universe and is branded by target, so can.view(actions()) does not compile.

Broad grants

Use breadth selectors for roles that should reach most of the system. Add .except([...]) to keep a selection broad while carving out a few definitions.

TS
// security/roles/billing-access.ts
import { actions, can, datasets, defineRole, ontology, workflows } from "@sixb/core"
import { financeAdmins } from "../groups/finance-admins"

export const financeAdminFullAccess = defineRole("finance-admin.full-access", {
  grantedTo: [financeAdmins],
  grants: [
    can.view(ontology.objects()),
    can.view(datasets()),
    can.apply(actions()),
    can.run(workflows()),
  ],
})
TS
// Grant every object type except Customer
can.view(ontology.objects().except([Customer]))

Application access

Application grants control whether a signed-in principal may open Atlas or the custom app. They are opt-in per application for compatibility: when no role grants an application, every authenticated user may open it. Once any role grants that application, it becomes an allowlist and principals without the grant receive an access-denied page before application data loads.

TS
import { applications, can, defineRole } from "@sixb/core"

export const securityAdminAtlasAccess = defineRole("security-admin.atlas-access", {
  grantedTo: [securityAdmins],
  grants: [can.access(applications.atlas)],
})

export const customerAppAccess = defineRole("customer.app-access", {
  grantedTo: [customers],
  grants: [can.access(applications.app)],
})

Application access complements resource grants rather than replacing them. For example, a user may be allowed into the custom app but still see only the object types granted to their groups. Atlas and the custom app enforce application grants at the session, HTTP, and WebSocket boundaries.

Membership policies

A membership policy says which groups can administer membership, which groups they may administer, and which operations they may perform. Inviting is one membership operation; managing existing members uses the same boundary.

TS
// security/policies/member-administration.ts
import { defineMembershipPolicy } from "@sixb/core"
import { financeAdmins } from "../groups/finance-admins"
import { teamMembers } from "../groups/team-members"

export const memberAdministration = defineMembershipPolicy("member-administration", {
  grantedTo: [financeAdmins],
  scope: [teamMembers],
  can: ["invite", "assignGroups", "suspend"],
})

Finance admins can now invite people into team-members, edit existing team-members users' groups, and suspend or reactivate those users. A group-less invitation is also allowed when a caller has the invite operation; the resulting user can authenticate but receives no group-derived grants.

OptionMeaning
grantedToGroups whose members hold the policy
scopeGroups those members may administer. Existing-member operations require every current target group to be in scope; group-less targets are allowed when the operation exists.
canMembership operations: invite, assignGroups, suspend

Membership operations are intentionally scoped:

OperationAllows
inviteCreate, list, and revoke invitations whose requested groups are all in scope. Empty group invitations are allowed when the caller has the operation.
assignGroupsReplace an existing user's groups when every current group and every requested group is in scope. A user cannot remove any of their own current groups.
suspendSuspend active users and reactivate suspended users when every current group is in scope. A user cannot suspend themself. Suspending revokes active sessions immediately; reactivation does not restore old sessions.

The server and Atlas use the same scope for visibility. Existing users are listed only when the caller can assign groups or suspend/reactivate over the user's current groups, so out-of-scope emails, statuses, and group membership are not exposed.

How principals join groups

Roles and membership policies act on group membership, so principals need a way into a group.

  • Bootstrap — the auth strategy's bootstrapGroups are applied to the first allowed user to sign in. This seeds the initial admins.
  • Invitations and member management — after that, members covered by a membership policy invite teammates, assign groups, and suspend or reactivate users inside the policy's scope.
TS
import { magicLink } from "@sixb/auth-magic-link"
import { financeAdmins } from "./security/groups/finance-admins"

auth: magicLink({
  allowedDomains: ["acme.com"],
  bootstrapUsers: ["admin@acme.com"],
  bootstrapGroups: [financeAdmins],
})

The first user to sign in as admin@acme.com lands in finance-admins, which (via the role and membership policy above) can then invite and manage the rest of the team. See Authentication for how strategies and bootstrapping work.

How grants are enforced

Grants are enforced through a scoped runtime. The raw sixb instance is privileged — it has no authorization context and bypasses all grant checks. That is intended for trusted system code (startup, syncs, projections, workers, tests).

To enforce a principal's grants, derive a scoped runtime with sixb.as(context):

TS
const scoped = sixb.as(authorizationContext)

await scoped.objects(Invoice).list()   // only if view:object covers Invoice
await scoped.requestAction(input)        // only if apply:action covers it
await scoped.runWorkflow(input)          // only if run:workflow covers it
await scoped.readEvents()                // events whose subject is visible

The scoped runtime is default-deny: any request without a covering grant throws, and listing APIs return only the definitions the principal can reach.

Scoped runtime surface

The scoped runtime exposes only operations whose grants are enforceable end to end. Catalog and read methods are filtered to what the principal may reach. Writes, links, telemetry appends, and auth administration stay on the privileged runtime.

MethodGated by
objects(Type), list, getObjectview:object
requestAction, requestActionAndWaitapply:action
runWorkflowrun:workflow
listDatasets, getDatasetByIdview:dataset
listActions, getActionByIdapply:action
listWorkflows, getWorkflowByIdrun:workflow
listSyncs, getSyncByIdrun:sync
listPipelines, getPipelineByIdrun:pipeline
requestAgentRun, listAgents, getAgentById, listThreads, getThreadrun:agent
readEventssubject visibility (see below)

Event visibility

There is no standalone "view events" capability. A principal sees a domain event only when it can view, apply, or run the event's subject:

Event topicVisible when
objects, telemetrycan view the object type
linkscan view both the source and target object types
actionscan apply the action (and view its object subject, if any)
rulescan view the object type the rule fired on
workflowscan run the workflow
syncscan run the sync
pipelinescan run the pipeline
datasetscan view the dataset
schedulesalways visible to an authorized reader (no subject grant yet)

Event filtering is fail-closed: an unmodeled topic is hidden. See Events for the event model.

With the server

The Sixb server does this for you. It resolves the session once per request and routes authenticated traffic through sixb.as(context) automatically, so grants are enforced without extra wiring. You define groups, roles, and membership policies; the server applies them. See the Server overview.

To build a context yourself in a custom integration, resolve it from the request:

TS
const context = await sixb.auth.createAuthorizationContext(request)
const scoped = sixb.as(context)

Convention

Put security definitions under security/, split by kind, and export them.

TXT
your-project/
  ontology/
    invoice.ts
    customer.ts
  actions/
    sendReminder.ts
  security/
    groups/
      team-members.ts
      finance-admins.ts
    roles/
      billing-access.ts
    policies/
      member-administration.ts
  sixb.config.ts

createSixb() discovers exported definitions from security/groups/, security/roles/, and security/policies/ automatically. See Project structure. You can also register them explicitly:

TS
import { createSixb } from "@sixb/core"
import { financeAdmins } from "./security/groups/finance-admins"
import { teamMembers } from "./security/groups/team-members"
import { memberAdministration } from "./security/policies/member-administration"
import { financeAdminFullAccess, teamMemberBillingAccess } from "./security/roles/billing-access"

export const sixb = await createSixb({
  groups: [teamMembers, financeAdmins],
  roles: [teamMemberBillingAccess, financeAdminFullAccess],
  membershipPolicies: [memberAdministration],
})

How to model authorization

Start from the people, not the permissions.

  1. List the kinds of user your app has, and turn each into a group.
  2. For each group, write one role describing what it can view, apply, and run.
  3. Start narrow with explicit grants; widen to a breadth selector or .except([...]) only when a group really needs broad reach.
  4. Add a membership policy so the right group can grow and manage the others.
  5. Set bootstrapGroups so the first sign-in can administer everything else.

Name groups and roles after the people and their access: team-members, finance-admins, team-member.billing-access, finance-admin.full-access.

Grants reference ontology, dataset, action, workflow, sync, and pipeline definitions by id and are validated against the registered runtime at startup, so a typo or unregistered definition fails fast.

Search docs

Search the documentation