Migración de AppSheet CRM a Express + Base de datos relacional

Se reemplazó un AppSheet CRM que alcanzaba sus límites con un backend Express tipado, base de datos relacional e interfaz React, con conciliación verificada registro a registro.

Proyecto de cliente · anonimizado

Un pequeño equipo de ventas superó a AppSheet: sincronizaciones lentas, falta de integridad relacional real, imposible de integrar con la facturación y precios por usuario que no escalan correctamente. Necesitaban la propiedad de sus datos y una API a la que otras herramientas pudieran llamar.

TL;DR

AspectoAntes (Hoja de aplicación)Después (Express + Base de datos)
Modelo de datosFlat Google Sheets, nombre del cliente duplicado en 5 pestañasTablas relacionales normalizadas (customers ↔ contacts ↔ deals)
IntegridadNinguno: tres grafías del mismo clienteClaves externas, restricciones UNIQUE, CHECK
IntegraciónTrucos de Google Apps Script, copiar y pegar en facturaciónAPI REST + webhooks salientes
Latencia de sincronizaciónViajes de ida y vuelta de 20 a 40 segundos, peor bajo cargaConsultas locales, <100 ms
Escalamiento de costosPor usuario de AppSheet (~$5–10/asiento/mes, en ascenso)Alojamiento plano (~$20/mes)
Pista de auditoría”Última edición por” en una celdaRegistro de eventos de solo agregar

Aprenderá las señales concretas de que un equipo ha superado AppSheet, el objetivo arquitectura a la que migramos a y el patrón de migración segura de cinco pasos (remodelado relacional → validar → ETL idempotente → ejecución paralela → transición) que saca una empresa de AppSheet sin perder un registro ni congelar el equipo.

El problema

AppSheet es una excelente herramienta para la primera versión de una aplicación de datos de campo: apúntelo en una hoja de Google, junte algunos formularios y envíelos a los teléfonos en una tarde. eso oculta la base de datos. Ésa es su fuerza y, eventualmente, su techo.

El equipo en este caso (ocho personas de ventas y operaciones en dos zonas horarias) había Viví en AppSheet CRM durante dos años. Cuando me llamaron, la herramienta que obtenerlos de 0→1 les estaba costando activamente cada semana:

Necesitaban la propiedad de sus datos y una API a la que otros sistemas pudieran llamar, sin perder dos años de registros o congelar el negocio durante un mes. Este es el forma de casi todos los proyectos de salida de AppSheet: no una reescritura por sí misma, sino una migración forzada por un muro que la herramienta nunca fue diseñada para superar.

Para ver la versión general de esta historia (hojas de cálculo en términos generales), consulte Cuando Google Sheets deja de escalar.

Señales de que se te ha quedado pequeña la hoja de aplicación

Si estás evaluando si quedarte o migrar, estas son las señales que busco. Tres o más significa que el costo de quedarse ya excede el costo de migrar.

1. La latencia de sincronización es el ritmo de la jornada laboral

AppSheet sincroniza cada cambio con la tienda de respaldo. A medida que la tienda crece y los usuarios simultáneos aumentan, las sincronizaciones van desde “instantáneas” hasta “esperar”. cuando el El equipo comienza a planificar sus ediciones en torno al tiempo de sincronización, la herramienta se opone a ellas.

2. Estás fingiendo relaciones con desreferencia

Las expresiones [_thisrow]/desreferencia de AppSheet simulan relaciones además de hojas planas. Funcionan hasta que dejan de hacerlo: filas huérfanas, referencias rotas después un cambio de nombre, informes que silenciosamente devuelven las filas incorrectas. Si has escrito un texto de 15 líneas expresión para emular un JOIN, la capa de la base de datos solicita ser real.

3. El precio por asiento es el elemento que la gente nota

El precio por usuario sin código es justo para cinco usuarios y doloroso para veinte. cuando el La factura mensual es una conversación recurrente, te has topado con el muro económico.

4. Otro sistema necesita leer o escribir tus datosEl momento en que la facturación, un ERP, un almacén de datos o una automatización deben tocar

sus datos de CRM, la falta de AppSheet de una API externa estable se convierte en el obstáculo. Ha vuelto a copiar y pegar o al frágil pegamento de Apps Script.

5. Necesita un registro de auditoría que AppSheet no puede proporcionar

Cumplimiento, finanzas o una disputa con un cliente eventualmente pregunta “¿quién cambió esto y cuando.” La “última edición por” de una celda no es un registro de auditoría.

Arquitectura

Una división limpia: API Express + base de datos relacional como fuente de verdad, un React SPA para el equipo y un puente ETL único que unía las hojas de AppSheet tablas relacionales normalizadas.

AppSheet to Express + Arquitectura de migración de bases de datos

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


                                  Express API (REST)

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

El patrón de migración segura

Este es el procedimiento que utilizo para cualquier movimiento de hoja de cálculo a base de datos, especializado aquí para AppSheet con transferencia de ejecución paralela en vivo.

Paso 1: Modele los datos relacionalmente, no como 1:1 de las hojas

Las pestañas planas de AppSheet tenían una columna de “cliente” duplicada en cinco hojas. el El primer trabajo es normalizar y rellenar las claves externas. Todo aguas abajo - facturación, informes, deduplicación, depende de esto.

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

