1

[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

  1. 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.
  2. Ter um e-commerce próprio, não um catálogo alugado que depende de plataforma de terceiros pra existir.
  3. 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

  1. Quem já fez sync offline-first (WatermelonDB, PouchDB etc.): como vocês tratam conflito quando a mesma base sincroniza depois de horas offline?

  2. Quem modelou catálogo com variações: guarda preço/estoque na variante ou tenta resolver no produto? Quero comparar abordagens.

  3. 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.

Carregando publicação patrocinada...