Andrade Systems
Todos os sistemas
Produção

ORKEN

Um ERP modular para diferentes ramos de operação.

ERP modular · SaaS multi-tenant

Produção
Status
5
Módulos verticais
Multi-tenant
Isolamento

Visão geral

O que este sistema resolve

ORKEN é o sistema de gestão de quem opera sob pressão sem equipe de TI: ponto de venda, estoque, caixa, vendas, clientes e financeiro no núcleo, e módulos verticais que se encaixam no ramo do cliente — restaurante, obra, serviços, varejo.

A aposta do produto é que a mesma pizzaria, a mesma construtora e a mesma barbearia podem ser atendidas por um produto só. O que muda entre elas é módulo e preset, não código duplicado nem instalação separada.

O desafio

O que tornava o problema difícil

Estes são os pontos que mais influenciaram a arquitetura.

  • Verticais que não se parecem em nada

    Restaurante tem comanda, mesa e cozinha. Obra tem etapa, orçamento e diário. Serviço tem agenda e profissional. Modelar isso como um produto só, sem transformar o núcleo num acúmulo de exceções, é o problema central do produto.

  • Isolamento entre empresas

    Não pode existir caminho em que a consulta de uma empresa alcance a linha de outra. Um erro desses não é um bug que se corrige na próxima sprint — é um incidente de dado que já aconteceu quando alguém percebe.

  • O que o cliente pode acessar depende do que ele assina

    A cobrança é por módulo. O acesso precisa ser verdade no servidor, não apenas um item escondido do menu — e precisa refletir a assinatura em tempo real, não no próximo login.

  • Estoque e caixa precisam explicar o saldo

    Quando o dono pergunta por que o caixa fechou com essa diferença, a resposta tem que estar no histórico. Permitir edição de movimento destrói exatamente a informação que responde a pergunta.

Arquitetura

Como o sistema é montado

Backend

.NET 8 · Clean Architecture

Domínio, aplicação, infraestrutura e API em projetos separados, com dependência apontando só para dentro.

Aplicação

Núcleo + módulos

Núcleo por área de operação; verticais isolados em módulos próprios que estendem o núcleo.

Dados

PostgreSQL · EF Core

Schema único, isolamento por linha imposto pelo ORM em toda entidade de negócio.

Cache

Redis

Opcional por design: cai o cache, o sistema continua vendendo.

Tempo real

SignalR

Telas operacionais que precisam refletir o estado sem o operador recarregar.

Frontend

React · TypeScript · Vite

Organizado por módulo, com carregamento sob demanda e guardas de rota por módulo e perfil.

Cobrança

Stripe

Assinatura por módulo, com webhook idempotente.

As camadas de isolamento entre empresasCinco pontos em sequência: o token carrega a empresa; um middleware valida o vínculo entre usuário e empresa; um filtro global adiciona a condição de empresa a toda leitura; um interceptor injeta a empresa na escrita e recusa escrita cruzada; e o PostgreSQL guarda tudo em schema único com isolamento por linha. Testes de integração criam duas empresas para verificar o conjunto.Token da requisiçãocarrega a empresaMiddleware de resoluçãovalida o vínculo usuário ↔ empresaFiltro global de consultacondição de empresa em toda leituraInterceptor de escritainjeta e recusa escrita cruzadaPostgreSQLschema único, isolamento por linha+ testes de integração criam duas empresas
O isolamento entre empresas não depende de o desenvolvedor lembrar de filtrar. Ele é imposto em cinco pontos por onde toda consulta e toda escrita passam obrigatoriamente — e o caminho errado deixa de existir.

Decisões de engenharia

Decisões e trade-offs

Cada decisão registra a justificativa e o trade-off aceito.

  1. Multi-tenancy por linha em schema único

    Schema por empresa multiplica cada migração pelo número de clientes: um cliente com migração falhada fica numa versão diferente do resto, e o suporte passa a lidar com N variações do mesmo sistema. Com isolamento por linha, existe um schema, uma migração e uma versão.

    Custo aceito · O isolamento passa a depender de disciplina — e é justamente por isso que ele foi movido para dentro do ORM e de um interceptor, em vez de ficar por conta de quem escreve a consulta.

  2. Orken Service como módulo único com presets internos

    Nove verticais de serviço tratados como nove produtos multiplicariam por nove o controle de acesso, a cobrança, as telas e os testes — e fragmentariam a evolução: uma melhoria feita para a clínica não chegaria ao pet shop. Com preset, o onboarding escolhe rótulos e capacidades sobre uma base única, e uma melhoria vale para os nove.

    Custo aceito · A base de telas precisa absorver as diferenças de forma genérica. Vazar terminologia de um vertical para outro é um risco real e permanente, que exige cuidado a cada tela nova.

  3. Movimentos de estoque e caixa são append-only

    O saldo é sempre uma função da soma dos movimentos, nunca um número guardado. Correção é lançamento compensatório que referencia o original. Assim o histórico explica como o saldo atual se formou, e a auditoria é o próprio registro de movimentos — não um log paralelo que pode divergir.

    Custo aceito · A operação humana precisa ser ensinada a estornar em vez de corrigir, e a interface tem a obrigação de tornar isso fácil. Também impede correções em massa por migração.

  4. Migrações sempre aditivas

    Coluna nova é anulável ou tem valor padrão; tabela nova não quebra o que já roda. Isso permite que a versão anterior e a nova coexistam durante o deploy, sem janela em que o sistema fica inconsistente.

    Custo aceito · O schema acumula colunas que já não são mais o caminho principal, e a limpeza precisa ser um trabalho deliberado em vez de uma consequência da mudança.

  5. O cache falha aberto

    Se o Redis está indisponível, o sistema continua operando sem ele. Uma PME não pode parar de vender porque um componente de otimização caiu — e cache, por definição, é otimização.

    Custo aceito · A exceção é a lista de tokens revogados, onde falhar aberto teria consequência de segurança. Essa área é tratada à parte, de forma explícita.

  6. Erros esperados retornados como resultado explícito

    Os serviços devolvem um tipo que representa sucesso ou falha, em vez de lançar. Isso obriga quem chama a tratar o caminho de erro — e mantém o rastreamento de exceção limpo, contendo apenas o que realmente não deveria ter acontecido.

    Custo aceito · Mais verbosidade em cada camada, e uma fronteira explícita onde o resultado é traduzido em resposta HTTP.

