Constellations
A constellation is your organization as code. You declare every Zodiac object in TypeScript: the Safes, the Roles Modifiers, the members, the permissions, and the wiring between them. The SDK compares your declaration with the chain and applies the difference. You do not deploy contracts, encode calls, or wire modules yourself — the constellation is the wrapper around all of it.
This page explains the model. The Roles quickstart walks through it hands-on.
Why a constellation
- One source of truth. The repository describes the whole setup. A pull request shows a permission change as a readable diff, before anything is onchain.
- Reviewable apply.
pushcomputes the difference against the live chain state and opens it for review in the Zodiac App. Nothing changes until the Safe signs. - No contract plumbing. Deployments,
enableModulewiring, and the configuration calls of the permission model are derived from your files.
The project
The template gives you this layout:
constellation/
index.ts # entry point; only exported nodes get deployed
allowances/ # shared allowance definitions
roles/<role>/
index.ts # the role
members.ts # who holds it
permissions.ts # what it allows
zodiac.config.ts # contract registry per chain
abis/ # manual ABIs for unverified contracts and proxies
.zodiac/ # generated code — do not edit
.env # the API key — do not commitThe keys in zodiac.config.ts become your typed permission paths: a contract registered
as eth: { weth: "0xC02a…" } appears as allow.eth.weth.* after pull-contracts. The
CLI reads zodiac.config.ts from the project root; pass -c <path> for a different
file.
Each non-view ABI function becomes a member named after it. When a contract overloads a name, no single member could mean either overload, so address the one you want by its full signature:
// sUSDS has both deposit(uint256,address) and deposit(uint256,address,uint16)
allow.eth.susds['deposit(uint256,address)'](
c.withinAllowance('savings'),
c.avatar,
)The bracketed form is also how you write a permission for any function whose name is not a valid JavaScript identifier.
Nodes
A constellation is a set of nodes on one chain:
const eth = constellation({ workspace: 'Production', label: 'Ops', chain: 1 })In a template project, constellation, allow, and c are globals, and
Permissions and Members are global types. Do not import them.
| Node | What it declares |
|---|---|
eth.safe["Treasury"] | A Safe: owners, threshold, enabled modules. |
eth.roles["Ops Roles"] | A Roles Modifier: its wiring (owner, avatar, target) and its roles, members, permissions, and allowances. |
eth.user["alice"] | A person in your workspace, usable wherever an address is expected. |
Three rules carry the model:
- Reference or create.
eth.safe["Treasury"]without arguments references what exists in your workspace. With arguments, the node declares state:noncedeploys a new instance,addressbinds an existing one — never both. - Nodes compose. A roles node points its
targetat a Safe node — or at another modifier node, which is how the Delay composition is declared. Forward references resolve by label at push time. - Export what you reference.
pushresolves referenced nodes by export name. Export every node that another node references, including a Safe that already exists. A missing export stops the push with the error:Node "…" is referenced but not included in the push() call.
Share the instance across files
The members.ts files need the constellation instance for eth.user["…"] accessors,
but an import from index.ts creates an import cycle. Create the instance in its own
module and import it from both sides:
// constellation/context.ts
export const eth = constellation({
workspace: 'Production',
label: 'Ops',
chain: 1,
})// constellation/roles/ops/members.ts
import { eth } from '../../context'
export default [eth.user['alice']] satisfies MembersConstellations manage Safes, Roles Modifiers, and users today. The delay
node appears in the Delay quickstart as a
draft, ahead of the SDK release.
The apply loop
bun pull → edit the files → bun push → review in the Zodiac App → sign with the Safepullfetches your workspace state and contract ABIs, and regenerates.zodiac/.pushsends the declared state to the Zodiac API, which computes the calls against the current onchain state. You review the change set in the app and sign with the Safe.- The diff is per role: a role you mention is replaced, a role you omit stays untouched,
and
nullclears a role key.
For a dry run in CI, send the same payload to POST /api/v1/workspace/<id>/constellation/resolve. The endpoint computes the change set and stores nothing.
Where the docs plug in
| In your constellation | The page that explains it |
|---|---|
permissions.ts entries | The permission model and Conditions |
A DeFi Kit preset in permissions.ts | DeFi Kit presets |
allowances/ definitions | Allowances |
members.ts entries | Members and role keys |
The target of a roles node | Delay composition |
| A role for a bot or an agent | Permission an AI agent |
Next steps
- Do it once end to end: Quickstart: Roles
- Configure without code instead: Create a policy in the app