"""initial schema
Revision ID: 0001_initial
Revises:
Create Date: 2026-05-17
"""
from collections.abc import Sequence
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
from alembic import op
revision: str = "0001_initial"
down_revision: str | None = None
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
def upgrade() -> None:
op.create_table(
"users",
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True),
sa.Column("auth_subject", sa.String(255), nullable=False),
sa.Column("email", sa.String(320), nullable=False),
sa.Column("display_name", sa.String(255), nullable=True),
sa.Column("locale", sa.String(32), nullable=True),
sa.Column("status", sa.String(32), nullable=False),
sa.Column("system_role", sa.String(32), nullable=False),
sa.Column("profile", postgresql.JSONB, nullable=False),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("last_seen_at", sa.DateTime(timezone=True), nullable=True),
)
op.create_index("ix_users_auth_subject", "users", ["auth_subject"], unique=True)
op.create_index("ix_users_email", "users", ["email"])
op.create_index("ix_users_status", "users", ["status"])
op.create_index("ix_users_system_role", "users", ["system_role"])
op.create_table(
"user_encryption_keys",
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True),
sa.Column("user_id", postgresql.UUID(as_uuid=True), nullable=False),
sa.Column("key_id", sa.String(64), nullable=False),
sa.Column("encrypted_key", sa.LargeBinary, nullable=False),
sa.Column("nonce", sa.LargeBinary, nullable=False),
sa.Column("algorithm", sa.String(64), nullable=False),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
sa.ForeignKeyConstraint(["user_id"], ["users.id"], ondelete="CASCADE"),
sa.UniqueConstraint("user_id"),
sa.UniqueConstraint("key_id"),
)
op.create_table(
"secrets",
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True),
sa.Column("user_id", postgresql.UUID(as_uuid=True), nullable=False),
sa.Column("title", sa.String(255), nullable=False),
sa.Column("purpose", sa.String(255), nullable=True),
sa.Column("category", sa.String(120), nullable=True),
sa.Column("source", sa.String(255), nullable=True),
sa.Column("notes", sa.String(140), nullable=True),
sa.Column("status", sa.String(24), nullable=False),
sa.Column("archived", sa.Boolean, nullable=False),
sa.Column("allow_ui", sa.Boolean, nullable=False),
sa.Column("allow_rest_api", sa.Boolean, nullable=False),
sa.Column("allow_mcp", sa.Boolean, nullable=False),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
sa.ForeignKeyConstraint(["user_id"], ["users.id"], ondelete="CASCADE"),
)
op.create_index("ix_secrets_user_id", "secrets", ["user_id"])
op.create_index("ix_secrets_status", "secrets", ["status"])
op.create_index("ix_secrets_archived", "secrets", ["archived"])
op.create_index("ix_secrets_user_title", "secrets", ["user_id", "title"])
op.create_index("ix_secrets_user_category", "secrets", ["user_id", "category"])
op.create_table(
"secret_versions",
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True),
sa.Column("secret_id", postgresql.UUID(as_uuid=True), nullable=False),
sa.Column("version_number", sa.Integer, nullable=False),
sa.Column("fields", postgresql.JSONB, nullable=False),
sa.Column("search_text", sa.Text, nullable=False),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
sa.ForeignKeyConstraint(["secret_id"], ["secrets.id"], ondelete="CASCADE"),
sa.UniqueConstraint("secret_id", "version_number"),
)
op.create_index("ix_secret_versions_secret_id", "secret_versions", ["secret_id"])
op.create_table(
"secret_tags",
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True),
sa.Column("secret_id", postgresql.UUID(as_uuid=True), nullable=False),
sa.Column("user_id", postgresql.UUID(as_uuid=True), nullable=False),
sa.Column("name", sa.String(80), nullable=False),
sa.ForeignKeyConstraint(["secret_id"], ["secrets.id"], ondelete="CASCADE"),
sa.ForeignKeyConstraint(["user_id"], ["users.id"], ondelete="CASCADE"),
sa.UniqueConstraint("secret_id", "name"),
)
op.create_index("ix_secret_tags_secret_id", "secret_tags", ["secret_id"])
op.create_index("ix_secret_tags_user_id", "secret_tags", ["user_id"])
op.create_index("ix_secret_tags_name", "secret_tags", ["name"])
op.create_table(
"api_tokens",
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True),
sa.Column("user_id", postgresql.UUID(as_uuid=True), nullable=False),
sa.Column("public_id", sa.String(32), nullable=False),
sa.Column("name", sa.String(120), nullable=False),
sa.Column("token_hash", sa.String(128), nullable=False),
sa.Column("scopes", postgresql.JSONB, nullable=False),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("revoked_at", sa.DateTime(timezone=True), nullable=True),
sa.Column("last_used_at", sa.DateTime(timezone=True), nullable=True),
sa.ForeignKeyConstraint(["user_id"], ["users.id"], ondelete="CASCADE"),
)
op.create_index("ix_api_tokens_user_id", "api_tokens", ["user_id"])
op.create_index("ix_api_tokens_public_id", "api_tokens", ["public_id"], unique=True)
op.create_index("ix_api_tokens_token_hash", "api_tokens", ["token_hash"], unique=True)
op.create_table(
"audit_events",
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True),
sa.Column("user_id", postgresql.UUID(as_uuid=True), nullable=True),
sa.Column("actor_user_id", postgresql.UUID(as_uuid=True), nullable=True),
sa.Column("api_token_id", postgresql.UUID(as_uuid=True), nullable=True),
sa.Column("secret_id", postgresql.UUID(as_uuid=True), nullable=True),
sa.Column("channel", sa.String(24), nullable=False),
sa.Column("action", sa.String(80), nullable=False),
sa.Column("ip_address", sa.String(80), nullable=True),
sa.Column("user_agent", sa.String(500), nullable=True),
sa.Column("metadata", postgresql.JSONB, nullable=False),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
sa.ForeignKeyConstraint(["user_id"], ["users.id"], ondelete="SET NULL"),
sa.ForeignKeyConstraint(["actor_user_id"], ["users.id"], ondelete="SET NULL"),
sa.ForeignKeyConstraint(["api_token_id"], ["api_tokens.id"], ondelete="SET NULL"),
)
op.create_index("ix_audit_events_user_id", "audit_events", ["user_id"])
op.create_index("ix_audit_events_secret_id", "audit_events", ["secret_id"])
op.create_index("ix_audit_events_channel", "audit_events", ["channel"])
op.create_index("ix_audit_events_action", "audit_events", ["action"])
op.create_index("ix_audit_events_created_at", "audit_events", ["created_at"])
op.create_table(
"sessions",
sa.Column("id", sa.String(128), primary_key=True),
sa.Column("user_id", postgresql.UUID(as_uuid=True), nullable=False),
sa.Column("data", postgresql.JSONB, nullable=False),
sa.Column("expires_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
sa.ForeignKeyConstraint(["user_id"], ["users.id"], ondelete="CASCADE"),
)
op.create_index("ix_sessions_user_id", "sessions", ["user_id"])
op.create_index("ix_sessions_expires_at", "sessions", ["expires_at"])
op.create_table(
"oauth_states",
sa.Column("state", sa.String(255), primary_key=True),
sa.Column("pkce_verifier", sa.String(255), nullable=False),
sa.Column("return_to", sa.Text, nullable=True),
sa.Column("scopes", postgresql.JSONB, nullable=False),
sa.Column("expires_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
)
op.create_index("ix_oauth_states_expires_at", "oauth_states", ["expires_at"])
op.create_table(
"rate_limits",
sa.Column("key", sa.String(255), primary_key=True),
sa.Column("count", sa.Integer, nullable=False),
sa.Column("window_start", sa.DateTime(timezone=True), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
)
def downgrade() -> None:
op.drop_table("rate_limits")
op.drop_table("oauth_states")
op.drop_table("sessions")
op.drop_table("audit_events")
op.drop_table("api_tokens")
op.drop_table("secret_tags")
op.drop_table("secret_versions")
op.drop_table("secrets")
op.drop_table("user_encryption_keys")
op.drop_table("users")