Por dentro da engenharia

Os problemas em detalhe

Cada bloco abre com o problema, mostra a solução e explica por que ela foi escolhida. Expanda o que interessar.

Isolamento de tenant imposto pelo ORM

Problema

Uma condição de filtro esquecida numa consulta é o bug mais caro possível em um sistema multiempresa: mostra dado de um cliente para outro, passa por revisão de código sem chamar atenção e não quebra teste nenhum.

Solução

Cinco camadas. O token carrega a empresa; um middleware valida o vínculo entre usuário e empresa e resolve o contexto antes de qualquer consulta; um filtro global aplicado a toda entidade de negócio adiciona automaticamente a condição de empresa; um interceptor de salvamento injeta a empresa nos inserts e lança se alguém tentar gravar em nome de outra ou alterar a empresa de uma linha existente; e os testes de integração criam duas empresas em todo caso que envolva isolamento. Desligar o filtro é proibido no código de aplicação.

Por que foi feito assim

Fazer o isolamento depender de cada desenvolvedor lembrar não escala com o time nem com a superfície do produto. Movendo a regra para os pontos por onde toda consulta e todo salvamento passam obrigatoriamente, o caminho errado deixa de ser possível — e a violação vira exceção alta, não linha silenciosa no resultado.

Rate limit particionado por identidade

Problema

Um limitador de login configurado como um balde único e global significa que algumas tentativas erradas de uma pessoa consomem a cota de todo mundo. É negação de serviço acidental — e um vetor trivial para quem queira derrubar o login do sistema inteiro de propósito.

Solução

O limitador particiona por identidade do solicitante, de modo que o contador de um não interfere no do outro.

Por que foi feito assim

A partição por identidade contém tentativas abusivas sem consumir a cota de usuários legítimos.

Webhook de cobrança idempotente

Problema

Provedores de pagamento reentregam eventos — por retry, por timeout de resposta, por reprocessamento do lado deles. Processar o mesmo evento duas vezes libera módulo em duplicidade ou registra cobrança repetida.

Solução

O identificador de cada evento processado com sucesso é registrado numa tabela própria. Antes de processar, o sistema consulta; depois de processar, insere.

Por que foi feito assim

A reentrega faz parte do contrato de entrega ao menos uma vez. O receptor registra o identificador do evento para garantir idempotência.

Motor de interpretação com confirmação humana

Problema

Lançar movimento financeiro à mão é lento, e a fonte da informação costuma ser um comprovante ou um texto solto — não um formulário.

Solução

Um pipeline que separa duas coisas que costumam ser confundidas: extração ("o que está escrito no documento") e interpretação ("o que isso significa dentro desta operação"). A interpretação produz uma sugestão em que cada campo carrega a origem da sugestão, e nada vira lançamento sem confirmação. Correções do usuário realimentam um perfil de memória por empresa.

Por que foi feito assim

Separar extração de interpretação permite trocar o extrator sem tocar em regra de negócio, e testar a regra sem depender de um documento real. A confirmação humana impede que um erro de interpretação vire lançamento contábil antes da revisão.

Condição climática no diário de obra

Problema

O diário de obra registra a condição do tempo por turno, e é esse registro que sustenta pedido de prorrogação de prazo por chuva. Preenchido de memória na sexta-feira, não sustenta nada.

Solução

Integração com um serviço público de meteorologia, que preenche a condição do dia a partir da coordenada da obra no momento do lançamento.

Por que foi feito assim

É dado de terceiro, com data, e não depende da lembrança de ninguém — que é exatamente o que dá valor probatório ao registro. Não há modelo nem inferência envolvida: é uma consulta a uma fonte externa. A integração consulta uma fonte externa; não há modelo nem inferência envolvidos.

Qualidade

O que os testes protegem

Duas suítes com propósitos distintos. A unitária cobre regra de domínio isolada. A de integração sobe um PostgreSQL real em contêiner e exercita o caminho completo — porque o que este sistema mais precisa garantir, o isolamento entre empresas, só é verificável de verdade contra um banco de verdade.

  • Que nenhuma consulta de uma empresa alcance a linha de outra — todo teste de isolamento cria duas
  • Que o interceptor recuse escrita cruzada e alteração da empresa de uma linha existente
  • Que o controle de acesso por módulo reflita a assinatura, e não o menu
  • Que os movimentos de estoque e caixa permaneçam append-only

Stack

Stack do projeto

O sistema em números

584 (285 unitários · 299 integração)
Testes automatizados
86
Tabelas mapeadas
63
Controllers de API
43
Migrations
15
Áreas do núcleo

conferido no repositório em 27/08/2026

Status

Produção

Backend

.NET 8C#Clean ArchitectureFluentValidation

Dados

PostgreSQLEF CoreRedis

Frontend

ReactTypeScriptViteTanStack QueryTailwind

Tempo real

SignalR

Integrações

StripeOpen-Meteo

Infraestrutura

RailwayDockerSerilog

Testes

xUnitTestcontainersVitest

Próximo sistema

Lumen+

Busca por documento sem armazenamento em texto legível.

Pré-lançamento