Documentação

Documentação técnica — Templo Virtual

Documentação técnica — Templo Virtual

Documento para quem vai desenvolver, revisar ou operar o sistema. Para a visão de produto, veja o Guia do Usuário; para contribuir, veja OPEN-SOURCE.md.


1. Visão geral

O Templo Virtual (hub de gerenciamento das ordens paramaçônicas; repositório histórico sgc-dm / SG-CDM) é uma aplicação full-stack TanStack Start (React + SSR) com Supabase (Postgres, Auth, Storage) como backend. Não existe servidor de API separado: a camada de servidor são server functions do TanStack Start, e a autorização real mora em políticas RLS no Postgres.

A aplicação é multi-inquilino por capítulo: quase toda tabela carrega chapter_id, e a associação usuário↔capítulo↔cargo vive em chapter_members. Acima disso há um escopo regional/estadual para lideranças (org_leaderships) — acompanhamento em leitura, com escrita de instituições/regiões/lideranças para o GME do estado. MCR e OE gerenciam instituições e membros da própria região e o calendário unificado; nomeação ritualística única por região via transfer_region_office. Cadastro de estados fica fora do produto (SQL/painel Supabase).

Stack

CamadaTecnologiaVersão
FrameworkTanStack Start^1.168.26
RoteamentoTanStack Router (file-based)^1.170.16
UIReact / React DOM^19.2.0
BuildVite^8.0.16
BuildVite + plugins (@vitejs/plugin-react, Tailwind, Nitro)^8.0.16
ServidorNitro (preset cloudflare-module)3.0.260603-beta
LinguagemTypeScript (strict, ES2022)^5.8.3
EstiloTailwind CSS v4 + shadcn/ui (new-york) + Radix^4.2.1
Estado de servidorTanStack Query^5.101.1
Banco / Auth / Storage@supabase/supabase-js^2.110.8
ValidaçãoZod^3.24.2
Formuláriosreact-hook-form + @hookform/resolvers^7.71.2
IAVercel AI SDK (ai) + provider OpenAI-compatible^7.0.37
Documentosjspdf, xlsx, qrcode, html5-qrcode
Markdown (docs no app)react-markdown + remark-gfm
Gráficosrecharts^2.15.4
Lint/formatESLint 9 (flat config) + Prettier

Gerenciador de pacotes: Bun (bun.lock, bunfig.toml). Existe um package-lock.json como alternativa com npm.

bunfig.toml define minimumReleaseAge = 86400 — pacotes publicados há menos de 24h são bloqueados, como proteção contra ataques de cadeia de suprimentos.


2. Estrutura de diretórios

public/              favicon, robots.txt
supabase/
  config.toml        project ref + [functions.send-email] (verify_jwt = false)
  functions/         Edge Functions versionadas no repo; deploy manual no dashboard
    send-email/      Auth Send Email Hook (Resend) — ver README da pasta
  migrations/        arquivos .sql — a única fonte de verdade do schema
src/
  server.ts          handler de fetch do SSR (normaliza erros 500 engolidos pelo h3)
  start.ts           createStart(): middlewares globais de auth, CSRF e erro
  router.tsx         cria router + QueryClient (staleTime 60s)
  routeTree.gen.ts   GERADO — não editar
  styles.css         tema Tailwind v4 e design tokens
  routes/            telas (roteamento por arquivo — ver src/routes/README.md)
  components/
    ui/              52 primitivos shadcn/ui — não editar à mão
    shell/           AppShell (sidebar desktop, abas mobile via visibleMobileTabs, troca de escopo)
    docs/            visualizador público de documentação (Markdown)
    members/ minutes/ settings/   componentes de feature
    *.tsx            EmptyState, PageHeader, PageSkeleton, QrScanner, ThemeToggle…
  context/           ActiveChapterContext, OrgScopeContext, ThemeContext
  hooks/             use-mobile, useCommissionAccess
  integrations/supabase/
    client.ts        cliente do browser (chave anônima)
    client.server.ts cliente service-role — ignora RLS, uso restrito
    auth-attacher.ts middleware de cliente: anexa o Bearer token
    auth-middleware.ts middleware de servidor: valida o JWT
    types.ts         GERADO do schema — não editar
  lib/
    *.functions.ts   server functions (camada de serviço)
    *.server.ts      código exclusivo de servidor
    *.ts             helpers puros (permissions, nav, format, terms, ics…)

