Protótipo web · PWA responsivo
MeAjuda Aí — documentação
Marketplace que conecta profissionais autônomos da construção e manutenção a ajudantes por diária. Publicar vaga → candidatar → aceitar → conversar → avaliar.
01 Visão geral
O que é o produto e para quem.
O MeAjuda Aí resolve um encontro do dia a dia da obra: o profissional que precisa de um par de mãos amanhã e o ajudante que procura diária na região. A entrega desta fase é uma aplicação web responsiva (PWA) — abre no navegador do celular e do desktop, sem instalação.
Cada profissional é dono de uma empresa (workspace) com sua equipe; o marketplace de vagas cruza essas empresas com os ajudantes. Tudo é isolado por empresa via Row-Level Security no banco.
🧰 Profissional
Publica vagas de diária, avalia candidatos, seleciona, conversa e avalia. É admin de uma empresa.
👷 Ajudante
Busca vagas na região, candidata-se, combina pelo chat, executa e avalia o profissional.
🛡️ Administrador
Modera denúncias, gerencia usuários e o banner da home. É sysadmin da plataforma.
Há ainda o papel funcionário: subordinado a um profissional, com acesso liberado módulo a módulo pelo dono da empresa.
02 Arquitetura
Next.js no servidor, Supabase como backend gerenciado.
O frontend e a lógica de escrita vivem no App Router do Next.js (Server Components + Server Actions). O Supabase fornece Postgres, autenticação, Realtime e Storage. A regra de acesso mora no banco, em políticas RLS — o código de aplicação não é a última linha de defesa.
Next.js · App Router
Supabase
Decisões que moldam o código
- Multi-tenant por RLS. Toda vaga, conversa e permissão pertence a um
workspace; funções SECURITY DEFINER checam participação sem recursão de política. - Três clientes Supabase.
server(sessão, respeita RLS),browser(Realtime) eadmin(service-role, ignora RLS — só no servidor, após os guards). - Funciona sem JavaScript. Formulários usam
<form action={serverAction}>; o POST nativo roda a ação no servidor e devolve estado. Progressive enhancement pensando em 4G de obra. - PII e coordenada exata isoladas. RLS esconde linha, não coluna — então dado sensível vive em tabela à parte (
profiles_pii,vaga_local).
03 Stack & estrutura de pastas
Onde cada coisa mora.
| Camada | Tecnologia |
|---|---|
| Frontend / rotas | Next.js 15 (App Router) · React 19 · TypeScript · Tailwind (Poppins) |
| Backend / banco | Supabase · PostgreSQL com RLS |
| Autenticação | Supabase Auth (e-mail/senha) |
| Tempo real | Supabase Realtime (mensagens e notificações in-app) |
| Mapas | Leaflet + OpenStreetMap · geocoding via Nominatim |
| Validação | Zod (schemas em lib/validation.ts) |
| Testes | Vitest (unidade + isolamento RLS opt-in) |
app/ Rotas: (auth) login/cadastro · (app) fluxo · (legal) termos/privacidade components/ UI, nav, cards, chat, mapa, avaliação, notificações lib/ supabase/ clientes server / browser / admin + tipos gerados auth/ guards de usuário, papel, workspace, módulos actions/ server actions (escrita, com Zod + service-role após guard) *.ts utilitários BR: format, categorias, cidades, validação, período… supabase/migrations/ 0001–0019 (schema, RLS, seeds, comentários) tests/ Vitest (unidade + rls.test.ts opt-in) docs/ esta documentação + apresentação
04 Modelo de dados
19 tabelas no schema public, todas com RLS ativa. Cada tabela e coluna tem COMMENT no banco (migration 0019) — visível no Dashboard e no \d+.
Identidade & acesso
| Tabela | Colunas-chave | Papel |
|---|---|---|
| profiles | user_id, nome, foto_url, cidade, tipo_base, nota_media, verificado, status, bio | Perfil público 1:1 com auth.users. nota_media é mantida por trigger. |
| profiles_pii | user_id, telefone, email | PII isolada; lê só o dono ou o sysadmin. RLS estrita |
Empresa (multi-tenant)
| Tabela | Colunas-chave | Papel |
|---|---|---|
| workspaces | id, owner_id, nome, cidade | Empresa/equipe — a unidade de isolamento multi-tenant. |
| workspace_members | (workspace_id, user_id), role | Vínculo usuário↔empresa (owner | membro). |
| user_modules | (user_id, workspace_id, module), allowed | Liga/desliga módulos e capacidades por funcionário. |
| invite | token, workspace_id, role, status | Convite por link + aprovação. Só o service-role manipula. |
Marketplace
| Tabela | Colunas-chave | Papel |
|---|---|---|
| vagas | id, workspace_id, titulo, categoria, cidade, valor_diaria, status, local_aprox_* | Vaga de diária. Coração do marketplace. |
| vaga_local | vaga_id, lat, lng | Coordenada exata; lê só equipe dona ou ajudante aceito. RLS estrita |
| candidaturas | (vaga_id, ajudante_id), status | Candidatura de um ajudante. Insert só em vaga aberta. |
| avaliacoes | vaga_id, avaliador_id, avaliado_id, nota | 1–5 estrelas entre partes da diária. Alimenta a média por trigger. |
| demanda_servico | (user_id, categoria, cidade) | Demanda reprimida em busca sem resultado. |
| categorias_servico | slug, nome, ordem | Catálogo de categorias (seed). |
Comunicação
| Tabela | Colunas-chave | Papel |
|---|---|---|
| conversas | id, workspace_id, tipo, ajudante_id | Canal da equipe · DM interna · DM externa (com ajudante). |
| conversa_membros | (conversa_id, user_id), lido_ate | Membros + ponteiro de leitura (não-lidas). |
| mensagens | id, conversa_id, remetente_id, conteudo | Mensagens do chat. Realtime |
| notificacoes | id, user_id, tipo, titulo, link | Notificações in-app. Realtime |
Agenda · moderação · admin
| Tabela | Colunas-chave | Papel |
|---|---|---|
| bloqueio_agenda | (ajudante_id, data) | Dias de indisponibilidade do ajudante (privado por dono). |
| denuncias | id, denunciante_id, alvo_tipo, alvo_id, motivo, status | Moderação; alvo polimórfico. Só sysadmin resolve. |
| home_banner | id=1, texto, ativo | Banner único da home (singleton), editável pelo sysadmin. |
Convenção: toda coluna *_id referencia auth.users(id) salvo indicação; created_at é timestamptz default now(). Chaves primárias em amarelo nas migrations.
05 Segurança & RLS
A regra de acesso é do banco. O código só orquestra.
Todas as tabelas têm Row-Level Security ativa. O papel do usuário viaja no JWT (app_metadata.app_role), injetado pelo auth hook custom_access_token_hook a partir de profiles.tipo_base. As políticas leem esse papel via current_app_role().
Para evitar recursão entre políticas (uma política de vagas que consulta candidaturas, e vice-versa), a checagem de participação fica em funções SECURITY DEFINER — elas não disparam RLS por dentro:
| Função | Responde |
|---|---|
| current_app_role() | Papel do usuário logado, lido do JWT. |
| is_workspace_member(ws) | Pertence à empresa? |
| can_manage_vaga(vaga) | É da equipe dona da vaga? |
| is_candidato(vaga) | Tem candidatura nesta vaga? |
| is_parte_vaga(vaga, user) | É candidato OU equipe da vaga? (gate de mensagem/avaliação) |
| vaga_aberta(vaga) | Vaga existe e está aberta? (gate de candidatura) |
| is_ajudante_aceito(vaga, user) | É o ajudante aceito? (libera coordenada exata) |
| is_conversa_membro(conversa) | Participa da conversa? (núcleo da RLS do chat) |
| has_capability(user, ws, cap) | Tem a capacidade liberada na empresa? |
🪪 PII separada
profiles_pii guarda telefone/e-mail; lê só o dono ou o sysadmin.
📍 Local exato protegido
Mapa público usa coordenada arredondada (~1 km). O ponto exato (vaga_local) só aparece para a equipe e o ajudante aceito.
👁 Contas demo read-only
Toda escrita passa por requireWriter(); contas de demonstração são bloqueadas para não alterarem dados compartilhados.
🕵️ Denúncia anônima
O denunciante fica gravado, mas só a moderação o vê — jamais exposto ao denunciado.
06 Papéis & permissões (RBAC)
Quatro papéis globais + módulos e capacidades por funcionário.
| Papel | É… | Pode |
|---|---|---|
| sysadmin | Dono da plataforma | Modera denúncias, gerencia usuários e banner; enxerga tudo (god-mode das policies). |
| admin | Profissional / dono de empresa | Publica vagas, gere equipe, convida, liga/desliga módulos dos funcionários. |
| funcionario | Subordinado ao admin | Acessa apenas os módulos/capacidades liberados pelo dono da empresa. |
| ajudante | Mão de obra | Candidata-se, conversa, avalia, gerencia a própria agenda. |
Módulos e capacidades (tabela user_modules)
Módulos de painel — vagas, equipe, financeiro, relatorios, mapa. Ausência de linha = default do papel.
Capacidades de ação — publicar_vagas, chat_ajudantes. Ausência de linha = OFF (menor privilégio).
Guards em lib/auth/: requireModule / guardModule (server action / página) e requireCapability. sysadmin e admin recebem tudo; funcionário depende das linhas explícitas.
07 Fluxos principais
Do anúncio à reputação, em cinco passos.
Ciclo de vida da vaga
O status da vaga guia a interface e dispara notificações a cada transição:
A candidatura tem o seu próprio ciclo: aguardando → aceito | recusado | cancelado. Aceitar coloca a vaga em andamento e abre (ou reaproveita) a DM externa equipe↔ajudante, que persiste entre diárias.
08 Server Actions
A superfície de escrita, em lib/actions/. Cada uma valida com Zod, passa pelos guards e revalida os caminhos afetados.
| Domínio | Ações |
|---|---|
| Autenticação | cadastrar, entrar, recuperarSenha, definirSenha, trocarMeuPapel, logout |
| Vagas | publicarVaga, editarVaga, mudarStatusVaga |
| Candidaturas | candidatar, cancelarCandidatura, responderCandidatura |
| Avaliações | avaliar |
| Chat & leitura | enviarMensagem, marcarNotificacoesVistas, marcarMensagensLidas |
| Perfil | salvarPerfil, removerFoto |
| Empresa | setActiveWorkspace, criarEmpresa, excluirEquipe, convidarMembro |
| Convites | criarConvite, aceitarConvite, aprovarConvite, recusarConvite |
| Equipe | setModuloFuncionario |
| Admin | definirPapel, criarAdmin |
| Moderação | criarDenuncia, moderarDenuncia |
| Outros | registrarDemanda, salvarBanner, alternarBloqueio, geocodeAddress |
Padrão: ações ligadas a <form> recebem FormData e devolvem EstadoForm (erro + valores preservados, nunca senha). Ações de botão devolvem ActionResult ({ ok, erro? }).
09 Rotas & telas
App Router, agrupado por área. Cada arquivo de rota tem um doc-comment de topo.
| Grupo | Rotas |
|---|---|
| Público | / landing + demo · /convite/[token] |
| (auth) | /login · /cadastro · /recuperar-senha · /nova-senha |
| (app) comum | /inicio · /vagas · /vagas/[id] · /mapa · /mensagens · /chat/[conversaId] · /notificacoes · /perfil/[id] · /perfil/editar |
| (app) profissional | /publicar · /minhas-vagas · /minhas-vagas/[id]/candidatos · /equipe · /financeiro · /relatorios |
| (app) ajudante | /minhas-diarias · /agenda · /avaliar/[vagaId] |
| (app) admin | /admin/usuarios · /admin/denuncias · /admin/demanda |
| (legal) | /termos · /privacidade |
| API | /api/demo/enter · /auth/confirmar |
10 Setup & execução
Do clone ao navegador.
Rodar localmente
npm install npm run dev # http://localhost:3000
O .env.local aponta para o projeto Supabase meajudaai-mvp. Para outro ambiente, veja .env.example (URL, anon key, service-role key, NEXT_PUBLIC_SITE_URL).
Scripts
| Comando | Faz |
|---|---|
| npm run dev | Servidor de desenvolvimento |
| npm run build | Build de produção |
| npm run typecheck | tsc --noEmit |
| npm run lint | ESLint (next lint) |
| npm test | Vitest (unidade) |
| npm run test:integration | Isolamento RLS no Supabase real (opt-in) |
Passo único no Supabase
Registre o auth hook para o papel entrar no JWT:
Dashboard → Authentication → Hooks → Custom Access Token → public.custom_access_token_hook.
O app funciona sem isso (lê o papel do banco como fallback), mas o hook é o caminho oficial.
As migrations 0001–0019 descrevem o schema, a RLS, os seeds e os comentários. No protótipo elas são aplicadas via conector Supabase MCP; a 0019 anexa os COMMENT ON de tabelas, colunas e funções.
11 Contas de demonstração
A landing (/) tem cards «Explore sem cadastro». O clique entra por /api/demo/enter?who=… com cookies session-only. Tudo somente leitura.
| Quem | Papel | |
|---|---|---|
| João Eletricista | Profissional | joao.demo@meajudaai.app |
| Carlos Silva | Ajudante | carlos.demo@meajudaai.app |
| Ana Assistente | Funcionário | ana.demo@meajudaai.app |
| Rafael Nunes | Moderador | admin.demo@meajudaai.app |
Os dados de exemplo (vagas, candidatura aceita, conversa, avaliações e denúncias) são semeados pelas migrations 0005 e 0007.