Sansoif, l’app de SXN Labs pour suivre sa consommation d’alcool entre amis, quitte Supabase pour une application Rails. Le serveur change en un déploiement, mais les apps installées sur les téléphones ne suivent pas : chaque version publiée continue de tourner tant que quelqu’un s’en sert, et il faut toutes les servir.

Les apps iOS, Android et Apple Watch appelaient 110 fonctions RPC et dix Edge Functions. Ces fonctions RPC, c’est du SQL dégueulasse, dur à lire et à maintenir. Pour être franc, c’est une plaie, même si c’est un agent et pas moi qui les lit et les maintient. Deux des Edge Functions n’avaient jamais été déployées en production, et la montre n’a pas fonctionné en autonomie pendant deux mois sans qu’aucune erreur ne remonte. Je voulais une stack que je maîtrise. La logique métier vit maintenant dans Rails, derrière une API REST décrite par un fichier OpenAPI, et les apps affichent ce que le serveur calcule.

Les anciens builds restent sur Supabase

Les builds publiés avant la migration parlent directement à Supabase. Chacun met à jour quand il veut, et Apple et Google relisent chaque version avant de la publier. Ces builds ne peuvent pas non plus recevoir la réponse HTTP 426 qui demande aux versions trop anciennes de se mettre à jour, puisqu’ils ne parlent pas à Rails. Couper Supabase les aurait arrêtés sans explication, donc Supabase reste en service et ses données sont copiées vers Rails.

Une synchronisation à sens unique

Un job Rails copie Supabase vers Rails toutes les quinze minutes, identités Apple et Google comprises, pour qu’une personne retrouve son profil en se connectant à la nouvelle app. À sa première session, Rails note la date dans people.rails_signed_in_at et devient la référence pour elle. Ses données ne sont plus copiées depuis Supabase, pas plus que les amitiés et les pokes qui la concernent, ni les groupes dont elle est propriétaire.

À gauche, les anciens builds écrivent dans Supabase. À droite, les nouveaux builds écrivent dans Rails. Une flèche à sens unique va de Supabase vers Rails, toutes les quinze minutes. Sous Rails, la première session d'une personne pose rails_signed_in_at, et la synchronisation ne touche plus ses données personnelles. En bas, la règle d'extinction : sept jours sans aucune écriture dans Supabase.
Les deux backends coexistent jusqu'à l'extinction, et chaque personne bascule à sa première connexion sur la nouvelle app.

J’ai écarté la synchronisation dans les deux sens, qui demandait plusieurs jours et une règle de conflit par table. Ce sont les personnes restées sur un ancien build qui en paient le prix. Elles ne voient plus l’activité de leurs amis passés sur Rails, et si le propriétaire d’un groupe a déjà basculé, un départ fait depuis l’ancienne app n’apparaît pas côté Rails. Une saisie faite sur un autre appareil après la dernière copie peut aussi manquer, même si le client iOS renvoie le journal conservé sur le téléphone.

Le premier passage a pris 36 secondes pour 48 personnes, 1 620 journées et 8 avatars, sans erreur. Il a aussi recopié un défaut des anciens builds, 55 000 invitations pour 5 groupes, parce qu’une ancienne fonction en créait une à chaque appel et que ces builds l’appellent en boucle. Je les copie telles quelles plutôt que de corriger la source pendant la transition.

Mesurer avant d’éteindre

Une carte du dashboard affiche combien de personnes sont encore sur Supabase et la date de la dernière écriture. Je couperai Supabase après sept jours sans aucune écriture, et au plus tard trente jours après la publication des apps Rails. Les versions iOS et Android sont publiées. Ceux qui mettront à jour après l’arrêt retrouveront dans Rails les données reprises par la synchronisation.

Les builds Rails vieilliront à leur tour

Les apps Rails publiées aujourd’hui seront les anciens builds de demain, et l’API continuera d’évoluer pendant qu’elles restent installées. Le client iOS généré depuis le contrat OpenAPI refusait toute une réponse dès qu’une énumération contenait une valeur inconnue. Ajouter un plan ou un badge aurait fait échouer, dans toutes les versions déjà publiées, le décodage des réponses contenant la nouvelle valeur.

Les énumérations des réponses sont maintenant lues comme des chaînes, avec une valeur de repli décidée écran par écran. Un plan inconnu s’affiche comme un plan payant, avec les limites renvoyées et appliquées par le serveur, et un badge inconnu n’apparaît pas sans empêcher le reste du tableau de s’afficher. Pour les versions Rails devenues trop anciennes, le 426 reste en place : contrairement aux builds Supabase, elles parlent au serveur qui peut leur demander de se mettre à jour.