Sovereign file transfer service for La Suite territoriale.
Initially forked from suitenumerique/messages.
- Backend: Django + DRF, PostgreSQL, Celery/Redis, S3 (RustFS in dev)
- Frontend: React (Vite + TanStack Router)
- Auth: ProConnect via OIDC (a local Keycloak stands in for it in dev)
make bootstrapServices:
- Frontend: http://localhost:8980
- API: http://localhost:8981
- Admin: http://localhost:8981/admin
- Mail: http://localhost:8984
- S3 Console: http://localhost:8987
- Keycloak (dev OIDC): http://localhost:8902 (
admin/admin)
Signing in: open the frontend, click Sign in, and use a seeded test user
(agent@collectivite.fr / transferts). Dev runs the real OIDC flow against a
local Keycloak — no ProConnect needed. See docs/authentication.md.
| Setting | Default | Effect |
|---|---|---|
TRANSFER_MAX_FILE_SIZE |
20 GiB | Cap per file |
TRANSFER_MAX_TOTAL_SIZE |
20 GiB | Cap on the sum of files in a transfer |
TRANSFER_MAX_FILES_PER_TRANSFER |
20 | Cap on file count per transfer |
TRANSFER_EXPIRY_CHOICES |
1,7,30 |
Expiry options offered in the UI (days) |
TRANSFER_DEFAULT_EXPIRY_DAYS |
1 | Default expiry; must be in TRANSFER_EXPIRY_CHOICES |
TRANSFER_PURGE_DELAY_HOURS |
6 | Grace period between link closure and S3 deletion (one-shot links + expiry + manual deactivation) |
Recipient count in email mode is hard-capped at 50 (in the serializer).
S3 / storage:
| Variable | Effect |
|---|---|
AWS_S3_ENDPOINT_URL |
S3 endpoint as seen from the backend |
AWS_S3_DOMAIN_REPLACE |
Hostname rewritten into presigned URLs (dev: backend sees objectstorage:9000, browser sees localhost:8986) |
AWS_S3_ACCESS_KEY_ID / AWS_S3_SECRET_ACCESS_KEY |
IAM credentials |
AWS_S3_REGION_NAME |
Region (default us-east-1) |
AWS_S3_SIGNATURE_VERSION |
s3v4 by default |
TRANSFERS_BUCKET_NAME |
Bucket name |
TRANSFER_PRESIGNED_URL_EXPIRY |
Presigned URL TTL in seconds (default 600) |
TRANSFER_CHUNK_SIZE |
Multipart chunk size (default 25 MiB) |
TRANSFER_UPLOAD_PARALLELISM |
Concurrent part uploads (default 4) |
For IAM permissions required on the bucket, see docs/S3.md.
Schedule is defined in src/backend/transferts/celery_app.py.
| Task | Cadence | Effect |
|---|---|---|
expire_transfers_task |
hourly (3600 s) | Flips ACTIVE → EXPIRED past expires_at, deletes S3 files |
cleanup_abandoned_drafts_task |
every 6 h (21600 s) | Drops drafts older than 24 h |
sweep_orphan_s3_storage_task |
daily (86400 s) | Safety net — should report 0; non-zero signals a leak in a per-row path |
send_recipient_invitations_task |
on-demand | Triggered by finalize (email mode) and resend |
The audit log records the client IP. It is read from X-Forwarded-For
by XForwardedForMiddleware (src/backend/core/middlewares.py), which
takes the rightmost entry — the IP appended by the trusted edge
proxy (Scalingo's router on production). The leftmost entry is
client-controlled and spoofable.
In production the request chain is:
Client → Edge router (Scalingo) → HAProxy → Caddy → Gunicorn
For this to work, Caddy must propagate the incoming X-Forwarded-For
header as-is, not overwrite it. src/frontend/caddy/Caddyfile
sets:
header_up X-Forwarded-For {http.request.header.x-forwarded-for}If you change it to {remote_host}, Caddy overwrites the chain with
the address of its immediate peer (HAProxy, in the 10.0.0.x range),
and the audit log loses the real client IP. The comment block above
the first reverse_proxy directive in the Caddyfile explains the
two-hop topology.
Set USE_X_FORWARDED_FOR=True in the production environment to
activate the middleware.
Instances can optionally allow users to attach files from a Drive instance. When enabled, an "Attach from Drive" button appears in the transfer form. Files are downloaded client-side (using the user's Drive session) and uploaded through the regular multipart flow — no reference to Drive is stored.
Transferts side — set these environment variables:
| Variable | Required | Default | Description |
|---|---|---|---|
DRIVE_BASE_URL |
Yes | (empty — feature disabled) | Base URL of the Drive instance |
DRIVE_SDK_URL |
No | /sdk |
Path (or absolute URL) to the Drive SDK endpoint |
DRIVE_API_URL |
No | /api/v1.0 |
Path (or absolute URL) to the Drive API |
DRIVE_APP_NAME |
No | Drive |
Display name shown in UI labels |
Drive side — the Drive instance must allow the Transferts origin:
CORS_ALLOWED_ORIGINS=[..., "https://transferts.example.gouv.fr"]
SDK_CORS_ALLOWED_ORIGINS=[..., "https://transferts.example.gouv.fr"]
CORS_ALLOW_CREDENTIALS=TrueBoth CORS lists are required: CORS_ALLOWED_ORIGINS covers the HTTP fetch to download file bytes, SDK_CORS_ALLOWED_ORIGINS covers the postMessage channel used by the picker SDK.
MIT