Skip to content

Database

Overview

PostgreSQL 16 with SQLAlchemy 2.x ORM and Alembic for migrations.

All primary keys are UUID v4. All tables (except pivot tables) have created_at, updated_at, and deleted_at columns.

Entity Relationship Diagram

users                    user_roles              roles
─────────────────        ──────────────          ─────────────────
id (UUID, PK)    ◄──────  user_id (FK)           id (UUID, PK)
email                     role_id (FK)  ────────► name
password_hash                                     description
first_name                                        created_at
last_name                                         updated_at
is_active                role_permissions         deleted_at
last_login               ────────────────
token_version            role_id (FK)   ────────► roles.id
created_at               permission_id (FK) ────► permissions.id
updated_at
deleted_at               permissions
                         ─────────────────
refresh_tokens           id (UUID, PK)
──────────────           name
id (UUID, PK)            description
user_id (FK) ──────────► users.id
token                    created_at
expires_at               updated_at
revoked                  deleted_at
created_at

Tables

users

Column Type Description
id UUID Primary key
email varchar(255) Unique, indexed
password_hash varchar(255) bcrypt hash
first_name varchar(100) Optional
last_name varchar(100) Optional
is_active bool Soft disable without deleting
last_login timestamptz Updated on every successful login
token_version int Incremented on logout-all to invalidate all tokens
created_at timestamptz Auto-set on insert
updated_at timestamptz Auto-set on update
deleted_at timestamptz Soft delete — null means active

roles

Column Type Description
id UUID Primary key
name varchar(50) Unique, e.g. admin
description varchar(255) Optional
created_at timestamptz
updated_at timestamptz
deleted_at timestamptz Soft delete

permissions

Column Type Description
id UUID Primary key
name varchar(100) Unique, e.g. users:read
description varchar(255) Optional
created_at timestamptz
updated_at timestamptz
deleted_at timestamptz Soft delete

refresh_tokens

Column Type Description
id UUID Primary key
user_id UUID FK → users.id, CASCADE DELETE
token varchar(512) JWT string, unique, indexed
expires_at timestamptz Token expiry
revoked bool Manually revoked flag
created_at timestamptz

user_roles (pivot)

Column Type Description
user_id UUID FK → users.id
role_id UUID FK → roles.id
created_at timestamptz

role_permissions (pivot)

Column Type Description
role_id UUID FK → roles.id
permission_id UUID FK → permissions.id
created_at timestamptz

Soft Delete

Records are never physically deleted. Instead, deleted_at is set to the current timestamp. All repository queries filter WHERE deleted_at IS NULL by default.

To permanently remove data, hard_delete() is available in BaseRepository but should be used sparingly.

Mixins

Shared columns are defined as mixins in app/models/base.py and composed into models:

class User(UUIDMixin, TimestampMixin, SoftDeleteMixin, Base):
    ...

Mixin Columns added
UUIDMixin id
TimestampMixin created_at, updated_at
SoftDeleteMixin deleted_at