Arquivos gerados — nunca editar à mão

  • src/routeTree.gen.ts — regenerado pelo plugin do TanStack Router a cada dev/build.
  • src/integrations/supabase/types.ts — regenerado a partir do schema do Supabase. Depois de criar uma migration, este arquivo precisa ser regerado, senão o TypeScript não conhece a tabela nova.

3. Arquitetura e fluxo de dados

Pontos de entrada

ArquivoPapel
src/server.tsEnvolve o server-entry do TanStack Start e converte erros 500 engolidos pelo h3 em uma página de erro legível
src/start.tscreateStart() — registra functionMiddleware: [attachSupabaseAuth] e requestMiddleware: [errorMiddleware, csrfMiddleware]
src/router.tsxCria o router e o QueryClient (staleTime 60s, sem refetch ao focar a janela), passado como contexto do router
src/routes/__root.tsxShell HTML, <head>, script inline anti-flash de tema, providers, toaster, boundaries de 404 e erro

Camadas

  1. UIsrc/routes/** (telas) sobre src/components/ui/** (shadcn/ui) e o AppShell.
  2. Estado de cliente — React Context para o que é transversal à sessão (ActiveChapterContext, OrgScopeContext, ThemeContext). Não há Redux, Zustand ou Jotai.
  3. Estado de servidor — TanStack Query para tudo que vem do banco.
  4. Camada de serviçosrc/lib/*.functions.ts. Cada export é um createServerFn({ method: "POST" }) com inputValidator Zod e .middleware([requireSupabaseAuth]). São endpoints RPC que rodam no servidor mas se importam e chamam como funções assíncronas comuns.
  5. Dados — Supabase Postgres. O context.supabase recebido pelo handler é um cliente vinculado ao JWT de quem chamou, então toda query continua sujeita a RLS.

Caminho completo de uma requisição

componente
  └─ useQuery / useMutation           chave ex.: ["cash-entries", chapterId, year, month]
       └─ serverFn({ data })          src/lib/*.functions.ts
            └─ attachSupabaseAuth     middleware de cliente — anexa Authorization: Bearer <access_token>
                 └─ requireSupabaseAuth   middleware de servidor — valida o JWT via getClaims()
                                          e monta context.supabase + context.userId
                      └─ inputValidator (Zod)
                           └─ supabase-js
                                └─ Postgres + políticas RLS
  ◀── dados ── cache do Query ── render
onSuccess da mutation → qc.invalidateQueries([...]) → refetch

Exceções ao caminho acima

  • Leituras diretas do browser para o Supabase, sem passar por server function — por exemplo o ActiveChapterContext consultando chapter_members/profiles, e todas as chamadas de supabase.auth. Nesses casos a RLS é a única barreira; não existe validação intermediária.
  • Escritas sensíveis passam por RPC do Postgres, não por escrita direta em tabela: create_member_with_pii, update_member_with_pii, add_member_guardian, reveal_member_pii.

4. Convenções de arquivo (importante)

O sufixo do arquivo carrega significado semântico e há lint reforçando isso.

SufixoSignificadoComo importar
*.functions.tsServer functions. O corpo roda no servidor, mas o módulo é importável do cliente — o bundler substitui pela chamada RPC.import { listCashEntries } from "@/lib/finance.functions" — normal, no topo
*.server.tsCódigo exclusivo de servidor. Nunca pode entrar no bundle do cliente.await import("@/lib/cash-validation.server")dinâmico, dentro do handler

Exemplos reais de *.server.ts: src/lib/cash-validation.server.ts, src/lib/ai-gateway.server.ts, src/integrations/supabase/client.server.ts.

O eslint.config.js proíbe importar o pacote server-only (padrão do Next.js) com uma mensagem explicando essa convenção — o TanStack Start usa *.server.ts ou @tanstack/react-start/server-only.

Outras convenções:

  • Idioma: strings da UI, segmentos de rota e comentários em português; identificadores de código em inglês. <html lang="pt-BR">.
  • Imports: alias @/./src/ (configurado no tsconfig.json).
  • Bibliotecas pesadas são carregadas sob demanda (jspdf, xlsx, qrcode, html5-qrcode todas atrás de await import()), para não inflar o bundle inicial.
  • src/components/ui/ é território do shadcn/ui — são primitivos gerados; alterações vão em componentes de feature, não neles.

5. Camada de serviço

Os arquivos em src/lib/*.functions.ts:

ArquivoCobre
accounts.functions.tsProvisão/vínculo de contas, e-mail de criação (link para senha), login por ID DeMolay, reset de senha
ai.functions.tsimproveText, composeEventDescription (provider OpenAI-compatible)
attendance.functions.tsChamada e registros de presença por evento de calendário
calendar.functions.tsCRUD de calendar_events, sessões em andamento
cash-subcategories.functions.tsConfiguração de subcategorias de caixa por comissão
chapter.functions.tsDados, configurações e identidade do capítulo
commissions.functions.tsComissões e seus membros por termo
events.functions.tsEventos de arrecadação: ingressos, e-mail do QR, mesas, assentos, check-ins
finance.functions.tsFluxo de caixa, categorias, mensalidades, assinantes do relatório
hospitality.functions.tsCardápios e escala de serviço
investigations.functions.tsFichas e processos de sindicância
members.functions.tsMembros, responsáveis, PII (revealMemberPii), histórico
minutes.functions.tsAtas, modelos e aprovações/assinaturas
minutes-share.functions.tsLink público da ata (senha), leitura e votos por e-mail
org.functions.tsEscopo regional/estadual, GME/MCR/OE: panorama, regiões, capítulos, membros, transferência MCR/OE
organization.functions.tsCargos e comissões do capítulo (não confundir com gestão estadual)

Helpers puros relevantes em src/lib/: permissions.ts (matriz de acesso), nav.ts (árvores de navegação — NAV_GROUPS/ORG_NAV_GROUPS para o sidebar; MOBILE_TABS/ORG_MOBILE_TABS como atalhos da barra inferior; visibleMobileTabs filtra a aba Eventos por comissão; /mais reutiliza visibleGroups/visibleOrgGroups via mobileOverflowGroups), terms.ts (ano/semestre), format.ts (BRL, datas, máscaras de PII), cash-categories.ts, chave-do-dia.ts, minute-vars.ts (interpolação de variáveis em modelos de ata), ics.ts, finance-pdf.ts, finance-xlsx.ts, minute-pdf.ts, chapter-logo.ts (URLs assinadas do bucket privado), query-keys.ts, error-capture.ts, error-page.ts.


6. Modelo de dados

Schema definido pelas migrations em supabase/migrations/; tipos gerados em src/integrations/supabase/types.ts.

Identidade e multi-inquilino

statesregionschapters (nome, número, cidade, primary_color, logo_url, settings JSONB, campos do encarregado LGPD) · profiles (1:1 com auth.users, guarda active_chapter_id e must_change_password; criado pelo trigger handle_new_user) · roles (catálogo) · chapter_members (usuário + capítulo + cargo + ativo — é o que concede todo o acesso ao capítulo) · org_leaderships (usuário + org_role + estado ou região + termo) · audit_logs.

Gestão estadual (GME): o Grande Mestre Estadual gerencia regiões, instituições e lideranças GME/MCE do próprio estado (is_gme). Telas em /regional/regioes, /regional/capitulos, /regional/liderancas. Estados não são criados/editados pelo app — apenas via SQL ou painel Supabase (operacional).

MCR / OE: papéis regionais (org_leaderships + cargos ritualísticos mestre_conselheiro_regional / oficial_executivo em member_positions). Podem criar/inativar instituições e membros da região e usar o calendário unificado. Nomeação: GME nomeia ambos; MCR nomeia MCR; OE nomeia OE e MCR — via RPC transfer_region_office (um titular ativo por região).

Pessoas

members (escopo de capítulo; user_id opcional → profiles — vínculo duro com a conta de login; status/kind; cpf_encrypted/cpf_last2, rg_encrypted/rg_last2, endereço JSONB, datas de graus e exames, demolay_id / masonic_id) · guardians (até 2 por membro, um principal via índice único parcial) · lgpd_consents.

Governança

positions (25 cargos semeados, de Mestre Conselheiro a Sentinela, mais cargos consultivos) ↔ member_positions (membro + cargo + term_year/term_semester) · commissions (9 semeadas: midia, novos_membros, manutencao, eventos, entretenimento, hospitalaria, auditoria, financas, sindicancias) ↔ commission_members (+ commission_role + termo) · chapter_lodges.

Calendário, presenças e atas

calendar_events (5 tipos, obrigatoriedade, aberto ao público, traje, local, lodge_id e related_event_id opcionais) · attendance_records (único por evento+membro; presente|ausente + justificativa) · session_minutes (1:1 com o evento de calendário; rascunho|em_revisao|aprovada; public_share_token opcional) · minute_approvals (por papel signatário) · minute_public_votes (feedback público por e-mail: aprovada|reprovada + justificativa; único por ata+e-mail) · minute_templates.

RPCs públicas da ata (anon): get_public_minute(token, password), submit_public_minute_vote(...). Gestão autenticada: ensure_minute_public_share_token, revoke_minute_public_share_token, get_minute_public_share_token. Senha fixa temporária: senha. Rota pública: /ata/$token (src/routes/ata.$token.tsx).

Finanças

cash_entries (kind, category texto, subcategory texto — snapshot, calendar_event_id, valor, data, comprovante) · cash_categories (por capítulo, is_system, único por capítulo+nome) · cash_subcategories (por capítulo, escopo eventos|hospitalaria, calendar_event_id opcional, ativo; índice único em capítulo+escopo+coalesce(evento)+lower(nome)) · member_dues (único por capítulo+membro+competence_year+competence_month; status em_aberto|pago|isento; cash_entry_id apontando de volta para cash_entries).

Eventos de arrecadação (distintos de calendar_events)

events (ticket_artwork_url) → ticket_typestickets (com qr_code; valido|cancelado|usado) → checkins (qr|nome) · event_tables (capacidade, pos_x/pos_y para o mapa) → seats. Arte em Storage event-artwork.

Comissões

investigation_filesinvestigation_processes (aberta|em_andamento|aprovada|reprovada|arquivada) · hospitality_menus (opcionalmente ligado a um evento de calendário, com custo estimado) · hospitality_duties.

Convenções do schema

Quase toda tabela de conteúdo carrega chapter_id (a chave de inquilino), created_by, created_at e updated_at — este último mantido pelo trigger tg_set_updated_at().

Enums (types.ts): attendance_status, calendar_event_type, cash_entry_kind, checkin_method, commission_role, due_status, event_status, investigation_status, member_status, minute_public_vote_decision, minute_signer_role, minute_status, org_role (gme/mce/mcr/oe), ticket_status.


7. Autenticação e autorização

Fluxo de autenticação

  1. /auth (src/routes/auth/index.tsx, ssr: false) — identificador (e-mail ou ID DeMolay) + senha. A autenticação passa por signInWithIdentifier em src/lib/accounts.functions.ts: se o identificador contém @, resolve como e-mail; senão busca members.demolay_id com user_id preenchido, resolve o e-mail via Admin API e autentica no servidor, devolvendo tokens para supabase.auth.setSession (o e-mail não é exposto na UI antes do login).
  2. Recuperação / primeiro acesso (mesmo diretório src/routes/auth/):
    • /auth/recuperar-senharequestPasswordResetresetPasswordForEmail (redirect para /auth/nova-senha)
    • /auth/nova-senha — define senha a partir do link de recovery
    • /auth/redefinir-senha — obrigatório quando profiles.must_change_password é true (senha temporária do provisão)
  3. Rotas públicas de documentação (fora de _authenticated): /documentacao, /documentacao/tecnica, /documentacao/guia, /documentacao/open-source — layout próprio em src/routes/documentacao/, renderizam os Markdowns de docs/ via react-markdown (src/lib/docs-catalog.ts, src/components/docs/).
  4. Outras rotas públicas (sem login): /mensalidades/$token, /fluxo-caixa/$token, /c/$token/* (lobby), /ata/$token (visão pública da ata com senha), /atualizar-cadastro.
  5. _authenticated/route.tsxbeforeLoad exige sessão; se must_change_password, redireciona para /auth/redefinir-senha; monta ActiveChapterProvider e OrgScopeProvider.
  6. _authenticated/index.tsx redireciona //inicio.
  7. _shell/route.tsx resolve o escopo de trabalho:
    • 0 vínculos de capítulo + ≥1 liderança → entra direto no escopo regional
    • 1 vínculo e nenhum escolhido → /selecionar-capitulo

    • 0 de ambos → mensagem de conta não vinculada
  8. O capítulo ativo é persistido em localStorage (sgcdm.activeChapterId) e espelhado em profiles.active_chapter_id para continuidade entre dispositivos. O escopo org fica em sgcdm.activeOrgScope.
  9. Toda chamada de server function leva Authorization: Bearer <access_token> e é verificada no servidor com supabase.auth.getClaims(token). Um middleware de CSRF protege essas requisições.
  10. O logout limpa o localStorage, chama supabase.auth.signOut() e navega com recarga completa para /auth.

Provisão de contas (MC / Admin Total)

Não há signup público. O fluxo é ficha primeiro, conta depois:

  1. Secretaria/admin cadastra o membro com e-mail válido.
  2. Na ficha (/membros/$id), o painel Acesso ao sistema (MemberAccountPanel) — visível só com permissão admin — chama provisionMemberAccount.
  3. A server function usa supabaseAdmin (auth.admin.createUser com senha aleatória, ou vincula conta já existente pelo e-mail), grava chapter_members, seta members.user_id e profiles.must_change_password = true.
  4. Se a conta é nova, gera um link de recuperação (auth.admin.generateLink) e envia e-mail de boas-vindas via Resend (definir senha — sem senha no e-mail). A senha temporária ainda aparece uma vez na UI para o MC repassar se precisar. Falha do e-mail não aborta a provisão. No primeiro login com senha temporária o usuário é forçado a /auth/redefinir-senha.
  5. Também há resetMemberTemporaryPassword e revokeMemberChapterAccess (desativa chapter_members.active no capítulo; não apaga auth.users).

Autorização em duas camadas

Camada 1 — UI/cliente: matriz em src/lib/permissions.ts, 8 cargos × 6 permissões.

Cargo (RoleName)RótuloPermissões
admin_totalAdministrador Totaladmin, secretaria, tesouraria, comissoes, conselho, visualizar
mestre_conselheiroMestre Conselheiroadmin, secretaria, tesouraria, comissoes, conselho, visualizar
consultorConsultorconselho, visualizar
presidente_conselhoPresidente do Conselhoconselho, visualizar
escrivaoEscrivãosecretaria, comissoes, visualizar
tesoureiroTesoureirotesouraria, visualizar
presidente_comissaoPresidente de Comissãocomissoes, visualizar
membroMembrovisualizar

can(roleName, perm) é o predicado usado na UI; canManageAttendance() é um atalho para secretaria ∪ conselho ∪ admin. Cargo desconhecido cai em ["visualizar"].

Camada 2 — banco: RLS em todas as tabelas, apoiada em funções SECURITY DEFINER: is_chapter_member, has_role, has_any_role, has_permission (espelha a matriz TypeScript), can_read_chapter, is_state_leader, is_region_leader, is_gme, can_lead_chapter, is_commission_member, is_commission_president, can_manage_commission.

As políticas de leitura foram ampliadas para can_read_chapter, de modo que lideranças regionais/estaduais enxerguem os capítulos sob sua jurisdição. As políticas de escrita continuam locais ao capítulo.

⚠️ A matriz existe duplicada: em TypeScript e em SQL. Alterar MATRIX em permissions.ts sem alterar has_permission na migration correspondente cria divergência silenciosa — a UI esconde o botão mas o banco continua aceitando a escrita (ou o contrário, e o usuário vê um erro sem explicação). Toda mudança de permissão precisa das duas pontas, no mesmo PR.

Visibilidade por comissão

Os setores de Eventos, Sindicâncias e Hospitalaria só aparecem para quem é membro da comissão correspondente (ou admin do capítulo). A regra fica em src/hooks/useCommissionAccess.ts e é aplicada por visibleGroups() em src/lib/nav.ts — tanto no sidebar desktop quanto na página /mais no mobile. A barra inferior mobile usa visibleMobileTabs() (Início · Calendário · Perfil · Mais).


8. PII e LGPD

O sistema lida com dados pessoais de menores de idade, o que eleva o rigor exigido.

  • CPF e RG são cifrados no Postgres pelas funções encrypt_pii/decrypt_pii. As colunas *_encrypted guardam o valor cifrado; as colunas *_last2 guardam apenas os dois últimos dígitos, para exibição.
  • A leitura em claro só existe pela RPC reveal_member_pii, exposta por revealMemberPii em src/lib/members.functions.ts e restrita a cargos de dentro do capítulo. Não há SELECT direto que devolva o valor.
  • A UI mascara por padrão com formatCpfMask/formatRgMask (src/lib/format.ts).
  • lgpd_consents registra os consentimentos coletados no cadastro; audit_logs registra acessos e alterações sensíveis.
  • No escopo regional/estadual, PII não é exposta — a busca de membros entre capítulos devolve dados mascarados.
  • Storage: bucket privado chapter-logos, com políticas exigindo que o primeiro segmento do caminho seja o UUID do capítulo. O acesso é por URL assinada (src/lib/chapter-logo.ts).

9. Módulo Financeiro

O módulo mais recente e o de acoplamento mais alto — vale ler antes de mexer.

Telas: /tesouraria/fluxo (tesouraria.fluxo.tsx), /tesouraria/mensalidades, /tesouraria/atrasados (tesouraria.atrasados.tsx) e /tesouraria/cobrancas. /financeiro é um redirecionamento legado para /tesouraria/fluxo.

Atrasados lista todos os membros do calendário anual de mensalidades, destacando quem tem competências em_aberto após o dia 15 (isDueOverdue). Por membro dá para copiar ou abrir o WhatsApp com mensagem pronta (meses atrasados 🔴, mês corrente 🟡, total, PIX de chapters.settings.pix_key e nome do Tesoureiro via getFinanceSigners). Helpers em dues-reminder.ts. Se o membro tiver phone no cadastro, o wa.me inclui o número; senão abre só com o texto.

Categorias em dois níveis

Categorias fixas são semeadas por capítulo pelo trigger tg_seed_cash_categories no insert de chapters — Eventos, Hospitalaria, Mensalidades, SCDB / GCE, Entretenimento, Outras (src/lib/cash-categories.ts). O capítulo pode criar categorias próprias pelo diálogo "Categorias".

Subcategorias dinâmicas existem só para as duas categorias que pertencem a comissões: Eventos (escopo eventos) e Hospitalaria (escopo hospitalaria). Só a comissão dona (ou admin/tesoureiro) define as subcategorias; o tesoureiro então precisa escolher uma ao lançar. A validação é server-side em src/lib/cash-validation.server.ts: resolveSubcategory confere que a subcategoria pertence ao capítulo, bate com o escopo e está ativa, e grava o nome como snapshot de texto em cash_entries.subcategory mais o calendar_event_id vinculado. Subcategorias de Eventos penduram em um evento de calendário real.

O snapshot de texto é intencional: renomear ou apagar uma subcategoria não reescreve o histórico contábil já lançado.

Acoplamento mensalidade ↔ caixa

Esta é a parte que quebra se mexida sem cuidado (src/lib/finance.functions.ts):

  • listDues só devolve membros com status ativo — Sênior DeMolay e Maçom são isentos por definição.
  • generateDues cria em lote as cobranças em_aberto de uma competência (ano+mês), com ignoreDuplicates.
  • upsertDue é bidirecional: marcar como pago insere automaticamente uma linha em cash_entries na categoria "Mensalidades", com descrição padronizada (duesDescription), e guarda o id em member_dues.cash_entry_id. Mudar o status para algo diferente de pago apaga essa entrada de caixa.
  • createManualDuesEntry cobre pagamentos negociados ou de vários meses: cria uma entrada de caixa e rateia o valor igualmente entre até 24 competências, marcando todas como pagas e ligando-as à mesma entrada.
  • A UI invalida as chaves ["dues"] e ["cash-entries"] dos dois lados, para as duas telas não divergirem.

Importação, exportação e relatório

  • Import XLSX com diálogo de conferência: parseCashSheetData | Tipo | Valor | Categoria | Descrição, converte datas dd/mm/aaaa e moeda R$ 1.234,56, e marca erro por linha; só as linhas válidas são enviadas. importCashEntries limita a 1000 linhas (src/lib/finance-xlsx.ts).
  • Export XLSX e download de modelo em branco (downloadCashTemplate).
  • Relatório PDF (src/lib/finance-pdf.ts, jsPDF): logo do capítulo, rótulo do período, tabela paginada, totais e uma página de assinaturas cujos nomes vêm de getFinanceSigners — que resolve PCC, MC, Tesoureiro e Consultor da Tesouraria do termo corrente (ano/semestre, ver src/lib/terms.ts).

Controle de acesso do módulo

  • UI: can(active?.role.name, "tesouraria") habilita as ações de escrita.
  • Banco: políticas cash_write / dues_write usam public.has_permission(chapter_id, 'tesouraria')admin_total, mestre_conselheiro, tesoureiro. As leituras usam can_read_chapter / is_chapter_member.

10. Ambiente e configuração

Variáveis de ambiente

VariávelConsumidorObrigatóriaObservação
VITE_SUPABASE_URLclient.tssimcliente do browser; embutida no build
VITE_SUPABASE_PUBLISHABLE_KEYclient.tssimchave anônima, pública por design
SUPABASE_URLauth-middleware.tssimcliente por requisição no SSR
SUPABASE_PUBLISHABLE_KEYauth-middleware.tssimidem
VITE_SUPABASE_PROJECT_ID / SUPABASE_PROJECT_ID.env, supabase/config.tomlsimreferência do projeto
SUPABASE_SERVICE_ROLE_KEYclient.server.tsnão está no .envignora RLS — injetar apenas no ambiente de deploy, jamais no cliente ou no repositório; usada em accounts.functions.ts para criar/vincular usuários
AI_API_KEY / OPENAI_API_KEYai.functions.tsnão está no .envsem ela, as funções de IA lançam "IA indisponível". Opcional: AI_BASE_URL, AI_MODEL
VITE_APP_URL / APP_URLaccounts.functions.ts (requestPasswordReset, generateLink)recomendada em produçãoorigem do login e do redirectTo de recuperação (/auth/nova-senha); fallback http://localhost:8080
RESEND_API_KEYemail.ts e Edge Function send-emailnão (sem ela o envio do app é skipped)API key do Resend; só servidor; nunca no cliente. No app: Worker/Cloudflare. Na hook: secret da Edge Function.
EMAIL_FROMemail.ts e Edge Function send-emailjunto com RESEND_API_KEYremetente no formato Nome <email@dominio-verificado>; domínio precisa estar Verified no Resend
SEND_EMAIL_HOOK_SECRETEdge Function send-emailse a hook estiver ligadagerado em Authentication → Hooks → Send Email (v1,whsec_…); não vai no Worker do app
TECH_COMMISSION_EMAILStech-commission.tsnãoCSV de e-mails que recebem notificações (ex.: solicitação de organização)
TECH_COMMISSION_CONTACTS_JSONtech-commission.tsnãoalternativa/complemento em JSON; se TECH_COMMISSION_EMAILS existir, ela tem prioridade

Modelo versionado: .env.example. Os .env / .env.* locais estão no .gitignore.

Auth vs app: o app envia transacional (criação de conta, ingresso, recuperação de senha, solicitação de organização) com sendTransactionalEmail (email.ts) usando RESEND_API_KEY + EMAIL_FROM no Worker/Vercel. A Edge Function supabase/functions/send-email continua disponível como Send Email Hook para outros e-mails nativos do Auth (signup, magic link, troca de e-mail), se a hook estiver ligada.

Checklist da hook (manual no projeto erjficqzodpfqqdurwgt; não deployar pelo MCP/CLI deste workspace):

  1. Criar a function send-email no dashboard, colar index.ts, Verify JWT = off.
  2. Secrets da function: RESEND_API_KEY, EMAIL_FROM, SEND_EMAIL_HOOK_SECRET, APP_URL (origem pública do app, para o logo).
  3. Authentication → Hooks → Send Email → HTTPS → https://erjficqzodpfqqdurwgt.supabase.co/functions/v1/send-email → Generate Secret.
  4. Testar /auth/recuperar-senha. Só então desligar o SMTP interno, se quiser.

Detalhe passo a passo: supabase/functions/send-email/README.md.

Arquivos de configuração

ArquivoPapel
vite.config.tsTanStack Start + React + Tailwind + tsconfigPaths + Nitro (cloudflare-module no build). Injeta VITE_* via define e alias @.
tsconfig.jsonstrict, ES2022, @/*./src/*, noEmit
components.jsonconfiguração do shadcn/ui (estilo new-york, base slate)
eslint.config.jsflat config; proíbe server-only; Prettier como regra
.prettierrcprintWidth: 100, aspas duplas, vírgula final
bunfig.tomlminimumReleaseAge de 24h
supabase/config.tomlproject ref + [functions.send-email] verify_jwt = false (deploy da function é manual no dashboard)

11. Scripts, build e deploy

ScriptComandoUso
devvite devdesenvolvimento local
buildvite buildbuild de produção
build:devvite build --mode developmentbuild com sourcemaps/modo dev
previewvite previewserve o build local
linteslint .lint + Prettier
formatprettier --write .formata

Build: gera um bundle Nitro com preset cloudflare-module (nodeCompat: true) em .output/.

Deploy: Cloudflare Workers, via npx wrangler deploy (e npx wrangler dev para preview). Não há wrangler.toml versionado — ele é gerado em .output/server/wrangler.json durante o build. Lembre de configurar SUPABASE_SERVICE_ROLE_KEY e AI_API_KEY (ou OPENAI_API_KEY) como secrets do Worker, não em arquivo.

Banco: mudanças de schema sempre por migration em supabase/migrations/, aplicadas via Supabase CLI ou dashboard. Depois de aplicar, regenere src/integrations/supabase/types.ts.


12. Estado atual e lacunas conhecidas

Registrado aqui de propósito, para ninguém descobrir do jeito difícil:

  • Não há framework de testes dedicado. Sem Vitest, Jest ou Playwright instalados; há alguns *.test.ts de helpers puros (ex.: src/lib/cash-totals.test.ts) rodados pontualmente, sem suíte/CI.

  • Não há CI. Não existe .github/, Makefile nem Dockerfile.

  • Não há script de typecheck. Com noEmit no tsconfig e sem script dedicado, erros de tipo só aparecem no editor ou no build.

  • O lint não passa hoje. eslint . reporta ~2.535 erros em 70 arquivos:

    RegraOcorrênciasNatureza
    prettier/prettier2.377só formatação — o Prettier nunca foi aplicado ao código gerado
    @typescript-eslint/no-explicit-any149tipagem frouxa
    react-hooks/exhaustive-deps12possíveis bugs de dependência de efeito
    react-refresh/only-export-components12atrapalha o hot reload
    react-hooks/rules-of-hooks9bug real em potencial — hook chamado condicionalmente

    Consequência prática: o lint não funciona como barreira de qualidade, já que ninguém consegue distinguir o erro novo do ruído de fundo. E rodar prettier --write . de uma vez reformataria 70 arquivos — ver o aviso em OPEN-SOURCE.md antes de fazer isso. Os 9 rules-of-hooks merecem investigação individual: são os únicos que apontam para defeito de execução, não de estilo.

  • improveText em src/lib/ai.functions.ts está exportada mas nenhuma tela a chama.

  • supabaseAdmin em src/integrations/supabase/client.server.ts está definida mas nunca importada — daí SUPABASE_SERVICE_ROLE_KEY ainda não ser necessária na prática.

  • A tela de login exibe credenciais de teste (src/routes/auth.tsx). Precisa sair antes de qualquer uso real.

  • Revisar histórico do git por .env legado com segredos (ver seção 10).

  • package.json.name ainda é tanstack_start_ts, herdado do template.

  • O histórico de commits não serve como documentação: a maioria são commits automáticos com a mensagem "Changes".


13. Manutenção desta documentação

Toda alteração no projeto deve atualizar a documentação correspondente no mesmo commit/PR. Ver a tabela de roteamento em docs/README.md.

Checklist específico deste documento:

Se você…Atualize
Adicionou uma rotaseção 2 (estrutura) e a lista de telas do Guia do Usuário
Criou um *.functions.tsa tabela da seção 5
Escreveu uma migrationseção 6 (modelo de dados); regenere types.ts
Mudou cargo ou permissãoseção 7 — nas duas pontas, TS e SQL — e o Guia do Usuário
Adicionou variável de ambientea tabela da seção 10 e o bloco .env.example em OPEN-SOURCE.md
Adicionou dependência relevantea tabela de stack da seção 1
Mudou build ou deployseção 11
Fechou uma das lacunas da seção 12remova o item de lá e do roadmap em OPEN-SOURCE.md