// Phase 1 — auth schema: users + refresh_tokens (sessions). /** @param {import('node-pg-migrate').MigrationBuilder} pgm */ exports.up = (pgm) => { // Case-insensitive text for the unique username (and non-unique email). pgm.createExtension('citext', { ifNotExists: true }); pgm.createTable('users', { id: { type: 'uuid', primaryKey: true, default: pgm.func('gen_random_uuid()') }, username: { type: 'citext', notNull: true, unique: true }, display_name: { type: 'text', notNull: true }, // Email is optional and NOT unique (contact field only, not a login credential). email: { type: 'citext' }, phone: { type: 'text' }, avatar_ref: { type: 'text' }, password_hash: { type: 'text', notNull: true }, created_at: { type: 'timestamptz', notNull: true, default: pgm.func('now()') }, updated_at: { type: 'timestamptz', notNull: true, default: pgm.func('now()') }, last_seen_at: { type: 'timestamptz' }, }); pgm.createTable('refresh_tokens', { id: { type: 'uuid', primaryKey: true, default: pgm.func('gen_random_uuid()') }, user_id: { type: 'uuid', notNull: true, references: 'users', onDelete: 'CASCADE', }, // Rotation lineage: reuse of any token in a family revokes the whole family. family_id: { type: 'uuid', notNull: true }, token_hash: { type: 'text', notNull: true, unique: true }, user_agent: { type: 'text' }, ip: { type: 'text' }, created_at: { type: 'timestamptz', notNull: true, default: pgm.func('now()') }, last_used_at: { type: 'timestamptz', notNull: true, default: pgm.func('now()') }, expires_at: { type: 'timestamptz', notNull: true }, revoked_at: { type: 'timestamptz' }, }); pgm.createIndex('refresh_tokens', 'user_id'); pgm.createIndex('refresh_tokens', 'family_id'); }; /** @param {import('node-pg-migrate').MigrationBuilder} pgm */ exports.down = (pgm) => { pgm.dropTable('refresh_tokens'); pgm.dropTable('users'); };