Channel permissions and account-bound invitations
Document: SPEC-022. Date: 2026-11-07. Lifecycle: approved implementation profile under D-122–D-125 and D-226. Delivery: base channel profile implemented and deployed in alpha.22; D-226 server invitations are added in alpha80; see the release record for validation and rollout evidence.
Scope and precedence
This is the successor design for SPEC-012's requireParentAccess prerequisite and SPEC-015's unrestricted ordinary-visitor admission. D-125 selects schema 9 migration preserving existing access. This profile supersedes the permission prerequisite and ordinary visitor scope portions of SPEC-012/015; their unrelated contracts remain authoritative. Channel hierarchy depth, immutable encryption, history intervals, moderation roles and explicit voice movement retain their existing owners.
Every ordinary channel uses the same controls, regardless of nesting depth. A channel inherits applicable permission entries when inheritance is enabled and adds its local permits and denials. An applicable denial wins over a permit, including a local permit against an inherited denial. Disabling inheritance removes inherited entries, not server bans or account/device revocation. Absence of a permit is not a denial. A local permit can authorize a child without permitting entry to an ancestor. Project the child under its nearest visible ancestor without revealing hidden ancestor identities.
A private channel may inherit a broad permit when its administrator leaves inheritance enabled. The client explicitly explains this during creation; the private label is not a second permission veto. An inherited permit satisfies its admission requirement. Public/private labels still govern share-link presentation, media routing and creation-time encryption defaults.
A server permit admits an account to the server and may propagate to channels. A channel permit may apply only here or propagate to inheriting subchannels. Non-propagating permits do not cross an inheritance edge. Independently acquired permits remain effective; adding a narrower permit does not revoke a broader one. Denials remain applicable across inheriting edges until inheritance is broken. Participation grants do not grant administrative roles. The existing owner protection remains.
Server access and channel access are distinct. A public-channel label never bypasses a non-propagating server permit or a denial. Direct conversations retain their separate SPEC-018 consent and visibility rules. Derived thread conversations retain the authorization and history rules of their containing ordinary channel.
Implementation contract
Advertise channel-permissions/1 only with the complete runtime. Persist channel entries separately from room/history membership; joining or leaving a channel never creates a propagating permit. The resolver is authoritative for listings, scoped state, history and linked-device copies, text and media access. ACL edits atomically close affected admitted intervals, revoke preparations/voice, invalidate affected outstanding invitations and advance enforcement through existing bounded lease/rekey mechanisms. Restoring a permit requires explicit admission and does not reveal an absence interval.
room.permission uses existing device-bound tickets, expected room revisions and idempotent operation results. Its payload is {target, effect, propagate?}: target is an existing host account UUID (including a record for a foreign account), or the owner-only server-members principal; effect is permit, deny or remove; propagate is a boolean allowed only with permit. Removing an entry leaves inherited permissions in effect. Existing owner/moderator target-role protections apply. The compatibility room.access command maps true to a local permit and false to a local denial. No invitation token bypasses an applicable denial.
room.settings gains inheritPermissions: boolean. There is no separate child-channel variant. server.permission uses {target, propagate, revision}, is owner-only, and checks both the command's room revision and current server-policy revision. Changing propagation preserves explicit channel grants. Unknown fields, null booleans, invalid targets and unauthorized role changes fail without mutation.
Named-account channel invitations
channel.invite uses {address} and the ordinary scoped command machinery. Parse a bare name relative to the host or a qualified account address using SPEC-010. A local address must already exist; local account creation remains a separate workflow. Existing eligible server accounts receive a non-propagating permit immediately, preserving any existing broader permit. A new foreign account gets a pending named authorization, without creating a password or assuming that the administrator has authenticated the recipient. New server admission requires the owner; admitted moderators may grant access only to existing ordinary members they can manage. Blocks, disabled accounts and applicable denials prevent issuance/redemption.
The copied HTTPS channel locator uses the existing #channel=<id> grammar. It is not a bearer credential and cannot grant access to another account. A pending invitation matches the qualified username returned by a freshly verified home authority during explicit ordinary visitor join. The host checks issuer authority, expiration, device proof, current bans and ACLs again inside the admission transaction. Successful admission creates a non-propagating server permit and non-propagating target-channel permit, then consumes the pending authorization. Existing independently acquired server/channel permissions are not narrowed. Direct-conversation authentication alone does not redeem an ordinary channel invitation.
General visitor admission may remain closed. Pending invitations have the existing 24-hour lifetime and share the existing bounded invitation capacity. channel.invite.cancel accepts {id} for a pending invitation in the current channel and permits cancellation by the owner or its issuer. Expired, cancelled or no-longer-authorized invitations cannot admit an account. No out-of-band message is sent automatically. Opening text and starting voice remain separate user actions.
Server invitations (D-226)
Advertise server-invitations/1 for owner-only server-context invitations, independent of any channel or landing channel. POST /api/server/tickets and /commands use the existing device-bound idempotent command contract. GET /api/server/accounts lists owner-visible accounts and pending invitations. Server state exposes the current server-policy revision; server invitation commands require that revision and advance it transactionally. No schema migration is needed: pending named server invitations use the existing channel-invitation records with room: "@server". Older clients retain channel-only behavior.
server.invite accepts exactly {address} for an existing local or foreign account. An existing ordinary server member immediately receives propagateToChannels: true; a new foreign or conversation-only account gets a pending named server authorization. On explicit verified ordinary visitor admission, consume it and grant propagating server membership. A copied server URL is a locator, never bearer authority. No arbitrary channel grant is added. Explicit denials, inheritance breaks, disabled accounts, blocked domains, current issuer authority and device verification remain authoritative. Existing narrower channel grants and history intervals are preserved. Server invitations do not join channels or start voice. server.invite.cancel accepts exactly {id} and cancels only a pending server authorization. Expiration and capacity share the existing invitation limits.
account.invite and account.invite.cancel are additionally available to the owner in server context. Account creation still uses the named, expiring bearer invitation and existing registration contract; successful creation grants ordinary propagating server membership. Existing channel-context account invitations retain their compatibility behavior. Creation/retry cannot revive a consumed or cancelled invitation.
Configuration
channel-permissions.inherit: persisted channel administrative boolean. Authorized writers follow existing room settings roles. A change takes effect transactionally. Ordinary creation default: true for both public and private channels at every depth, CONFIRMED D-125.room.createacceptsinheritPermissions; omission selects true. Null is invalid; omission during an update preserves the value.channel-permissions.permits: persisted map of account UUID to propagation boolean. A false value grants access only to this channel.room.permissionwith omitted propagation selects false, consistent with channel-only grants. The internalserver-membersprincipal represents ordinary server permits and matches only accounts whose server permit propagates; it must not include restricted invitees. The only non-account principal isserver-members; only the owner can edit/remove that entry. Reject arbitrary principals.channel-permissions.denials: persisted unique account UUID list, optionally including the internal server-members principal. Denial precedence is fixed, not configurable.User.propagateToChannels: persisted server permit propagation boolean. Existing ordinary membership semantics require true; a new channel-only invitation explicitly selects false. Only the owner changes it through server permission controls. It is host-owned and never imported from the home identity's roles or attestations.- Channel invitation expiration/capacity: fixed existing SPEC-009
LIMITS.invitationMsandLIMITS.invitations; no new setting. The 24-hour pending lifetime does not expire a successfully granted membership. - Existing-channel migration: preserve current access, CONFIRMED D-125. Transactionally move schema 8 to 9, preserving account/device IDs, keys, login deadlines, messages and membership intervals. Evaluate old channel and ancestor rules before changing entries. Existing channels become independent. Unrestricted public branches receive the explicit
server-memberspermit with account denials for prior exclusions; private or ancestor-restricted branches receive non-propagating permits for previously eligible accounts. Preserve local explicit denials. Existing ordinary server permits propagate; conversation-only visitors remain excluded. Remove legacyrequireParentAccessafter evaluating it. A repeated migration does nothing; older backends reject schema 9. New channels still default to inheritance, independently of migration presets.
Startup preservation for the historical direct-creation marker defect: before applying legacy migrations to a known marker 5–8, restore only a later schema floor proved by persisted state. Existing channel-permission records or a stored boolean account propagation field prove schema 9; a stored boolean server-policy PTT requirement proves schema 8. False booleans are evidence as well as true. Advance the durable marker transactionally and the effective migration version together, then run only remaining forward migrations. This cannot reconstruct overwritten ACLs/hierarchy/policy or infer past removal intent; absent proof retains the genuine predecessor migration path, and unknown future schemas still fail closed. No new schema number or configuration setting is introduced.
Testing
D-226 tests cover server-context new local and existing/foreign invitations, empty hosts, nested inheritance/private channels, denial and inheritance breaks, owner-only authorization, blocked/disabled targets, strict payloads, stale revisions, exact replay, cancellation/expiration, forwarded locators, explicit admission without voice/history mutation and restart. Native widget/service fixtures cover the server context-menu entry, separate new/existing fields, original account/server ownership, capability fallback, copied links and both invitation actions.
tests/audit-schema-startup.test.ts covers already-lowered markers with intact current ACLs/hierarchy/policy/history, a false propagation sentinel, an empty-channel authority with only schema-eight policy evidence, genuine schema 5–8 predecessors, repeated restart and failed marker persistence. The fixture for a genuine predecessor removes all later-schema evidence rather than lowering only a current marker.
Pure resolver units cover absent permissions, propagating/non-propagating permits, denial precedence, inheritance breaks, server-member matching and malformed stored entries. Server tests must cover consistent decisions at depths one through eight, hidden ancestors, bans independent of ACL inheritance, role limits, atomic revisions, active voice revocation, history gaps, invitation expiration/cancellation/issuer changes, forwarded locators, closed-host admission and durable restart. Migration tests must compare pre/post effective access for private/public nested combinations and existing denies before testing new admissions. Isolated two-authority integration must verify home-account binding, E2EE text, explicit voice and no unrelated-channel disclosure. UI tests must exercise both explicit lists, inheritance, both propagation controls and Add server invitation redemption. Passing resolver units alone is not release acceptance.
Acceptance criteria
- D-226: an owner can invite/create a new local or existing local/foreign account directly from a Qt server context menu, including an empty server; successful membership grants all channels inheriting server permission while preserving denials and inheritance breaks.
- D-226: cancellation, expiry, wrong recipient, lost issuer authority, bans and disabled accounts cannot redeem a pending server invitation; commands use original server/device scope and exact retries. No invitation automatically joins text/voice or reveals absence history.
- A current authority with the historically lowered marker retains its surviving selected ACLs, hierarchy, policies, sessions and retained content across deployment/restart; only a proved schema floor may suppress an already-completed legacy migration. Genuine predecessors still upgrade and unknown future schemas remain rejected.
- An owner can invite an existing foreign account into a closed server and exactly one channel without creating a local account.
- The recipient cannot view or join unrelated public/private channels or inheriting descendants when the invitation grants do not propagate.
- A forwarded locator does not admit a different account; cancellation, expiration and issuer authority loss prevent pending redemption.
- Every ordinary channel exposes identical permission features at every supported depth.
- Denials defeat permits while inherited; breaking inheritance never bypasses a server ban.
- Changing access ends affected text/media authority and preserves history interval boundaries.
- Existing effective access survives the approved migration without exposing private channels.
- Component specifications, configuration, UI, protocol compatibility and integration evidence agree before release.