8.3 KiB
8.3 KiB
SQLite Repository Implementation — Design & Session State
For Claude: This document captures the full project state and in-progress design decisions for the SQLite repository implementation. Resume from "Next Steps" section.
Project State (as of 2026-02-12)
Git
- Branch:
main(10 commits, no feature branches) - HEAD:
9d7a67b fix: add collision retry for userid generation - Working tree: Clean (untracked:
.idea/,README.md,docs/) - All 17 tests passing, all quality checks green
What Exists
| File | Purpose |
|---|---|
src/fastapi_oidc_op/config.py |
Settings(BaseSettings) with env_prefix="OIDC_OP_", StorageBackend enum (sqlite/mongodb), sqlite_path default data/oidc_op.db |
src/fastapi_oidc_op/models.py |
User, WebAuthnCredential, PasswordCredential, MagicLink, CredentialType enum. Datetimes use datetime.UTC. MagicLink.expires_at is required (no default). |
src/fastapi_oidc_op/store/protocols.py |
@runtime_checkable Protocol interfaces: UserRepository, CredentialRepository, MagicLinkRepository |
src/fastapi_oidc_op/userid.py |
generate_userid() (pure, proquint 32-bit), generate_unique_userid(user_repo, max_retries=10) (async, collision retry) |
src/fastapi_oidc_op/app.py |
create_app(settings) factory. /health endpoint. Settings on app.state. Docs only in debug mode. |
tests/conftest.py |
settings fixture, async client fixture via httpx ASGITransport |
scripts/check.sh |
Quality gate: ruff format, ruff check, ty check, pytest |
pyproject.toml |
Python 3.13, hatchling build, src layout. Deps: fastapi, idpyoidc, pydantic-settings, fido2, argon2-cffi, motor, aiosqlite, proquint, httpx. Dev: ruff, ty, pytest, pytest-asyncio. |
Package Structure
src/fastapi_oidc_op/
├── __init__.py
├── app.py
├── config.py
├── models.py
├── userid.py
├── oidc/__init__.py
├── authn/__init__.py
├── manage/__init__.py
├── invite/__init__.py
└── store/
├── __init__.py
├── protocols.py
├── mongodb/__init__.py
└── sqlite/__init__.py
Existing Protocol Signatures (must implement exactly)
class UserRepository(Protocol):
async def create(self, user: User) -> User
async def get_by_userid(self, userid: str) -> User | None
async def get_by_username(self, username: str) -> User | None
async def update(self, user: User) -> User
async def list_users(self, offset: int = 0, limit: int = 100) -> list[User]
async def delete(self, userid: str) -> bool
class CredentialRepository(Protocol):
async def create_webauthn(self, credential: WebAuthnCredential) -> WebAuthnCredential
async def create_password(self, credential: PasswordCredential) -> PasswordCredential
async def get_webauthn_by_user(self, user_id: str) -> list[WebAuthnCredential]
async def get_webauthn_by_credential_id(self, credential_id: bytes) -> WebAuthnCredential | None
async def get_password_by_user(self, user_id: str) -> PasswordCredential | None
async def update_webauthn(self, credential: WebAuthnCredential) -> WebAuthnCredential
async def delete_webauthn(self, user_id: str, credential_id: bytes) -> bool
async def delete_password(self, user_id: str) -> bool
class MagicLinkRepository(Protocol):
async def create(self, link: MagicLink) -> MagicLink
async def get_by_token(self, token: str) -> MagicLink | None
async def mark_used(self, token: str) -> bool
async def delete_expired(self) -> int
Design Document
Full design doc at docs/plans/2026-02-11-oidc-op-design.md. Key points:
- SQLite uses
aiosqlitewith raw SQL - Storage backend selected at startup via config
- SQLite is the default test backend (no external deps)
Known Quirks
- ty 0.0.16:
python-versiongoes under[tool.ty.environment], nosrcfield at top level - ruff 0.15: version command is
ruff versionnotruff --version - User.userid (no underscore) vs credential.user_id (underscore) — intentional, matches design doc
Design Decisions (confirmed by user)
1. Schema Initialization: SQL Migration Files
- Directory:
src/fastapi_oidc_op/store/sqlite/migrations/ - Numbered SQL files:
001_initial.sql,002_..., etc. - Tracker table
_migrationsrecords applied files - Migration runner at startup: read files, skip applied, execute in order, record
2. Connection Strategy: Single Shared Connection
- One
aiosqliteconnection created at startup, shared by all repositories - WAL mode enabled for concurrent reads
- Foreign keys enabled (
PRAGMA foreign_keys = ON) - Connection closed on shutdown
3. Dependency Injection: Lifespan + app.state
- App lifespan (
@asynccontextmanager) creates DB connection and repositories - Stored on
app.state(e.g.app.state.user_repo) - FastAPI
Depends()retrieves fromrequest.app.state
4. Groups Storage: Separate Join Table
user_groups(userid TEXT, group_name TEXT)with composite PKON DELETE CASCADEfrom users table- Repository handles join/split when reading/writing User models
5. Bytes Columns: BLOB
credential_idandpublic_keystored as BLOB- aiosqlite handles bytes natively
6. Datetimes: ISO 8601 TEXT
- All datetime columns stored as ISO 8601 text strings
- Converted to/from
datetimeobjects in the repository layer
7. Booleans: INTEGER 0/1
- SQLite has no native bool; use INTEGER with 0/1
Schema (approved — Section 1)
-- _migrations tracker
CREATE TABLE IF NOT EXISTS _migrations (
id INTEGER PRIMARY KEY,
filename TEXT NOT NULL UNIQUE,
applied_at TEXT NOT NULL DEFAULT (datetime('now'))
);
-- 001_initial.sql
CREATE TABLE users (
userid TEXT PRIMARY KEY,
username TEXT NOT NULL UNIQUE,
preferred_username TEXT,
given_name TEXT,
family_name TEXT,
nickname TEXT,
email TEXT,
email_verified INTEGER NOT NULL DEFAULT 0,
phone_number TEXT,
phone_number_verified INTEGER NOT NULL DEFAULT 0,
picture TEXT,
locale TEXT,
active INTEGER NOT NULL DEFAULT 1,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE TABLE user_groups (
userid TEXT NOT NULL REFERENCES users(userid) ON DELETE CASCADE,
group_name TEXT NOT NULL,
PRIMARY KEY (userid, group_name)
);
CREATE TABLE webauthn_credentials (
user_id TEXT NOT NULL REFERENCES users(userid) ON DELETE CASCADE,
credential_id BLOB NOT NULL,
public_key BLOB NOT NULL,
sign_count INTEGER NOT NULL DEFAULT 0,
device_name TEXT NOT NULL DEFAULT '',
created_at TEXT NOT NULL,
PRIMARY KEY (user_id, credential_id)
);
CREATE TABLE password_credentials (
user_id TEXT PRIMARY KEY REFERENCES users(userid) ON DELETE CASCADE,
password_hash TEXT NOT NULL,
created_at TEXT NOT NULL
);
CREATE TABLE magic_links (
token TEXT PRIMARY KEY,
username TEXT NOT NULL,
expires_at TEXT NOT NULL,
used INTEGER NOT NULL DEFAULT 0,
created_by TEXT,
note TEXT
);
Status: Schema approved. Implementation complete.
Implementation Status
All SQLite repository tasks are complete. See docs/plans/2026-02-13-sqlite-repositories-plan.md for the full implementation plan.
Completed (commits bfa5b2e through a45604f)
- SQL migration file (
001_initial.sql) - Migration runner (
run_migrations()with_migrationstracker table) DuplicateErrordomain exceptionSQLiteUserRepositorywith testsSQLiteCredentialRepositorywith testsSQLiteMagicLinkRepositorywith tests- Lifespan integration and dependency injection (
dependencies.py)
Quality Gate
All 64 tests passing. ./scripts/check.sh all green (ruff format, ruff check, ty check, pytest).
Roadmap
Per the design doc, the roadmap is:
Scaffolding(done)SQLite repositories(done)- Authentication (WebAuthn + password) — next phase
- OIDC provider integration (idpyoidc)
- Management UI (OIDC RP that dogfoods the OP)
Process Notes
- User prefers to be asked design questions before implementation begins
- Use brainstorming skill: one question at a time, present design in 200-300 word sections, validate each
- Use subagent-driven development for implementation
- Plans live in
docs/plans/ - Quality gate:
./scripts/check.sh(ruff format, ruff check, ty check, pytest)