-- Mobile-first auth: phone is the login identifier, email becomes an optional -- contact field, plus a table of short-lived one-time SMS codes for verifying -- a phone (registration/login completion and number changes). -- Email becomes optional (login no longer uses it). Postgres treats NULLs as -- distinct, so the existing UNIQUE constraint keeps working for users without one. ALTER TABLE users ALTER COLUMN email DROP NOT NULL; ALTER TABLE users ADD COLUMN IF NOT EXISTS phone VARCHAR; ALTER TABLE users ADD COLUMN IF NOT EXISTS "phoneVerified" BOOLEAN NOT NULL DEFAULT FALSE; -- Unique per non-null phone (NULLs allowed for legacy email-only staff accounts). CREATE UNIQUE INDEX IF NOT EXISTS users_phone_unique ON users (phone) WHERE phone IS NOT NULL; -- One-time SMS verification codes (hashed). DO $$ BEGIN CREATE TYPE verification_codes_purpose_enum AS ENUM ('login', 'change_phone'); EXCEPTION WHEN duplicate_object THEN null; END $$; CREATE TABLE IF NOT EXISTS verification_codes ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), "userId" UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, purpose verification_codes_purpose_enum NOT NULL, destination VARCHAR NOT NULL, "codeHash" VARCHAR NOT NULL, "expiresAt" TIMESTAMPTZ NOT NULL, attempts INT NOT NULL DEFAULT 0, "consumedAt" TIMESTAMPTZ, "createdAt" TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE INDEX IF NOT EXISTS verification_codes_user_purpose_idx ON verification_codes ("userId", purpose);