Sylvino Prevot

Case Study // 002

3R
Barber

Em Produção
Monorepo

Core Technologies

Hono (Cloudflare Workers) bolt
Cloudflare D1 + Drizzle ORM database
Better Auth (RBAC) lock
React + Vite + React Router 7 web
Tailwind CSS v4 + shadcn/ui format_paint
Playwright (E2E) task_alt

Project Stats

Lojas Atendidas

2

Papéis de Acesso

3

01. Technical Overview

3R Barber é um app de agendamento para barbearias construído como monorepo pnpm (apps/api, apps/web, packages/shared), rodando inteiramente no free tier da Cloudflare. O cliente reserva horário sem criar conta — escolhendo loja, serviço, barbeiro (ou 'qualquer barbeiro') e horário — e acompanha o agendamento por um link com token único. Barbeiros e administradores têm dashboards próprios para gerenciar fila, agenda, relatórios e configurações da loja.

warning

The Challenge

O desafio central era entregar uma experiência de produto completa (reserva pública, fila em tempo real, dashboards com métricas) sem depender de nenhum serviço pago, mantendo consistência de horários entre fusos e isolando corretamente o acesso de cada papel de usuário:

  • / Converter corretamente horários de funcionamento em BRT para timestamps UTC na geração de slots de agendamento.
  • / Suportar agendamentos com 'qualquer barbeiro', onde o primeiro a atender ganha o cliente, sem gerar conflitos de concorrência.
  • / Isolar dados por papel (client/barber/admin) garantindo que um barbeiro só acesse a própria fila e agenda.
  • / Evitar agendamentos duplicados por reenvio de formulário (duplo clique, retry de rede) sem exigir conta do cliente.

02. The Solution

A API em Hono roda como Cloudflare Worker com Drizzle ORM sobre D1, validando toda mutação com schemas Zod (zValidator). A autenticação e o RBAC usam Better Auth com sessão por cookie, e cada rota de barbeiro busca o registro pelo userId autenticado antes de tocar em qualquer dado da loja. A conversão de horário fica centralizada em helpers de BRT no backend, e a proteção contra reserva duplicada combina checagem por telefone+loja+horário com um índice único no banco como reforço contra a corrida.

apps/api/src/utils.ts
// Slot generation: BRT hora + 3 = UTC hora
const BRT_TO_UTC_H = 3;

export function getDayBounds(dateBRT: string) {
  const [y, m, d] = dateBRT.split("-").map(Number);
  const startTs = Date.UTC(y, m - 1, d, BRT_TO_UTC_H, 0) / 1000;
  const endTs = startTs + 24 * 60 * 60 - 1;
  return { startTs, endTs };
}

Infrastructure

architecture

Deploy & CI/CD

API na Cloudflare Workers e frontend na Cloudflare Pages, ambos no free tier. Fluxo de reserva coberto por testes E2E com Playwright rodando contra um banco D1 isolado, recriado do zero a cada execução.

Previous Project Portfólio Pessoal
Next Project Em Breve