This file contains guidelines for AI agents coding in this repository.
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
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
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 servicesTests 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)# 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# 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).
make mails-install # Install dependencies
make mails-build # Convert MJML to HTML + plaintextmake 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- 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
- 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
- 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
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)
- 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
- 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
- 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
- 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)
helmfile.yaml.gotmpl- Helmfile defining releases and environmentsimpress/- Helm chart with templates and default valuesenv.d/{env}/- Environment-specific values (dev=local, feature=CI)
- 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
- Kind cluster with mkcert HTTPS, local registry (localhost:5001)
- CoreDNS for nip.io domains, dev-backend deps (Keycloak, PostgreSQL, Minio)
- Never commit secrets; use Got templating
{{ now | unixEpoch }} - Follow semantic versioning; values use
@paramannotations
<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)
Update CHANGELOG.md under [Unreleased] with format: - <gitmoji>(type) description (max 80 chars per line).
Run before submitting: make lint && make frontend-lint && make test
- 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)
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.
make i18n-generate # Extract translation strings (backend + frontend)
make i18n-compile # Compile translations for all appsInternationalize user-facing strings with _() (backend) or i18next (frontend).
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