Trouvez toutes les destinations TGVmax réservables depuis votre gare, sur une carte interactive.
Gratuit, sans publicité, sans compte, sans cookie. Open source sous AGPL-3.0.
Vous avez un abonnement TGVmax et trois jours de libres. L'application SNCF vous demande où vous voulez aller — mais la vraie question est l'inverse : où peut-on aller ? Trouver la réponse suppose de tester les destinations une par une.
Trainquillou renverse la question. Vous donnez une gare et une date, il affiche sur une carte toutes les destinations où il reste des places TGVmax.
Une gare de départ, une gare d'arrivée (laissez-la vide pour dire n'importe où) et une date. Les cinq façons de chercher découlent de ces trois champs, il n'y a pas de mode à choisir :
| Vous remplissez | Question à laquelle ça répond |
|---|---|
| Depuis seul | Où puis-je aller depuis Lyon samedi ? |
| Vers seul | D'où peut-on rejoindre Biarritz ce jour-là ? |
| Depuis + aller-retour | Je pars vendredi soir et je rentre dimanche : quelles destinations ont les deux trajets réservables ? |
| Depuis + plusieurs jours | Sur la semaine du 10, quelles destinations sont joignables, et combien de jours chacune ? |
| Depuis + Vers | Pas de TGVmax direct Nantes-Grenoble : quelles correspondances le rendent possible ? |
Et aussi :
- Carte interactive MapLibre GL : les destinations sont reliées à votre gare, et un clic sur une ville ouvre ses horaires, sa durée de trajet et ses liens de réservation.
- Filtres par durée de trajet et par période de départ, appliqués à la liste et à la carte — cocher « ≤ 2h » dessine le cercle des deux heures autour de votre gare.
- Tri par durée, par ordre alphabétique ou par notoriété touristique.
- Dates de retour disponibles pour chaque destination, en un clic.
- Liens de réservation vers SNCF Connect et Trainline.
- Recherches partageables : l'URL contient toute la recherche, un lien collé rejoue le même écran.
- Jours de repli suggérés quand la date demandée ne donne aucun itinéraire.
- Voyage en plusieurs étapes : deux à six gares dans l'ordre, une boucle qui revient à son
point de départ, ou une excursion à la journée avec un temps minimum sur place. Chaque étape
indique les jours où elle est réservable, et le trajet complet celui qui arrive le plus tôt.
Pas encore annoncé : le lien n'apparaît que si
NUXT_PUBLIC_PLANNERest activée.
Deux services : l'application Nuxt et l'API Django qui la sert en données. Prérequis : Node 20+, pnpm, et uv pour la partie Python.
git clone https://github.com/csanchez-jetdev/trainquillou.git
cd trainquillou
pnpm install
# API : migrations, puis une première ingestion de l'open data SNCF (~6 s)
DJANGO_DEBUG=1 uv run --directory backend manage.py migrate
DJANGO_DEBUG=1 uv run --directory backend manage.py fetch_tgvmax --now
# Deux terminaux
DJANGO_DEBUG=1 uv run --directory backend manage.py runserver 8000
pnpm dev # http://localhost:3001, /api/* est relayé vers le port 8000Aucune clé d'API, aucun compte : l'open data SNCF est ouvert et le référentiel des gares est
embarqué dans le dépôt. DJANGO_DEBUG=1 fournit une clé secrète de développement ; hors
développement, son absence arrête le démarrage plutôt que de signer avec une valeur publique.
pnpm test # tests TypeScript, sans accès réseau
uv run --directory backend pytest # tests Python, sans accès réseau
pnpm build # build de productionLocust rejoue le parcours du client sur les huit routes publiques (recherche, puis itinéraires et dates de retour d'une destination réellement renvoyée). Les gares et les jours interrogés sont lus sur l'instance visée au démarrage du tir.
cd backend
uv run --group loadtest locust -f loadtest/locustfile.py --host http://127.0.0.1:8000
# sans interface, 100 utilisateurs pendant une minute :
uv run --group loadtest locust -f loadtest/locustfile.py --host http://127.0.0.1:8000 \
--headless -u 100 -r 20 -t 60sChaque utilisateur simulé porte sa propre adresse dans X-Forwarded-For, sinon le budget de
300 requêtes par minute et par adresse refuserait le tir au bout de quelques secondes.
TRAINQUILLOU_SPOOF_IP=0 désactive cette usurpation, pour mesurer justement ce refus.
Un tir contre un serveur derrière Caddy mesure le proxy et son propre étranglement : viser
gunicorn directement pour mesurer l'API.
Trois conteneurs : l'application Nuxt, l'API Django, et un proxy qui répartit /api/* vers la
seconde et le reste vers la première. La base est un fichier SQLite sur volume, alimenté une fois
par jour.
docker compose -f infra/compose.yml up -d --build
docker compose -f infra/compose.yml exec django python manage.py migrate
docker compose -f infra/compose.yml exec django python manage.py fetch_tgvmaxinfra/ contient le Caddyfile, les deux Dockerfile, compose.yml et deploy.sh. Comptez
environ 700 Mo de RAM pour l'ensemble. Rien à configurer sur l'hôte : l'ingestion quotidienne se
programme elle-même dans la file, chaque exécution armant la suivante.
/admin/ donne à lire ce que l'ingestion a écrit : fraîcheur de la donnée, volumes, offres par
jour, et l'état des dernières tâches de fond avec leur trace en cas d'échec. Tout y est en lecture
seule, y compris pour un superutilisateur — une offre modifiée à la main serait écrasée par l'export
suivant, et une ligne d'historique supprimée ne se retrouve pas.
docker compose -f infra/compose.yml exec django python manage.py createsuperuserLe proxy place une authentification HTTP devant /admin/, indépendante de celle de Django : le
formulaire de connexion n'est jamais atteignable directement. deploy.sh génère ces identifiants
au premier déploiement et les affiche une seule fois.
La base est le seul état non reconstructible du projet. Les offres du jour se réingèrent en
quelques secondes, mais l'historique de disponibilité accumulé jour après jour, non : l'amont est
une fenêtre glissante de 30 jours. Chaque ingestion en écrit donc une copie compressée dans
sauvegardes/, sur le volume, et garde les trente dernières. Sur le même disque que la base :
cela protège d'une migration ratée ou d'une suppression, pas de la perte du disque — une copie
hors serveur reste à brancher.
| Variable | Effet |
|---|---|
DJANGO_SECRET_KEY |
Requise hors développement, sinon l'API refuse de démarrer |
DJANGO_ALLOWED_HOSTS |
Noms d'hôtes acceptés, séparés par des virgules |
DJANGO_DB_PATH |
Chemin du fichier SQLite |
DJANGO_BACKUP_DIR |
Où l'ingestion dépose ses copies (défaut : sauvegardes/ à côté de la base) |
NUXT_PUBLIC_SITE_URL |
URL publique, pour les liens canoniques et le sitemap |
NUXT_PUBLIC_RYBBIT_SITE_ID |
Active la mesure d'audience Rybbit avec votre identifiant. Non définie, aucun script tiers n'est chargé |
NUXT_PUBLIC_RYBBIT_HOST |
Instance Rybbit qui sert le script et reçoit les mesures (défaut : https://app.rybbit.io, le service hébergé) |
BACKEND_URL |
En développement seulement : où le serveur Nuxt relaie /api/* (défaut http://127.0.0.1:8000) |
NUXT_PUBLIC_PLANNER |
true affiche le lien vers le planificateur multi-étapes. Sans elle, la page reste joignable par son URL mais n'est liée de nulle part. Activée d'office en développement |
La licence AGPL-3.0 vous autorise à héberger votre propre instance, y compris modifiée, à condition de publier vos modifications.
Un abonnement SNCF pour les 16-27 ans qui donne accès à un nombre illimité de trajets sur les trains éligibles, dans la limite des places réservées à l'abonnement. Ces places sont contingentées : un train peut circuler sans être ouvert à l'abonnement. C'est ce contingent que Trainquillou rend visible.
La SNCF a renommé l'offre MAX JEUNE en 2023. « TGVmax » reste le nom sous lequel la plupart des abonnés la connaissent, et celui du dataset open data : les deux termes cohabitent donc dans l'interface et dans les métadonnées, sans en privilégier un.
Non. Il montre où il reste des places et renvoie vers SNCF Connect ou Trainline pour la réservation. Il n'est pas affilié à la SNCF.
Parce que les places à 0 € n'ouvrent que 30 jours avant le départ. Ce n'est pas une limite de Trainquillou : le jeu de données SNCF s'appelle littéralement « Disponibilité à 30 jours de places MAX JEUNE et MAX SENIOR ouvertes à la réservation » et ne contient rien au-delà. Le sélecteur de date s'arrête donc à cette échéance plutôt que de renvoyer une page vide qui ressemblerait à une panne.
Elles viennent du dataset open data SNCF tgvmax, que la SNCF republie une fois par jour, vers
04h30 UTC. Trainquillou l'ingère chaque matin et sert ses réponses depuis cette copie : interroger
l'amont plus souvent ne donnerait rien de plus récent. Une place peut donc partir entre l'affichage
et votre réservation.
Non, et ce n'est pas prévu. Il n'y a ni compte, ni paywall, ni publicité. Trainquillou est un service gratuit, sans but lucratif.
L'instance officielle mesure son audience avec Rybbit : sans cookie, sans identifiant persistant, sans profil publicitaire, et sans stockage des adresses IP, politique de confidentialité.
Au-delà des pages vues, trois événements anonymes sont envoyés : search (mode, gare, nombre de
résultats), booking_click (revendeur, ville, mode) et returns_lookup (ville). Ils indiquent
quelles fonctionnalités servent — pas qui les utilise.
Le site lui-même tourne sur un VPS OVH à Gravelines (Nord, France), sans CDN intermédiaire.
Rien de tout cela n'est actif dans le code que vous clonez : la mesure ne s'active que si vous fournissez votre propre identifiant au build (voir ci-dessous). Une instance auto-hébergée n'envoie donc rien, ni à nous ni à personne.
Les 341 gares présentes dans le dataset TGVmax, dont les destinations à l'étranger : Bruxelles, Genève, Luxembourg, Barcelone, Milan, Berlin, Munich, Francfort.
Depuis le référentiel SNCF « liste des gares », embarqué dans le dépôt. Les libellés des deux jeux de données ne correspondent presque jamais à l'identique, donc la résolution passe par une normalisation puis deux tables explicites — voir docs/architecture.md. Une gare dont les coordonnées restent introuvables est listée sans marqueur, jamais placée au hasard.
Oui, sous AGPL-3.0 : vous pouvez l'utiliser, le modifier et l'héberger, à condition de publier le code source de votre version, y compris si vous ne la distribuez que comme service en ligne.
Le client ne parle qu'à ces routes, jamais directement à la SNCF — pour le CORS et un format
stable. Elles lisent la base locale, jamais l'amont : une recherche répond en moins d'une
milliseconde. Utilisables telles quelles si vous auto-hébergez, et décrites en OpenAPI sur
/api/openapi.json.
| Route | Description |
|---|---|
GET /api/stations |
Libellés des gares, pour l'autocomplétion |
GET /api/search?origin=&date=&mode=&dateTo= |
Destinations réservables, enrichies des coordonnées |
GET /api/returns?origin=&dest=&from= |
Dates de retour disponibles pour un trajet |
GET /api/route?from=&to=&date=&stops= |
Itinéraires avec correspondances |
GET /api/multileg?stops=A|B|C&date=&dateTo=&minStay= |
Voyage en plusieurs étapes, boucle, excursion |
GET /api/health |
État du service et de sa base |
mode vaut from (défaut), to, roundtrip ou range. Les modes roundtrip et range
exigent dateTo.
curl 'http://localhost:3001/api/search?origin=LYON%20(intramuros)&date=2026-08-14'Nuxt 4 · Vue 3 · TypeScript strict · Tailwind v4 · MapLibre GL — pour l'interface Django 6 · Django Ninja · SQLite — pour l'API et l'ingestion quotidienne
Personne n'interroge l'open data SNCF pendant qu'un visiteur attend : un export quotidien (35 000 à 45 000 offres, six secondes) alimente une base locale que toutes les routes lisent. C'est aussi ce stockage qui donne un historique de disponibilité, là où l'amont est une fenêtre glissante de 30 jours.
Le détail des choix et de leurs raisons est dans docs/architecture.md.
Si vous attaquez le dataset TGVmax pour votre propre projet, docs/sncf-api.md documente ses comportements non documentés — dont un endpoint qui tronque silencieusement la liste des gares aux deux tiers.
Trois jeux sont pré-calculés hors ligne et commités, pour que l'application n'ait aucune dépendance réseau à l'exécution. À relancer ponctuellement, ces données évoluent lentement.
uv run scripts/build-popularity.py # score de notoriété touristique par gare
uv run scripts/build-booking.py # slugs de ville pour les liens de réservation
uv run scripts/build-rail-network.py # tracé du réseau ferré, fond de la carteIls nécessitent uv ; les dépendances sont déclarées dans l'en-tête de chaque script.
L'image d'accueil est l'élément LCP de la page : ses variantes sont générées à la main et
commitées, plutôt que produites par un module Nuxt. L'image ne change jamais, et .output/
doit rester sans binaire natif pour qu'un build lancé depuis un Mac arm64 tourne dans le
conteneur amd64 (voir infra/deploy.sh). Deux variantes AVIF (69 % de moins que le JPEG à
qualité indiscernable), hero.jpg en repli pour les navigateurs sans AVIF.
Pour les régénérer, avec vips (brew install vips) :
vips thumbnail public/hero.jpg 'public/hero-800.avif[Q=58]' 800
vips thumbnail public/hero.jpg 'public/hero-1672.avif[Q=58]' 1672
vips thumbnail public/hero.jpg public/hero-800.jpg 800
vips thumbnail public/hero.jpg public/hero-1200.jpg 1200Ne pas utiliser sips pour l'AVIF : il produit un fichier dont Chromium lit les dimensions
mais pas les pixels, et le hero s'affiche vide.
Les contributions sont bienvenues. Quelques conventions :
- Commits conventionnels (
feat:,fix:,docs:,chore:). - TypeScript strict, pas de
anysilencieux. - La logique pure va dans
backend/tgvmax/et se teste sans réseau. Les tests ne doivent jamais appeler l'API SNCF : utilisez les fixtures debackend/tests/fixtures/. - Pas de paywall, pas d'authentification, pas de publicité, pas de profilage publicitaire. Ce n'est pas négociable, c'est la raison d'être du projet. La mesure d'audience de l'instance officielle est sans cookie et désactivée par défaut dans le code.
Avant d'ouvrir une PR : pnpm test && pnpm build && uv run --directory backend pytest.
- Disponibilités TGVmax : open data SNCF, dataset
tgvmax. - Coordonnées des gares : référentiel SNCF « liste des gares ».
- Tracé des voies sur la carte : open data SNCF, dataset
vitesse-maximale-nominale-sur-ligne— le réseau ferré national exploité, avec la vitesse de chaque tronçon. - Coordonnées d'appoint (gares étrangères, arrêts hors référentiel) : OpenStreetMap via Nominatim.
- Score de notoriété : nombre d'éditions linguistiques Wikipédia de la commune.
- Fond de carte : OpenStreetMap, rendu MapLibre GL.
Les jeux de données SNCF sont diffusés sous ODbL : attribution obligatoire et partage à
l'identique des bases dérivées. Les fichiers qui en dérivent — backend/tgvmax/data/ et
public/rail-network.geojson — restent sous ODbL, indépendamment de la licence du code.
AGPL-3.0. Vous pouvez héberger, modifier et redistribuer ce projet ; toute version hébergée modifiée doit republier son code source. Autrement dit : personne ne peut refermer Trainquillou derrière un paywall.