Migration eines AppSheet CRM zur Express + relationalen Datenbank

Ersatz eines an Grenzen stoßenden AppSheet-CRMs durch ein typisiertes Express-Backend, relationale Datenbank und React-UI — mit 100% verifiziertem Datensatzabgleich.

Kundenprojekt · anonymisiert

Ein kleines Vertriebsteam ist über AppSheet hinausgewachsen: langsame Synchronisierungen, keine wirkliche relationale Integrität, keine Integration in die Abrechnung und schlechte Preisskalierung pro Benutzer. Sie brauchten den Besitz ihrer Daten und eine API, die andere Tools aufrufen konnten.

TL;DR

AspektVorher (AppSheet)Nachher (Express + Datenbank)
DatenmodellFlache Google Sheets, Kundenname auf 5 Registerkarten dupliziertNormalisierte relationale Tabellen (customers ↔ contacts ↔ deals)
IntegritätKeine – drei Schreibweisen desselben KundenFremdschlüssel, UNIQUE-, CHECK-Einschränkungen
IntegrationGoogle Apps Script-Hacks, kopieren und in die Abrechnung einfügenREST-API + ausgehende Webhooks
Latenz synchronisieren20–40 Sekunden Hin- und Rückfahrt, schlimmer unter LastLokale Abfragen, <100 ms
KostenskalierungPro AppSheet-Benutzer (~5–10 $/Sitzplatz/Monat, gestiegen)Flat-Hosting (~20 $/Monat)
Audit-Trail„Zuletzt bearbeitet von“ in einer ZelleNur anhängendes Ereignisprotokoll

Sie lernen die konkreten Anzeichen dafür kennen, dass ein Team dem Ziel AppSheet entwachsen ist Architektur, auf die wir migrieren, und das fünfstufige Muster für die sichere Migration (relationale Neumodellierung → Validierung → idempotentes ETL → Parallellauf → Umstellung) das verschiebt ein Unternehmen aus AppSheet, ohne einen Datensatz zu verlieren oder das Team einzufrieren.

Das Problem

AppSheet ist ein hervorragendes Tool für die erste Version einer Felddaten-App: point it Ziehen Sie in einem Google Sheet einige Formulare zusammen und senden Sie sie an einem Nachmittag an Mobiltelefone. Es verbirgt die Datenbank. Das ist seine Stärke und letztendlich seine Obergrenze.

Das Team in diesem Fall – acht Vertriebs- und Betriebsmitarbeiter in zwei Zeitzonen – hatte lebte zwei Jahre lang in einem AppSheet CRM. Als sie mich anriefen, war das Werkzeug da Habe sie von 0→1 bekommen, habe sie jede Woche aktiv gekostet:

Sie brauchten den Besitz ihrer Daten und eine API, die andere Systeme aufrufen konnten – ohne Verlust von Aufzeichnungen aus zwei Jahren oder Einfrieren des Geschäfts für einen Monat. Das ist das Form fast jedes AppSheet-Exit-Projekts: kein Umschreiben um seiner selbst willen, aber Eine durch eine Mauer erzwungene Migration, für deren Bewältigung das Tool nie gedacht war.

Die allgemeine Version dieser Geschichte (im Großen und Ganzen Tabellenkalkulationen) finden Sie unter Wenn Google Sheets die Skalierung stoppt.

Anzeichen dafür, dass Sie AppSheet entwachsen sind

Wenn Sie überlegen, ob Sie bleiben oder migrieren sollen, sind dies die Signale, nach denen ich suche. Drei oder mehr bedeuten, dass die Kosten für den Aufenthalt bereits die Kosten für die Migration übersteigen.

1. Die Synchronisierungslatenz ist der Rhythmus des Arbeitstages

AppSheet synchronisiert jede Änderung zurück mit dem Sicherungsspeicher. Wenn der Laden wächst und Gleichzeitige Benutzer nehmen zu, Synchronisierungen reichen von „sofort“ bis „warten darauf“. Wenn die Wenn das Team rund um die Synchronisierungszeit mit der Planung seiner Änderungen beginnt, bekämpft das Tool sie.

2. Du täuschst Beziehungen durch Dereferenzierung vor

Die [_thisrow]/Dereferenzierungsausdrücke von AppSheet simulieren Beziehungen darüber flache blätter. Sie funktionieren, bis sie es nicht mehr tun – verwaiste Zeilen, kaputte Referenzen danach Eine Umbenennung führt zu Berichten, die stillschweigend die falschen Zeilen zurückgeben. Wenn Sie eine 15-zeilige geschrieben haben Wenn ein Ausdruck einen JOIN emuliert, verlangt die Datenbankschicht, dass er real ist.

