Como construí e mantenho o sistema de gestão da Develop Sistemas: web, Windows e iPhone
sobre a mesma API, com cada empresa isolada no seu próprio esquema do banco.
Papel
Fundador e engenheiro: arquitetura, API, telas, apps, deploy
15.262 commits¹ · 4.506 rotas de API¹ · 339 arquivos de rota¹ · ~90 esquemas no servidor²
O problema
Indústrias, distribuidoras, óticas e lojas usavam um ERP antigo, montado em Appsmith, com mais de
160 telas: lento, difícil de mudar e preso a uma plataforma de terceiros. Elas não podiam parar de
vender durante a troca, e o banco já tinha dezenas de clientes em produção, todos com o mesmo
formato de tabelas.
Meu papel
Sou o fundador da Develop e quem escreve a maior parte do código: arquitetura, API, telas web, o app
de Windows, o de iPhone, deploy e suporte de segundo nível. Trabalho com um time pequeno e uso agentes
de IA como par de programação, com regras escritas para o que nunca pode acontecer sem pedido explícito,
como mudar a estrutura do banco compartilhado.
A arquitetura
Três clientes, uma API. O navegador (React + Vite, na Vercel), o app de Windows (Tauri) e o de
iPhone (Expo) falam com a mesma API Node + Express, no Railway.
O login carrega a empresa. O token traz o esquema do cliente, e toda consulta roda dentro dele.
Um servidor MySQL no Azure, um esquema por empresa, com SSL obrigatório.
Integrações de nota fiscal (Focus NFe), WhatsApp, cobrança (Asaas, Pagar.me) e Brasil API.
Três decisões, e o porquê
1. Um esquema de banco por cliente, no mesmo servidor
Os clientes já existiam assim no sistema antigo. Juntar todos num banco só, com uma coluna de
empresa em cada tabela, exigiria migrar e parar todo mundo. Mantendo um esquema por cliente, o
isolamento vem da construção e não de um filtro que alguém pode esquecer: uma empresa não enxerga
a outra porque a consulta nem roda no esquema dela.
O preço: o servidor é compartilhado por dezenas de empresas, então qualquer mudança
de estrutura só acontece com pedido explícito. É regra escrita do projeto, inclusive para os agentes de IA.
2. A venda nasce no disco (PDV offline)
Quando a internet cai, o caixa não pode parar. No app de Windows cada venda é gravada primeiro num
diário só de acréscimo, com fsync, escrito em Rust; depois entra numa fila que um agente envia a cada
90 segundos. Cada venda guarda o número oficial que o servidor deu a ela e, antes de fechar de novo,
o caixa confere se ela já foi finalizada: reenviar nunca cobra duas vezes. A venda online e a sincronizada passam pela mesma função de gravação, então não
existem duas regras de venda para manter.
O preço: o caixa precisa de um catálogo local e de um selo que mostre o que ainda
não subiu, para ninguém confiar num número que o servidor ainda não viu.
3. Trocar o sistema antigo tela por tela, com ele no ar
Uma troca de uma vez pararia dezenas de empresas no mesmo dia. Cada tela nova lê as mesmas tabelas,
com os nomes exatos do esquema real, e o cliente passa a usá-la quando ela fica pronta. O sistema
antigo e o novo convivem sem migração de dados.
O preço: conviver com os nomes de coluna do legado em vez de redesenhar o banco do zero.
O que deu errado
Um controle de acesso registrado sem caminho (router.use()) passou a valer para todos os
módulos montados depois dele: 13 módulos responderam "acesso negado" por uma hora e meia. Hoje um teste
automático barra esse padrão antes de qualquer publicação.
O que eu faria diferente
Separar as rotas por módulo desde o primeiro dia: alguns arquivos passaram de 500 KB.
Escrever os testes antes de crescer, e não depois.
Migrações de banco versionadas, em vez de scripts revisados à mão.
Case study · Develop ERP
One ERP, dozens of companies, no downtime.
How I built and run the Develop Sistemas business system: web, Windows and iPhone on the same
API, with every company isolated in its own database schema.
Role
Founder and engineer: architecture, API, UI, apps, deployment
15,262 commits¹ · 4,506 API routes¹ · 339 route files¹ · ~90 schemas on the server²
The problem
Manufacturers, distributors, optical stores and retailers ran on an old ERP built in Appsmith, with more
than 160 screens: slow, hard to change and locked into a third-party platform. They could not stop selling
during the switch, and the database already held dozens of live customers, all with the same table layout.
My role
I founded Develop and I write most of the code: architecture, API, web screens, the Windows app, the
iPhone app, deployment and second-line support. I work with a small team and use AI agents as pair
programmers, with written rules for what must never happen without an explicit request, such as changing
the structure of the shared database.
The architecture
Three clients, one API. The browser (React + Vite, on Vercel), the Windows app (Tauri) and the
iPhone app (Expo) talk to the same Node + Express API, on Railway.
The login carries the company. The token holds the customer's schema, and every query runs inside it.
One MySQL server on Azure, one schema per company, SSL required.
Integrations for invoicing (Focus NFe), WhatsApp, payments (Asaas, Pagar.me) and Brasil API.
Three decisions, and why
1. One database schema per customer, on the same server
Customers already lived this way in the old system. Merging everyone into one database, with a company
column on every table, would mean migrating and stopping all of them. Keeping one schema per customer
makes isolation structural rather than a filter someone can forget: one company cannot see another
because the query never runs in its schema.
The cost: dozens of companies share the server, so any structural change only
happens on explicit request. It is a written project rule, AI agents included.
2. The sale is born on disk (offline POS)
When the internet drops, the register cannot stop. In the Windows app every sale is first written to an
append-only journal with fsync, in Rust; then it joins a queue that an agent sends every 90 seconds. Each
sale keeps the official number the server gave it, and before closing again the register checks whether
it was already finalized: resending never charges twice. Online
and synced sales go through the same write function, so there are never two sets of sales rules.
The cost: the register needs a local catalog and a badge showing what has not been
uploaded yet, so nobody trusts a number the server has not seen.
3. Replace the old system screen by screen, while it stays live
A big-bang switch would stop dozens of companies on the same day. Each new screen reads the same tables,
with the exact names of the real schema, and customers move to it when it is ready. Old and new run side
by side with no data migration.
The cost: living with legacy column names instead of redesigning the database from scratch.
What went wrong
An access check registered without a path (router.use()) applied to every module mounted
after it: 13 modules answered "access denied" for an hour and a half. Today an automated test blocks that
pattern before any release.
What I would do differently
Split routes by module from day one: some files grew past 500 KB.
Write the tests before growing, not after.
Versioned database migrations instead of hand-reviewed scripts.
Case · Develop ERP
Un ERP, decenas de empresas, ninguna parada.
Cómo construí y mantengo el sistema de gestión de Develop Sistemas: web, Windows e iPhone sobre
la misma API, con cada empresa aislada en su propio esquema de base de datos.
Rol
Fundador e ingeniero: arquitectura, API, pantallas, apps, despliegue
15.262 commits¹ · 4.506 rutas de API¹ · 339 archivos de rutas¹ · ~90 esquemas en el servidor²
El problema
Industrias, distribuidoras, ópticas y tiendas usaban un ERP antiguo, armado en Appsmith, con más de 160
pantallas: lento, difícil de cambiar y atado a una plataforma de terceros. No podían dejar de vender durante
el cambio, y la base ya tenía decenas de clientes en producción, todos con el mismo formato de tablas.
Mi rol
Soy el fundador de Develop y quien escribe la mayor parte del código: arquitectura, API, pantallas web, la
app de Windows, la de iPhone, despliegue y soporte de segundo nivel. Trabajo con un equipo pequeño y uso
agentes de IA como compañeros de programación, con reglas escritas para lo que nunca puede pasar sin pedido
explícito, como cambiar la estructura de la base compartida.
La arquitectura
Tres clientes, una API. El navegador (React + Vite, en Vercel), la app de Windows (Tauri) y la de
iPhone (Expo) hablan con la misma API Node + Express, en Railway.
El login lleva la empresa. El token trae el esquema del cliente, y toda consulta corre dentro de él.
Un servidor MySQL en Azure, un esquema por empresa, con SSL obligatorio.
Integraciones de factura electrónica (Focus NFe), WhatsApp, cobros (Asaas, Pagar.me) y Brasil API.
Tres decisiones, y el porqué
1. Un esquema de base por cliente, en el mismo servidor
Los clientes ya existían así en el sistema antiguo. Juntarlos en una sola base, con una columna de empresa
en cada tabla, exigiría migrar y detener a todos. Con un esquema por cliente, el aislamiento viene de la
construcción y no de un filtro que alguien puede olvidar: una empresa no ve a otra porque la consulta ni
corre en su esquema.
El precio: decenas de empresas comparten el servidor, así que cualquier cambio de
estructura solo ocurre con pedido explícito. Es regla escrita del proyecto, también para los agentes de IA.
2. La venta nace en el disco (POS offline)
Cuando cae internet, la caja no puede parar. En la app de Windows cada venta se graba primero en un diario
solo de adición, con fsync, escrito en Rust; luego entra en una cola que un agente envía cada 90 segundos.
Cada venta guarda el número oficial que le dio el servidor y, antes de cerrarla otra vez, la caja
confirma si ya fue finalizada: reenviar nunca cobra dos veces.
La venta online y la sincronizada pasan por la misma función de grabación, así que no existen dos reglas
de venta.
El precio: la caja necesita un catálogo local y una señal de lo que aún no subió, para
que nadie confíe en un número que el servidor todavía no vio.
3. Cambiar el sistema antiguo pantalla por pantalla, con él en marcha
Un cambio de una sola vez detendría decenas de empresas el mismo día. Cada pantalla nueva lee las mismas
tablas, con los nombres exactos del esquema real, y el cliente pasa a usarla cuando está lista. El sistema
antiguo y el nuevo conviven sin migración de datos.
El precio: convivir con los nombres de columna del legado en vez de rediseñar la base desde cero.
Lo que salió mal
Un control de acceso registrado sin ruta (router.use()) pasó a valer para todos los módulos
montados después: 13 módulos respondieron "acceso denegado" durante una hora y media. Hoy una prueba
automática bloquea ese patrón antes de cualquier publicación.
Lo que haría distinto
Separar las rutas por módulo desde el primer día: algunos archivos pasaron de 500 KB.
Escribir las pruebas antes de crecer, no después.
Migraciones de base versionadas, en vez de scripts revisados a mano.