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
| Aspecto | Antes (Hoja de aplicación) | Después (Express + Base de datos) |
|---|---|---|
| Modelo de datos | Flat Google Sheets, nombre del cliente duplicado en 5 pestañas | Tablas relacionales normalizadas (customers ↔ contacts ↔ deals) |
| Integridad | Ninguno: tres grafías del mismo cliente | Claves externas, restricciones UNIQUE, CHECK |
| Integración | Trucos de Google Apps Script, copiar y pegar en facturación | API REST + webhooks salientes |
| Latencia de sincronización | Viajes de ida y vuelta de 20 a 40 segundos, peor bajo carga | Consultas locales, <100 ms |
| Escalamiento de costos | Por usuario de AppSheet (~$5–10/asiento/mes, en ascenso) | Alojamiento plano (~$20/mes) |
| Pista de auditoría | ”Última edición por” en una celda | Registro 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:
- La latencia de sincronización dominó la jornada laboral. Cada edición iba de ida y vuelta al Hoja de Google subyacente. En ~50.000 filas, la sincronización tardó entre 20 y 40 segundos, y más bajo ediciones simultáneas. El equipo había aprendido a realizar cambios por lotes para evitar el retraso.
- No había integridad relacional. Debajo de la aplicación todavía había un piso Sheet, por lo que un nombre de cliente escrito de tres maneras generaba tres “clientes”. Los informes eran permanentemente sospechosos.
- No se pudo comunicar con la facturación. Finanzas copió los datos del acuerdo y los pegó en un archivo separado. sistema todas las semanas porque AppSheet no tenía API a la que sus herramientas pudieran llamar.
- Precios ajustados en función del personal. El modelo por usuario de AppSheet significó que cada nuevo La contratación agregó una línea de pedido, sin ofrecer más capacidad.
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 (Google Sheets) ──ETL──▶ Relational DB (normalized)
│
▼
Express API (REST)
│
┌─────────────┴─────────────┐
▼ ▼
React SPA Billing / webhooks
- Capa de consulta escrita para consultas seguras y secuencias de comandos de migración (los cambios de esquema se envían como archivos de migración revisados).
- Validación de Zod en cada límite de ruta y nuevamente en el límite de ETL.
- Un período de sombra de solo lectura: ambos sistemas se ejecutaron en paralelo durante dos semanas mientras reconciliamos el recuento de filas todas las noches.
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
- Modelo relacional, no 1:1 con las hojas. La columna de cliente duplicada fue la raíz de los problemas de presentación de informes. Normalizando la primera facturación desbloqueada y reportándose como efectos secundarios casi libres.
- Mantener AppSheet como espejo de lectura durante la transición. Se eliminó el mensaje “todos deben “cambiar de inmediato” y dio a los que se resisten una red de seguridad.
- No replique la UX de AppSheet pantalla por pantalla. Una migración es poco común oportunidad de corregir flujos de trabajo que se acumulaban alrededor de los límites de una herramienta. Reconstruimos el tres flujos que la gente realmente usó y abandonó dos que existían solo para funcionar alrededor de AppSheet.
- Hacer que el ETL se pueda volver a ejecutar por diseño. Idempotente
onConflictDoUpdatemás el El cursor de marca de tiempo significaba que podíamos ejecutar el importador cada hora durante el período de sombra. sin miedo.
Lecciones aprendidas
- Normalizar las fechas y horas a UTC al ingresar. AppSheet almacena las fechas y horas como el
Hora local del editor, sin etiqueta. Una conversión única a UTC con un explícito
La columna
tzevitó una desviación sutil en los informes. - Valide antes de migrar. El pase de Zod que reveló aproximadamente un 3 % de datos basura era la hora de mayor apalancamiento del proyecto.
- La ejecución paralela supera al big-bang. La sombra de dos semanas detectó errores en el mapeo el ensayo no pudo y dejó que el equipo siguiera trabajando mientras verificábamos.
- Presupuesto para los datos, no para la aplicación. La interfaz de usuario de React fue la parte fácil; deduplicar Los clientes y las referencias de reparación tomaron más tiempo que construir la nueva interfaz.
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.