"""push notifications about new mail (Web Push, RFC 8030/8291/8292) ``push_subscriptions`` holds one row per browser (or installed app) that asked to be told about new mail: the push service URL it gave, and the two keys the message is encrypted to. ``session_sid`` is the sign-in it was made under — once that session is gone (signed out, revoked, expired) nothing is sent there any more, so a signed-out device never shows a subject line again. The server signs every push with its own key pair (VAPID). It is generated on first use and kept in ``app_settings``; the private half is vault-encrypted with MASTER_KEY like every other server secret. Revision ID: 0022 Revises: 0021 Create Date: 2026-10-08 """ from __future__ import annotations from collections.abc import Sequence from alembic import op revision: str = "0022" down_revision: str | None = "0021" branch_labels: str | Sequence[str] | None = None depends_on: str | Sequence[str] | None = None def upgrade() -> None: op.execute( """ CREATE TABLE push_subscriptions ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE, endpoint text NOT NULL UNIQUE, p256dh text NOT NULL, auth text NOT NULL, session_sid text NOT NULL, user_agent text NOT NULL DEFAULT '', created_at timestamptz NOT NULL DEFAULT now(), last_used_at timestamptz ) """ ) op.execute("CREATE INDEX ix_push_subscriptions_user ON push_subscriptions (user_id)") op.execute("ALTER TABLE app_settings ADD COLUMN vapid_public text NOT NULL DEFAULT ''") op.execute("ALTER TABLE app_settings ADD COLUMN vapid_private_ct bytea") def downgrade() -> None: op.execute("ALTER TABLE app_settings DROP COLUMN IF EXISTS vapid_private_ct") op.execute("ALTER TABLE app_settings DROP COLUMN IF EXISTS vapid_public") op.execute("DROP TABLE IF EXISTS push_subscriptions")