CLAUDE.md

September 10, 2026 · View on GitHub

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

  • Do not preserve backward compatibility.
  • Choose the simplest implementation that fully meets the current requirements.
  • Prefer established, well-maintained libraries over custom implementations.
  • Make architectural decisions for the long term. Do not accept a stopgap that only works for now and is meant to be replaced later.

Project Overview

SnackBase is a Python/FastAPI-based Backend-as-a-Service (BaaS) designed as an open-source, self-hosted alternative to PocketBase. It provides auto-generated REST APIs, multi-tenancy, row-level security, authentication, and GxP-compliant audit logging.

Current State: Full-stack implementation with React admin UI, comprehensive testing (1,161 tests), and 18+ API routers including realtime (WebSocket/SSE), file storage, collection rules, and email templates.

Package Management

This project uses uv (not pip, poetry, or pdm) for package management:

uv sync                    # Install dependencies
uv add <package>           # Add a dependency
uv run python -m snackbase <command>  # Run CLI commands

Development Commands

Backend (Python/FastAPI)

# Server management
uv run python -m snackbase serve          # Start server (0.0.0.0:8000)
uv run python -m snackbase serve --reload # Dev mode with auto-reload
uv run python -m snackbase info           # Show configuration

# Database
uv run python -m snackbase init-db        # Initialize database (dev only)
uv run python -m snackbase create-superadmin  # Create superadmin user

# Migrations
uv run python -m snackbase migrate upgrade    # Apply pending migrations
uv run python -m snackbase migrate downgrade  # Rollback last migration
uv run python -m snackbase migrate history    # Show migration history

# Interactive shell
uv run python -m snackbase shell          # IPython REPL with pre-loaded context

# Code quality
uv run ruff check .                        # Lint
uv run ruff format .                       # Format
uv run mypy src/                           # Type check

# Testing
# Safe to run from the repository root: tests isolate via
# SNACKBASE_TEST_DATA_DIR (default: a temp directory) and refuse to
# touch ./sb_data/snackbase.db. Pointing SNACKBASE_DATABASE_URL at a
# snackbase.db outside that directory raises, naming the path and
# SNACKBASE_TEST_DATA_DIR.
uv run pytest                              # Run all tests
uv run pytest tests/unit/                  # Unit tests only
uv run pytest tests/integration/           # Integration tests only
uv run pytest --cov=snackbase              # With coverage
uv run pytest -k "test_name"               # Run specific test

Frontend (React/TypeScript)

cd ui
npm run dev        # Start dev server (Vite)
npm run build      # Production build
npm run lint       # ESLint
npm run preview    # Preview production build

Python Version

python 3.14 is required. The project uses .python-version for version specification.

Architecture

The project follows Clean Architecture with three layers:

src/snackbase/
├── core/                         # Cross-cutting concerns (no external deps)
│   ├── config.py                 # Pydantic Settings for env-based config
│   ├── logging.py                # Structured logging with structlog
│   ├── configuration/            # Configuration registry (provider system)
│   ├── hooks/                    # Hook registry, decorator, events (STABLE API)
│   ├── macros/                   # SQL macro execution engine
│   └── rules/                    # Rule engine (lexer, parser, AST, evaluator)
├── domain/                       # Core business logic (no external dependencies)
│   ├── entities/                 # Business entities (dataclasses)
│   └── services/                 # Business logic interfaces
├── application/                  # Use cases and orchestration
│   ├── commands/                 # Write operations (placeholder)
│   └── queries/                  # Read operations (placeholder)
└── infrastructure/               # External concerns
    ├── api/
    │   ├── app.py                # FastAPI app factory, lifespan, middleware
    │   ├── dependencies.py       # FastAPI dependencies (auth, session, API key auth)
    │   ├── routes/               # 14+ API routers
    │   ├── schemas/              # Pydantic request/response models
    │   └── middleware/           # Authorization middleware
    ├── persistence/
    │   ├── database.py           # SQLAlchemy 2.0 async engine
    │   ├── models/               # ORM models (Account, User, Role, Configuration, ApiKey, etc.)
    │   ├── repositories/         # Repository pattern (12+ repositories)
    │   └── table_builder.py      # Dynamic table creation
    ├── auth/                     # JWT service, password hasher (Argon2), API key service
    ├── configuration/            # Provider handlers (OAuth, SAML, email)
    │   └── providers/            # Provider implementations
    ├── hooks/                    # Built-in hooks implementation
    ├── security/                 # Encryption service
    ├── services/                 # Token service, email service
    ├── realtime/                 # WebSocket/SSE via realtime_manager, event_broadcaster, realtime_auth
    └── storage/                  # File storage service

