messages.ciphertext is the only content column on the messages table. System messages (device add/revoke, membership changes) are produced by JSON.stringify({userId, change}) and written directly into that same field (emitDeviceChangeEvent in routes/devices.ts). This conflates genuine E2EE ciphertext (opaque, per-device-encrypted) with structured unencrypted metadata in one column.
Acceptance criteria:
- A new dedicated jsonb column (
systemPayload) is added to messages for content_type = 'system' rows, with a migration
emitDeviceChangeEvent and any other system-message producer write to the new column, leaving ciphertext null for system messages
- Read paths read
systemPayload for system messages instead of JSON-parsing ciphertext
- A check constraint ensures
content_type = 'system' rows have ciphertext IS NULL and non-null systemPayload
messages.ciphertextis the only content column on themessagestable. System messages (device add/revoke, membership changes) are produced byJSON.stringify({userId, change})and written directly into that same field (emitDeviceChangeEventinroutes/devices.ts). This conflates genuine E2EE ciphertext (opaque, per-device-encrypted) with structured unencrypted metadata in one column.Acceptance criteria:
systemPayload) is added tomessagesforcontent_type = 'system'rows, with a migrationemitDeviceChangeEventand any other system-message producer write to the new column, leavingciphertextnull for system messagessystemPayloadfor system messages instead of JSON-parsingciphertextcontent_type = 'system'rows haveciphertext IS NULLand non-nullsystemPayload