Plataforma web para bares, restaurantes e eventos. O cliente lê o QR code da mesa, vê o cardápio do espaço, monta o carrinho e faz o pedido pelo celular. A cozinha recebe tudo num quadro Kanban que se atualiza sozinho, e o cliente acompanha o status do pedido em tempo quase real.
É multi-espaço: uma mesma instalação atende vários estabelecimentos, cada um com seu cardápio, estoque, mesas, equipe e QR codes. A mesma conta de cliente funciona em qualquer espaço.
Sobre esta versão. Versão anonimizada de um sistema real que desenvolvi para uma empresa; nomes, marca, domínios, conteúdo proprietário e dados foram substituídos por fictícios. A arquitetura, as regras de negócio e o código são os do sistema real.
English summary: Multi-tenant QR-code ordering platform for restaurants and events (React 19 + TypeScript + Vite + Tailwind / Node.js + Express 5 + PostgreSQL). Customers scan the table's QR code, browse the venue's menu and order from their phone. Staff work on an auto-refreshing kitchen Kanban (new → preparing → ready → delivered), manage menu, stock and tables, print per-table QR codes, take manual orders, close the kitchen with a message, and read customer reviews. Role-based access (platform admin, kitchen manager, waiter, customer) is enforced per venue by middleware. Optional WhatsApp Cloud API notification on each new order. Runs locally with one docker compose up.
- O que a plataforma faz
- Arquitetura
- Stack
- Rodar localmente
- Contas de demonstração
- Roteiro para testar
- Integrações
- Segurança
- Estrutura do repositório
- Variáveis de ambiente
- Decisões técnicas
| Módulo | O que faz |
|---|---|
| Entrada pela mesa | O QR code abre /e/<espaço>/m/<mesa>. Dá para ler o QR pela própria tela (câmera do aparelho, decodificada com jsQR) ou colar o link. Se o cliente já estava em outra mesa, o app pergunta se ele quer trocar. |
| Conta | Cadastro e login com e-mail e senha. O cadastro público só cria cliente: tentar mandar outro papel é recusado pela API. |
| Cardápio | Produtos por categoria, com busca. Itens desabilitados não aparecem, itens sem estoque aparecem como esgotados e o preço pode ficar oculto (por exemplo, "prato do dia"). O cardápio é recarregado a cada 2 s, então o estoque na tela acompanha a cozinha. |
| Carrinho e pedido | Carrinho salvo no aparelho, com observação por item. O pedido baixa o estoque dentro de uma transação. Também há o pedido por texto livre ("traz mais pão, por favor"). |
| Acompanhamento | Status do pedido (aguardando, em preparo, pronto) atualizado a cada 2 s, e histórico com "pedir de novo". |
| Cozinha fechada | Quando a equipe fecha a cozinha, o cliente vê a mensagem e o horário de volta, e a API recusa novos pedidos. |
| Avaliação | Nota de 1 a 5, motivo e comentário, com opção de enviar de forma anônima. |
- Painel (
/dashboard): lista os espaços de que a pessoa faz parte. - Kanban da cozinha (
/e/<espaço>/kds): colunas Novos → Em preparo → Pronto, atualizadas a cada 2 s. Marcar como entregue tira o pedido do quadro e manda para o histórico. - Pedido avulso: a equipe lança um pedido para um cliente (com busca entre quem já pediu no espaço) ou para um nome qualquer, com mesa e observação.
- Cardápio e estoque (gerente): categorias, produtos com foto por URL, edição, reposição de estoque, habilitar/desabilitar e excluir.
- Abrir e fechar a cozinha com mensagem e horário de volta opcional.
- Avaliações recebidas e histórico de pedidos entregues.
- Mesas e QR codes (
/e/<espaço>/qr): cria e remove mesas e imprime um QR por mesa. - Equipe (
/e/<espaço>/manage): o gerente cadastra garçons. O administrador da plataforma cadastra gerentes e garçons e vincula contas que já existem.
- Cria espaços (o slug vira o endereço e não muda depois, para não invalidar QR codes impressos).
- Ativa e desativa espaços. Espaço inativo some para o cliente.
- Acessa o painel de qualquer espaço para dar suporte.
A API confere tudo em cada requisição. Papel global vem do usuário, e o acesso a um espaço vem do vínculo (space_memberships).
| Ação | Cliente | Garçom | Gerente | Admin |
|---|---|---|---|---|
| Ver cardápio, pedir, avaliar | ✓ | |||
| Ver pedidos e mudar status | ✓ | ✓ | ✓ | |
| Cardápio, estoque, avaliações, pedido avulso | ✓ | ✓ | ||
| Abrir/fechar cozinha, cadastrar garçom | ✓ | ✓ | ||
| Mesas e QR codes¹ | ✓ | ✓ | ||
| Criar/desativar espaços, cadastrar gerente | ✓ |
¹ A tela de mesas e QR codes é do gerente e do admin. Na API, as rotas de mesas aceitam qualquer membro da equipe do espaço.
flowchart LR
subgraph Cliente
C[Celular do cliente<br/>React SPA]
end
subgraph Equipe
K[Kanban da cozinha<br/>e painéis]
end
subgraph API[API Node.js / Express 5]
MW[Middlewares<br/>JWT, papel, espaço via X-Space-ID]
R[Rotas REST<br/>auth, spaces, items, orders, reviews]
WA[Aviso de pedido<br/>opcional]
end
DB[(PostgreSQL)]
META[WhatsApp Cloud API]
C -- Bearer + X-Space-ID --> MW
K -- polling 2s --> MW
MW --> R
R --> DB
R --> WA
WA -.-> META
Fluxo principal
- O administrador cria o espaço. A API cria as mesas padrão (Mesa-1 a Mesa-5 e Balcão), e a equipe imprime os QR codes.
- O cliente lê o QR. O front busca o espaço pelo slug e, dali em diante, manda o id do espaço no cabeçalho
X-Space-IDem toda chamada. - O pedido é gravado numa transação que trava cada produto (
SELECT ... FOR UPDATE), confere disponibilidade e estoque e dá a baixa. Se um item falhar, nada é gravado. - A cozinha vê o pedido no Kanban e muda o status. O celular do cliente acompanha pelo endpoint de rastreio, que só devolve pedidos do próprio cliente.
- Opcionalmente, cada pedido novo dispara um template do WhatsApp para a equipe.
Frontend: React 19, TypeScript 5.9, Vite 7, Tailwind CSS 3, React Router 7, Zustand (sessão, carrinho e espaço atual, com persist), Framer Motion, qrcode.react (geração), jsqr (leitura), react-hot-toast, Lucide.
Backend: Node.js 22, Express 5, PostgreSQL (pg), JWT (jsonwebtoken), senha com scrypt do crypto nativo (salt por usuário e comparação em tempo constante), express-rate-limit, CORS configurável.
Infra: Dockerfiles (a imagem da API roda sem root) e docker-compose para o ambiente local. Em produção roda numa VPS com deploy por GitHub Actions; essa parte foi retirada deste repositório.
Pré-requisito: Docker Desktop.
git clone https://github.com/vitormouramaker/comanda-digital-platform.git
cd comanda-digital-platform
docker compose up --buildNa subida, o compose cria o banco, as tabelas e os dados de demonstração.
Pré-requisitos: Node.js 22+ e PostgreSQL 14+ rodando localmente.
# 1. API
cd backend
cp .env.example .env # ajuste DB_USER / DB_PASSWORD / DB_PORT se precisar
npm install
npm run db:init # cria o banco comanda_digital e as tabelas
npm run seed:demo # contas, espaço e cardápio de demonstração (senha Demo@123)
npm run dev # http://localhost:4444
# 2. Front (outro terminal)
cd frontend
npm install
npm run dev # http://localhost:5173O front usa http://localhost:4444 por padrão. Para apontar para outra API, defina VITE_API_URL em frontend/.env.local.
| Sem... | Comportamento |
|---|---|
WhatsApp (WHATSAPP_*) |
O pedido é feito normalmente e o aviso aparece só no console da API. |
Criadas por npm run seed:demo (ou automaticamente no Docker). Senha de todas: Demo@123. As contas existem só no seu banco local. O roteiro se recusa a rodar com NODE_ENV=production.
| Perfil | Onde entrar | |
|---|---|---|
| Administrador da plataforma | master@demo.comanda.test |
http://localhost:5173/login |
| Gerente de cozinha do "Bistrô Demonstração" | cozinha@demo.comanda.test |
http://localhost:5173/login |
| Garçom do "Bistrô Demonstração" | garcom@demo.comanda.test |
http://localhost:5173/login |
| Cliente | cliente@demo.comanda.test |
http://localhost:5173/e/bistro-demo/m/Mesa-1 |
O seed também cria o espaço bistro-demo, as mesas padrão e um cardápio fictício com produtos em estoque, com estoque infinito, com preço oculto e um produto indisponível (que só a equipe vê).
- Abra http://localhost:5173/e/bistro-demo/m/Mesa-1 e entre como cliente (ou crie uma conta nova).
- Coloque dois itens no carrinho, com observação, e faça o pedido. Mande também um pedido por texto.
- Numa janela anônima, entre como cozinha em
/login, abra o espaço e vá ao Kanban. Os pedidos aparecem em até 2 s. Mova um deles até "Pronto" e veja o status mudar no celular do cliente. - Ainda como cozinha: reponha estoque, desabilite um produto (ele some do cardápio do cliente), lance um pedido avulso e feche a cozinha com uma mensagem. Tente pedir como cliente: a mensagem aparece e a API recusa.
- Como garçom, confira que dá para tocar o Kanban, mas não editar o cardápio.
- Como master, crie um espaço novo, cadastre um gerente para ele e abra
/e/<slug>/qrpara imprimir os QR codes. - Como cliente, avalie o atendimento e veja a avaliação no painel da cozinha.
- Implementado em
backend/src/services/whatsapp.js. - A cada pedido (por itens ou por texto), envia um template aprovado com três parâmetros: cliente, espaço e itens (com observações).
- O envio não segura a resposta do pedido, e uma falha no WhatsApp vira só log: o pedido nunca é perdido por causa do aviso.
- Token, id do número, destinatário, template, idioma e versão da API vêm de variáveis de ambiente. Sem elas, a integração fica desligada.
- Senhas com
scrypte salt aleatório por usuário, comparadas comtimingSafeEqual. - JWT assinado com segredo vindo do ambiente (HS256 fixo na verificação). Em produção a API não sobe sem um segredo de pelo menos 32 caracteres. A cada requisição, o usuário é recarregado do banco, então papel e vínculos valem na hora, não quando o token foi emitido.
- Autorização em camadas:
authenticateToken→requireRole(papel global) →requireSpaceAccess(vínculo com o espaço doX-Space-ID) →requireSpaceMemberRole(papel dentro do espaço). Toda query de escrita filtra também porspace_id, para um espaço não alterar dados de outro. - Cadastro público só cria cliente. Vincular uma conta existente à equipe exige que ela já tenha o mesmo papel, para não promover cliente a equipe.
- O pedido valida quantidade (inteira, de 1 a 99) e status (lista fechada), e o rastreio só devolve pedidos do próprio cliente.
- Rate limit em login e cadastro, CORS com lista de origens e
x-powered-bydesligado. A rota pública do espaço não expõe ids internos de usuário. - Todas as queries são parametrizadas.
comanda-digital-platform/
├── docker-compose.yml # Postgres + API + front para rodar local
├── backend/
│ ├── src/
│ │ ├── app.js # bootstrap, CORS, rate limit, rotas
│ │ ├── config/env.js # leitura e validação do ambiente
│ │ ├── routes/ # auth, spaces (espaços, mesas, equipe, cozinha),
│ │ │ # delivery (categorias, itens, pedidos, avaliações)
│ │ ├── models/ # SQL por domínio (usuários, espaços, cardápio/pedidos)
│ │ ├── security/ # sessão/JWT e middlewares de acesso, hash de senha
│ │ ├── services/ # aviso de pedido pelo WhatsApp
│ │ └── server/ # conexão, schema idempotente, init do banco
│ └── scripts/seed-demo.js # dados de demonstração
└── frontend/
└── src/
├── pages/ # cardápio do cliente, Kanban, painéis, mesas/QR, equipe
├── components/ # carrinho, leitor de QR, estoque, pedido avulso, avaliações...
├── store/ # Zustand: sessão, carrinho, espaço atual
└── lib/ # cliente da API (token + X-Space-ID), mesas/QR
Todas estão documentadas em backend/.env.example e frontend/.env.example.
| Variável | Para quê | Obrigatória localmente |
|---|---|---|
PORT |
Porta da API (padrão 4444) | não |
DB_* |
Conexão com o PostgreSQL | sim (o exemplo já funciona) |
JWT_SECRET |
Assinatura dos tokens (≥ 32 caracteres; obrigatória em produção) | sim (o exemplo já funciona) |
JWT_EXPIRES_IN |
Validade da sessão (padrão 7d) |
não |
CORS_ORIGINS |
Origens liberadas, separadas por vírgula (vazio = qualquer uma) | não |
WHATSAPP_TOKEN, WHATSAPP_PHONE_NUMBER_ID, WHATSAPP_NOTIFY_TO |
Liga o aviso de pedido novo | não |
WHATSAPP_TEMPLATE, WHATSAPP_TEMPLATE_LANGUAGE, WHATSAPP_API_VERSION |
Ajustes do template | não |
VITE_API_URL (front) |
Endereço da API usado pelo front | não |
- Multi-tenant por cabeçalho. O front resolve o espaço pelo slug da URL uma vez e manda o id em
X-Space-ID. As rotas de cardápio e pedidos ficam iguais para todos os espaços, e o middleware garante o isolamento. - Estoque consistente sob concorrência. O pedido trava as linhas dos produtos (
FOR UPDATE) e faz tudo numa transação.stock = NULLsignifica estoque infinito, e a reposição religa o produto automaticamente quando o estoque volta a ser positivo. - Schema idempotente no código (
ensure_schema.js). A cada subida, a API confere e completa o banco (CREATE ... IF NOT EXISTS,ADD COLUMN IF NOT EXISTS). Um banco vazio e um de produção chegam ao mesmo estado sem ferramenta de migração. - Polling em vez de WebSocket. Kanban e acompanhamento consultam a API a cada 2 s. Para o volume de um salão, isso é simples, aguenta queda de rede sem lógica de reconexão e passa por qualquer proxy.
- Slug imutável. O endereço do espaço vai impresso nos QR codes das mesas, então não pode mudar depois de criado.
- Integração que não derruba o fluxo. O aviso de WhatsApp é disparado sem bloquear a resposta e sem propagar erro.
Autor: @vitormouramaker
Este código é exibido como portfólio. Ele não tem licença de uso aberta: todos os direitos são reservados.