Migration d'un CRM AppSheet vers Express + Base de données relationnelle
Remplacement d’un CRM AppSheet atteignant ses limites par un backend Express typé, une base relationnelle et une interface React — avec réconciliation vérifiée à 100%.
Projet client · anonymisé
Une petite équipe commerciale a dépassé AppSheet : synchronisations lentes, pas de véritable intégrité relationnelle, impossible à intégrer avec la facturation et tarification par utilisateur mal évolutive. Ils avaient besoin de la propriété de leurs données et d'une API que d'autres outils pourraient appeler.
TL;DR
| Aspects | Avant (AppSheet) | Après (Express + Base de données) |
|---|---|---|
| Modèle de données | Flat Google Sheets, nom du client dupliqué sur 5 onglets | Tables relationnelles normalisées (customers ↔ contacts ↔ deals) |
| Intégrité | Aucun — trois orthographes du même client | Clés étrangères, contraintes UNIQUE, CHECK |
| Intégration | Hacks Google Apps Script, copier-coller dans la facturation | API REST + webhooks sortants |
| Latence de synchronisation | 20 à 40 s aller-retour, pire sous charge | Requêtes locales, <100 ms |
| Échelle des coûts | Par utilisateur AppSheet (~ 5 à 10 $/siège/mois, escalade) | Hébergement à plat (~ 20 $/mois) |
| Piste d’audit | ”Dernière modification par” sur une cellule | Journal des événements en ajout uniquement |
Vous découvrirez les signes concrets indiquant qu’une équipe est devenue trop grande pour AppSheet, la cible l’architecture vers laquelle nous migrons vers et le modèle de migration sécurisée en cinq étapes (remodelage relationnel → validation → ETL idempotent → exécution parallèle → basculement) qui déplace une entreprise hors d’AppSheet sans perdre d’enregistrement ni geler l’équipe.
Le problème
AppSheet est un excellent outil pour la première version d’une application de données de terrain : pointez-le sur une feuille Google, rassemblez quelques formulaires et expédiez-les aux téléphones dans l’après-midi. Il cache la base de données. C’est sa force et, finalement, son plafond.
L’équipe dans ce cas – huit personnes commerciales et opérationnelles réparties sur deux fuseaux horaires – avait a vécu dans un CRM AppSheet pendant deux ans. Au moment où ils m’ont appelé, l’outil qui les obtenir de 0 → 1 leur coûtait activement chaque semaine :
- La latence de synchronisation dominait la journée de travail. Chaque édition faisait un aller-retour vers le Google Sheet sous-jacente. À environ 50 000 lignes, la synchronisation a pris 20 à 40 secondes, voire plus. sous modifications simultanées. L’équipe avait appris à regrouper les modifications pour éviter les décalages.
- Il n’y avait pas d’intégrité relationnelle. Sous l’application, c’était toujours un appartement Feuille, donc un nom de client tapé de trois manières produit trois « clients ». Le reportage était en permanence suspect. - Il n’a pas pu communiquer avec la facturation. Finance a copié les données de la transaction dans un fichier séparé. système chaque semaine, car AppSheet ne disposait d’aucune API que ses outils pouvaient appeler.
- Prix évolutif en fonction de l’effectif. Le modèle par utilisateur d’AppSheet signifiait que chaque nouveau Hire a ajouté un élément de campagne, tout en n’offrant plus de fonctionnalités.
Ils avaient besoin de la propriété de leurs données et d’une API que d’autres systèmes pourraient appeler, sans perdre deux ans de dossiers ou geler l’entreprise pendant un mois. C’est le forme de presque tous les projets de sortie AppSheet : pas une réécriture en soi, mais une migration forcée par un mur que l’outil n’a jamais été conçu pour surmonter.
Pour la version générale de cette histoire (feuilles de calcul en général), voir Quand Google Sheets arrête de évoluer.
Signes que vous êtes devenu trop grand pour AppSheet
Si vous envisagez de rester ou de migrer, ce sont les signaux que je recherche. Trois ou plus signifie que le coût du séjour dépasse déjà le coût de la migration.
1. La latence de synchronisation est le rythme de la journée de travail
AppSheet synchronise chaque modification avec le magasin de sauvegarde. Au fur et à mesure que le magasin grandit et le nombre d’utilisateurs simultanés augmente, les synchronisations s’étendent de « instantanées » à « attendre ». Quand le L’équipe commence à planifier ses modifications en fonction du temps de synchronisation, l’outil les combat.
2. Vous simulez des relations avec déréférencement
Les expressions [_thisrow]/déréférencement d’AppSheet simulent des relations au-dessus de
draps plats. Ils fonctionnent jusqu’à ce qu’ils ne le fassent plus : lignes orphelines, références brisées après
un changement de nom, des rapports qui renvoient discrètement les mauvaises lignes. Si vous avez écrit un texte de 15 lignes
expression pour émuler un JOIN, la couche de base de données demande à être réelle.
3. La tarification par siège est l’élément de campagne que les gens remarquent
La tarification sans code par utilisateur est juste pour cinq utilisateurs et pénible pour vingt. Quand le La facture mensuelle est une conversation récurrente, vous avez heurté le mur économique.
4. Un autre système doit lire ou écrire vos donnéesLe moment où la facturation, un ERP, un entrepôt de données ou une automatisation doit intervenir
vos données CRM, l’absence d’API externe stable dans AppSheet devient le bloqueur. Vous êtes de retour au copier-coller ou à la fragile colle Apps Script.
5. Vous avez besoin d’une piste d’audit qu’AppSheet ne peut pas fournir
La conformité, la finance ou un litige client demande finalement “qui a changé cela et quand.” La « dernière modification par » d’une cellule n’est pas un journal d’audit.
Architecture
Une répartition nette : API Express + base de données relationnelle comme source de vérité, un React SPA pour l’équipe et un pont ETL unique qui a intégré les feuilles d’AppSheet dans tables relationnelles normalisées.