Una restricción CHECK en stage y una UNIQUE en email son la diferencia entre datos limpios y tres semanas de limpieza antes de poder confiar en un informe.

Paso 2: validar antes de migrar

AppSheet exporta números como texto, permite que los correos electrónicos tengan formato incorrecto y quedan huérfanos referencias. Un pase de Zod sobre las filas exportadas muestra basura antes de que llegue la base de datos: en este proyecto, alrededor del 3 % de las filas no superaron la validación y fueron enrutadas a una cola de revisión en lugar de a la base de datos.

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

Las filas que fallan aterrizan en una tabla migration_rejections con el motivo, así que nada se cae silenciosamente y nada sucio ingresa al sistema limpio.

Paso 3: ETL idempotente con un cursor

Cada fila de AppSheet lleva un updatedAt. El importador rastrea lo último visto marca de tiempo por tabla, por lo que volver a ejecutar el ETL nunca realiza inserciones dobles y siempre se pone al día. La idempotencia es lo que hace que la ejecución paralela en el Paso 4 sea segura.

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

Paso 4: ejecución paralela con conciliación nocturna

Durante dos semanas ambos sistemas estuvieron funcionando. Las escrituras todavía iban a AppSheet (donde el equipo estaba cómodo); el ETL los reflejaba en la base de datos todas las noches. Cada mañana un El trabajo automatizado comparó los dos e informó una desviación.

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

Una coincidencia de recuento es necesaria pero no suficiente, por lo que el trabajo también ejecutó una coincidencia a nivel de fila. verificación de hash en una muestra del 1% para detectar ediciones que mantuvieron los recuentos estables pero cambiaron contenido. La ejecución paralela detectó tres errores de mapeo que tenía un script de prueba perdido: exactamente su propósito.

Paso 5: Transición, con un espejo de lectura para los que se resisten

Durante la transición, las escrituras pasan a la nueva aplicación. AppSheet se mantuvo viva como solo lectura mirror durante dos semanas a través de un trabajo de exportación programado, por lo que cualquiera que aún no esté en el nuevo El flujo mantuvo la visibilidad sin producir datos divergentes. Luego lo apagamos.

Decisiones interesantes

Lecciones aprendidas

Pros y contras

Pros: propiedad total de los datos, una API estable a la que otros servicios pueden llamar, predecible costo fijo, restricciones de integridad reales y un registro de auditoría adecuado.

Contras: ahora opera una base de datos: las copias de seguridad, el monitoreo y las migraciones son suyas. AppSheet ocultó todo eso. El comercio es responsabilidad operativa de la propiedad. y capacidad, y es el oficio correcto una vez que has topado con la pared de arriba.

Preguntas frecuentes

¿Podemos conservar AppSheet para la captura de datos de campo? Sí. AppSheet sigue siendo un cliente ligero potente. Después de la migración, algunos equipos lo conservan. exclusivamente para captura móvil sin conexión, escribiendo en la nueva API en lugar de un respaldo hoja. Lo que deja atrás es AppSheet como sistema de registro, no como una interfaz de usuario.

¿Cuánto tiempo lleva una migración como esta? Para ~50.000 filas y un equipo en el rango de 8 a 20, espere de 3 a 6 semanas de tiempo transcurrido, de que aproximadamente la mitad es la limpieza de datos y la verificación de ejecución paralela, no codificación. La interfaz de usuario es la parte rápida.

¿Qué pasa si no estamos listos para abandonar AppSheet? Ejecute la verificación de cinco signos anterior. Si ve menos de tres, el costo de la estadía todavía está por debajo del costo de migrar, y esa es una razón legítima para esperar. Migre cuando la pared esté claramente frente a usted, no como precaución.

¿Perderemos datos? No con este patrón. La validación enruta las filas incorrectas a una cola de revisión, el ETL es idempotente, y la ejecución paralela demuestra la paridad antes de la transición. “Sin perder un “un solo disco” es el estándar, no la esperanza.

¿Quieres esto para tu aplicación AppSheet?

Si su equipo se está topando con AppSheet: sincronizaciones lentas, sin API, costos por puesto escalada: mapeo la salida en una única llamada de descubrimiento: qué migrar, cuál es el cómo se ve la arquitectura de destino y una línea de tiempo realista. Conservas tus datos y tu impulso.Reservar una llamada de descubrimiento →

Evaluador interactivo de riesgo y ROI de Google Sheets

Evalúa la salud de tu hoja y el tiempo semanal perdido estimado.

Puntuación de riesgo de la hoja80 / 100
Fricción estimada~10 h/semana perdidas
0 · Saludable40 · Moderado70 · Crítico100
Curva de límite de escala y latencia
<25k Seguro 25-60k Lento >60k Límite
0%40%70%100%025k50k75k100k50,000 rows · 80%
Volumen de filas50,000 / 100k
Concurrencia8 / 25 editores
Profundidad de VLOOKUP15 / 40 fórmulas
Riesgo crítico — migración inmediata recomendadaLa lentitud y las sobrescrituras silenciosas ya están costando velocidad al equipo.