4. Define the constellation
constellation/index.ts is what gets pushed onchain. Here you declare the Safes
you operate and the Roles modules attached to them. Only the nodes you export
end up in the constellation.
Scope to a workspace and chain
Start by scoping a constellation. constellation() is a global — no import
needed:
// constellation/index.ts
const eth = constellation({
workspace: 'Default workspace',
label: 'Backend Operator',
chain: 1, // ethereum
})Because of the codegen from bun pull, the workspace name, chain id, and the
account/role/user labels you reference all autocomplete and type-check.
Existing vs. new nodes
Bracket access on .safe, .roles, and .user is your single editor for both
existing accounts and new ones you’re declaring for the first time:
// Reference an existing Safe verbatim — no invocation
const treasury = eth.safe['GG Treasury']
// Tweak an existing Safe — invoke with overrides; all fields optional
export const tweaked = eth.safe['GG Treasury']({ threshold: 4 })
// Declare a NEW Safe — required fields are enforced at compile time
export const backendSafe = eth.safe['Backend Safe']({
nonce: 0n,
threshold: 2,
owners: ['0xcAF075b4102d96CdF9Bc5A2cEDb170a8b69697E8', eth.user['alice']],
modules: [eth.roles['Backend Operator']],
})For a Safe, the required fields are nonce, threshold, and owners. The SDK
injects no runtime defaults — a missing required field is a TypeScript error,
not a surprise onchain.
Attach a Roles module
The modules: [eth.roles['Backend Operator']] above is a forward reference:
an uninvoked factory that points at a Roles module you declare next. Both sides
just need to be exported so push() can resolve them by name:
import { backend_operator } from './roles'
export const backendRoles = eth.roles['Backend Operator']({
nonce: 0n,
target: backendSafe,
owner: backendSafe,
avatar: backendSafe,
roles: { backend_operator },
})A Roles node takes either nonce (deploy a new module) or address
(bind to one already deployed onchain) — never both. Binding by address lets
Zodiac diff your spec against a live module and update only what changed.
Reference users without hardcoding addresses
eth.user['alice'] resolves to that user’s personal Safe address on the
constellation’s chain — handy in owner lists and role-member arrays:
owners: ['0x1234567890123456789012345678901234567890', eth.user['alice']]You’ve wired up the accounts. Now scope what the role can actually do → Roles & permissions.