Files
mantra-kmp/composeApp
Kgothatso Ngako f6217ce6ea feat(subgroups): schema 17 -- four nullable columns, and not one of them a foreign key
Phase 2 of docs/subgroups.md. Somewhere to put a parent, now that Phase 1 can
prove one.

| table | column | filled from | trusted? |
|---|---|---|---|
| GroupKeyState | parentChatRoomId | the state's parent tag | yes -- the certificate was checked |
| GroupKeyState | birthCertificateJson | the state's certificate tag | yes -- same |
| ChatRoom | parentChatRoomId | the verified key state | yes |
| DkgSession | parentChatRoomId | a tag on a ceremony proposal | **no** -- a screen's title |

The trust column is the point of the table and is written into the KDoc of each
one. Three of these are written only after a signature has been checked; the
fourth is an unverified claim off a wire message, and a column that mixed the two
would be a column no reader could act on. Nothing may be granted on the strength
of `DkgSession.parentChatRoomId` that would not be granted without it.

**None of the four is a foreign key, and that is the change most likely to be
"fixed" by somebody later.** `GroupKeyState`, `DkgSession` and `GroupSignedEvent`
all declare `ForeignKey(onDelete = CASCADE)` onto ChatRoom, so pointing a parent
column at ChatRoom the same way is the obvious next move. It would mean deleting
a parent room deletes every subgroup row beneath it -- and then, by their own
cascades, each subgroup's messages, participants, key state, signing sessions and
signed events. A user tidying away a group they had left would silently destroy a
group they are still in.

RESTRICT is no better: it would make a parent undeletable while any child row
exists, which is a foreign key deciding a product question. And neither would
work anyway, because a parent pointer routinely names a room this device does not
have at all -- a member of a subgroup who was never in its parent holds the id
off a certificate and nothing else. A dangling reference is the normal, expected
state here, and readers resolve it with a lookup allowed to return null.

**The certificate is stored whole, as JSON, rather than as its signature.** A
signature plus a rule for rebuilding the event it covers is a rule that breaks
silently the first time the certificate's shape changes: a rebuild differing by
one byte hashes to an id whose signature fails, and is indistinguishable from a
forgery. A few hundred bytes inside an encryption removes the class. The parent
column beside it is an index into that event, never a second source of truth --
the two are written together or not at all.

Four DAO reads, each with the limits of what it answers written down.
`GroupKeyStateDao.getByParentChatRoomId`/`observe` list the children whose state
this device holds, which is *verified* but not complete -- a certified child whose
room was never created here leaves no state at all.
`ChatRoomDao.observeByParentChatRoomId` lists the ones there is something to open,
excluding soft-deleted rooms so a room the user cleared away does not reappear
because its parent lists it. `DkgSessionDao.getByParentChatRoomId` is how a member
gets back into a subgroup flow they closed the app during, since before the
certificate is signed the ceremony is the only thing on the device that knows the
flow was started.

`AutoMigration(16, 17)`: nullable additions are a shape Room migrates itself, and
17.json exports with no new foreign key on any of the three tables.

Nine tests in `SubgroupDaoJvmTest`, all on properties the compiler cannot see: a
state and a room may each name a parent this device holds no room for; deleting a
parent leaves its child, its child's key state and its child's lineage standing;
the parent lists its children newest-first and filters on *which* parent rather
than on having one; a soft-deleted subgroup drops out; and a ceremony round-trips
the parent it was opened for. 386 common tests and 673 jvm tests pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 23:17:18 +02:00
..
2026-09-08 09:11:13 +02:00