Also the AGENTS.md is now track to have all the same one and track any modification made on it.
16 KiB
Agent Guidelines for Docs
This file contains guidelines for AI agents coding in this repository.
Project Overview
La Suite Docs is a collaborative text editor built by DINUM (French government) and ZenDiS (German government). It features real-time editing, offline support, AI actions, and multi-format export (PDF/DOCX/ODT).
Repository: https://github.com/suitenumerique/docs
Monorepo Structure
src/backend/- Django REST API (Python 3.14+)src/frontend/apps/impress/- Main Next.js 15 application (TypeScript)src/frontend/apps/e2e/- Playwright E2E testssrc/frontend/packages/i18n/- Shared i18n utilitiessrc/frontend/packages/eslint-plugin-docs/- Custom ESLint pluginsrc/frontend/servers/y-provider/- Conversion service only (Express,POST /api/convert/). It no longer serves any websocketsrc/yhub-server/- Collaboration server: a thin TypeScript wrapper around@y/hub(websockets, REST routes, worker). It holds the content of the documentssrc/mail/- Email templates (MJML)src/helm/- Kubernetes/Helm deployment
Development Commands
Setup and Services
make bootstrap # Full dev setup (build + migrate + demo + run)
make build # Build all Docker containers
make run # Start all services
make stop # Stop services
make build-yhub # Build the collaboration server (yhub) image
make migrate-yhub # Create/upgrade the yhub database schema (`yarn init-db`), safe to re-run
make status # Check running services
Backend (Python/Django)
Tests run inside Docker containers. You must first build the backend image and ensure the lasuite-network Docker network exists before running tests.
make build-backend # Build the backend Docker image (required before first test run)
docker network create lasuite-network # Create the external network (required once)
bin/pytest -n auto # Run all backend tests in parallel
bin/pytest -n auto path/to/test # Run specific test file/directory
bin/pytest -n auto path/to/test.py::TestClass::test_method # Run single test via docker compose
make lint # ruff format + ruff check + pylint
make lint-ruff-format # Format only
make lint-ruff-check # Lint only
make migrate # Run database migrations
make makemigrations # Create new migrations
make resetdb # Flush DB + create superuser (admin/admin)
Frontend (TypeScript/Next.js)
# From src/frontend/apps/impress/:
yarn dev # Development server (port 3000)
yarn build # Production build (includes prettier + stylelint checks)
yarn lint # TypeScript check + ESLint
yarn test # Run Vitest tests
yarn prettier # Format code
yarn stylelint # Lint CSS
# From project root:
make frontend-lint # Lint all frontend workspaces
make frontend-test # Run frontend tests
Collaboration server (yhub, TypeScript)
# From src/yhub-server/ (standalone package: its own package.json and yarn.lock,
# not a workspace of src/frontend):
yarn test # Vitest unit tests (@y/hub is mocked, no store needed)
yarn typecheck # tsc --noEmit
yarn lint # ESLint
yarn build # Compile to dist/
yarn init-db # Create/upgrade the yhub postgres schema (what `make migrate-yhub` runs)
The container starts with node --import ./dist/sentry.js dist/server.js: keep the
--import when overriding the command. src/yhub-server/README.md is the reference
for everything this server does (permissions, roles, migration, storage, metrics).
Mails
make mails-install # Install dependencies
make mails-build # Convert MJML to HTML + plaintext
Helm/Kubernetes Deployment
make build-k8s-cluster # Create local Kind cluster
make start-tilt # Start Tilt for hot-reload
# From src/helm/: helmfile -n impress -e dev apply/destroy/diff
Dev Service URLs
- Frontend: http://localhost:3000
- Backend API/Admin: http://localhost:8071
- Keycloak (auth): http://localhost:8083
- MinIO (S3): http://localhost:9000
- Collaboration server (yhub): http://localhost:3002 (websocket on
/collaboration/ws/v1/docs/{docid}) - yhub PostgreSQL: localhost:5433 (its own database, apart from the backend's on 15432)
- Mailcatcher: http://localhost:1081
Architecture
Backend
- Framework: Django + DRF, configured via
django-configurations(src/backend/impress/settings.py) - Main app:
src/backend/core/(models, API viewsets, services, authentication) - API: REST on
/api/v1.0/with nested routes (e.g.,/documents/{id}/accesses/) - Auth: OIDC via
mozilla-django-oidc(Keycloak in dev) - Background tasks: Celery + Redis
- Database: PostgreSQL 16 with
django-treebeardfor document hierarchy - Storage: S3-compatible (MinIO in dev) for attachments and media. The content of a document is not stored by the backend anymore: it lives in the collaboration server (yhub).
Document.content,save_contentand the S3 object versions are gone;file_keyonly survives as a pointer for the migration of legacy documents - Collaboration server client:
core/services/yhub_services.py(YHubService:get_ydoc,create_ydoc,delete_ydoc,restore_ydoc,reset_ydoc,migrate,reset_connections). It is called synchronously by document creation from a file,duplicate,formatted-content, and from Celery tasks for delete/restore/access changes. Mock it in tests (CELERY_TASK_ALWAYS_EAGER=Truein theTestconfiguration, so tasks run inline) - Service-to-service auth: short-lived RS256 JWTs with an
audclaim, never a shared bearer token. The backend signs withJWT_PRIVATE_KEY(core/services/jwt_services.py) and publishes its keys on/api/v1.0/jwks; yhub signs withYHUB_JWT_PRIVATE_KEYand publishes/collaboration/jwks/v1. Dev keys are generated indata/jwt/bymake bootstrap - Removed endpoints:
documents/{id}/content/(GET and PATCH),documents/{id}/versions/,documents/{id}/can-edit/. New ones:documents/{id}/content-updated/(called by yhub) anddocuments/{id}/accesses/me/.documents/{id}/formatted-content/now reads the content from yhub - Legacy documents:
python manage.py migrate_documentshands the S3 content of existing documents to yhub (resumable, idempotent);SOFT_MIGRATION=trueon yhub seeds a document on first open. SeeUPGRADE.md - Search: Full-text indexing via
manage.py index - Load-test tooling:
src/backend/loadtest/is a separate Django application that mints sessions for existing users (create_load_test_sessions,revoke_load_test_sessions). It is only installed by theLoadTestconfiguration (DJANGO_CONFIGURATION=LoadTest, a subclass ofProduction);LOAD_TEST_TOOLS_ENABLEDisFalseeverywhere else, pinned inProduction, and never read from the environment. Anything added for load tests goes there, not incore, and must stay off inProduction
Frontend
- Editor: BlockNote.js 0.46.x (Tiptap-based block editor)
- Real-time: Yjs with the plain
y-websocketWebsocketProvider(features/docs/doc-management/stores/useProviderStore.tsx), and an HTTP polling fallback (@y/yhub-http-fallback) used while the websocket cannot be opened. Hocuspocus is gone - Offline: documents are kept locally (
y-indexeddb) and a service worker (features/service-worker/) caches pages and API answers and replays queued mutations when back online - Version history: built from the activity API of yhub, not from the backend
- State: Zustand (broadcast store for cross-tab sync), TanStack React Query for server state
- UI: Mantine 8 + Cunningham design system
- Export: BlockNote XL packages (PDF, DOCX, ODT)
- i18n: i18next with Crowdin for community translations
Collaboration Flow
Clients connect via WebSocket to the collaboration server (src/yhub-server, built on @y/hub), authenticated by their Django session cookie.
- On every connection (and permission recheck, REST call, fallback poll) yhub asks the backend who the caller is and what they may do:
GET /users/me/,GET /documents/{id}/, andGET /documents/{id}/accesses/me/for the history. The backend stays the source of truth for access rights - Updates travel between replicas through Redis/Valkey streams, so yhub replicas are stateless and need no sticky sessions. A worker (
YHUB_ROLE=all|server|worker) compacts the stream into yhub's own PostgreSQL database (optionally S3 for the blobs) - After a compaction that found new content, yhub calls
POST /documents/{id}/content-updated/so that the backend updatesupdated_atand the search index - The backend reads and writes content through the REST routes of yhub with an admin JWT (see
YHubService)
Observability
- Metrics (opt-in,
PROMETHEUS_METRICS_ENABLED+ a requiredPROMETHEUS_API_KEYbearer token, both services refuse to start without the key):- backend: django-prometheus on
/metrics, deliberately outside/api/, protected bycore.middleware.PrometheusAuthMiddleware. Multiprocess mode is set up automatically for the uvicorn workers. Seedocumentation/metrics.md - yhub:
src/yhub-server/src/metrics.ts, served on its own port (9464) in every role, the worker included. See the "Metrics" section ofsrc/yhub-server/README.md - chart:
ingressMetricspublishes/metrics,/metrics/yhuband/metrics/yhub-workeron a dedicated host - never put a document id, a user id or a path in a metric label
- backend: django-prometheus on
- Sentry: backend (
SENTRY_DSN), y-provider, and yhub (src/yhub-server/src/sentry.ts, preloaded,SENTRY_*settings;error/fatallog lines of the shared pino logger are reported) - Profiling: django-silk, opt-in with
SILK_ENABLED(documentation/profiling.md) documentation/load-testing.mdis the load-testing guide: tooling, setup of an instance, scenarios. Never against production- Load generators:
src/loadtest/k6/(plain k6 scripts: the page-open HTTP sequence and the heavy endpoints; every request tagged with anamefree of identifiers, CSRF by double-submit with the frontend'sOrigin) andsrc/loadtest/swarm/(standalone TypeScript package, likesrc/yhub-server) opens many Yjs clients on yhub with the frontend's client stack, reading the manifest ofcreate_load_test_sessions. Tests run against an in-process y-websocket server (__tests__/_server.ts);src/loadtest/canary/(same shape, Playwright library) drives a few real Chromium pairs through the frontend to measure page open, editor ready and keystroke propagation, with the manifest's cookies (session +csrftoken) set on the context
Code Style Guidelines
Python (Backend)
- Formatter: Ruff (line-length: 88)
- Linter: Ruff + Pylint (pylint runs only on files changed from
origin/main) - Imports (Ruff enforced): future, stdlib, django, third-party, impress (
core), first-party, local - Naming: PascalCase classes, snake_case functions/variables, UPPER_SNAKE_CASE constants
- Print statements are forbidden (T20 rule) - CI will reject them in backend code
- Use
@transaction.atomic,select_related/prefetch_related, type hintslist[str] - Run migrations after model changes
- In
settings.py,post_setupruns after django-configurations has handed the settings to Django: change lists and dicts in place there (append,insert, item assignment), a reassignment is silently ignored - Use specific exceptions, no unused imports, double quotes, f-strings
- Tests: Prefer the
settingsfixture frompytest-djangooverdjango.test.override_settings
TypeScript/React (Frontend)
- Formatter: Prettier (single quotes, trailing commas, semicolons, 80 char width)
- Linter: ESLint 9 with custom
eslint-plugin-docs - Style: Stylelint for CSS
- Imports: React & Next.js, libraries, internal, types, styles
- Naming: PascalCase components/types, camelCase functions,
use*hooks, UPPER_SNAKE_CASE constants - Strict mode, interfaces for shapes, Zustand for state, React Query for data
TypeScript (yhub-server)
- Every environment variable is read, validated and refused at import time in
src/config.ts(secrets throughsecret()insrc/env.ts, which supportsNAME_FILE): add new settings there, with a test in__tests__/config.spec.ts - Prettier style with single quotes; tests are Vitest and mock
@y/hub @y/ymust stay the very copy@y/hubresolves (__tests__/dependencies.spec.ts)
Helm/Kubernetes Deployment
Structure
helmfile.yaml.gotmpl- Helmfile defining releases and environmentsimpress/- Helm chart with templates and default valuesenv.d/{env}/- Environment-specific values (dev=local, feature=CI)
Services
- Backend (Django + Celery), Frontend (Next.js), yhub (collaboration server, plus an optional separate worker with
yhub.worker.enabled), yProvider (conversion only) - Two standalone Valkey instances from the official
valkey/valkeychart:valkey-docs(backend cache, sessions, Celery) andvalkey-yhub devonly (monitoring: truein the helmfile environment): a trimmedkube-prometheus-stackrelease (env.d/dev/values.prometheus.yaml.gotmpl, operator + CRDs + one Prometheus at https://docs-prometheus.127.0.0.1.nip.io) scraping the backend and yhub through theserviceMonitorof the chart, with the token in thedocs-metricsSecret it creates, and a Grafana at https://docs-grafana.127.0.0.1.nip.io (admin/admin) whose sidecar loads the dashboards ofsrc/loadtest/dashboards/from ConfigMaps (legend{{escaped for helm's tpl, as the values file explains)- Websocket ingress on
/collaboration/ws/with noupstream-hash-by: any yhub replica serves any document - DocSpec (conversion), Posthog (analytics) - optional
Deployment Model
- Kind cluster with mkcert HTTPS, local registry (localhost:5001)
- CoreDNS for nip.io domains, dev-backend deps (Keycloak, PostgreSQL, Minio)
Helm Guidelines
- Never commit secrets; use Got templating
{{ now | unixEpoch }} - Follow semantic versioning; values use
@paramannotations
Git Conventions
Commit Format
<gitmoji>(type) lowercase title
Mandatory description explaining why.
- No space between emoji and
(type), one space after closing parenthesis - Types:
backend,frontend,ci,docker,dependencies,e2e,export,auth, etc. - Gitmoji examples:
✨feature,🐛fix,♻️refactor,⬆️dependency upgrade,🔒️security - Commits must be signed (
-S) and signed-off (--signoff)
Changelog
Update CHANGELOG.md under [Unreleased] with format: - <gitmoji>(type) description (max 80 chars per line).
PR Checklist
Run before submitting: make lint && make frontend-lint && make test
General Practices
- Commit messages should be clear and descriptive
- Run
make lintand tests before committing - Use DRF for REST APIs with proper serializers
- Implement proper error handling with try/except blocks
- Use Django settings for configuration
- Internationalize user-facing strings with
_() - Cache expensive operations, use database indexes
- Write docstrings for classes and complex functions
- Use type checking: TypeScript (frontend), mypy optional (Python)
Environment Configuration
Config files in env.d/development/: common, postgresql, kc_postgresql, crowdin, yhub, yhub-postgres. The collaboration server reads its own two files and none of the backend's. Create .local overrides (gitignored) for custom settings.
i18n
make i18n-generate # Extract translation strings (backend + frontend)
make i18n-compile # Compile translations for all apps
Internationalize user-facing strings with _() (backend) or i18next (frontend).
Key Directories
src/backend/- Django REST API backendsrc/frontend/- Next.js (apps/impress, packages/i18n, servers/y-provider)src/yhub-server/- Collaboration server (yhub)src/helm/- Helmfile and Helm chart for Kubernetes deploymentsrc/mail/- Email template generator (MJML)documentation/- Documentation.github/- GitHub workflows and templates