Skip to content

Groups & Governance

Governance in Calimero is layered: a namespace scopes an application instance; groups (and subgroups) within it hold members with roles and capabilities and own contexts. This guide covers the pieces you’ll touch most. The exhaustive method list is in the admin API reference.

A namespace groups contexts and members under a shared target application.

const { namespaceId } = await sdk.admin.createNamespace({
applicationId,
name: 'production',
});
const { namespaces } = await sdk.admin.listNamespaces();
const ns = await sdk.admin.getNamespace(namespaceId);
const identity = await sdk.admin.getNodeIdentity();

Invite and join:

const invitation = await sdk.admin.createNamespaceInvitation(namespaceId);
// on the joining node:
// `memberAccount` is what the member-addressing calls below take;
// `memberIdentity` is the key it signs with.
const { groupId, memberIdentity, memberAccount } = await sdk.admin.joinNamespace(
namespaceId,
{ invitation },
);

When a person follows an invitation link, call redeemInvitation, not joinNamespace. A join waits for a member of the namespace to come online, and that can outlast any proxy in front of the node. The request then fails even though the join lands. redeemInvitation sends the join once, then checks listNamespaces. A failed request whose namespace is listed is reported as already-member, which is a success.

import { shouldRetain } from '@calimero-network/mero-js';
const outcome = await sdk.admin.redeemInvitation(namespaceId, invitation, {
groupName: 'Design',
});
switch (outcome.status) {
case 'joined':
case 'already-member':
openNamespace(outcome.namespaceId);
break;
case 'failed':
// `reason`: 'expired' | 'invalid' | 'refused' | 'signed-out'
// | 'no-one-online' | 'node-unreachable' | 'unknown'
showError(outcome.reason, outcome.message);
if (!shouldRetain(outcome)) discardInvitation();
break;
}

It never throws for an ordinary failure. The reason comes from the node’s status on core 0.11.0-rc.56 and later, and from its message on older nodes. retryable (and shouldRetain) say whether to keep the invitation for another attempt. An expired, invalid or refused one is final. One that failed because no member was online, or because the node could not be reached, is worth keeping.

A join that is more than one call, such as a namespace followed by a context inside it, can use the standalone redeemInvitation(parsed, redeemer). Pass your own join, plus a memberships that lists namespace ids.

const info = await sdk.admin.getGroupInfo(groupId);
const { members } = await sdk.admin.listGroupMembers(groupId);
const { contexts } = await sdk.admin.listGroupContexts(groupId);
await sdk.admin.addGroupMembers(groupId, { /* members */ });
await sdk.admin.updateMemberRole(groupId, identity, { role: 'admin' });
await sdk.admin.removeGroupMembers(groupId, { /* identities */ });

Subgroups nest under a parent group:

const subgroups = await sdk.admin.listSubgroups(groupId);
await sdk.admin.createGroupInNamespace(namespaceId, { /* … */ });

A member’s permissions are a u32 bitmask. The SDK ships the bit constants and pure helpers to read and edit a mask — no bitwise operators at your call sites.

import { CAPABILITIES, hasCap, withCap, withoutCap } from '@calimero-network/mero-js';
// Read the current mask
const { capabilities } = await sdk.admin.getMemberCapabilities(groupId, identity);
// Test / add / remove bits
const canInvite = hasCap(capabilities, CAPABILITIES.CAN_INVITE_MEMBERS);
const next = withCap(capabilities, CAPABILITIES.MANAGE_MEMBERS);
const revoked = withoutCap(next, CAPABILITIES.MANAGE_APPLICATION);
// Persist
await sdk.admin.setMemberCapabilities(groupId, identity, { capabilities: next });

The bits:

Constant Value Grants
CAN_CREATE_CONTEXT 1 << 0 create contexts in the group
CAN_INVITE_MEMBERS 1 << 1 invite new members
CAN_JOIN_OPEN_SUBGROUPS 1 << 2 join open subgroups
MANAGE_MEMBERS 1 << 3 add/remove members, change roles
MANAGE_APPLICATION 1 << 4 manage the group’s application
CAN_CREATE_SUBGROUP 1 << 5 create subgroups
CAN_DELETE_SUBGROUP 1 << 6 delete subgroups
CAN_MANAGE_VISIBILITY 1 << 7 change subgroup visibility
CAN_MANAGE_METADATA 1 << 8 edit group/member/context metadata

Set the default mask new members receive, or read/change subgroup visibility:

await sdk.admin.setDefaultCapabilities(groupId, {
defaultCapabilities: withCap(0, CAPABILITIES.CAN_CREATE_CONTEXT),
});
const defaults = await sdk.admin.getDefaultCapabilities(groupId); // number
const visibility = await sdk.admin.getSubgroupVisibility(groupId); // string

Attach a small named key/value record to a group, member, or context:

await sdk.admin.setGroupMetadata(groupId, {
name: 'Production',
data: { region: 'eu', tier: 'gold' },
});
const meta = await sdk.admin.getGroupMetadata(groupId); // MetadataRecord | null
// meta.name, meta.data, meta.updatedAt, meta.updatedBy

The same pair exists for members (set/getMemberMetadata) and contexts (set/getContextMetadata).

A group can restrict which nodes may join to those running in an approved Trusted Execution Environment. The policy lists the allowed measurements.

await sdk.admin.setTeeAdmissionPolicy(groupId, {
allowedMrtd: ['<mrtd-hex>'],
allowedRtmr0: [],
allowedRtmr1: [],
allowedRtmr2: [],
allowedRtmr3: [],
allowedTcbStatuses: ['UpToDate'],
acceptMock: false, // true only for local/dev
});
const policy = await sdk.admin.getTeeAdmissionPolicy(groupId);

Admission decides which TEEs may join; the authoring policy decides which of the admitted ones may write as the namespace’s TEE authority (TeeOnly storage and #[app::tee] methods). It is namespace-scoped, so set it on the namespace root.

await sdk.admin.setTeeAuthoringPolicy(namespaceId, { allowedMrtd: ['<mrtd-hex>'] });
// Turn TEE authorship off. Admitted TEEs stay members; none can author.
await sdk.admin.disableTeeAuthoringPolicy(namespaceId);
// Equivalent: an empty allowlist applies the same op.
await sdk.admin.setTeeAuthoringPolicy(namespaceId, { allowedMrtd: [] });

The node has no read-back route for this policy.

Nodes can also attest directly. Verify a quote in the client with createQuoteVerifier (see sealed transport), not by asking a node:

const info = await sdk.admin.getTeeInfo(); // { cloudProvider, osImage, mrtd }
const attestation = await sdk.admin.teeAttest({ nonce }); // { quoteB64, quote }

For cloud-managed TEE redundancy, see high availability.

When an application ships a new version, drive the group upgrade and watch its progress:

await sdk.admin.upgradeGroup(groupId, { targetApplicationId, cascade: true });
const status = await sdk.admin.getGroupUpgradeStatus(groupId);
const migration = await sdk.admin.getMigrationStatus(namespaceId);
// migration.rollup.allMigrated === true when done