3. Die Preisgestaltung pro Sitzplatz ist die Werbebuchung, die den Leuten auffällt

Der No-Code-Preis pro Benutzer ist bei fünf Benutzern fair und bei zwanzig Benutzern schmerzhaft. Wenn die Wenn die monatliche Rechnung ein wiederkehrendes Gespräch ist, sind Sie an der wirtschaftlichen Grenze angelangt.

4. Ein anderes System muss Ihre Daten lesen oder schreibenDer Moment, in dem die Abrechnung, ein ERP, ein Data Warehouse oder eine Automatisierung miteinander in Berührung kommen müssen

Wenn Sie Ihre CRM-Daten nicht verwalten, wird das Fehlen einer stabilen externen API bei AppSheet zum Hindernis. Sie sind zurück zum Kopieren und Einfügen oder zum fragilen Apps Script-Kleber.

5. Sie benötigen einen Audit-Trail, den AppSheet nicht bieten kann

Compliance, Finanzen oder ein Kundenstreit fragen schließlich: „Wer hat das geändert?“ wann.“ Die Angabe „Zuletzt bearbeitet von“ einer Zelle ist kein Prüfprotokoll.

Architektur

Eine saubere Aufteilung: Express API + relationale Datenbank als Quelle der Wahrheit, ein React SPA für das Team und eine einmalige ETL-Brücke, die die Blätter von AppSheet einzog normalisierte relationale Tabellen.

AppSheet to Express + Datenbankmigrationsarchitektur

AppSheet (Google Sheets) ──ETL──▶  Relational DB (normalized)


                                  Express API (REST)

                          ┌─────────────┴─────────────┐
                          ▼                           ▼
                    React SPA               Billing / webhooks

Das sichere Migrationsmuster

Dies ist das Verfahren, das ich für alle Tabellenkalkulations-zu-Datenbank-Verschiebungen verwende, hier speziell für AppSheet mit Live-Parallellauf-Umstellung.

Schritt 1 – Modellieren Sie die Daten relational, nicht als 1:1 der Blätter

Die flachen Registerkarten von AppSheet enthielten eine Spalte „Kunde“, die auf fünf Blätter dupliziert war. Die Die erste Aufgabe besteht darin, Fremdschlüssel zu normalisieren und aufzufüllen. Alles stromabwärts — Abrechnung, Berichterstattung, Deduplizierung – hängt davon ab.

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);

Eine CHECK-Einschränkung für stage und eine UNIQUE für email sind der Unterschied zwischen sauberen Daten und drei Wochen Bereinigung, bevor Sie einem Bericht vertrauen können.

Schritt 2 – Validieren Sie vor der Migration

AppSheet exportiert Zahlen als Text, lässt E-Mails fehlerhaft formatieren und überträgt verwaiste Nachrichten Referenzen. Ein Zod-Durchlauf über die exportierten Zeilen bringt den Müll zum Vorschein, bevor er ihn erreicht die Datenbank – in diesem Projekt haben etwa 3 % der Zeilen die Validierung nicht bestanden und wurden weitergeleitet in eine Überprüfungswarteschlange statt in die Datenbank verschoben.

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>;

Zeilen, die fehlschlagen, landen in einer migration_rejections-Tabelle mit dem Grund, also nichts wird lautlos fallen gelassen und nichts Schmutziges gelangt in das saubere System.

Schritt 3 – Idempotentes ETL mit einem Cursor

Jede AppSheet-Zeile trägt ein updatedAt. Der Importeur verfolgt das zuletzt Gesehene Zeitstempel pro Tabelle, daher führt die erneute Ausführung des ETL nie und immer zu doppelten Einfügungen holt auf. Idempotenz macht den Parallellauf in Schritt 4 sicher.

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;
}

Schritt 4 – Parallellauf mit nächtlichem Abgleich

Zwei Wochen lang liefen beide Systeme live. Die Schreibvorgänge gingen immer noch an AppSheet (wo das Team war bequem); Die ETL hat sie jede Nacht in die Datenbank gespiegelt. Jeden Morgen ein Der automatisierte Job verglich die beiden und meldete eine Abweichung.

-- 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);

