// Phase 6.5 — notifications: device registry, per-user settings, per-chat mutes. /** @param {import('node-pg-migrate').MigrationBuilder} pgm */ exports.up = (pgm) => { // Push credentials, one row per device/subscription. `token` is the stable, // globally-unique identity: the FCM registration token for native, or the // Web Push endpoint URL for web (p256dh/auth hold the web subscription keys). pgm.createTable('device_tokens', { id: { type: 'uuid', notNull: true, default: pgm.func('gen_random_uuid()'), primaryKey: true }, user_id: { type: 'uuid', notNull: true, references: 'users', onDelete: 'CASCADE' }, platform: { type: 'text', notNull: true }, token: { type: 'text', notNull: true }, web_p256dh: { type: 'text' }, web_auth: { type: 'text' }, failure_count: { type: 'integer', notNull: true, default: 0 }, last_success_at: { type: 'timestamptz' }, disabled_at: { type: 'timestamptz' }, created_at: { type: 'timestamptz', notNull: true, default: pgm.func('now()') }, updated_at: { type: 'timestamptz', notNull: true, default: pgm.func('now()') }, }); pgm.addConstraint('device_tokens', 'device_tokens_platform_check', { check: "platform IN ('web', 'android', 'ios')", }); // A token identifies exactly one device; re-registering rebinds it to its user. pgm.addConstraint('device_tokens', 'device_tokens_token_unique', { unique: ['token'] }); pgm.createIndex('device_tokens', 'user_id', { name: 'device_tokens_active_by_user', where: 'disabled_at IS NULL', }); // Per-user notification preferences. Quiet hours are minutes-since-midnight in // the user's IANA timezone; NULL start/end means "no quiet hours". pgm.createTable('notification_settings', { user_id: { type: 'uuid', notNull: true, references: 'users', onDelete: 'CASCADE', primaryKey: true, }, enabled: { type: 'boolean', notNull: true, default: true }, quiet_hours_start: { type: 'smallint' }, quiet_hours_end: { type: 'smallint' }, timezone: { type: 'text', notNull: true, default: 'UTC' }, updated_at: { type: 'timestamptz', notNull: true, default: pgm.func('now()') }, }); pgm.addConstraint('notification_settings', 'notification_settings_quiet_range_check', { check: ` (quiet_hours_start IS NULL OR (quiet_hours_start >= 0 AND quiet_hours_start <= 1439)) AND (quiet_hours_end IS NULL OR (quiet_hours_end >= 0 AND quiet_hours_end <= 1439)) `, }); // Per-conversation mute. muted_until NULL means muted indefinitely. pgm.createTable('conversation_mutes', { conversation_id: { type: 'uuid', notNull: true, references: 'conversations', onDelete: 'CASCADE', }, user_id: { type: 'uuid', notNull: true, references: 'users', onDelete: 'CASCADE' }, muted_until: { type: 'timestamptz' }, created_at: { type: 'timestamptz', notNull: true, default: pgm.func('now()') }, }); pgm.addConstraint('conversation_mutes', 'conversation_mutes_pkey', { primaryKey: ['conversation_id', 'user_id'], }); }; /** @param {import('node-pg-migrate').MigrationBuilder} pgm */ exports.down = (pgm) => { pgm.dropTable('conversation_mutes'); pgm.dropTable('notification_settings'); pgm.dropTable('device_tokens'); };