[PITCH] Construí um PDV que se integra a um e-commerce e marketplaces feito em ElysiaJS e Electron
Cansei de catálogo online que joga o cliente no WhatsApp, então construí um PDV com e-commerce próprio.
Toda loja pequena que eu via tinha o mesmo "e-commerce": um catálogo bonitinho que termina num botão de "comprar pelo WhatsApp". O cliente escolhe o produto, clica, cai numa conversa manual onde alguém do outro lado pergunta "qual a cor?", "tem no tamanho M?", "como você quer pagar?". Isso de certa forma não é vender online.
Foi essa dor que me fez construir o papayaPDV: um PDV desktop em feito em Electron com microsserviços em Bun, onde a loja cadastra o produto de verdade, com variações, e ganha um catálogo/e-commerce próprio que fecha a venda sozinho. Hoje roda em 5 lojas de mini comércio. E neste post eu queria dar start, sobre as decisões técnicas por trás disso e rotina para manter isso em dois desenvolvedores e um cara do marketing.
O que eu queria resolver
- Cadastrar produto com variação sem sofrer. Uma camiseta não é "um produto", é cor ×, é tamanho y, cada combinação com seu próprio estoque e às vezes seu próprio preço. A maioria das ferramentas trata isso mal, ou nem trata.
- Ter um e-commerce próprio, não um catálogo alugado que depende de plataforma de terceiros pra existir.
- Ser uma plataforma que vai alem de vendas, foque no usuário, na acessibilidade, na transparência, sem complexidades.
Modelar produto com variações
Separei o produto(a ideia) das variantes (o que tem estoque e código de barras de verdade):
// Produto = a "família". Não é o que se vende diretamente.
interface Produto {
id: string;
nome: string; // "Camiseta Básica"
descricao: string;
opcoes: Opcao[]; // [{ nome: "Cor", valores: ["Preto","Branco"] },
// { nome: "Tamanho", valores: ["P","M","G"] }]
}
// Variante = a combinação concreta. É o que tem preço, estoque e SKU.
interface Variante {
id: string;
produtoId: string;
combinacao: Record<string, string>; // { Cor: "Preto", Tamanho: "M" }
sku: string;
precoEmCentavos: number; // inteiro, nunca float
estoque: number;
}
A regra é: o carrinho e o estoque nunca falam com o Produto, só com a Variante. Camiseta Preta M e Camiseta Branca G são itens de estoque independentes. Quando o cliente adiciona ao carrinho no catálogo, o que vai pro carrinho é o varianteId, não o produto genérico. É isso que permite fechar a venda sem alguém precisar perguntar "qual cor?" no WhatsApp.
Arquitetura: offline-first no balcão
Um PDV não pode parar quando a internet cai. Por isso o papayaPDV é offline-first, dividido em quatro serviços (cada um no seu repositório), com RabbitMQ costurando tudo de forma assíncrona:
frontrepo-pdv (Electron) papaya-ecommerce
WatermelonDB / SQLite local catálogo + checkout (AbacatePay)
vende no balcão (offline-first) vende online
▲ │ venda venda │ ▲
estoque│ ▼ ▼ │ estoque
atualiza│ papaya-api (ElysiaJS) ──eventos──► RabbitMQ ──► papaya-worker (Kotlin)
└──────────── baixa de estoque ◄────────── fonte da verdade do estoque
Diagrama feito por ia
-
frontrepo-pdv (Electron): o app do balcão. Uso WatermelonDB, um banco reativo em cima de SQLite, pra registrar a venda localmente e na hora, mesmo sem internet. A UI lê do banco local, então nada trava esperando rede.
-
Microsserviço em bun: a camada de sincronização. Escolhi ElysiaJS pelo runtime rápido e por ser o ponto único por onde as vendas dos dois canais entram e as mudanças de estoque saem. Em vez de processar tudo na hora, ela publica eventos no RabbitMQ e responde rápido.
-
RabbitMQ: desacopla os canais do processamento. A venda cai numa fila e é tratada no
-
ritmo do worker; se o worker está ocupado ou caiu, nada se perde, só espera. É o que me
-
deixa tranquilo com pico de venda.
-
Worker (Kotlin): consome os eventos e é a fonte da verdade do estoque. Cada venda, do balcão ou do site, vira uma baixa que ele aplica e propaga de volta pros dois canais. Kotlin pela robustez em processamento contínuo.
-
Microsserviço do ecommerce em ElysiaJS: a vitrine online, catálogo próprio com checkout via AbacatePay.
Estoque é um só
O erro clássico do "PDV + lojinha online" é tratar os dois estoques como coisas separadas. Aí a loja vende a última peça no balcão, o site continua anunciando disponível, alguém compra online, e você tem um cliente irritado e um pedido que não dá pra atender.
No papayaPDV isso não acontece: a venda no e-commerce dá baixa no estoque do PDV, e a venda no balcão dá baixa no site. Os dois canais escrevem no mesmo estoque, e o papaya-worker é a fonte da verdade:
-
Venda acontece (balcão ou site) → evento entra na fila.
-
O worker aplica a baixa na variante certa (estoque vive na variante, não no produto).
-
O novo estoque volta pro WatermelonDB do PDV e pro catálogo online.
É um sistema offline-first e omnichannel ao mesmo tempo, e a consistência de estoque é a parte mais delicada de tudo isso.
E sim, é stack "poliglota" de propósito: Bun na API pela velocidade de iteração, Kotlin no worker pela robustez, RabbitMQ pra desacoplar. Cada peça na função onde rende mais.
E o pagamento?
No balcão, o pagamento presencial acontece na própria loja. A parte difícil foi o checkout online do papaya-ecommerce: qual gateway usar sem comer a margem do mini comércio?
Avaliei a Polar, mas a taxa era alta demais pro perfil dessas lojas. Fui de AbacatePay, atraído pela taxa fixa de R$ 0,80 por Pix e sem mensalidade, que faz mais sentido pra ticket baixo. É um começo pragmático: cadastrei manualmente na AbacatePay pra validar o checkout antes de investir em integração automatizada. É uma decisão de "por agora", não definitiva. E é justamente aqui que quero outra opinião de vocês.
O que já está de pé
-
Cadastro de produto com variações (cor, tamanho etc.) e estoque por variante/marketplace
-
Venda offline-first no balcão com WatermelonDB, sincronizando em segundo plano
-
Sync assíncrono via papaya-api (Bun) + RabbitMQ + papaya-worker (Kotlin)
-
Estoque omnichannel: venda online dá baixa no PDV e venda no balcão dá baixa no site
-
Catálogo/e-commerce próprio (papaya-ecommerce), checkout sem redirect pro WhatsApp
-
Checkout online com pagamento via AbacatePay (cadastro manual, por enquanto)
-
Emissão de NFC-e integrada à SEFAZ (serviço dedicado), com fila offline pra
-
quando a SEFAZ cai
-
Integração com Mercado Livre: sync de catálogo/estoque, import de pedidos, publicação automática de produto
Onde quero feedback se puderem contribuir
-
Quem já fez sync offline-first (WatermelonDB, PouchDB etc.): como vocês tratam conflito quando a mesma base sincroniza depois de horas offline?
-
Quem modelou catálogo com variações: guarda preço/estoque na variante ou tenta resolver no produto? Quero comparar abordagens.
-
Quem já rodou Pix em e-commerce de ticket baixo: a taxa fixa compensou mesmo, ou o percentual dos grandes acaba valendo a pena no volume? Fui de AbacatePay depois de achar a Polar cara, quero saber se alguém se arrependeu de decisão parecida, a Polar é uma ótima opção por não precisar ter que cadastrar manualmente os clientes, tem provider de auth com Better-Auth, cria a conta e ja cria no serviços deles.
Estou começando a documentar essa jornada, grato se me derem os feedbacks sobre a documentação também :D.
Fonte: https://papayapdv.com.br