// Phase 3 — rooms, membership, and messages. // Messages use a crypto-agnostic content envelope so E2EE can be layered on // later with no schema change (content holds plaintext now, ciphertext later). /** @param {import('node-pg-migrate').MigrationBuilder} pgm */ exports.up = (pgm) => { pgm.createTable('rooms', { id: { type: 'uuid', primaryKey: true, default: pgm.func('gen_random_uuid()') }, name: { type: 'text', notNull: true }, created_by: { type: 'uuid', notNull: true, references: 'users', onDelete: 'RESTRICT' }, created_at: { type: 'timestamptz', notNull: true, default: pgm.func('now()') }, }); pgm.createTable('room_members', { room_id: { type: 'uuid', notNull: true, references: 'rooms', onDelete: 'CASCADE' }, user_id: { type: 'uuid', notNull: true, references: 'users', onDelete: 'CASCADE' }, role: { type: 'text', notNull: true, default: 'member' }, joined_at: { type: 'timestamptz', notNull: true, default: pgm.func('now()') }, }); pgm.addConstraint('room_members', 'room_members_pkey', { primaryKey: ['room_id', 'user_id'], }); pgm.createIndex('room_members', 'user_id'); pgm.createTable('messages', { id: { type: 'uuid', primaryKey: true, default: pgm.func('gen_random_uuid()') }, // Server-assigned monotonic ordering. seq: { type: 'bigserial', notNull: true }, room_id: { type: 'uuid', notNull: true, references: 'rooms', onDelete: 'CASCADE' }, sender_id: { type: 'uuid', notNull: true, references: 'users', onDelete: 'RESTRICT' }, // Client-generated idempotency / dedupe key. client_msg_id: { type: 'text', notNull: true }, // Opaque content envelope (see file header). content: { type: 'text', notNull: true }, content_type: { type: 'text', notNull: true, default: 'text' }, encryption: { type: 'text' }, created_at: { type: 'timestamptz', notNull: true, default: pgm.func('now()') }, }); pgm.createIndex('messages', 'seq', { unique: true }); pgm.createIndex('messages', ['room_id', 'seq']); // Idempotent sends: one message per (room, sender, client_msg_id). pgm.addConstraint('messages', 'messages_dedupe_uq', { unique: ['room_id', 'sender_id', 'client_msg_id'], }); }; /** @param {import('node-pg-migrate').MigrationBuilder} pgm */ exports.down = (pgm) => { pgm.dropTable('messages'); pgm.dropTable('room_members'); pgm.dropTable('rooms'); };