ui/                               # React + TypeScript Admin UI
├── src/
│   ├── pages/                    # Dashboard, Accounts, Collections, Roles, ApiKeys, etc.
│   ├── components/               # React components (Radix UI + TailwindCSS)
│   ├── services/                 # API clients (axios)
│   ├── stores/                   # Zustand state management
│   └── lib/                      # Utilities, axios config

Key Principles:

  • Domain layer has ZERO dependencies on FastAPI or infrastructure
  • Infrastructure layer contains ALL external dependencies
  • Repository pattern abstracts data access (20 repositories)
  • SQLAlchemy event listeners trigger hooks on model changes

Multi-Tenancy Model

Accounts represent isolated tenants within a single database using row-level isolation via account_id column.

Two-Tier Table Architecture

  1. Core System Tables - Schema changes via releases only (accounts, users, roles, permissions, collections, macros, migrations)
  2. User-Created Collections - Global tables shared by ALL accounts (e.g., posts, products)

Critical: User-created collections are SINGLE physical tables where all accounts' data is stored together, isolated by account_id. The collections table stores schema definitions/metadata only.

Account ID Format

Accounts use auto-generated IDs in format XX#### (2 letters + 4 digits, e.g., AB1234).

  • id: XX#### format (primary key, immutable)
  • slug: URL-friendly identifier for login (globally unique)
  • name: Display name (not unique)

Superadmin vs Admin

AspectSuperadminAdmin (role)
AccountLinked to system account (ID: SY0000)Linked to specific account
AccessAll accounts and system operationsFull CRUD within their account only

Authentication

Enterprise Multi-Account Model

Users can belong to multiple accounts with the same email address. User identity is (email, account_id) tuple.

  • Passwords are per-account - each (email, account_id) has its own password
  • Login always requires account context (slug/ID from URL or request body)
  • JWT with access token (1 hour) and refresh token (7 days) with rotation

API Key Authentication

Alternative authentication method for service-to-service communication:

  • API keys use format: sb_ak.<payload>.<signature> — JWT-style tokens signed with the token secret; the SHA-256 hash of the full key is stored server-side
  • Authenticate via Authorization: Bearer <api_key> header
  • Scoped to account level with optional name for identification
  • Can be revoked via API (DELETE /api/v1/admin/api-keys/{key_id}) or admin UI
  • Managed via /api/v1/admin/api-keys endpoints

Single-Tenant Mode

Optional deployment mode where all users join a pre-configured account instead of creating their own:

  • Enabled via SNACKBASE_SINGLE_TENANT_MODE=true
  • SNACKBASE_SINGLE_TENANT_ACCOUNT defines the account slug
  • SNACKBASE_SINGLE_TENANT_ACCOUNT_NAME for display name
  • Account auto-created on startup if it doesn't exist
  • Useful for SaaS applications, internal tools, or dedicated deployments

Platform Trusted-Issuer Authentication (optional, default-off)

