Database Architecture

AllSafe Fast is designed to integrate into your existing data architecture rather than imposing a separate database or schema. The authentication framework uses four dedicated tables with a clear separation of concerns, allowing you to run auth tables alongside your application's own tables in the same PostgreSQL database.

Design Philosophy

Many authentication frameworks force you to adopt their database schema, often requiring a separate database or a monolithic user table that tries to do everything. AllSafe Fast takes a different approach:

  • Integration, not isolation — Auth tables live in your existing PostgreSQL database alongside your application tables.
  • Separation of concerns — Authentication data (users, accounts, sessions, verifications) is kept in dedicated tables, separate from your application's domain data.
  • Protocol-based storage — The storage layer is defined by protocols, so you can implement your own adapters or use your existing models.
  • Async-first — All database operations use async SQLAlchemy with asyncpg, matching modern FastAPI patterns.
ℹ Application Models + Auth Models

Your application models (orders, products, articles, etc.) coexist with AllSafe Fast's auth models in the same database. The auth tables are prefixed with allsafe_ to avoid naming conflicts. You reference users by their UUID, which serves as a foreign key from your application tables to allsafe_users.id.

Data Model Diagram

allsafe_users
id (UUID PK), email, email_verified, name, image, is_active, roles, permissions, created_at, updated_at
↓ 1:N (FK CASCADE)
allsafe_accounts
id (UUID PK), user_id (FK), provider, provider_account_id, password_hash, access_token, refresh_token, scope
allsafe_users
↓ 1:N (FK CASCADE)
allsafe_sessions
id (UUID PK), user_id (FK), refresh_token_hash, expires_at, created_at
allsafe_users
↓ 1:N (FK CASCADE)
allsafe_verifications
id (UUID PK), user_id (FK), token_hash, type, expires_at, created_at

The Four Tables

allsafe_users

The central user table. Stores the user's identity, profile information, and authorization data (roles and permissions).

id
UUID primary key. Used as the user identifier in JWT claims and foreign keys.
email
Unique, indexed. The user's email address. Used for sign-in and verification.
email_verified
Boolean. Whether the email has been verified via a verification token.
name
Display name. Optional, can be null.
image
Profile image URL. Optional, can be null.
is_active
Boolean. If false, the user cannot sign in even with valid credentials.
roles
ARRAY(String). The user's roles (e.g., ["admin", "user"]). Embedded in JWT claims.
permissions
ARRAY(String). The user's permissions (e.g., ["users:read"]). Embedded in JWT claims.
created_at
Timestamp. When the user was created.
updated_at
Timestamp. When the user was last updated.

allsafe_accounts

Links users to authentication providers. A user can have multiple accounts (e.g., a password account and a Google OAuth account), enabling account linking.

id
UUID primary key.
user_id
UUID foreign key to allsafe_users.id. CASCADE on delete.
provider
String. The authentication provider (e.g., password, google, github).
provider_account_id
String. The provider-specific account ID. Combined with provider, this is unique.
password_hash
String. Argon2id hash of the password. Null for OAuth/OIDC accounts.
access_token
String. OAuth access token (encrypted). Null for password accounts.
refresh_token
String. OAuth refresh token (encrypted). Null for password accounts.
scope
String. OAuth scopes granted. Null for password accounts.

allsafe_sessions

Tracks active refresh token sessions. Enables session revocation, session listing, and theft detection.

id
UUID primary key.
user_id
UUID foreign key to allsafe_users.id. CASCADE on delete.
refresh_token_hash
String. SHA-256 hash of the refresh token. Unique and indexed.
expires_at
Timestamp. When the session expires. Indexed for efficient cleanup queries.
created_at
Timestamp. When the session was created.

allsafe_verifications

Stores verification tokens for email verification, password reset, and magic link flows.

id
UUID primary key.
user_id
UUID foreign key to allsafe_users.id. CASCADE on delete.
token_hash
String. SHA-256 hash of the verification token. Indexed.
type
String. The verification type (e.g., email_verify, password_reset, magic_link). Indexed.
expires_at
Timestamp. When the token expires. Indexed for efficient cleanup queries.
created_at
Timestamp. When the token was created.

Relationships and Foreign Keys

All three child tables (allsafe_accounts, allsafe_sessions, allsafe_verifications) have a foreign key to allsafe_users.id with ON DELETE CASCADE. This means:

  • When a user is deleted, all their accounts, sessions, and verification tokens are automatically deleted.
  • You never need to manually clean up child records when removing a user.
  • The cascade is enforced at the database level, ensuring consistency even if the application crashes mid-operation.
# SQLAlchemy relationship definition (simplified)
class AllSafeUser(Base):
    __tablename__ = "allsafe_users"
    id = Column(UUID, primary_key=True, default=uuid4)
    email = Column(String, unique=True, index=True, nullable=False)
    roles = Column(ARRAY(String), default=list)
    permissions = Column(ARRAY(String), default=list)
    # ... other fields

    accounts = relationship("AllSafeAccount", back_populates="user", cascade="all, delete-orphan")
    sessions = relationship("AllSafeSession", back_populates="user", cascade="all, delete-orphan")
    verifications = relationship("AllSafeVerification", back_populates="user", cascade="all, delete-orphan")

Indexing Strategy

AllSafe Fast creates indexes on columns that are frequently queried to ensure fast lookups:

TableIndexed ColumnIndex TypeQuery Pattern
allsafe_usersemailUnique indexSign-in by email, user lookup
allsafe_accountsprovider + provider_account_idUnique composite indexOAuth account lookup
allsafe_sessionsrefresh_token_hashUnique indexToken validation, session lookup
allsafe_sessionsexpires_atRegular indexSession cleanup, expiry queries
allsafe_verificationstoken_hashRegular indexToken validation
allsafe_verificationstypeRegular indexFilter by verification type
allsafe_verificationsexpires_atRegular indexToken cleanup, expiry queries
💡 Why Hash Tokens?

Refresh tokens and verification tokens are stored as SHA-256 hashes, not plaintext. This means if the database is compromised, the attacker cannot use the stored tokens directly. The hash is computed when the token is issued, and the same hash is computed when the token is presented for validation.

Connection Management

AllSafe Fast uses async SQLAlchemy with asyncpg for PostgreSQL connections. The connection management is handled through three functions:

from allsafe_fast import create_engine, create_session_factory, get_session

# 1. Create the async engine
# Database URL format: postgresql+asyncpg://user:pass@host:port/dbname
engine = create_engine("postgresql+asyncpg://user:pass@localhost:5432/myapp")

# 2. Create a session factory
create_session_factory(engine)  # Returns an async session factory

# 3. Use get_session as a FastAPI dependency
@app.get("/api/data")
async def get_data(session = Depends(get_session(factory))):
    # session is an async SQLAlchemy session
    result = await session.execute(select(MyModel))
    return result.scalars().all()

Migrations

AllSafe Fast provides CLI commands for database migrations. The migration workflow uses Alembic under the hood:

terminal
# Generate a new migration after model changes allsafe db generate # Apply pending migrations to the database allsafe db migrate # Check database health and migration status allsafe doctor
allsafe db generate
Compares your current models to the database schema and generates a new migration script with the necessary changes.
allsafe db migrate
Applies all pending migrations to the database, bringing the schema up to date.
allsafe doctor
Checks the health of your AllSafe Fast installation, including database connectivity, migration status, and configuration validation.
⚠ Migration Safety

Always review generated migration scripts before applying them, especially in production. Some schema changes (like dropping columns) can cause data loss. Test migrations on a staging database first.