Skip to Content
Constellation as Code4. Define the constellation

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.

Last updated on