AppSheet (Google Sheets) ──ETL──▶ Relational DB (normalized)
│
▼
Express API (REST)
│
┌─────────────┴─────────────┐
▼ ▼
React SPA Billing / webhooks
- Couche de requêtes typées pour les requêtes sécurisées et les scripts de migration (les modifications de schéma sont expédiées sous forme de fichiers de migration révisés).
- Validation Zod à chaque limite de route, et encore à la limite ETL.
- Une période fantôme en lecture seule : les deux systèmes ont fonctionné en parallèle pendant deux semaines tandis que nous avons réconcilié le nombre de lignes tous les soirs.
Le modèle de migration sûre
C’est la procédure que j’utilise pour tout déplacement d’une feuille de calcul vers une base de données, spécialisée ici pour AppSheet avec basculement en direct en parallèle.
Étape 1 — Modélisez les données de manière relationnelle, et non à l’échelle 1:1 des feuilles
Les onglets plats d’AppSheet comportaient une colonne « client » dupliquée sur cinq feuilles. Le le premier travail consiste à normaliser et à remplir les clés étrangères. Tout en aval — la facturation, le reporting, la déduplication — cela dépend de cela.
CREATE TABLE customers (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
name text NOT NULL,
email citext UNIQUE NOT NULL,
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE contacts (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
customer_id uuid NOT NULL REFERENCES customers(id) ON DELETE CASCADE,
name text NOT NULL,
email citext,
phone text
);
CREATE TABLE deals (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
customer_id uuid NOT NULL REFERENCES customers(id),
stage text NOT NULL CHECK (stage IN ('lead','qualified','won','lost')),
amount_cents integer NOT NULL DEFAULT 0,
closed_at timestamptz,
-- the ETL cursor anchor: every migrated row keeps its AppSheet "updatedAt"
appsheet_row_updated_at timestamptz NOT NULL
);
CREATE INDEX deals_customer_idx ON deals(customer_id);
Une contrainte CHECK sur stage et une contrainte UNIQUE sur email font la différence
entre des données propres et trois semaines de nettoyage avant de pouvoir faire confiance à un rapport.
Étape 2 — Validez avant de migrer
AppSheet exporte les nombres sous forme de texte, permet aux e-mails d’être mal formés et les rend orphelins références. Un passage Zod sur les lignes exportées fait apparaître des déchets avant qu’ils n’atteignent la base de données — dans ce projet, environ 3 % des lignes ont échoué à la validation et ont été routées vers une file d’attente de révision au lieu de la base de données.
const AppSheetDeal = z.object({
DealID: z.string().min(1),
Customer: z.string().min(1),
Stage: z.enum(["lead", "qualified", "won", "lost"]),
Amount: z.string().regex(/^\d+(\.\d{1,2})?$/), // exported as text, not a number
UpdatedAt: z.string(),
});
export type AppSheetDeal = z.infer<typeof AppSheetDeal>;
Les lignes qui échouent atterrissent dans une table migration_rejections avec la raison, donc rien
est lâché silencieusement et rien de sale ne pénètre dans le système propre.
Étape 3 — ETL idempotent avec un curseur
Chaque ligne AppSheet comporte un updatedAt. L’importateur suit le dernier vu
horodatage par table, donc réexécuter l’ETL ne fait jamais de double insertion et toujours
rattrape. L’idempotence est ce qui rend l’exécution parallèle de l’étape 4 sûre.
async function importDealsSince(cursor: Date): Promise<number> {
const rows = await appSheet.list("Deals", {
// AppSheet filters client-side on the export; the cursor narrows the set
filter: (row) => new Date(row.UpdatedAt) > cursor,
});
for (const row of rows) {
const parsed = AppSheetDeal.parse(row); // Step 2 validation
const customer = await upsertCustomerByName(parsed.Customer);
await db
.insert(deals)
.values({
customerId: customer.id,
stage: parsed.Stage,
amountCents: Math.round(parseFloat(parsed.Amount) * 100),
appsheetRowUpdatedAt: new Date(parsed.UpdatedAt),
})
.onConflictDoUpdate({ // idempotent re-runs
target: deals.id,
set: {
stage: parsed.Stage,
amountCents: sql`excluded.amount_cents`,
appsheetRowUpdatedAt: new Date(parsed.UpdatedAt),
},
});
}
return rows.length;
}
Étape 4 — Exécution parallèle avec réconciliation nocturne
Pendant deux semaines, les deux systèmes ont fonctionné en direct. Les écritures allaient toujours vers AppSheet (où l’équipe était confortable); l’ETL les a mis en miroir dans la base de données tous les soirs. Chaque matin un le travail automatisé a comparé les deux et a signalé une dérive.
-- Per-table count drift between the AppSheet snapshot and database
SELECT 'deals' AS table_name,
a.row_count AS appsheet,
d.row_count AS db_count,
a.row_count - d.row_count AS drift
FROM appsheet_snapshot a
JOIN db_counts d USING (table_name);
Une correspondance de nombre est nécessaire mais pas suffisante, le travail a donc également exécuté une analyse au niveau des lignes. vérification de hachage sur un échantillon de 1 % pour détecter les modifications qui ont maintenu les comptes stables mais modifiés contenu. L’exécution parallèle a détecté trois bogues de cartographie qu’un script d’exécution à sec avait détecté. manqué - exactement son but.
Étape 5 — Transition, avec un miroir de lecture pour les récalcitrants
Lors du basculement, les écritures sont transférées vers la nouvelle application. AppSheet est resté en vie en tant que lecture seule miroir pendant deux semaines via une tâche d’exportation planifiée, afin que quiconque ne soit pas encore sur le nouveau le flux a gardé la visibilité sans produire de données divergentes. Ensuite, nous l’avons éteint.
Décisions intéressantes
- Modèle relationnel, et non 1:1 avec les feuilles. La colonne client dupliquée était à l’origine des problèmes de reporting. Normalisation de la première facturation déverrouillée et signalé comme des effets secondaires presque gratuits.
- Conservez AppSheet comme miroir de lecture pendant le basculement. Il a supprimé le message “tout le monde doit “changer immédiatement” et a donné aux récalcitrants un filet de sécurité.
- Ne reproduisez pas l’expérience utilisateur d’AppSheet écran par écran. Une migration est rare chance de corriger les flux de travail qui s’accumulent autour des limites d’un outil. Nous avons reconstruit le trois flux que les gens utilisaient réellement et en abandonnaient deux qui n’existaient que pour travailler autour d’AppSheet.
- Rendre l’ETL réexécutable de par sa conception. Idempotent
onConflictDoUpdateplus le le curseur d’horodatage signifiait que nous pouvions exécuter l’importateur toutes les heures pendant la période d’ombre sans crainte.
Leçons apprises
- Normaliser les dates et heures au format UTC à l’arrivée. AppSheet stocke les dates et heures comme
heure locale de l’éditeur, sans étiquette. Une conversion unique en UTC avec un explicite
La colonne
tza empêché une dérive subtile dans les rapports. - Validez avant de migrer. Le pass Zod qui a fait apparaître environ 3 % de données indésirables a été l’heure à effet de levier le plus élevé du projet.
- L’exécution parallèle bat le big-bang. L’ombre de deux semaines a détecté des bugs de cartographie l’essai à sec n’a pas pu et a laissé l’équipe continuer à travailler pendant que nous vérifiions.
- Budget pour les données, pas pour l’application. L’interface utilisateur de React était la partie la plus facile ; déduplication les clients et la réparation des références ont pris plus de temps que la création de la nouvelle interface.
Avantages et inconvénients
Avantages : propriété totale des données, API stable que d’autres services peuvent appeler, prévisible un coût forfaitaire, de réelles contraintes d’intégrité et un journal d’audit approprié.
Inconvénients : vous exploitez désormais une base de données : les sauvegardes, la surveillance et les migrations vous appartiennent. AppSheet a caché tout cela. Le commerce est une responsabilité opérationnelle de la propriété et la capacité, et c’est le bon métier une fois que vous avez heurté le mur au-dessus.
FAQ
Pouvons-nous conserver AppSheet pour la capture des données sur le terrain ? Oui. AppSheet reste un client léger performant. Après la migration, certaines équipes le conservent uniquement pour la capture mobile hors ligne, en écrivant sur la nouvelle API au lieu d’un support feuille. Ce que vous laissez derrière vous, c’est AppSheet en tant que système d’enregistrement, et non en tant qu’interface utilisateur.
Combien de temps prend une migration comme celle-ci ? Pour environ 50 000 lignes et une équipe comprise entre 8 et 20, attendez-vous à 3 à 6 semaines de temps écoulé, de dont environ la moitié est le nettoyage des données et la vérification en parallèle - pas codage. L’interface utilisateur est la partie rapide.
Et si nous ne sommes pas prêts à quitter AppSheet ? Exécutez la vérification des cinq signes ci-dessus. Si vous en voyez moins de trois, le coût du séjour est encore inférieur au coût de la migration, et c’est une raison légitime d’attendre. Migrez lorsque le mur est clairement devant vous, et non par précaution.
Allons-nous perdre des données ? Pas avec ce modèle. La validation achemine les lignes incorrectes vers une file d’attente de révision, l’ETL est idempotent, et l’exécution parallèle prouve la parité avant le basculement. « Sans perdre un single record” est la norme, pas l’espoir.
Vous voulez ceci pour votre application AppSheet ?
Si votre équipe se heurte au mur d’AppSheet : synchronisations lentes, pas d’API, coûts par siège escalade - je cartographie la sortie en un seul appel de découverte : quoi migrer, ce que le à quoi ressemble l’architecture cible et un calendrier réaliste. Vous conservez vos données et votre élan.Réserver un appel découverte →
Évaluateur interactif du risque et du ROI Google Sheets
Évaluez la santé de votre feuille et le temps hebdomadaire perdu estimé.