Skip to content

Working in the codebase

Archetype’s backend is a layered Django codebase with enforced app boundaries; the frontend is a Next.js app. This page explains the architecture rules you must follow when changing code, and the exact commands to run the tests, linters and boundary checks before you push.

AreaPathNotes
Backend appsapi/apps/*Django feature apps; routes under /api/v1/*
Search registryapi/apps/search/registry.pySingle source of truth for index types
Public sitefrontend/app/(site)/*App Router pages
Backofficefrontend/app/backoffice/*Staff authoring and management UI
Data accessfrontend/lib/api-fetch.ts, frontend/services/backoffice/Public fetch layer; backoffice API client
Request logicfrontend/proxy.tsCSP nonce, auth and feature gating (Next.js 16’s middleware.ts)
Production stackinfrastructure/compose.yamlFull stack behind nginx

The Django backend lives in api/apps/ (the backend repository, cloned as api/), one directory per feature app:

apps/{common,users,manuscripts,symbols_structure,scribes,annotations,annotation_standards,publications,pages,worksets,search,uploads}

(annotation_standards serves the IIIF Presentation and W3C Web Annotation output; uploads handles chunked image uploads.) Inter-app imports are not free-for-all — they follow a fixed dependency graph. common depends on nothing; search may read from manuscripts, scribes, symbols_structure, annotations and publications; manuscripts depends only on common and annotations; and so on. The full graph is declared in scripts/check_architecture_boundaries.py. Every app under apps/ (any directory with an apps.py) must have an entry there, or the check fails.

Run the boundary check before you push:

Terminal window
just check-architecture # uv run python scripts/check_architecture_boundaries.py

Keep each layer doing its one job:

  • Views are transport-only. Validation, HTTP status mapping, and response shape — nothing else. No orchestration, no Celery dispatch.
  • Application services own orchestration. Cross-model workflows and all Celery task dispatch live here (e.g. apps/manuscripts/services/), not in views.
  • Domain services own mutation workflows. Write logic belongs in domain services.
  • Serializers do not embed write logic. They shape data in and out; they must not drive mutations.

If you find yourself reaching for a model or task inside a view or a serializer’s create/update, push that logic down into a service instead.

Search is registry-driven: apps/search/registry.py is the single declarative source of truth for every Meilisearch index. Each IndexType maps to one IndexRegistration holding the model, the document builder (from apps/search/documents/), the filterable/sortable/facet/searchable attribute lists, and the ORM prefetch spec — nothing about an index lives anywhere else.

Adding a search index is a three-part edit made together: the registry entry, the builder function in apps/search/documents/, and the tests. Land them in the same change — a half-registered index is a broken one.

After changing indexes, recreate and resync them:

Terminal window
just setup-search-indexes # create the Meilisearch indexes
just sync-search-index item-parts # sync one index (positional arg)
just sync-all-search-indexes # sync everything

The backend runs via Docker Compose through just recipes. conftest.py auto-selects SQLite when run outside Docker, and just pytest-focused forces SQLite inside the container, so the focused loop is fast:

Terminal window
just pytest # full pytest-django suite
just pytest-focused # fast SQLite subset (annotations + search services)
just pytest-search # search tests only
just coverage # full run, --cov-fail-under=55
just clean # ruff check --fix
just lint # mirrors the CI lint job (ruff check, ruff format --check, mypy)

Frontend tests use Vitest with a jsdom environment. Each just recipe runs inside the frontend dev container; the pnpm equivalents work host-natively:

Terminal window
just test # pnpm test → vitest run
just lint # pnpm lint → oxlint
just format # pnpm format → prettier --check
just bundle-budget # CI gate: builds, then fails if the gzip bundle exceeds .bundle-budget.json

Both repos ship pre-commit hooks in .githooks/ that run fast CI-equivalent checks (backend: ruff check, ruff format --check, uv lock --check, the architecture-boundary check; frontend: pnpm lint, pnpm format). They are not active until you enable them per clone with git config core.hooksPath .githooks — see the Contribution workflow. If a hook fails, fix it (uvx ruff format . or pnpm format:fix && pnpm lint:fix) and re-commit. The frontend bundle-budget gate can trip even when lint and tests pass — run just bundle-budget-update (or pnpm build && pnpm bundle-budget:update) and commit the refreshed budget after intentional growth.

For running an instance locally, see Configuration and Load your own corpus; for the HTTP surface, see The API.