Group
What is group
Group is a logical collection of nodes and subgroups. It is the base context for nodes to exist for users. Think of it as a home. A user can have multiple groups.
Why it is needed
Groups simplify user access control: what nodes a user can reach is scoped by the group or subgroup they have access to.
Pre-requisites
User is registered and authenticated
Access Control
Group Permissions
Primary Access: Full access to group and all subgroups
Can create subgroups
Can add/remove nodes
Can update group/subgroup names
Can delete group
Can share/unshare group/subgroups with others
Secondary Access: Limited access to group
Can create subgroups
Can delete subgroups
Cannot add/remove nodes
Can update group/subgroup names
Cannot delete group
Cannot share/unshare group/subgroups
Sub-entity Access: Access only to specific subgroups
Can update subgroup names
Can list nodes in accessible subgroups
Cannot create subgroups
Cannot add/remove nodes
Cannot delete subgroups
Cannot share/unshare subgroups
Naming rules
Group/subgroup name: User-provided string, cannot be empty.
Group/subgroup Id:
Group ID: 6 characters. The first character is a lowercase letter (to avoid all-numeric IDs), followed by 5 alphanumeric characters (lowercase letters and digits). Lowercase so that customer support is easier without worrying about case sensitivity.
Subgroup ID: 3 characters, consisting of lowercase letters and digits.
Key Rules
A node can belong to maximum 3 subgroups (subgrp1, subgrp2, subgrp3)
A node can only be in one group at a time
Primary access to group implies access to all subgroups
Scope encoding
A member’s scope is carried entirely by sub_entity_ids on their rmng-user-group-assoc row, and an empty list is the canonical encoding of full-group access. Every consumer relies on this: subgroup filters treat “no subgroup named” as “not restricted”, and the assume-role access map buckets a row into full-group or subgroup-scoped by whether the list is empty rather than by reading access_type.
Two invariants follow, and both are enforced when the row is written rather than when it is read:
A
subentitygrant must always name at least one subgroup. Asubentityrow with an empty list would be read as access to the whole group by every consumer, so it is rejected at write time.Group-level access supersedes subgroup access. When a member holding
subentityaccess accepts a group share,sub_entity_idsis reset to empty and they are promoted to full-group access. The reverse is refused: a subgroup grant cannot be added on top of group-level access.
Consequently access_type alone is not sufficient to determine a member’s scope, and the two fields must be written together. This is why access_type is validated against primary, secondary and subentity before a sharing request is created: an unrecognised value resolves to an empty permission set, which fails every authorized read of that group for that member.
Capacity Limits
Creating groups is unbounded. The number of groups a user can operate at one time is not, and the ceiling is imposed by AWS rather than chosen by RainMaker.
Device control runs over MQTT, and the credentials for it come from POST /v1/assumed-roles. That endpoint returns an STS session policy which enumerates one set of AWS IoT topic ARNs per accessible group (see user_auth.md §3.3). STS caps an inline session policy at 2048 characters, so the number of groups a user can hold credentials for is bounded by how many ARNs fit in that document.
Rule |
Limit |
|---|---|
Groups a user may create |
Unbounded |
Groups usable in one session |
4 with full access |
Shared subgroups usable in one session |
8 |
Mixed allowance |
|
Subgroups per group |
No enforced limit |
Session policy size (AWS/STS) |
2048 characters |
Sharing request validity |
24 hours from creation |
Pending sharing requests per group/invitee pair |
No enforced limit — repeated shares create additional requests |
See also AWS Service Limits for the limits this deployment operates against.
Properties of the allowance that clients and product need to account for:
It is per user and pooled. Groups the user owns and groups shared to them draw on the same allowance and cost exactly the same. Four people each sharing one group with a user can exhaust it.
Subgroups a user creates inside their own group are free: the group-level grant already covers them with a
subgroups/*wildcard. Only a subgroup shared to a user, from a group they do not otherwise have access to, consumes allowance.The number of nodes in a group does not affect the allowance.
The
iot:Connectresource embeds the caller’s user name, so a long email address reduces the remaining allowance. A user with both an email and a phone number on record consumes twoiot:Connectresources.A mapping row that points at a group or subgroup which has since been deleted still consumes allowance, because the access map is resolved from
rmng-user-group-assocrather than from the group table.
Behaviour when the allowance is exceeded. The credential request is rejected as a whole: POST /v1/assumed-roles returns HTTP 500 with "Unable to issue credentials: too many accessible groups to encode in a session policy". The failure is not scoped to the group that crossed the threshold — the user loses MQTT access to every group, including groups they own. REST endpoints are unaffected, so GET /v1/groups continues to list every group; a client in this state shows a complete group list with no live device data.
Sharing is not capacity-checked. POST /v1/groups/{groupId}/sharing-requests does not evaluate the invitee’s remaining allowance. A share can therefore be created (201) and approved (200) successfully and still leave the invitee unable to reach any device, with no error surfaced to either party. Clients should treat an over-allowance assumed-roles failure as an actionable state — prompting the user to leave a group — rather than as a transient fault to retry.
The per-group and per-subgroup character costs above follow from the ARN templates the MQTT session policy emits and from the region and account-id lengths of the deployment. Adding or lengthening any per-group ARN lowers the group ceiling for every user, so these figures need re-deriving whenever that document changes.
Known limitation: the ceiling is a property of the unscoped route
The allowance is consumed only because the unscoped POST /v1/assumed-roles
encodes every group the caller can reach into one policy. The group-scoped
routes
POST /v1/groups/{groupId}/assumed-roles
POST /v1/groups/{groupId}/subgroups/{subGroupId}/assumed-roles
encode exactly one group, at ~850 characters regardless of how many groups the
caller has — but they are restricted to super-admins, so a regular client
cannot use them to sidestep the ceiling. Opening them to regular users would
require resolving the caller’s own scope for the requested group; the
super-admin path grants full-group access through a system actor, and reusing it
for a regular user would hand a subentity member the whole group’s ARNs.
Two further properties are worth knowing:
Even under a scoped policy, a member holding many shared subgroups of the same group still accumulates one ARN pair per subgroup, so roughly 8 shared subgroups overruns a single-group policy.
The over-allowance failure surfaces as HTTP 500, which a client cannot distinguish from a transient fault. Clients must treat a repeated
assumed-roles500 alongside a non-empty group list as this condition.
APIs
Group Create
External Flow
User clicks on the “create group” button in the client
Adds name to the group
Client calls group create API (
POST /v1/groups)User is redirected to the group details page and get group API is called
Internal Flow
API: POST /v1/groups
Request:
{
"group_name": "Living Room",
"capabilities": ["matter"]
}
capabilities is optional. Include ["matter"] to create the group as a Matter fabric; omit it for a plain group.
Process:
Generate unique
group_id(retries up to 3 times if duplicate)Create group entry in database
TableName:
groupsAttributes:
group_id(PK),sub_group_id(SK),group_namegroup_id: Generated IDsub_group_id:"NONE"(indicates main group)group_name: Provided nameIf a Matter fabric, the main group entry gains a
capabilitieslist containing"matter", plus a singlecap_mattercolumn holding the fabric data as a JSON string. Capability data lives in acap_<name>column, and only on the main group entry — never on subgroups. The keys insidecap_matterare:fabric_id— 64-bit (8 bytes, 16 hex chars). Derived from thegroup_idby hex-encoding its ASCII bytes and right-padding with zeros to 16 hex chars. Example:group_id"abc123"→ hex616263313233→ padded6162633132330000.group_cat_id_admin— 32-bit. Random hex value in range 0x0100-0x03FF (256–1023 decimal) + version"0001", e.g.03A30001.group_cat_id_operate— 32-bit. Random hex value in range 0x0600-0x08FF (1536–2303 decimal) + version"0001", e.g.06D90001.root_ca— ECDSA SHA256 Root CA certificate (PEM format)Certificate type: CA certificate (self-signed)
Algorithm: ECDSA P-256 with SHA256 signature
Key Usage: CRLSign and CertSign
Subject: contains Fabric ID and Root CA ID (RCAC ID) as Matter-specific attributes (ExtraNames), e.g.
1.3.6.1.4.1.37244.1.4=AAD09FFFE7D8B03B, 1.3.6.1.4.1.37244.1.5=6162633132330000Fabric ID: UTF-8 encoded Matter Fabric ID — ASN.1 OID
1.3.6.1.4.1.37244.1.5(MATTER_FABRIC_ID)RCAC ID: randomly generated token (64-bit, e.g.
1234567890ABCDEF), UTF-8 encoded — ASN.1 OID1.3.6.1.4.1.37244.1.4(MATTER_RCAC_ID)
Serial Number: randomly generated big integer (0-255), e.g.
42Validity: 15 years from creation date
root_ca_priv_key— Root CA private key (PEM). Stored, never returned in any API response.ipk— Identity Protection Key, 128-bit (16-byte random hex string), e.g.0123456789ABCDEF0123456789ABCDEF.
A group is a Matter fabric when
"matter"appears in itscapabilitieslist.
Create user-group mapping in database
TableName:
rmng-user-group-assocAttributes:
user_id(PK),group_id(SK),sub_entity_ids,access_typeuser_id: Current authenticated usergroup_id: Generated group IDsub_entity_ids: Empty listaccess_type:"primary"(creator gets primary access)
If the user-group mapping fails, rollback by deleting the group
Response:
{
"group_id": "abc123"
}
If matter fabric, the fabric data is returned under a nested matter object (root_ca_priv_key is never returned):
{
"group_id": "abc123",
"matter": {
"fabric_id": "6162633132330000",
"root_ca": "-----BEGIN CERTIFICATE-----\nMIIBpjCCAUygAwIBAgIIX...\n-----END CERTIFICATE-----\n",
"ipk": "0123456789ABCDEF0123456789ABCDEF",
"group_cat_id_admin": "03A30001",
"group_cat_id_operate": "06D90001"
}
}
Sequence Diagram:
sequenceDiagram
title Group Create - Internal Flow
participant Client
participant GroupAPI as "Group Create API"
participant GroupDB as "DynamoDB groups"
participant UserGroupDB as "DynamoDB rmng-user-group-assoc"
Client->>GroupAPI: POST /v1/groups<br/>{group_name, capabilities}
activate GroupAPI
loop Retry up to 3 times
GroupAPI->>GroupAPI: Generate group_id
GroupAPI->>GroupDB: PutItem (group_id, "NONE", group_name)
activate GroupDB
GroupDB->>GroupAPI: Success/Error
deactivate GroupDB
alt Success
GroupAPI->>GroupAPI: Break loop
else Duplicate ID
GroupAPI->>GroupAPI: Retry with new ID
end
end
alt Matter fabric
GroupAPI->>GroupAPI: Derive fabric_id<br/>(hex(group_id) padded to 16 hex chars)
GroupAPI->>GroupAPI: Generate group_cat_id_admin<br/>(0x0100-0x03FF + "0001")
GroupAPI->>GroupAPI: Generate group_cat_id_operate<br/>(0x0600-0x08FF + "0001")
GroupAPI->>GroupAPI: Generate root_ca certificate<br/>(ECDSA P-256 SHA256)
GroupAPI->>GroupAPI: Generate root_ca_priv_key
GroupAPI->>GroupAPI: Generate ipk<br/>(128 bit random hex)
GroupAPI->>GroupDB: Update group<br/>(capabilities=["matter"], cap_matter JSON:<br/>fabric_id, root_ca, root_ca_priv_key, ipk,<br/>group_cat_id_admin, group_cat_id_operate)
GroupDB->>GroupAPI: Success
end
GroupAPI->>UserGroupDB: CreateUserGroup<br/>(user_id, group_id, primary,<br/>cat_id if Matter)
activate UserGroupDB
alt Success
UserGroupDB->>GroupAPI: Success
alt Matter fabric
GroupAPI->>Client: Response:<br/>{group_id, matter: {fabric_id,<br/>root_ca, ipk, group_cat_id_admin,<br/>group_cat_id_operate}}
else Regular group
GroupAPI->>Client: Response: {group_id}
end
else Failure
UserGroupDB->>GroupAPI: Error
GroupAPI->>GroupDB: DeleteGroup (rollback)
GroupAPI->>Client: Error Response
end
deactivate UserGroupDB
deactivate GroupAPI
List Groups
External Flow
User navigates to groups list page
Client calls list groups API
Groups are displayed with their subgroups and nodes
Internal Flow
API: GET /v1/groups
Request: No request body
Process:
Query user’s accessible groups
TableName:
rmng-user-group-assocQuery by
user_id(PK) to get all accessible groups
For each group:
Load group details
TableName:
groupsQuery by
group_id(PK) wheresub_group_id = "NONE"for main groupQuery by
group_id(PK) wheresub_group_id != "NONE"for subgroups
Load nodes with capability data
TableName:
rmng-group-node-assocColumn
Type
Notes
capabilitiesList<String>
Capability names for this node.
rmng= RainMaker node;matter= Matter (fabric) node;acc= feature capability. Node-level, not group-level.aliasString
Optional. ACC endpoint ID (detail for the
acccapability). Also GSI partition key.Query by
group_id(PK) to get all nodes in the groupExtract subgroup memberships from
subgrp1,subgrp2,subgrp3fieldsBuild each node’s
capabilitiesarray from the stored list, and itscapability_detailsmap from per-capability data:acc→{endpoint_id: alias}matter→{matter_node_id}, wherematter_node_idis derived from the node_id — not stored
If group has Matter capability, extract matter details from main group entry (where
sub_group_id = "NONE"):fabric_id: Matter fabric IDroot_ca: Root CA certificate (PEM format)ipk: Identity Protection Keygroup_cat_id_admin: CAT ID for admin accessgroup_cat_id_operate: CAT ID for operate access
Filter subgroups based on user’s access permissions
Populate
node_detailswith capability information for nodes that have capabilitiesReturn groups with their subgroups, nodes, node capability details, and matter details (if matter fabric)
Response:
{
"groups": [
{
"group_id": "abc123",
"group_name": "Living Room",
"access_type": "primary",
"node_ids": ["node1", "node2"],
"node_details": {
"node1": {
"capabilities": ["rmng", "matter", "acc"],
"capability_details": {
"acc": { "endpoint_id": "de152ff2-f070-4d0e-94da-770828b1770f" },
"matter": { "matter_node_id": "A1B2C3D4E5F60718" }
}
},
"node2": {
"capabilities": ["matter"],
"capability_details": {
"matter": { "matter_node_id": "B2C3D4E5F6071829" }
}
}
},
"subgroups": [
{
"subgroup_id": "sg1",
"subgroup_name": "Corner Lights",
"node_ids": ["node1"]
}
]
}
]
}
Notes:
access_typeindicates the user’s access level for the group:primary,secondary, orsubgroupnode_detailsis optional and only included for nodes that have capability dataPer node:
capabilitiesis the list of capability names;capability_details[name]holds that capability’s data (when any)Node capabilities and when they are assigned (at node association time, persisted on the
rmng-group-node-assocrow):rmng: a RainMaker node — set when a node joins via the RainMaker (challenge-response) association, or via the Matter flow whosevendor_reserved1matches a registered RainMaker devicematter: a Matter (fabric) node — set when a node joins via the Matter (nocsr_elements) flow and receives a device NOC. Itsmatter_node_idis derived from the node_id at response time, not stored. A pure Matter node (novendor_reserved1/ not a registered device) getsmatteronly, withoutrmngacc: feature capability withendpoint_id, set asynchronously by the ACC capability lambda after confirm
If matter fabric, response includes matter details:
{
"groups": [
{
"group_id": "abc123",
"group_name": "Living Room",
"access_type": "primary",
"node_ids": ["node1", "node2"],
"node_details": {
"node1": {
"capabilities": ["rmng", "matter"],
"capability_details": {
"matter": { "matter_node_id": "A1B2C3D4E5F60718" }
}
}
},
"subgroups": [
{
"subgroup_id": "sg1",
"subgroup_name": "Corner Lights",
"node_ids": ["node1"]
}
],
"matter": {
"fabric_id": "6162633132330000",
"root_ca": "-----BEGIN CERTIFICATE-----\nMIIBpjCCAUygAwIBAgIIX...\n-----END CERTIFICATE-----",
"ipk": "0123456789ABCDEF0123456789ABCDEF",
"group_cat_id_admin": "01000001",
"group_cat_id_operate": "06000001"
}
}
]
}
Notes:
node_detailsis optional and only present for nodes with capabilitiesEach entry in
node_detailscontains acapabilitieslist and acapability_detailsmapCapability data structure depends on the capability type:
acc: containsendpoint_idfor the ACC (Amazon Connectivity) capability
When the group is a Matter fabric, its fabric data is returned under a separate top-level
matterobject on the group
Sequence Diagram:
sequenceDiagram
title List Groups - Internal Flow
participant Client
participant ListAPI as "List Groups API"
participant UserGroupDB as "DynamoDB rmng-user-group-assoc"
participant GroupDB as "DynamoDB groups"
participant GroupNodeDB as "DynamoDB rmng-group-node-assoc"
Client->>ListAPI: GET /v1/groups
activate ListAPI
ListAPI->>UserGroupDB: Query rmng-user-group-assoc<br/>(user_id)
UserGroupDB->>ListAPI: Return accessible groups<br/>{group_id, access_type}
loop For each group
ListAPI->>GroupDB: Query groups<br/>(group_id)
GroupDB->>ListAPI: Return group entries<br/>{group_id, sub_group_id, group_name,<br/>matter, root_ca, cat_id_admin,<br/>cat_id_operate, ipk, fabric_id}
ListAPI->>GroupNodeDB: Query rmng-group-node-assoc<br/>(group_id)
GroupNodeDB->>ListAPI: Return node mappings<br/>{node_id, capabilities, subgrp1, subgrp2, subgrp3}
ListAPI->>ListAPI: Populate node_details<br/>with capability data (acc, etc.)
ListAPI->>ListAPI: Filter subgroups<br/>based on access permissions
ListAPI->>ListAPI: Extract matter details<br/>(if matter fabric)
end
ListAPI->>Client: Response:<br/>{groups: [{group_id, group_name,<br/>node_ids, node_details,<br/>matter (if matter fabric),<br/>subgroups: [{subgroup_id, subgroup_name,<br/>node_ids}]}]}
deactivate ListAPI
Update Group
External Flow
User clicks edit on a group
User changes the group name
Client calls update group API
Group name is updated
Internal Flow
API: PATCH /v1/groups/{groupId}
Request:
{
"group_name": "Updated Name"
}
Process:
Get user-group mapping to verify access and set permissions
TableName:
rmng-user-group-assocQuery by
user_id(PK) andgroup_id(SK)Verify user has primary/secondary
access_typeto the group
Update group entry in database
TableName:
groupsUpdate
group_namewheregroup_id = groupId(PK) andsub_group_id = "NONE"(SK)
Response:
{
"message": "success"
}
Convert Group into Fabric (Enable Capabilities)
Enables one or more capabilities on an existing group. Enabling the matter capability
converts a plain group into a Matter fabric, generating its Root CA, IPK and CAT IDs.
External Flow
User opens an existing (non-Matter) group
User chooses to enable Matter / convert to fabric
Client calls the capabilities API with
{"capabilities": ["matter"]}The group becomes a Matter fabric; members can now request NOCs
Internal Flow
API: POST /v1/groups/{groupId}/capabilities
Request:
{
"capabilities": ["matter"]
}
Process:
Validate every requested capability is known; reject unknown names.
Get user-group mapping to set the caller’s permissions.
TableName:
rmng-user-group-assoc, query byuser_id(PK) +group_id(SK)
For each capability, generate its data and persist it.
TableName:
groups(PKgroup_id, SKsub_group_id = "NONE")Single atomic
UpdateItem: appends the name to thecapabilitieslist and writes thecap_<name>column.Authorization: requires
group:updatecapabilities, which onlyprimaryaccess carries — i.e. owner-only. Secondary/subentity callers are rejected.Enable-once: the write is conditional on
cap_<name>being absent, so a capability already enabled is rejected (the group is already a fabric).
Matter controller Node IDs require no per-user backfill; they are derived when NOCs are issued.
Response (matter capability shown):
{
"matter": {
"fabric_id": "6162633132330000",
"root_ca": "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----",
"ipk": "0123456789ABCDEF0123456789ABCDEF",
"group_cat_id_admin": "01000001",
"group_cat_id_operate": "06000001"
}
}
Errors:
Condition |
Status |
|---|---|
Missing/empty or unknown |
400 |
Capability already enabled on the group |
409 |
Caller is not the group owner |
403 |
Note: 403 is the intended status for the owner-only check. The current handler only maps the “already enabled” case to 409 and lets every other error (including the DB-layer authorization rejection for a non-owner caller) fall through to 500, so today a non-owner caller receives a 500 rather than a 403.
Note: A group that has shared subgroups can still be converted. The shared subgroup users keep subgroup-only access and cannot obtain fabric NOCs — see the FAQ.
Delete Group
Deletes an empty group. The group must have no subgroups and no user nodes; a group that still has either is rejected with 409. Remove all nodes and delete all subgroups first, then delete the group. Deleting the group also removes the Matter fabric data, since that data lives on the main group entry.
External Flow
User removes all nodes and deletes all subgroups from the group
User clicks delete on the (now empty) group
User confirms deletion
Client calls delete group API
The group entry and all user-access mappings for it are deleted
Internal Flow
API: DELETE /v1/groups/{groupId}
Request: No request body
Process:
Get user-group mapping to verify access and set permissions
TableName:
rmng-user-group-assocQuery by
user_id(PK) andgroup_id(SK)Verify user has primary
access_typeto the group
Verify the group is empty
Reject with 409 if any subgroup exists (empty subgroups count too).
Reject with 409 if any user node is still attached. Child nodes (named with the
--parent-managed convention) are not counted, so a group whose only remaining nodes are child nodes can still be deleted.
Delete all group entries from database (the main group entry, which carries any Matter fabric data)
TableName:
groupsQuery by
group_id(PK) and batch delete all rows
Delete all user-group mappings from database
TableName:
rmng-user-group-assocIndexName:
rmng-user-group-assoc-by-group-idQuery by
group_idusing secondary index and batch delete
Response:
{
"message": "Group deleted successfully"
}
If the group still has subgroups or user nodes, the request is rejected:
{
"message": "group not empty"
}
(HTTP 409)
Sequence Diagram:
sequenceDiagram
title Group Delete - Internal Flow
participant Client
participant DeleteAPI as "Delete Group API"
participant UserGroupDB as "DynamoDB rmng-user-group-assoc"
participant GroupDB as "DynamoDB groups"
participant GroupNodeDB as "DynamoDB rmng-group-node-assoc"
Client->>DeleteAPI: DELETE /v1/groups/{groupId}
activate DeleteAPI
DeleteAPI->>UserGroupDB: Query rmng-user-group-assoc<br/>(user_id, group_id)
UserGroupDB->>DeleteAPI: Return mapping
DeleteAPI->>DeleteAPI: Verify primary access_type
DeleteAPI->>GroupDB: Load group (subgroups)
DeleteAPI->>GroupNodeDB: Query by group_id (user nodes)
alt Has subgroups or user nodes
DeleteAPI->>Client: 409 group not empty
else Empty
DeleteAPI->>GroupDB: BatchDelete all group entries
DeleteAPI->>UserGroupDB: Query by group_id (GSI)
DeleteAPI->>UserGroupDB: BatchDelete all user mappings
DeleteAPI->>Client: Success Response
end
deactivate DeleteAPI
Create Subgroup
External Flow
User navigates to group details page
User clicks “Create Subgroup/Room”
User enters subgroup name
Client calls create subgroup API
Subgroup is created and displayed
Internal Flow
API: POST /v1/groups/{groupId}/subgroups
Request:
{
"subgroup_name": "Corner Lights"
}
Process:
Verify user has access to parent group
TableName:
rmng-user-group-assocQuery by
user_id(PK) andgroup_id(SK)Verify user has primary or secondary
access_typeto the group
Generate unique
subgroup_id(retries up to 5 times if duplicate)Create subgroup entry in database
TableName:
groupsAttributes:
group_id(PK),sub_group_id(SK),group_namegroup_id: Parent group IDsub_group_id: Generated subgroup IDgroup_name: Provided subgroup name
Response:
{
"subgroup_id": "sg7"
}
Update Subgroup
External Flow
User clicks edit on a subgroup
User changes the subgroup name
Client calls update subgroup API
Subgroup name is updated
Internal Flow
API: PATCH /v1/groups/{groupId}/subgroups/{subGroupId}
Request:
{
"subgroup_name": "Updated Subgroup Name"
}
Process:
Verify user has access to parent group OR specific subgroup: primary or secondary or sub-entity access
TableName:
rmng-user-group-assocQuery by
user_id(PK) andgroup_id(SK)Verify user has:
Primary or secondary
access_typeto the group (grants access to all subgroups), ORSub-entity
access_typewith the specificsubGroupIdinsub_entity_idslist
Update subgroup entry in database
TableName:
groupsUpdate
group_namewheregroup_id = groupId(PK) andsub_group_id = subGroupId(SK)
Response:
{
"message": "success"
}
Delete Subgroup
Deletes an empty subgroup. The subgroup must have no user nodes; a subgroup that still has nodes is rejected with 409. Remove all nodes from the subgroup first, then delete it. Nodes are not deleted and stay in the parent group.
External Flow
User removes all nodes from the subgroup
User navigates to group details page
User clicks “Delete” on the (now empty) subgroup
User confirms deletion
Client calls delete subgroup API
Subgroup is deleted; nodes remain in the parent group
Users with sub-entity access to only this subgroup lose access entirely
Internal Flow
API: DELETE /v1/groups/{groupId}/subgroups/{subGroupId}
Request: No request body
Process:
Verify user has access to parent group
TableName:
rmng-user-group-assocQuery by
user_id(PK) andgroup_id(SK)Verify user has primary or secondary
access_type
Verify the subgroup is empty
TableName:
rmng-group-node-assocQuery by
group_id(PK) and check which nodes carrysubGroupIdinsubgrp1,subgrp2, orsubgrp3.Reject with 409 if any user node is still in the subgroup. Child nodes (the
--parent-managed convention) are not counted.
Delete subgroup entry from database
TableName:
groupsDeleteItem by
group_id(PK) andsub_group_id(SK)
Scrub the subgroup from every user’s sub-entity access
TableName:
rmng-user-group-assocIndexName:
rmng-user-group-assoc-by-group-idQuery by
group_idand removesubGroupIdfrom each user’ssub_entity_ids. Applied unconditionally, so a subgroup that was shared but had no nodes is also cleaned up.If a user’s
sub_entity_idsbecomes empty, delete the entire mapping entry.
No node data is deleted. See the Data Cleanup Reference for the full comparison.
Response:
{
"message": "Subgroup deleted successfully"
}
If the subgroup still has user nodes, the request is rejected with 409:
{
"message": "subgroup not empty"
}
Errors:
Condition |
Status |
|---|---|
Subgroup still has user nodes |
409 |
Subgroup does not exist (parent group accessible) |
404 |
Caller has sub-entity (subgroup-only) access, which lacks delete permission |
403 |
Caller has no access to the group, or the group does not exist |
400 |
Sub-entity users can see the subgroup but cannot delete it: the delete permission is carried only by primary and secondary access.
Sequence Diagram:
sequenceDiagram
title Delete Subgroup - Internal Flow
participant Client
participant DeleteSubAPI as "Delete Subgroup API"
participant UserGroupDB as "DynamoDB rmng-user-group-assoc"
participant GroupDB as "DynamoDB groups"
participant GroupNodeDB as "DynamoDB rmng-group-node-assoc"
Client->>DeleteSubAPI: DELETE /v1/groups/{groupId}/subgroups/{subGroupId}
activate DeleteSubAPI
DeleteSubAPI->>UserGroupDB: Query (user_id, group_id)
UserGroupDB->>DeleteSubAPI: Return mapping
DeleteSubAPI->>DeleteSubAPI: Verify primary/secondary access
DeleteSubAPI->>GroupNodeDB: Query by group_id
GroupNodeDB->>DeleteSubAPI: Return node mappings
alt Subgroup still has user nodes
DeleteSubAPI->>Client: 409 subgroup not empty
else Empty
DeleteSubAPI->>GroupDB: DeleteItem (group_id, sub_group_id)
DeleteSubAPI->>UserGroupDB: Query by group_id (GSI)
UserGroupDB->>DeleteSubAPI: Return sub-entity users
loop For each user with this subgroup
alt Last subgroup for user
DeleteSubAPI->>UserGroupDB: DeleteItem (user_id, group_id)
else Has other subgroups
DeleteSubAPI->>UserGroupDB: Remove subgroup from sub_entity_ids
end
end
DeleteSubAPI->>Client: Success Response
end
deactivate DeleteSubAPI
Add Node to Subgroup
External Flow
User navigates to group/subgroup details
User selects a node from the group
User clicks “Add to Subgroup”
User sees all subgroups in the group with their names
Client calls add node to subgroup API
Node is added to the subgroup
Internal Flow
API: PUT /v1/groups/{groupId}/subgroups/{subGroupId}/nodes/{nodeId}
Request: No request body
Process:
Verify user has permission on the group
TableName:
rmng-user-group-assocQuery by
user_id(PK) andgroup_id(SK)Verify user has:
Primary or secondary
access_typeto the group (grants access to all subgroups), ORSub-entity
access_typewith the specificsubGroupIdinsub_entity_idslist
Verify node exists in the group
TableName:
rmng-group-node-assocQuery by
group_id(PK) andnode_id(SK)
Check if node is already in 3 subgroups (maximum limit)
Check if
subgrp1,subgrp2, andsubgrp3are all populated
Update group-node mapping entry
TableName:
rmng-group-node-assocPrimary Key:
group_id(PK),node_id(SK)Add
subGroupIdto first available slot (subgrp1,subgrp2, orsubgrp3)Full item structure:
{ "group_id": "group123", "node_id": "node456", "subgrp1": "sg1", // Optional, first subgroup "subgrp2": "sg2", // Optional, second subgroup "subgrp3": "sg3" // Optional, third subgroup (newly added) }
Update node shadow with new group info via MQTT
Migrate shadow data from old shadow name to new shadow name
Old shadow name format:
params-<group_id>[-<subgroup>…]New shadow name format:
params-<group_id>-<subgrp1>[-<subgrp2>…](with the updated subgroups, sorted alphabetically)Copy shadow state from old shadow to new shadow (only if same group)
Delete old shadow after migration
Publish updated group info to node via MQTT
Topic:
rainmaker/nodes/<node_id>/from_cloudMessage format:
{ "event": ["getGroupInfo"], "getGroupInfo": { "pgrp": "group123", "subgrps": ["sg1"] } }
Response:
{
"message": "success"
}
Remove Node from Subgroup
External Flow
User navigates to subgroup details
User selects a node in the subgroup
User clicks “Remove from Subgroup”
Client calls remove node from subgroup API
Node is removed from the subgroup
Data deletion: this also unshares the node for users who had it shared via this subgroup. Node data — user tags, automations, schedules, timeseries and scenes — is not deleted, because it carries no record of which user created it and deleting it would destroy other members’ data. Remaining members can delete what they no longer need.
Internal Flow
API: DELETE /v1/groups/{groupId}/subgroups/{subGroupId}/nodes/{nodeId}
Request: No request body
Process:
Verify user has permission on the group
TableName:
rmng-user-group-assocQuery by
user_id(PK) andgroup_id(SK)Verify user has:
Primary or secondary
access_typeto the group (grants access to all subgroups), ORSub-entity
access_typewith the specificsubGroupIdinsub_entity_idslist
Verify node exists in the group and subgroup
TableName:
rmng-group-node-assocOperation: GetItem query by
group_id(PK) andnode_id(SK)Verify node is in the specified subgroup:
Check if
subGroupIdmatches any of the subgroup fields (subgrp1,subgrp2, orsubgrp3)
Update group-node mapping entry
TableName:
rmng-group-node-assocRemove
subGroupIdfrom the appropriate slot (subgrp1,subgrp2, orsubgrp3)
Update node shadow with new group info via MQTT (same as above)
Response:
{
"message": "success"
}
List Sharing Requests
External Flow
User navigates to sharing requests page
Client calls list sharing requests API
Pending requests are displayed
Internal Flow
API: GET /v1/sharing-requests/received
Request: No request body
Process:
Query sharing requests for current user
TableName:
rmng-sharing-reqsQuery by
user_id(PK) to get all requests for the current user
Filter out expired requests (compare
expiration_timewith current time)Enrich each request with the inviting user’s details (email and phone number)
Return all pending requests
Response:
{
"sharing_requests": [
{
"sharing_request_id": "uuid-123",
"group_id": "abc123",
"subgroup_id": "",
"access_type": "primary",
"primary_user_id": "user-abc-123",
"primary_email": "owner@example.com",
"primary_phone_number": "+1234567890"
}
]
}
The primary_* fields identify the user who created (sent) the sharing request.
Approve/Decline Group/Subgroup Sharing Request
External Flow
User views sharing requests
User clicks “Accept” or “Reject” on a request
Client calls approve/decline API
If accepted, user gains access to the group/subgroup
Internal Flow
API:
Accept:
POST /v1/sharing-requests/{requestId}/acceptReject:
POST /v1/sharing-requests/{requestId}/reject
Request: No request body. The request is identified by requestId in the path.
Process (Accept):
Get sharing request
TableName:
rmng-sharing-reqsQuery by
user_id(PK) andsharing_request_id(SK)
Verify request exists and is not expired
Verify existing access:
If group sharing request and user already has the same access type, reject with “no change to user group mapping”
If subgroup sharing request and user already has group level access, reject with “cannot add sub-group access to a group level access”
If subgroup sharing request and user already has the same subgroup in
sub_entity_ids, reject with “no change to user group mapping”
Create user-group mapping in database
TableName:
rmng-user-group-assocAttributes:
user_id(PK),group_id(SK)user_id: Current usergroup_id: From sharing requestsub_entity_ids:Empty list if full group (if user previously had subgroup access, the list is reset to empty as group access supersedes)
Append subgroup Id to list if subgroup
Never empty for
subentityaccess — see Scope encoding. Asubentitygrant naming no subgroup is rejected at write time.
access_type: From sharing request (primary,secondary, orsubentity). When switching betweenprimaryandsecondary, the access type is overwritten.
Delete sharing request
TableName:
rmng-sharing-reqsDelete by
user_id(PK) andsharing_request_id(SK)
Process (Reject):
Get sharing request
TableName:
rmng-sharing-reqsQuery by
user_id(PK) andsharing_request_id(SK)
Delete sharing request
TableName:
rmng-sharing-reqsDelete by
user_id(PK) andsharing_request_id(SK)
Response:
{
"message": "Sharing request processed"
}
Sequence Diagram:
sequenceDiagram
title Approve Sharing Request - Internal Flow
participant User
participant SharingAPI as "Sharing Request API"
participant SharingDB as "DynamoDB rmng-sharing-reqs"
participant UserGroupDB as "DynamoDB rmng-user-group-assoc"
User->>SharingAPI: POST /v1/sharing-requests/{requestId}/accept
activate SharingAPI
SharingAPI->>SharingDB: GetSharingRequest<br/>(user_id, sharing_request_id)
SharingDB->>SharingAPI: Return request<br/>{group_id, sub_entity_id, access_type, expiration_time}
SharingAPI->>SharingAPI: Verify request exists<br/>Verify not expired
SharingAPI->>UserGroupDB: Query rmng-user-group-assoc<br/>(user_id, group_id)
UserGroupDB->>SharingAPI: Return existing mapping (if any)
alt Group sharing request
SharingAPI->>SharingAPI: Verify user doesn't have<br/>group level access
else Subgroup sharing request
SharingAPI->>SharingAPI: Verify user doesn't have<br/>group level access
end
SharingAPI->>UserGroupDB: Create user-group mapping<br/>(user_id, group_id, sub_entity_ids,<br/>access_type from request)
activate UserGroupDB
UserGroupDB->>SharingAPI: Success
deactivate UserGroupDB
SharingAPI->>SharingDB: DeleteSharingRequest<br/>(user_id, sharing_request_id)
SharingDB->>SharingAPI: Success
SharingAPI->>User: Success Response
deactivate SharingAPI
List Group Users
External Flow
User navigates to group settings / members page
Client calls list group users API
Users with access to the group are displayed with their access type
Internal Flow
API: GET /v1/groups/{groupId}/users
Access Control: Requires group-level access (primary or secondary). The listing scope is determined by the caller’s access type:
Primary Access: Sees all users with group-level access (primary and secondary) and all subentity users
Secondary Access: Sees only primary users (to discover group owners)
Subentity Access: Denied (subentity users have no group-level access)
Request: No request body
Process:
Verify caller has group-level access to the group
TableName:
rmng-user-group-assocQuery by
user_id(PK) andgroup_id(SK)Reject if user has only subentity access (no group-level entry exists for this user)
List all users for the group based on caller’s permission level
TableName:
rmng-user-group-assoc, IndexName:rmng-user-group-assoc-by-group-idQuery by
group_idto get all user entriesIf caller has
group:listuserspermission (primary access), return all usersIf caller has only
group:listprimaryuserspermission (secondary access), filter to primary users only
Batch-fetch user details (email, phone) from
user_detailstableDegrades gracefully if fetch fails (returns user IDs and access types without email/phone)
Map
sub_entityaccess type to"subgroup"in responseFor subentity users, include their list of subgroups they have access to
Response:
{
"users": [
{
"user_id": "user123",
"email": "user@example.com",
"phone": "+1234567890",
"access_type": "primary"
},
{
"user_id": "user456",
"email": "other@example.com",
"access_type": "secondary"
},
{
"user_id": "user789",
"email": "subgroup-user@example.com",
"access_type": "subgroup",
"subgroups": ["sg1", "sg2"]
}
]
}
Errors:
Condition |
Status |
|---|---|
Caller has no group-level access (subentity-only) |
400 |
Caller not authorized to access the group |
400 |
Notes:
subgroupsis only present for users withsubgroupaccess type and shows all subgroups the user has access toaccess_typevalues:"primary","secondary","subgroup"Secondary users can list group users to discover who the owners are, but see only primary users
List Subgroup Users
External Flow
User navigates to subgroup settings / members page
Client calls list subgroup users API
Users with access to the subgroup are displayed with their access type
Internal Flow
API: GET /v1/groups/{groupId}/subgroups/{subGroupId}/users
Access Control: Requires access to the specified subgroup (primary, secondary, or subentity access that includes this subgroup).
Request: No request body
Process:
Verify caller has access to the subgroup (primary, secondary group-level, or subentity access that includes this subgroup)
TableName:
rmng-user-group-assocQuery by
user_id(PK) andgroup_id(SK), then check if user has group-level access or ifsubgroup_idis insub_entity_idsReject if user has no access to this subgroup
Determine the listing scope based on caller’s access level
If caller has
group:listuserspermission (primary access), return the full membershipOtherwise, use the DB layer to return primary-only users
TableName:
rmng-user-group-assoc, IndexName:rmng-user-group-assoc-by-group-idQuery by
group_idand filter to users with access to the specifiedsubGroupIdIf caller has only
group:listprimaryuserspermission, filter to primary users only
Batch-fetch user details (email, phone) from
user_detailstableScope the
subgroupsfield in response to only the requested subgroup (to prevent leaking which other subgroups a user belongs to)Map
sub_entityaccess type to"subgroup"in response
Response:
{
"users": [
{
"user_id": "user123",
"email": "owner@example.com",
"phone": "+1234567890",
"access_type": "primary"
},
{
"user_id": "user789",
"email": "subgroup-user@example.com",
"access_type": "subgroup",
"subgroups": ["subgrp-001"]
}
]
}
Errors:
Condition |
Status |
|---|---|
Missing group ID or subgroup ID |
400 |
Caller has no access to the subgroup |
400 |
Notes:
Every returned user’s
subgroupsfield is scoped to only the requested subgroup, regardless of their actual subgroup memberships. This prevents the endpoint from leaking which other subgroups a user belongs to.access_typevalues:"primary","secondary","subgroup"Listing scope is determined by caller’s access level: primary callers see all members; secondary and subgroup-only callers see only primary owners
Get NOC for user
External Flow
User wants to get a NOC (Node Operational Certificate) for a fabric
Client generates a 32-bit nonce
Each app installation creates and securely retains its own ECDSA P-256 operational key, then creates a CSR for that key
Client calls the get NOC for user API
User receives the NOC certificate for the fabric
Internal Flow
API: POST /v1/groups/{groupId}/matter-nocs
Request:
{
"csr": "<valid csr>"
}
Process:
Verify user has access to the group specified in the request — any access works
TableName:
rmng-user-group-assocQuery by
user_id(PK) andgroup_id(SK) for the group in the requestRead
access_typeto select the CAT ID
Verify if the group is a Matter fabric
Validate the CSR
Load the group’s Matter fabric data and generate the NOC
TableName:
groupsQuery by
group_id(PK)Get
root_ca,root_ca_key,cat_id_admin,cat_id_operateVerify
matteris"true"Derive the controller Node ID from the fabric ID, authenticated rmng user ID, and CSR operational public key.
SHA-256 hashes length-prefixed domain
rmng-matter-user-node-id-v1, Fabric ID, rmng user ID, and canonical PKIX/SPKI public-key DER.Map the digest into
1..0xFFFFFFEFFFFFFFFFand encode it as 16 uppercase hex characters.
Generate the user NOC certificate from the CSR and
root_ca,root_ca_keyCertificate details:
Type: End-entity certificate (IsCA: false)
Validity: 10 years (not exceeding Root CA validity)
Key Usage: Digital Signature
Extended Key Usage: Server Auth and Client Auth
Subject ExtraNames: Matter Fabric ID (OID 1.3.6.1.4.1.37244.1.5), derived controller Node ID (OID 1.3.6.1.4.1.37244.1.1), Matter Group CAT ID (OID 1.3.6.1.4.1.37244.1.6) - all UTF-8 encoded. CAT ID uses
cat_id_adminfor primary access orcat_id_operatefor secondary/sub-entity accessSerial Number: Cryptographically secure random number 0-255
Associate the CAT ID with the user and group
TableName:
rmng-user-group-assocAttributes:
user_id(PK),group_id(SK),cat_id:cat_id_admin/cat_id_operate
The NOC itself is not stored.
Return the NOC
Response:
{
"noc": "user_noc_certificate",
"matter_node_id": "0123456789ABCDEF"
}
Sequence diagram
sequenceDiagram
title Get NOC for User - Internal Flow
participant Client
participant NOCAPI as "Get NOC API"
participant UserGroupDB as "DynamoDB rmng-user-group-assoc"
participant GroupDB as "DynamoDB groups"
Client->>NOCAPI: POST /v1/groups/{groupId}/matter-nocs<br/>{csr}
activate NOCAPI
NOCAPI->>NOCAPI: Validate CSR
NOCAPI->>UserGroupDB: Query rmng-user-group-assoc<br/>(user_id, group_id)
UserGroupDB->>NOCAPI: Return mapping<br/>{access_type}
NOCAPI->>NOCAPI: Verify user has access<br/>(any access_type works)
NOCAPI->>GroupDB: Query groups<br/>(group_id, sub_group_id="NONE")
GroupDB->>NOCAPI: Return group<br/>{matter, root_ca, root_ca_key,<br/>cat_id_admin, cat_id_operate}
NOCAPI->>NOCAPI: Verify matter = "true"
NOCAPI->>NOCAPI: Determine CAT ID<br/>(cat_id_admin for primary,<br/>cat_id_operate for secondary/sub-entity)
NOCAPI->>NOCAPI: Generate user NOC certificate<br/>(from CSR, root_ca, root_ca_key)<br/>Type: End-entity (IsCA: false)<br/>Validity: 10 years<br/>Key Usage: Digital Signature<br/>Extended Key Usage: Server Auth, Client Auth<br/>Subject ExtraNames: Fabric ID, User ID, CAT ID<br/>Serial Number: Random (0-255)
NOCAPI->>UserGroupDB: Update rmng-user-group-assoc<br/>(cache cat_id)
UserGroupDB->>NOCAPI: Success
NOCAPI->>Client: Response:<br/>{noc, matter_node_id}
deactivate NOCAPI
FAQs
Can a node belong to multiple groups?
No. A node can only belong to one main group at a time. When a node is associated with a new group, it is automatically removed from its previous group.
How many subgroups can a node belong to?
A node can belong to a maximum of 3 subgroups within its main group. This is enforced by the
subgrp1,subgrp2, andsubgrp3fields in thermng-group-node-assoctable.
What happens to nodes when a group is deleted?
A group can only be deleted once it is empty — it must have no subgroups and no user nodes. Remove all nodes and delete all subgroups first; otherwise the delete is rejected with 409. Nodes are never deleted by the group-delete operation.
How are group id and sub group ids guaranteed to be unique?
The write uses a DynamoDB conditional check; if the generated group ID already exists, the operation is retried with a fresh ID.
Can I change my access type for a group?
No. Access types are set when sharing is approved. To change/override access type, you would need to create a new sharing request and reshare with the higher access type.
When a subgroup is shared, the shared user also has the
group_id— which group-level APIs work for them?Four things:
GET /v1/groups(the group appears withaccess_type: "subgroup", listing only their subgroups and its nodes),PATCH .../subgroups/{subGroupId}to rename the shared subgroup, the twousersGETs (which fall back to the primary-users-only view), andDELETE .../subgroups/{subGroupId}/users/meto leave. Everything else on the parent group is refused.Node-scoped routes under the shared subgroup work, except
.../nodes/{nodeId}/tags.
What levels of groups are supported?
Just 2 levels: main group and subgroups.
Can a subgroup be shared?
Yes. A subgroup is shared with
subentityaccess: the target user can access only the nodes in that subgroup, and can never re-share it further. Sharing is approval-based (a sharing request is created, then the target user approves it).
Can a subgroup be shared on a Matter fabric?
Yes. Subgroup share/unshare is supported on matter-enabled groups. The subgroup-shared (
subentity) user gets subgroup-only access but cannot obtain Matter NOCs, since NOC generation requires group-level access.
Can a group that already has shared subgroups be converted into a fabric?
Yes, conversion is allowed. Existing subgroup-shared (
subentity) users keep subgroup-only access and cannot obtain Matter NOCs. Primary and secondary members require no per-user provisioning and can request NOCs immediately.
Who can convert a group into a fabric / enable capabilities?
Only the group owner (
primaryaccess). Enabling capabilities requires thegroup:updatecapabilitiespermission, which onlyprimaryaccess carries.
What happens to existing members when a group is converted to a fabric?
No Matter-specific membership backfill is required. Controller Node IDs are derived during NOC issuance.
How many groups can a user have?
There is no limit on how many a user can create, but only about 4 can be used at once, because every accessible group is encoded into the 2048-character STS session policy that
POST /v1/assumed-rolesissues. Shared subgroups are cheaper — roughly 8 fit. See Capacity Limits.
Does a group shared with me count against my own limit?
Yes. The allowance is per user and pooled: a group shared to you costs exactly as much as one you own. This means other users’ sharing decisions consume your allowance, and nothing warns either party when that happens.
A user says all their devices stopped responding, but the app still lists every group. Why?
This is the signature of an exceeded session-policy allowance. REST endpoints keep working, so the group list renders normally, but
POST /v1/assumed-rolesfails and the client holds no MQTT credentials, so no device in any group responds. Check the group count and theassumed-rolesresponse; the user must leave a group to recover.