"""Push subscriptions table for Web Push notifications Revision ID: 012 Revises: 011 Create Date: 2026-03-26 Creates the push_subscriptions table so the gateway can store browser push subscriptions and deliver Web Push notifications when an AI employee responds and the user's WebSocket is not connected. No RLS policy is applied — the API filters by user_id at the application layer (push subscriptions are portal-user-scoped, not tenant-scoped). """ from __future__ import annotations from typing import Sequence, Union import sqlalchemy as sa from alembic import op from sqlalchemy.dialects import postgresql revision: str = "012" down_revision: Union[str, None] = "011" branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: op.create_table( "push_subscriptions", sa.Column( "id", postgresql.UUID(as_uuid=True), server_default=sa.text("gen_random_uuid()"), nullable=False, ), sa.Column( "user_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("portal_users.id", ondelete="CASCADE"), nullable=False, ), sa.Column( "tenant_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("tenants.id", ondelete="SET NULL"), nullable=True, comment="Optional tenant scope for notification routing", ), sa.Column( "endpoint", sa.Text, nullable=False, comment="Push service URL (browser-provided)", ), sa.Column( "p256dh", sa.Text, nullable=False, comment="ECDH public key for payload encryption", ), sa.Column( "auth", sa.Text, nullable=False, comment="Auth secret for payload encryption", ), sa.Column( "created_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False, ), sa.Column( "updated_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False, ), sa.PrimaryKeyConstraint("id"), sa.UniqueConstraint("user_id", "endpoint", name="uq_push_user_endpoint"), ) op.create_index("ix_push_subscriptions_user_id", "push_subscriptions", ["user_id"]) op.create_index("ix_push_subscriptions_tenant_id", "push_subscriptions", ["tenant_id"]) def downgrade() -> None: op.drop_index("ix_push_subscriptions_tenant_id", table_name="push_subscriptions") op.drop_index("ix_push_subscriptions_user_id", table_name="push_subscriptions") op.drop_table("push_subscriptions")