Instances can optionally accept RS256/ES256 access tokens from a configured external issuer (SnackBase Cloud or a self-hosted OIDC provider). Every SNACKBASE_PLATFORM_* setting defaults to unset — an instance with no platform configuration behaves exactly as it does without this feature: no JWKS fetch, no key cache, no alternate auth path.

  • Enable with SNACKBASE_PLATFORM_ISSUER, SNACKBASE_PLATFORM_JWKS_URL, and SNACKBASE_PLATFORM_AUDIENCE
  • Independent of instance tenancy: single- and multi-tenant instances both support it
  • A platform principal is the operator of the instance and resolves into the system account (SY0000) — the same identity a self-hosted superadmin logs in as. The accounts inside a multi-tenant instance are the customer's own end-tenants, who authenticate directly against the instance and never traverse the platform gateway
  • Only snackbase_role: admin is accepted; any other role is rejected at authentication, because require_superadmin admits on system-account membership alone
  • If the instance's bootstrap superadmin already exists with the same email, that row is adopted unmodified so break-glass password login keeps working
  • Audit entries record auth_method=platform
  • Realtime connections authenticate through the same Authenticator as HTTP; platform socket lifetime is capped separately from token exp
  • Cloud Console integrated Studio lives in ui/ platform mode (VITE_IS_PLATFORM=true); docs in docs/guides/cloud-studio.mdx and docs/guides/platform-oidc-self-host.mdx

Key Technical Decisions

Database

  • Default: SQLite (aiosqlite driver) - In-memory for tests, file-based for dev
  • Production: PostgreSQL (asyncpg driver)
  • ORM: SQLAlchemy 2.0+ async with async_session
  • Account ID generator produces XX#### format
  • Pooling: 5 base connections, 10 max overflow (configurable)

Hook System (Stable API v1.0)

The hook registry is a STABLE API contract. Changing the registration mechanism is a breaking change.

# Hook registration (decorator syntax via app.hook)
@app.hook.on_record_after_create("posts", priority=10)
async def send_post_notification(record, context):
    await notification_service.send(record.created_by, "Post created!")

# Built-in hooks (cannot be unregistered):
# - timestamp_hook (auto-sets created_at/updated_at)
# - account_isolation_hook (enforces account_id filtering)
# - created_by_hook (sets created_by user)

Hook categories: App Lifecycle, Model Operations, Record Operations, Collection Operations, Auth Operations, Request Processing.

Rule Engine

Custom DSL for permission expressions with full lexer/parser/AST implementation:

# Supported syntax
user.id == "user_abc123"
@has_role("admin") and @owns_record()
status in ["draft", "published"]

Permission Resolution

  • Role-based with wildcard collection support (*)
  • OR logic for multiple permissions
  • Field-level access control
  • 5-minute TTL cache (invalidated on permission changes)

Configuration/Provider System

Hierarchical Configuration Model: External service provider settings (auth, email, storage) with two levels:

  1. System-level (account_id: 00000000-0000-0000-0000-000000000000) - Default configs for all accounts
  2. Account-level - Per-account overrides that take precedence over system defaults

Key Components:

  • ConfigurationRegistry (src/snackbase/core/configuration/config_registry.py) - Central registry for provider definitions and hierarchical resolution with 5-minute cache
  • ConfigurationModel - ORM model with encrypted config JSON field
  • Provider handlers in src/snackbase/infrastructure/configuration/providers/:
    • auth/ - Email/password authentication
    • oauth/ - OAuth providers (Google, GitHub, Microsoft, Apple)
    • saml/ - SAML providers (Azure AD, Okta, generic)

Architecture Pattern:

  • Provider definitions registered via config_registry.register_provider_definition()
  • Config resolution: account-level config → system-level fallback
  • All sensitive config values encrypted at rest
  • Built-in providers marked with is_builtin flag (cannot be deleted)
  • is_default flag per (category, account_id) designates the active default provider; indexed via ix_configurations_is_default
  • Verified providers: Some providers (e.g., Google OAuth) are marked with a verified badge in the UI indicating they have been manually tested

API Structure