Eine Übereinstimmung der Anzahl ist notwendig, aber nicht ausreichend, daher wurde der Job auch auf Zeilenebene ausgeführt Hash-Prüfung einer 1 %-Stichprobe, um Änderungen zu erkennen, die die Zählungen stabil hielten, sich aber änderten Inhalt. Beim Parallellauf wurden drei Mapping-Fehler festgestellt, die ein Probelauf-Skript aufwies verfehlt – genau seinen Zweck.

Schritt 5 – Umstellung, mit einem Lesespiegel für Holdouts

Bei der Umstellung wurden die Schreibvorgänge auf die neue App umgestellt. AppSheet blieb als schreibgeschützte Version am Leben Spiegel für zwei Wochen über einen geplanten Exportjob, damit niemand noch auf dem neuen ist Der Datenfluss behielt die Transparenz, ohne abweichende Daten zu erzeugen. Dann haben wir es ausgeschaltet.

Interessante Entscheidungen

Gelernte Lektionen

Vor- und Nachteile

Vorteile: Vollständiger Dateneigentum, eine stabile API, die andere Dienste aufrufen können, vorhersehbar Pauschalkosten, echte Integritätsbeschränkungen und ein ordnungsgemäßes Prüfprotokoll.

Nachteile: Sie betreiben jetzt eine Datenbank – Backups, Überwachung, Migrationen liegen bei Ihnen. AppSheet hat das alles versteckt. Dem Gewerbe obliegt die betriebliche Verantwortung für das Eigentum und Fähigkeiten, und es ist der richtige Beruf, wenn Sie erst einmal an die Wand oben gestoßen sind.

FAQ

Können wir AppSheet für die Felddatenerfassung behalten? Ja. AppSheet ist immer noch ein starker Thin Client. Nach der Migration behalten einige Teams es rein für die mobile Offline-Erfassung, Schreiben in die neue API anstelle einer Sicherung Blatt. Was Sie zurücklassen, ist AppSheet als System of Record, nicht als Benutzeroberfläche.

Wie lange dauert eine solche Migration? Für ca. 50.000 Reihen und ein Team im Bereich von 8–20 müssen Sie mit einer verstrichenen Zeit von 3–6 Wochen rechnen Davon entfällt etwa die Hälfte auf die Datenbereinigung und die Parallellaufüberprüfung – nicht Codierung. Die Benutzeroberfläche ist der schnelle Teil.

Was passiert, wenn wir nicht bereit sind, AppSheet zu verlassen? Führen Sie die obige Fünf-Zeichen-Prüfung durch. Wenn Sie weniger als drei sehen, fallen die Übernachtungskosten an liegt immer noch unter den Migrationskosten, und das ist ein berechtigter Grund zu warten. Migrieren Sie, wenn sich die Wand deutlich vor Ihnen befindet, und nicht als Vorsichtsmaßnahme.

Werden wir Daten verlieren? Nicht mit diesem Muster. Die Validierung leitet fehlerhafte Zeilen an eine Überprüfungswarteschlange weiter, die ETL idempotent, und der Parallellauf beweist die Parität vor der Umstellung. „Ohne einen zu verlieren Single Record” ist der Standard, nicht die Hoffnung.

Möchten Sie dies für Ihre AppSheet-App?

Wenn Ihr Team an die AppSheet-Grenze stößt – langsame Synchronisierungen, keine API, Kosten pro Arbeitsplatz Klettern – Ich ordne den Ausgang in einem einzigen Discovery-Aufruf zu: was migriert werden soll, was wie die Zielarchitektur aussieht, und einen realistischen Zeitplan. Sie behalten Ihre Daten und Dein Schwung.Einen Entdeckungsanruf buchen →

Interaktiver Risiko- und ROI-Assessor für Google Sheets

Bewerten Sie Tabellenzustand und geschätzte wöchentlich verlorene Zeit.

Tabellen-Risikowert80 / 100
Geschätzte Reibung~10 Std./Woche verloren
0 · Gesund40 · Mittel70 · Kritisch100
Skalierungsgrenze und Latenzkurve
<25k Sicher 25-60k Verzögerung >60k Grenze
0%40%70%100%025k50k75k100k50,000 rows · 80%
Zeilenvolumen50,000 / 100k
Gleichzeitigkeit8 / 25 Bearbeiter
VLOOKUP-Tiefe15 / 40 Formeln
Kritisches Risiko — sofortige Migration empfohlenNeuberechnung und stille Überschreibungen kosten bereits Teamgeschwindigkeit.