Autenticação & CORS
Obtenha o token via Supabase Auth (sign-in) e envie no header de todas as rotas privadas:Authorization: Bearer {access_token}
CORS — Todas as respostas incluem headers CORS. Preflight OPTIONS retorna 204.Access-Control-Allow-Origin · Access-Control-Allow-Methods · Access-Control-Allow-Headers · Access-Control-Allow-Credentials
Rotas públicas (sem token):/api/navbar
/api/pagination
/api/agrotv/streams
/api/agrotv/proxy
Rotas privadas (token obrigatório):/api/calendar
/api/kanban
/api/chat
/api/mail/*
/api/horizon-assist
/api/horizon-assist/voice
/api/horizon-assist/tts
/api/confirm-plan
Resposta ao enviar token inválido ou ausente:401 · { "message": "Token inválido ou expirado." }
Supabase Auth — Login / Cadastro
O login é feito direto com o SDK Supabase (não passa pelo backend). Use o SDK do seu framework (Flutter, Swift, Kotlin).import { createClient } from '@supabase/supabase-js'
Credenciais Supabase (públicas):URL: https://ouihikenidhsdjlpciox.supabase.co · Anon Key: sb_publishable_mEuhFLh-NOwvPgynX4YpyQ_6wbaJjCQ
Login com e-mail/senha · supabase.auth.signInWithPassword({ email, password }) · Retorna: { session: { access_token, refresh_token }, user }POSTsupabase.auth.signInWithPassword()
Cadastro · supabase.auth.signUp({ email, password, options: { data: { displayName } } }) · Cria user + profile automático via triggerPOSTsupabase.auth.signUp()
Login OAuth (Google/GitHub) · supabase.auth.signInWithOAuth({ provider, options: { redirectTo } })POSTsupabase.auth.signInWithOAuth()
Logout · supabase.auth.signOut()POSTsupabase.auth.signOut()
Resetar senha · supabase.auth.resetPasswordForEmail(email, { redirectTo })POSTsupabase.auth.resetPasswordForEmail()
Atualizar senha (logado) · supabase.auth.updateUser({ password })POSTsupabase.auth.updateUser()
Após login bem-sucedido, leia o perfil do usuário:supabase.from('profiles').select('*').eq('id', user.id).single() → { id, role, plan, display_name, photo_url, phone_number, country, address, state, city, zip_code, about, owner_id, member_status }
RPC auto-aceitar convite (chamar após login):supabase.rpc('accept_pending_invitation', { p_user_id: user.id }) → aceita convites pendentes para o e-mail do usuário
Roles, Planos & Permissões
Roles do sistema (profiles.role):superadmin
owner
manager
operator
viewer
Hierarquia de roles:superadmin → acesso total (admin da plataforma) · owner → dono da conta (gerencia membros) · manager → gerente (edita tudo) · operator → operador (edita dados) · viewer → visualizador (somente leitura)
Planos (profiles.plan):explorar (free)
fireshield
agriintelligence
connectedfarm
Limite de membros por plano:explorar: 0 (sem membros) · fireshield: 6 · agriintelligence: 12 · connectedfarm: 24
Módulos por plano:Firepage: explorar, fireshield, connectedfarm · Layers (GeoRef): explorar, agriintelligence, connectedfarm · Members: apenas planos pagos + role owner
Mudança de role ao trocar plano:Plano pago (fireshield/agriintelligence/connectedfarm) + viewer → vira owner · Plano explorar + owner → volta para viewer
Tabelas Supabase (SDK direto)
Estas tabelas são acessadas diretamente pelo SDK Supabase no client (com RLS). O backend NÃO é intermediário para elas.Use supabase.from('tabela').select/insert/update/delete no app mobile
profiles — Perfil do usuário (RLS: próprio user ou owner do time)id (uuid, PK = auth.uid) · role · plan · display_name · photo_url · email · phone_number · country · address · state · city · zip_code · about · owner_id · member_status (active/inactive)
account_invitations — Convites de membros (RLS: owner do time)id (uuid) · owner_id (quem convidou) · email · role (manager/operator) · status (pending/accepted) · token · expires_at · created_at
kanban-attachments — Storage Bucket (público)Upload: supabase.storage.from('kanban-attachments').upload(path, file) · URL: supabase.storage.from('kanban-attachments').getPublicUrl(path)
Calendar (edge)
Listar todos os eventos + tarefas Kanban com data · Resposta: { events: [...] }GET/api/calendar
Criar evento · Body: { eventData: { id, title, description?, allDay?, color?, start, end } } · Resposta: { event }POST/api/calendar
Atualizar evento · Body: { eventData: { id, title, description?, allDay?, color?, start, end } } · Resposta: { event }PUT/api/calendar
Deletar evento · Body: { eventId } · Resposta: { eventId }PATCH/api/calendar
Cores de prioridade nas tarefas Kanban no calendário:low: #22C55E (verde) · medium: #FFAB00 (amarelo) · high: #FF5630 (vermelho)
Kanban (edge)
Obter board completo · Cria board padrão 'Meu Quadro' se não existir · Resposta: { board: { columns: [...], tasks: { [colId]: [...] } } }GET/api/kanban
Criar coluna · Body: { columnData: { id, name } } · Resposta: { column }POST/api/kanban?endpoint=create-column
Renomear coluna · Body: { columnId, columnName } · Resposta: { columnId, columnName }POST/api/kanban?endpoint=update-column
Reordenar colunas · Body: { updateColumns: [{ id, position, ... }] } · Resposta: { columns }POST/api/kanban?endpoint=move-column
Limpar tarefas da coluna · Body: { columnId } · Resposta: { columnId }POST/api/kanban?endpoint=clear-column
Deletar coluna (CASCADE) · Body: { columnId } · Resposta: { columnId }POST/api/kanban?endpoint=delete-column
Criar tarefa · Body: { columnId, taskData: { id, name, status?, priority?, description?, labels?, due?, reporter?, attachments?, assignee?, subtasks?, comments? } } · Resposta: { columnId, taskData }POST/api/kanban?endpoint=create-task
Atualizar tarefa · Body: { taskData: { id, name, status, priority, description?, labels?, due?, reporter?, attachments?, assignee?, subtasks?, comments? } } · Resposta: { task }POST/api/kanban?endpoint=update-task
Mover tarefa entre colunas · Body: { updateTasks: Record<colId, [{ id, ... }]> } · Resposta: { tasks }POST/api/kanban?endpoint=move-task
Deletar tarefa · Body: { taskId } · Resposta: { taskId }POST/api/kanban?endpoint=delete-task
Campos da tarefa (JSONB desnormalizado):id, name, status, priority (low/medium/high), description, labels[], due[start,end], reporter_data{}, assignees_data[], comments_data[], subtasks_data[], attachments[], reporter_id, column_id, position
Horizon Assist — IA (nodejs, maxDuration: 120s)
Listar conversas do usuário · Resposta: { conversations: [{ id, title, created_at, updated_at }] }GET/api/horizon-assist?endpoint=conversations
Obter conversa completa com mensagens · Resposta: { conversation, messages: [{ id, role, content, image_url?, created_at }] }GET/api/horizon-assist?endpoint=conversation&conversationId={id}
Criar nova conversa · Body: { endpoint: "create" } · Resposta: { conversation }POST/api/horizon-assist
STREAMING — Enviar mensagem retorna text/event-stream (SSE). Cada chunk: data: { "content": "..." }. Fim: data: [DONE]POST /api/horizon-assist · Body: { endpoint: "message", conversationId, content, imageBase64? }
Renomear conversa · Body: { conversationId, title } · Resposta: { conversation }PUT/api/horizon-assist
Deletar conversa · Resposta: { success: true }DELETE/api/horizon-assist?conversationId={id}
Transcrição de áudio (Whisper) · maxDuration: 30sPOST /api/horizon-assist/voice · Content-Type: multipart/form-data · Campo: "audio" (Blob .webm) · Idioma forçado: pt · Resposta: { text }
Text-to-Speech (TTS-1, voz: nova) · maxDuration: 30sPOST /api/horizon-assist/tts · Body: { text } (máx 1000 chars) · Resposta: audio/mpeg stream binário
Tabelas Supabase usadas:ai_conversations (id, user_id, title, created_at, updated_at) · ai_messages (id, conversation_id, role, content, image_url, created_at)
Planos — Ativação
Confirmar plano · Body: { plan } · Usa JWT para identificar user · Atualiza profile: plano pago + viewer → owner · explorar + owner → viewer · Resposta: { success, plan, role }POST/api/confirm-plan
Planos válidos:explorar (free) · fireshield · agriintelligence · connectedfarm
Chat (edge, mock)
Buscar contatos · Resposta: { contacts: [{ id, name, avatarUrl, ... }] }GET/api/chat?endpoint=contacts
Listar conversas · Resposta: { conversations: [{ id, type, participants, messages, unreadCount }] }GET/api/chat?endpoint=conversations
Detalhes de conversa · Resposta: { conversation }GET/api/chat?conversationId={id}&endpoint=conversation
Marcar conversa como lida · Resposta: { conversationId }GET/api/chat?conversationId={id}&endpoint=mark-as-seen
Criar nova conversa · Body: { conversationData } · Resposta: { conversation }POST/api/chat
Enviar mensagem · Body: { conversationId, messageData: { id, body, contentType, attachments?, senderId, createdAt } } · Resposta: { conversation }PUT/api/chat
Mail (edge, mock)
Listar labels · Resposta: { labels: [{ id, type, name, color, unreadCount }] }GET/api/mail/labels
Listar e-mails por label · Resposta: { mails: [{ id, from, to, subject, message, isStarred, isImportant, createdAt, ... }] }GET/api/mail/list?labelId={labelId}
Detalhes do e-mail · Resposta: { mail }GET/api/mail/details?mailId={mailId}
Labels padrão:inbox · all · starred · important · custom
AgroTV (público)
Listar canais e streams HLS ao vivo · Resposta: { channels: [{ id, name, description, type, source, poster, iframeFallback, isLive? }] }GET/api/agrotv/streams
Tipos de canal:equipea — HLS stream Equipea · restreamer — M3U8 direto · iframe — embed HTML · youtube — YouTube Live channel
Proxy CORS para streams m3u8/ts · Resposta: raw body com CORS headersGET/api/agrotv/proxy?url={hls_url}
Domínios permitidos no proxy:live.agrobrasiltv.com.br · aovivo*.equipea.com.br
13 canais disponíveis:Canal do Boi · AgroCanal · Terra Viva · AgroNegócios · Canal do Campo · AgroBrasil TV · TV Notícias Agrícolas · Canal Rural · Terraviva (Band) · e mais...
Navegação (público, edge)
Obter itens de menu · Resposta: { navItems: [...] }GET/api/navbar
Paginação (público, edge)
Listar produtos paginados · Resposta: { products: [...], totalPages, totalItems, categoryOptions: [...] }GET/api/pagination?page={page}&perPage={perPage}&search={term}&category={cat}
Telas do App — Todas as Roles
Padrão de URL: /dashboard/{role}/... — {role} = superadmin | owner | manager | operator | viewerO role vem do campo 'role' no profile do usuário. Use para montar a navegação do app mobile.
Páginas Auth (sem login):/auth/portal/sign-in
/auth/portal/sign-up
/auth/portal/verify
/auth/portal/reset-password
/auth/portal/update-password
Páginas Públicas:/pricing
/payment
/about-us
/contact-us
/faqs
/coming-soon
/maintenance
Dashboard — owner | manager | operator | viewer:/{role}/
/{role}/analytics
/{role}/mail
/{role}/firepage
/{role}/layers
/{role}/kanban
/{role}/calendar
/{role}/horizon-assist
/{role}/agrotv
/{role}/pricing
/{role}/invoice
/{role}/invoice/{id}
/{role}/user/account
/{role}/members
/{role}/file-manager
/{role}/gestao-rural
/{role}/gestao-rural/safras
/{role}/gestao-rural/talhoes
/{role}/gestao-rural/insumos
/{role}/gestao-rural/maquinario
/{role}/gestao-rural/financeiro
/{role}/gestao-rural/mao-de-obra
/{role}/gestao-rural/pecuaria
/{role}/gestao-rural/colheita
/{role}/gestao-rural/comercializacao
/{role}/gestao-rural/ambiental
/{role}/gestao-rural/indicadores
Dashboard — superadmin (exclusivo):/superadmin/
/superadmin/analytics
/superadmin/mail
/superadmin/firepage
/superadmin/layers
/superadmin/georef
/superadmin/kanban
/superadmin/calendar
/superadmin/horizon-assist
/superadmin/invoice
/superadmin/invoice/new
/superadmin/invoice/{id}
/superadmin/invoice/{id}/edit
/superadmin/user/list
/superadmin/user/new
/superadmin/user/{id}/edit
/superadmin/user/account
/superadmin/file-manager
Visibilidade condicional:Members → só owner + plano pago · Firepage → explorar, fireshield, connectedfarm · Layers → explorar, agriintelligence, connectedfarm · GeoRef → apenas superadmin
Resumo Técnico
Total de rotas API:13 rotas · 4 públicas · 9 privadas · Edge: 12 · Node.js: 1
Integrações externas:Supabase (Auth + DB + Storage) · OpenAI GPT-4o (IA) · OpenAI Whisper (Transcrição) · OpenAI TTS-1 (Voz) · Mapbox (Mapas) · Equipea/YouTube (AgroTV)
URLs de produção:API: https://api.terraviva.com.br · App: https://app.terraviva.com.br · Supabase: https://gxkkjfyoxzjjrzweyvdn.supabase.co
Headers obrigatórios em todas as requests:Content-Type: application/json · Authorization: Bearer {token} (rotas privadas)
Formato de erro padrão:{ "message": "Descrição do erro" } · Status: 400 (bad request) · 401 (não autenticado) · 404 (não encontrado) · 500 (erro interno)