/health, /ready, /live          # Health checks (no prefix)
/api/v1/
├── /auth/                      # Register, login, refresh, me
├── /collections/               # Collection CRUD (superadmin)
├── /accounts/                  # Account management (superadmin)
├── /users/                     # User management (superadmin)
├── /roles/                     # Role management
├── /permissions/               # Permission management
├── /macros/                    # SQL macro management
├── /groups/                    # Group management
├── /invitations/               # User invitations
├── /api-keys/                  # API key management (create, list, revoke)
├── /dashboard/                 # Dashboard statistics
├── /audit-logs/                # Audit log retrieval and export
├── /migrations/                # Alembic migration status
├── /admin/                     # Admin API (system/config management)
├── /oauth/                     # OAuth flow endpoints
├── /saml/                      # SAML flow endpoints
├── /collection-rules/          # Collection-level access rule management
├── /email-templates/           # Email template management
├── /files/                     # File upload/download
├── /realtime/                  # WebSocket (/ws) and SSE (/events) endpoints
└── /{collection}/              # Dynamic collection CRUD (records_router)

Route Registration Order Matters: records_router (dynamic /api/v1/records/{collection}) must be registered last to avoid capturing specific routes like /invitations.

Testing

Tools: pytest, pytest-asyncio, httpx AsyncClient with ASGITransport

Test directories: tests/unit/, tests/integration/, tests/security/, tests/infrastructure/, tests/verification/

Fixtures (in tests/conftest.py):

  • db_session - In-memory SQLite session
  • client - Test HTTP client with dependency overrides
  • superadmin_token - Pre-authenticated superadmin JWT
  • regular_user_token - Pre-authenticated regular user JWT

Test Markers:

  • @pytest.mark.enable_audit_hooks - Enable audit logging hooks for tests (adds overhead, use only when needed)

Important Constraints

  • Hook system is stable API - Cannot change registration mechanism once implemented
  • Macros are global - Defined once, shared by all accounts
  • Migrations are global - Affect ALL accounts (collections are global tables)
  • Audit logs are immutable - Once written, cannot be modified
  • Configuration hierarchy - System-level configs use 00000000-0000-0000-0000-000000000000 as account_id, not SY0000
  • Built-in providers - Cannot be deleted (is_builtin flag), only disabled
  • SQLAlchemy event listeners - Hook system triggered via ORM events (after_insert, after_update, after_delete) in infrastructure/persistence/event_listeners.py

Environment Variables

Key configuration via .env or environment:

# Application
SNACKBASE_ENVIRONMENT=development
SNACKBASE_DEBUG=false

# Database (default: SQLite)
SNACKBASE_DATABASE_URL=sqlite+aiosqlite:///./sb_data/snackbase.db
# For PostgreSQL:
# SNACKBASE_DATABASE_URL=postgresql+asyncpg://user:pass@localhost/dbname

# Security (generate with: openssl rand -hex 32)
SNACKBASE_SECRET_KEY=your-secret-key
SNACKBASE_ENCRYPTION_KEY=your-encryption-key

# CORS
SNACKBASE_CORS_ORIGINS=["http://localhost:3000","http://localhost:8000"]

# Single-Tenant Mode (optional)
SNACKBASE_SINGLE_TENANT_MODE=false
# SNACKBASE_SINGLE_TENANT_ACCOUNT=my-app

# Rate Limiting (optional)
SNACKBASE_RATE_LIMIT_ENABLED=false
SNACKBASE_RATE_LIMIT_PER_MINUTE=60

See .env.example for complete configuration options.

Frontend Tech Stack

  • React 19 + React Router v7 + Vite 7
  • TailwindCSS 4 + Radix UI components
  • TanStack Query for data fetching
  • Zustand for state management
  • Zod for validation
  • ShadCN: Use existing components in ui/src/components/ui/.
  • Intall new ShadCN components using npx shadcn@latest add {component name}
  • Never create ShadCN component file. Always install using CLI.
  • TypeScript interfaces in Vite: Always use import type { Foo } (not import { Foo }) when importing TypeScript interfaces or type aliases across modules. Vite's dev server serves native ESM and strips type-only exports at runtime, so mixing value and type imports in a single import { ... } causes "does not provide an export named" errors in the browser.

General Note

  • Always run cleanup before running any test using uv run python cleanup_dev.py -y
  • Always run all tests to ensure no other feature is breaking using uv run pytest -v
  • Always run cleanup even in between running tests.