papyrus/api/routes: FastAPI routers and HTTP-facing request handling.papyrus/services: service-layer business logic. Prefer new domain modules here instead of growing route handlers.papyrus/schemas: Pydantic request and response models.papyrus/models: SQLAlchemy models and metadata exports used by Alembic.papyrus/core: shared infrastructure such as config, database, exceptions, and security.alembic: Alembic environment and migration revisions.tests/api/routes: endpoint behavior and contract tests.tests/services: service-layer tests.tests/conftest.py: shared fixtures and test database setup.
- Keep route handlers thin. They should parse input, enforce dependencies/auth, call a service, translate errors, and return schemas.
- Put business rules, query orchestration, and transaction-aware logic in
papyrus/services, not in route modules. - Reuse or extend existing schema modules before creating new top-level packages.
- Follow existing async patterns with
AsyncSession, explicit return types, and Pydantic schemas. - Use vertical spacing to separate logical steps inside functions. Keep related statements together, but add a blank line when moving between setup, validation, branching, and side effects.
- Prefer readable spacing over dense blocks. Short guard clauses, temporary assignments, and context-manager branches should usually be visually separated the way
papyrus/services/email.pyandpapyrus/services/auth/are structured. - Treat any multiline statement or block as a visual boundary inside a function. If one adjacent statement is multiline and both sides are real code, separate the two statements with a blank line.
- Apply that rule to multiline conditionals, loops,
withblocks,tryblocks, multiline calls, multiline literals, and multiline return values. Treatif/elif/elseas one block. - Do not add a blank line just because a docstring appears above the first statement in a function. The multiline-block rule applies between code statements, not between a docstring and the first line of code.
- Add a blank line between query execution and result extraction, especially around
session.execute(...)and subsequentscalar*()reads. - Add a blank line before persistence and side-effect boundaries such as
session.add(...),send_email(...),commit(), redirects, and returned result objects when they start a new phase of the function. - Keep tightly coupled short sequences together when they are clearly one step. Do not add spacing mechanically when it makes a two-line operation harder to read.
- Prefer extra spacing around state transitions and persistence boundaries rather than packing setup, branching, and writes into a single block.
- When adding a new router module, register it in
papyrus/api/routes/__init__.py. - Avoid adding dependencies unless the user explicitly asks for them.
- Keep changes scoped. Do not refactor unrelated areas as part of a focused fix.
- Do not leave inline comments. Sufficiently complex behavior that requires explanation should be documented using docstrings.
- Install dependencies:
uv sync --extra dev - Start Postgres:
docker compose up -d database - Stop services:
docker compose down - Run the API locally:
uv run uvicorn papyrus.main:app --reload - Apply migrations:
uv run alembic upgrade head - Create a migration:
uv run alembic revision --autogenerate -m "<message>" - Run all tests:
uv run pytest - Run integration tests:
uv run pytest -m integration - Run one test module:
uv run pytest tests/api/routes/test_<module>.py - Lint:
uv run ruff check . - Format:
uv run ruff format . - Preferred typecheck command when available:
uv run pyright - Current repo-configured fallback typecheck:
uv run mypy .
- Add or update tests for every behavior change.
- Prefer route tests for HTTP contract changes and service tests for business logic.
- If database behavior changes, add the narrowest regression test that proves the query or transaction behavior.
- Do not claim success without running the most relevant checks, or explicitly stating why they were not run.
- Add an Alembic revision for every schema change, constraint change, index change, or persisted-data backfill.
- Update SQLAlchemy models first, then review autogenerated Alembic output before committing it.
- Make sure new or changed models are imported from
papyrus.modelsso Alembic metadata can see them. - Keep migrations small and pair them with the application code that depends on them.
- Do not ship destructive or irreversible migrations without explicit user approval.
- Relevant tests pass, or any unrun checks are called out with the reason.
- Ruff passes on the changed scope.
- Typechecking is run on the changed scope. Use
pyrightwhen available, otherwise use the repo's currentmypysetup. - Schema changes include a migration and verification.
- New backend behavior follows thin-route and service-layer separation.
- No new dependencies were added unless explicitly requested.
Local auth testing supports Mailpit for SMTP capture, a dev auth sandbox at /__dev/auth-sandbox, and opt-in provider smoke tests.
See docs/auth-testing.md for the exact .env values, Google OAuth setup, and end-to-end test workflow.
For Flutter client integration guidance, see docs/flutter-auth-integration.md.
For the self-hosted PowerSync sandbox and sync validation workflow, see docs/powersync-sandbox.md.
To build the dev sandbox assets without the Vite dev server:
npm --prefix frontend/dev-pages run build