SimpleORM + Supabase NEW

Integre seu projeto Delphi com Supabase usando o mesmo padrao SimpleORM que voce ja conhece. Zero dependencias externas.

Introducao

A integracao SimpleORM + Supabase permite conectar sua aplicacao Delphi diretamente ao Supabase, um backend-as-a-service baseado em PostgreSQL. O driver traduz operacoes SQL geradas pelo TSimpleDAO<T> em chamadas HTTP para a API PostgREST do Supabase.

A integracao e composta por 3 componentes independentes:

ComponenteUnitResponsabilidade
TSimpleQuerySupabaseSimpleQuerySupabase.pasDriver de query — traduz SQL para chamadas REST PostgREST
TSimpleSupabaseAuthSimpleSupabaseAuth.pasAutenticacao — SignIn, SignUp, SignOut, RefreshToken, auto-refresh
TSimpleSupabaseRealtimeSimpleSupabaseRealtime.pasRealtime — monitoramento de mudancas via polling com callbacks
Sem dependencias externas: Todos os componentes usam System.Net.HttpClient nativo do Delphi. Nao e necessario instalar SDKs, DLLs ou componentes de terceiros.

Instalacao

Via Boss (recomendado)

boss install academiadocodigo/SimpleORM

Manual

Adicione o diretorio src/ ao Library Path do Delphi e inclua as units necessarias na clausula uses:

uses
  SimpleInterface,        // Interfaces (iSimpleQuery, iSimpleDAO, iSimpleSupabaseAuth, etc.)
  SimpleTypes,            // Tipos (TSQLType, TSupabaseRealtimeEvent, etc.)
  SimpleDAO,              // TSimpleDAO<T>
  SimpleAttributes,       // Atributos de entidade ([Tabela], [Campo], [PK], etc.)
  SimpleQuerySupabase,    // Driver Supabase (TSimpleQuerySupabase)
  SimpleSupabaseAuth,     // Autenticacao (TSimpleSupabaseAuth)
  SimpleSupabaseRealtime; // Realtime (TSimpleSupabaseRealtime)
Minimo necessario: Para CRUD basico, voce precisa apenas de SimpleQuerySupabase, SimpleDAO, SimpleAttributes e SimpleInterface. Auth e Realtime sao opcionais.

Configuracao Basica

TSimpleQuerySupabase oferece 3 construtores para diferentes cenarios de uso:

1. Basico — URL + API Key

Usa a service_role key como token de autorizacao. Acesso total, sem Row Level Security.

var
  LQuery: iSimpleQuery;
begin
  LQuery := TSimpleQuerySupabase.New(
    'https://abcdefghij.supabase.co',    // URL do projeto
    'eyJhbGciOiJIUzI1NiIsInR5cCI6...'   // service_role key
  );
end;

2. Com Token JWT Estatico

Util quando voce ja possui um JWT obtido por outro meio (ex: login externo).

var
  LQuery: iSimpleQuery;
begin
  LQuery := TSimpleQuerySupabase.New(
    'https://abcdefghij.supabase.co',    // URL do projeto
    'eyJhbGciOiJIUzI1NiIsInR5cCI6...',  // anon key
    'eyJhbGciOiJIUzI1NiIsInR5cCI6...'   // JWT token do usuario
  );
end;

3. Com Objeto Auth (recomendado para RLS)

O token e gerenciado automaticamente pelo TSimpleSupabaseAuth, incluindo auto-refresh.

var
  LAuth: iSimpleSupabaseAuth;
  LQuery: iSimpleQuery;
begin
  LAuth := TSimpleSupabaseAuth.New(
    'https://abcdefghij.supabase.co',
    'eyJhbGciOiJIUzI1NiIsInR5cCI6...'   // anon key
  );
  LAuth.SignIn('usuario@email.com', 'senha123');

  LQuery := TSimpleQuerySupabase.New(
    'https://abcdefghij.supabase.co',
    'eyJhbGciOiJIUzI1NiIsInR5cCI6...',  // anon key
    LAuth                                 // objeto auth
  );
end;

Onde encontrar URL e API Keys

No painel do Supabase:

DadoOnde encontrarUso
Project URLSettings > API > Project URLPrimeiro parametro de todos os construtores
anon keySettings > API > Project API Keys > anon/publicAcesso publico com RLS ativo
service_role keySettings > API > Project API Keys > service_roleAcesso total, ignora RLS
Seguranca: A service_role key tem acesso total ao banco. NUNCA exponha esta chave em aplicacoes cliente distribuidas. Use anon key + Auth para aplicacoes de usuario final.

CRUD Completo

O driver Supabase funciona como qualquer outro driver SimpleORM. Voce usa TSimpleDAO<T> normalmente — o driver traduz o SQL gerado em chamadas HTTP para a API PostgREST.

Entidade de Exemplo

Todos os exemplos CRUD usam esta entidade:

type
  [Tabela('produto')]
  TProduto = class
  private
    FId: Integer;
    FNome: String;
    FPreco: Double;
    FAtivo: Boolean;
  published
    [Campo('id'), PK, AutoInc]
    property Id: Integer read FId write FId;
    [Campo('nome'), NotNull]
    property Nome: String read FNome write FNome;
    [Campo('preco')]
    property Preco: Double read FPreco write FPreco;
    [Campo('ativo')]
    property Ativo: Boolean read FAtivo write FAtivo;
  end;

Criando o DAO

var
  LQuery: iSimpleQuery;
  LDAO: iSimpleDAO<TProduto>;
begin
  LQuery := TSimpleQuerySupabase.New(
    'https://abcdefghij.supabase.co',
    'sua-api-key-aqui'
  );

  LDAO := TSimpleDAO<TProduto>.New(LQuery);
end;

Insert

Insere um novo registro na tabela do Supabase.

var
  LProduto: TProduto;
begin
  LProduto := TProduto.Create;
  try
    LProduto.Nome := 'Notebook Dell';
    LProduto.Preco := 4599.90;
    LProduto.Ativo := True;

    LDAO.Insert(LProduto);
    Writeln('Produto inserido com sucesso!');
  finally
    LProduto.Free;
  end;
end;

O que acontece internamente

EtapaDetalhe
SQL gerado por TSimpleSQLINSERT INTO produto (nome, preco, ativo) VALUES (:nome, :preco, :ativo)
Metodo HTTPPOST
URLhttps://abcdefghij.supabase.co/rest/v1/produto
Body JSON{"nome":"Notebook Dell","preco":4599.90,"ativo":true}
Headersapikey: <key>, Authorization: Bearer <token>, Prefer: return=representation
AutoInc: Campos marcados com [AutoInc] sao excluidos automaticamente do INSERT. O id e gerado pelo banco de dados do Supabase (PostgreSQL).

Find (Select)

Buscar todos os registros

var
  LList: TObjectList<TProduto>;
  I: Integer;
begin
  LList := LDAO.Find;
  try
    for I := 0 to LList.Count - 1 do
      Writeln(LList[I].Nome, ' - R$ ', LList[I].Preco:0:2);
  finally
    LList.Free;
  end;
end;

// SQL gerado: SELECT * FROM produto
// HTTP: GET /rest/v1/produto?select=*

Buscar por ID (chave primaria)

var
  LProduto: TProduto;
begin
  LProduto := TProduto.Create;
  try
    LProduto.Id := 42;
    LDAO.Find(LProduto);
    Writeln('Encontrado: ', LProduto.Nome);
  finally
    LProduto.Free;
  end;
end;

// SQL gerado: SELECT * FROM produto WHERE id = :id
// HTTP: GET /rest/v1/produto?id=eq.42

Buscar com filtro Where

var
  LList: TObjectList<TProduto>;
begin
  LList := LDAO
    .SQL
      .Where('ativo = :ativo')
    .&End
    .Find;
  try
    Writeln('Produtos ativos: ', LList.Count);
  finally
    LList.Free;
  end;
end;

// SQL gerado: SELECT * FROM produto WHERE ativo = :ativo
// HTTP: GET /rest/v1/produto?ativo=eq.1

Buscar com campos especificos

var
  LList: TObjectList<TProduto>;
begin
  LList := LDAO
    .SQL
      .Fields('id, nome')
    .&End
    .Find;
  try
    Writeln('Total: ', LList.Count);
  finally
    LList.Free;
  end;
end;

// SQL gerado: SELECT id, nome FROM produto
// HTTP: GET /rest/v1/produto?select=id,nome
Filtros suportados: O driver converte condicoes WHERE campo = :param para o formato PostgREST campo=eq.valor. Multiplas condicoes com AND sao separadas por & na URL.

Update

Atualiza um registro existente. O WHERE e gerado automaticamente com base na chave primaria ([PK]).

var
  LProduto: TProduto;
begin
  LProduto := TProduto.Create;
  try
    LProduto.Id := 42;

    // Buscar o produto atual
    LDAO.Find(LProduto);

    // Alterar campos
    LProduto.Nome := 'Notebook Dell Atualizado';
    LProduto.Preco := 3999.90;

    // Salvar
    LDAO.Update(LProduto);
    Writeln('Produto atualizado!');
  finally
    LProduto.Free;
  end;
end;

O que acontece internamente

EtapaDetalhe
SQL geradoUPDATE produto SET nome = :nome, preco = :preco, ativo = :ativo WHERE id = :id
Metodo HTTPPATCH
URLhttps://abcdefghij.supabase.co/rest/v1/produto?id=eq.42
Body JSON{"nome":"Notebook Dell Atualizado","preco":3999.90,"ativo":true}

Delete

Remove um registro pela chave primaria.

var
  LProduto: TProduto;
begin
  LProduto := TProduto.Create;
  try
    LProduto.Id := 42;
    LDAO.Delete(LProduto);
    Writeln('Produto removido!');
  finally
    LProduto.Free;
  end;
end;

O que acontece internamente

EtapaDetalhe
SQL geradoDELETE FROM produto WHERE id = :id
Metodo HTTPDELETE
URLhttps://abcdefghij.supabase.co/rest/v1/produto?id=eq.42
Body(vazio)
SoftDelete: Se a entidade usar [SoftDelete('campo')], o TSimpleDAO gera um UPDATE SET campo = 1 em vez de DELETE. O driver traduz normalmente para PATCH.

Paginacao

Use Skip e Take para paginar resultados. O driver converte para limit e offset na URL.

var
  LList: TObjectList<TProduto>;
begin
  // Pagina 1: primeiros 10 registros
  LList := LDAO
    .SQL
      .Skip(0)
      .Take(10)
    .&End
    .Find;
  try
    Writeln('Pagina 1: ', LList.Count, ' registros');
  finally
    LList.Free;
  end;

  // Pagina 2: registros 11 a 20
  LList := LDAO
    .SQL
      .Skip(10)
      .Take(10)
    .&End
    .Find;
  try
    Writeln('Pagina 2: ', LList.Count, ' registros');
  finally
    LList.Free;
  end;

  // Pagina 3: registros 21 a 30
  LList := LDAO
    .SQL
      .Skip(20)
      .Take(10)
    .&End
    .Find;
  try
    Writeln('Pagina 3: ', LList.Count, ' registros');
  finally
    LList.Free;
  end;
end;

Mapeamento de paginacao

SimpleORMSQL Gerado (depende do SQLType)URL PostgREST
.Skip(0).Take(10)LIMIT 10 OFFSET 0 (MySQL)?limit=10&offset=0
.Skip(10).Take(10)LIMIT 10 OFFSET 10?limit=10&offset=10
.Skip(0).Take(50)FIRST 50 SKIP 0 (Firebird)?limit=50&offset=0
Dialeto SQL nao importa: O driver reconhece automaticamente FIRST/SKIP (Firebird), LIMIT/OFFSET (MySQL/SQLite), e OFFSET ROWS FETCH NEXT (Oracle) e converte todos para limit/offset do PostgREST.

Batch Operations

Operacoes em lote executam a mesma acao para cada item de uma lista.

var
  LList: TObjectList<TProduto>;
  LProduto: TProduto;
begin
  LList := TObjectList<TProduto>.Create;
  try
    // Criar varios produtos
    LProduto := TProduto.Create;
    LProduto.Nome := 'Produto A';
    LProduto.Preco := 100.00;
    LProduto.Ativo := True;
    LList.Add(LProduto);

    LProduto := TProduto.Create;
    LProduto.Nome := 'Produto B';
    LProduto.Preco := 200.00;
    LProduto.Ativo := True;
    LList.Add(LProduto);

    LProduto := TProduto.Create;
    LProduto.Nome := 'Produto C';
    LProduto.Preco := 300.00;
    LProduto.Ativo := True;
    LList.Add(LProduto);

    // Inserir todos de uma vez
    LDAO.InsertBatch(LList);
    Writeln('3 produtos inseridos!');

    // UpdateBatch e DeleteBatch funcionam da mesma forma:
    // LDAO.UpdateBatch(LList);
    // LDAO.DeleteBatch(LList);
  finally
    LList.Free;
  end;
end;
Transacoes em REST: O driver Supabase opera sobre HTTP/REST, que e stateless. As chamadas StartTransaction, Commit e Rollback sao no-ops (retornam Self sem fazer nada). Cada POST/PATCH/DELETE e executado individualmente. Se um item falhar no meio do batch, os anteriores ja foram persistidos.

SQL para REST — Mapeamento Completo

O TSimpleQuerySupabase parseia o SQL gerado pelo TSimpleSQL<T> e traduz cada operacao para a chamada HTTP equivalente da API PostgREST do Supabase.

Operacoes CRUD

Operacao SQLMetodo HTTPURL PostgRESTBody
INSERT INTO tabela (c1, c2) VALUES (:c1, :c2) POST /rest/v1/tabela {"c1":"v1","c2":"v2"}
SELECT * FROM tabela GET /rest/v1/tabela?select=* (vazio)
SELECT c1, c2 FROM tabela GET /rest/v1/tabela?select=c1,c2 (vazio)
SELECT * FROM tabela WHERE pk = :pk GET /rest/v1/tabela?pk=eq.valor (vazio)
SELECT * FROM tabela WHERE c1 = :c1 AND c2 = :c2 GET /rest/v1/tabela?c1=eq.v1&c2=eq.v2 (vazio)
SELECT * FROM tabela LIMIT 10 OFFSET 5 GET /rest/v1/tabela?limit=10&offset=5 (vazio)
UPDATE tabela SET c1 = :c1 WHERE pk = :pk PATCH /rest/v1/tabela?pk=eq.valor {"c1":"novo_valor"}
DELETE FROM tabela WHERE pk = :pk DELETE /rest/v1/tabela?pk=eq.valor (vazio)

Headers enviados em todas as requisicoes

HeaderValorDescricao
Content-Typeapplication/jsonFormato do body
apikeySua API key do SupabaseIdentificacao do projeto
AuthorizationBearer <token>Token de autenticacao (auth token, JWT estatico, ou API key)
Preferreturn=representationRetornar o registro afetado na resposta

Prioridade do token de autorizacao

O header Authorization e preenchido seguindo esta prioridade:

PrioridadeCondicaoToken Usado
1 (maior)FAuth atribuido e IsAuthenticated = TrueFAuth.Token (com auto-refresh)
2FToken nao vazioJWT estatico passado no construtor
3 (menor)Nenhum dos acimaFAPIKey (api key do projeto)

Conversao de tipos no JSON

Tipo Delphi (TParam.DataType)Tipo JSONExemplo
ftInteger, ftSmallint, ftWord, ftLargeint, ftAutoInc, ftShortintNumber (inteiro)42
ftFloat, ftCurrency, ftBCD, ftFMTBcd, ftExtended, ftSingleNumber (decimal)4599.90
ftBooleanBooleantrue
ftString, ftWideString, outrosString"texto"
Null / Emptynullnull

Autenticacao

O TSimpleSupabaseAuth integra com o GoTrue (servico de autenticacao do Supabase). Ele gerencia SignIn, SignUp, SignOut, tokens JWT e auto-refresh transparente.

API Key (sem autenticacao de usuario)

A forma mais simples de conectar. Usa a service_role key diretamente. Nao passa por Row Level Security. Ideal para scripts, migracao de dados, ou back-office.

var
  LQuery: iSimpleQuery;
  LDAO: iSimpleDAO<TProduto>;
begin
  // service_role key = acesso total
  LQuery := TSimpleQuerySupabase.New(
    'https://abcdefghij.supabase.co',
    'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJl' +
    'ZiI6ImFiY2RlZmdoaWoiLCJyb2xlIjoic2VydmljZV9yb2xlIiwiaWF0IjoxNjk5MDAwMDAwfQ...'
  );

  LDAO := TSimpleDAO<TProduto>.New(LQuery);
  // LDAO tem acesso total a tabela produto, sem restricoes RLS
end;
Atencao: A service_role key ignora TODAS as politicas RLS. Use apenas em ambientes confiáveis (servidor, scripts internos).

JWT Token Estatico

Quando voce ja tem um JWT obtido por outro sistema de autenticacao (SSO, Auth0, Firebase, etc.), passe-o diretamente no terceiro parametro.

var
  LQuery: iSimpleQuery;
  LToken: String;
begin
  LToken := ObterTokenDoSistemaExterno(); // Seu metodo personalizado

  LQuery := TSimpleQuerySupabase.New(
    'https://abcdefghij.supabase.co',
    'eyJ...anonKey...',  // anon key
    LToken               // JWT do sistema externo
  );
end;

Voce tambem pode alterar o token apos a criacao usando o metodo Token:

var
  LQuery: iSimpleQuery;
begin
  LQuery := TSimpleQuerySupabase.New(
    'https://abcdefghij.supabase.co',
    'eyJ...anonKey...'
  );

  // Definir token depois
  (LQuery as TSimpleQuerySupabase).Token('novo-jwt-aqui');
end;

SignIn / SignUp

SignIn — Login com email e senha

var
  LAuth: iSimpleSupabaseAuth;
begin
  LAuth := TSimpleSupabaseAuth.New(
    'https://abcdefghij.supabase.co',
    'eyJ...anonKey...'
  );

  LAuth.SignIn('usuario@email.com', 'minhaSenha123');

  if LAuth.IsAuthenticated then
  begin
    Writeln('Login realizado com sucesso!');
    Writeln('Token: ', Copy(LAuth.Token, 1, 50), '...');
    Writeln('User: ', LAuth.User);
    Writeln('Expira em: ', DateTimeToStr(LAuth.ExpiresAt));
  end;
end;

// HTTP: POST /auth/v1/token?grant_type=password
// Body: {"email":"usuario@email.com","password":"minhaSenha123"}

SignUp — Criar nova conta

var
  LAuth: iSimpleSupabaseAuth;
begin
  LAuth := TSimpleSupabaseAuth.New(
    'https://abcdefghij.supabase.co',
    'eyJ...anonKey...'
  );

  LAuth.SignUp('novo@email.com', 'senhaSegura456');

  if LAuth.IsAuthenticated then
    Writeln('Conta criada e logado automaticamente!')
  else
    Writeln('Conta criada. Verifique seu email para confirmar.');
end;

// HTTP: POST /auth/v1/signup
// Body: {"email":"novo@email.com","password":"senhaSegura456"}
Confirmacao de email: Se o Supabase estiver configurado para exigir confirmacao de email, o SignUp retorna o usuario mas IsAuthenticated pode ser False ate que o email seja confirmado.

SignOut

Encerra a sessao do usuario. O token, refresh token, dados do usuario e data de expiracao sao limpos.

var
  LAuth: iSimpleSupabaseAuth;
begin
  LAuth := TSimpleSupabaseAuth.New(
    'https://abcdefghij.supabase.co',
    'eyJ...anonKey...'
  );

  LAuth.SignIn('usuario@email.com', 'senha123');
  Writeln('Autenticado: ', LAuth.IsAuthenticated);  // True

  LAuth.SignOut;
  Writeln('Autenticado: ', LAuth.IsAuthenticated);  // False
  Writeln('Token: "', LAuth.Token, '"');             // "" (vazio)
  Writeln('User: "', LAuth.User, '"');               // "" (vazio)
end;

// HTTP: POST /auth/v1/logout
// Header: Authorization: Bearer <access_token>

Refresh Token

Renova o access token usando o refresh token obtido no SignIn.

var
  LAuth: iSimpleSupabaseAuth;
begin
  LAuth := TSimpleSupabaseAuth.New(
    'https://abcdefghij.supabase.co',
    'eyJ...anonKey...'
  );
  LAuth.SignIn('usuario@email.com', 'senha123');

  // ... muito tempo depois, o token pode ter expirado ...

  LAuth.RefreshToken;
  Writeln('Novo token: ', Copy(LAuth.Token, 1, 50), '...');
  Writeln('Nova expiracao: ', DateTimeToStr(LAuth.ExpiresAt));
end;

// HTTP: POST /auth/v1/token?grant_type=refresh_token
// Body: {"refresh_token":"seu-refresh-token-aqui"}
Pre-requisito: RefreshToken requer que SignIn tenha sido chamado antes (para ter um refresh token disponivel). Chamar sem refresh token levanta excecao: Supabase Auth: no refresh token available. Call SignIn first.

Auto-Refresh

O TSimpleSupabaseAuth renova o token automaticamente quando voce acessa a propriedade Token. Se faltam menos de 30 segundos para expirar, o refresh e executado de forma transparente.

var
  LAuth: iSimpleSupabaseAuth;
  LToken: String;
begin
  LAuth := TSimpleSupabaseAuth.New(
    'https://abcdefghij.supabase.co',
    'eyJ...anonKey...'
  );
  LAuth.SignIn('usuario@email.com', 'senha123');

  // Em qualquer momento posterior:
  LToken := LAuth.Token;
  // Se faltam < 30 segundos para expirar:
  //   1. Chama RefreshToken internamente
  //   2. Obtem novo access_token e refresh_token
  //   3. Atualiza ExpiresAt
  //   4. Retorna o NOVO token
  // Senao:
  //   Retorna o token atual sem nenhuma chamada HTTP
end;

Fluxo interno do Auto-Refresh

CondicaoAcao
Token nao vazio E ExpiresAt > 0 E Now >= ExpiresAt - 30sChama RefreshToken automaticamente
Token nao vazio E ExpiresAt > 0 E Now < ExpiresAt - 30sRetorna token atual (nenhuma chamada HTTP)
Token vazioRetorna string vazia (nao autenticado)
Falha silenciosa: Se o auto-refresh falhar (ex: rede indisponivel), o token atual e retornado mesmo assim. A excecao de refresh e capturada internamente. Isso evita que sua aplicacao quebre por uma falha transiente de rede.
Integracao com TSimpleQuerySupabase: Quando voce usa o construtor com iSimpleSupabaseAuth, o driver chama FAuth.Token em CADA requisicao HTTP. Isso significa que o auto-refresh acontece transparentemente a cada operacao CRUD.

Row Level Security (RLS)

RLS permite que o Supabase filtre automaticamente registros com base no usuario autenticado. Para ativar RLS com SimpleORM, use anon key + TSimpleSupabaseAuth.

Exemplo: cada usuario ve apenas seus proprios dados

Tabela tarefa no Supabase com RLS ativado e politica:

-- SQL no Supabase Dashboard (SQL Editor):
CREATE POLICY "Usuarios veem suas tarefas"
  ON tarefa FOR SELECT
  USING (auth.uid() = user_id);

CREATE POLICY "Usuarios inserem suas tarefas"
  ON tarefa FOR INSERT
  WITH CHECK (auth.uid() = user_id);

CREATE POLICY "Usuarios editam suas tarefas"
  ON tarefa FOR UPDATE
  USING (auth.uid() = user_id);

CREATE POLICY "Usuarios deletam suas tarefas"
  ON tarefa FOR DELETE
  USING (auth.uid() = user_id);

Codigo Delphi com RLS

var
  LAuth: iSimpleSupabaseAuth;
  LQuery: iSimpleQuery;
  LDAO: iSimpleDAO<TTarefa>;
  LList: TObjectList<TTarefa>;
begin
  // 1. Autenticar com anon key
  LAuth := TSimpleSupabaseAuth.New(
    'https://abcdefghij.supabase.co',
    'eyJ...anonKey...'   // anon key (NAO service_role)
  );
  LAuth.SignIn('joao@email.com', 'senha123');

  // 2. Criar query com auth
  LQuery := TSimpleQuerySupabase.New(
    'https://abcdefghij.supabase.co',
    'eyJ...anonKey...',
    LAuth
  );

  // 3. Usar DAO normalmente
  LDAO := TSimpleDAO<TTarefa>.New(LQuery);

  // Este Find retorna APENAS tarefas do joao@email.com
  // O Supabase aplica a politica RLS automaticamente
  LList := LDAO.Find;
  try
    Writeln('Tarefas de Joao: ', LList.Count);
  finally
    LList.Free;
  end;
end;
Importante: RLS so funciona com anon key. A service_role key ignora todas as politicas RLS. Se voce esta usando service_role key e nao viu dados filtrados, troque para anon key + Auth.

Realtime

O TSimpleSupabaseRealtime monitora mudancas em tabelas do Supabase via polling. Uma thread em background consulta periodicamente as tabelas inscritas e dispara callbacks quando novos registros sao detectados.

Implementacao: A versao atual usa polling HTTP (consulta periodica via REST), nao WebSockets. Isso garante compatibilidade maxima com todas as versoes do Delphi e ambientes de rede, sem dependencias externas.

Configuracao

Crie uma instancia de TSimpleSupabaseRealtime com URL, API key e intervalo de polling opcional.

var
  LRealtime: iSimpleSupabaseRealtime;
begin
  // Intervalo padrao: 2000ms (2 segundos)
  LRealtime := TSimpleSupabaseRealtime.New(
    'https://abcdefghij.supabase.co',
    'eyJ...anonKey...'
  );

  // Ou com intervalo personalizado: 5000ms (5 segundos)
  LRealtime := TSimpleSupabaseRealtime.New(
    'https://abcdefghij.supabase.co',
    'eyJ...anonKey...',
    5000
  );
end;

Callbacks Globais

Callbacks globais sao disparados para QUALQUER tabela inscrita. Use aEvent.Table para identificar a origem.

var
  LRealtime: iSimpleSupabaseRealtime;
begin
  LRealtime := TSimpleSupabaseRealtime.New(
    'https://abcdefghij.supabase.co',
    'eyJ...apiKey...'
  );

  // Callback para INSERTs em qualquer tabela inscrita
  LRealtime.OnInsert(
    procedure(aEvent: TSupabaseRealtimeEvent)
    begin
      Writeln('[INSERT] Tabela: ', aEvent.Table);
      Writeln('  Novo registro: ', aEvent.NewRecord);
      Writeln('  Tipo: ', Ord(aEvent.EventType));
    end
  );

  // Callback para UPDATEs em qualquer tabela inscrita
  LRealtime.OnUpdate(
    procedure(aEvent: TSupabaseRealtimeEvent)
    begin
      Writeln('[UPDATE] Tabela: ', aEvent.Table);
      Writeln('  Registro atualizado: ', aEvent.NewRecord);
    end
  );

  // Callback para DELETEs em qualquer tabela inscrita
  LRealtime.OnDelete(
    procedure(aEvent: TSupabaseRealtimeEvent)
    begin
      Writeln('[DELETE] Tabela: ', aEvent.Table);
      Writeln('  Registro removido: ', aEvent.OldRecord);
    end
  );

  // Inscrever em tabelas e conectar
  LRealtime
    .Subscribe('produto')
    .Subscribe('cliente')
    .Connect;
end;
Thread Safety: Os callbacks sao disparados via TThread.Queue, o que significa que eles executam na thread principal (main thread). E seguro atualizar componentes visuais (VCL/FMX) diretamente dentro dos callbacks.

Callbacks por Tabela

Use OnChange para registrar um callback especifico para uma tabela. Este callback e disparado para QUALQUER tipo de evento (INSERT, UPDATE, DELETE) naquela tabela.

var
  LRealtime: iSimpleSupabaseRealtime;
begin
  LRealtime := TSimpleSupabaseRealtime.New(
    'https://abcdefghij.supabase.co',
    'eyJ...apiKey...'
  );

  // Callback especifico para a tabela 'produto'
  LRealtime.OnChange('produto',
    procedure(aEvent: TSupabaseRealtimeEvent)
    begin
      Writeln('Mudanca na tabela PRODUTO!');
      Writeln('  Tipo: ', Ord(aEvent.EventType));
      Writeln('  Dados: ', aEvent.NewRecord);
    end
  );

  // Callback especifico para a tabela 'pedido'
  LRealtime.OnChange('pedido',
    procedure(aEvent: TSupabaseRealtimeEvent)
    begin
      Writeln('Mudanca na tabela PEDIDO!');
      case aEvent.EventType of
        TSupabaseEventType.setInsert: Writeln('  Novo pedido criado!');
        TSupabaseEventType.setUpdate: Writeln('  Pedido atualizado!');
        TSupabaseEventType.setDelete: Writeln('  Pedido removido!');
      end;
    end
  );

  LRealtime
    .Subscribe('produto')
    .Subscribe('pedido')
    .Connect;
end;
Callbacks globais + por tabela: Se voce definir AMBOS (global e por tabela), os dois serao disparados para eventos naquela tabela. O callback global dispara primeiro, seguido pelo callback por tabela.

Subscribe / Unsubscribe

Subscribe — Inscrever em tabelas

Registra uma tabela para monitoramento. Pode ser chamado multiplas vezes (fluent interface).

LRealtime
  .Subscribe('produto')
  .Subscribe('cliente')
  .Subscribe('pedido')
  .Subscribe('categoria');

// Inscrever a mesma tabela duas vezes nao causa duplicacao
LRealtime.Subscribe('produto'); // Ignorado — ja inscrito

Unsubscribe — Cancelar inscricao

Remove uma tabela do monitoramento. Se a tabela nao esta inscrita, nada acontece.

// Parar de monitorar 'pedido'
LRealtime.Unsubscribe('pedido');

// Nenhum efeito se nao esta inscrito
LRealtime.Unsubscribe('tabela_inexistente');
Subscribe dinamico: Voce pode chamar Subscribe e Unsubscribe mesmo apos Connect. As mudancas sao aplicadas no proximo ciclo de polling (thread-safe via TCriticalSection).

Connect / Disconnect

Connect

Inicia a thread de polling. A partir deste ponto, os callbacks comecam a ser disparados.

LRealtime
  .Subscribe('produto')
  .OnInsert(
    procedure(aEvent: TSupabaseRealtimeEvent)
    begin
      Writeln('Novo produto: ', aEvent.NewRecord);
    end
  )
  .Connect;

Writeln('Monitoramento ativo. Pressione ENTER para parar...');
Readln;

Disconnect

Para a thread de polling. Aguarda a thread finalizar (WaitFor) e libera os recursos.

// Parar monitoramento
LRealtime.Disconnect;
Writeln('Monitoramento encerrado.');

// Chamar Disconnect novamente nao causa erro
LRealtime.Disconnect; // No-op, ja desconectado
Destrutor automatico: Se o objeto TSimpleSupabaseRealtime for destruido (referencia de interface zerada), o Disconnect e chamado automaticamente no destrutor.

Verificar estado de conexao

if LRealtime.IsConnected then
  Writeln('Realtime esta ativo')
else
  Writeln('Realtime esta parado');

Intervalo de Polling

O intervalo de polling define a cada quantos milissegundos a thread consulta as tabelas.

No construtor

// Polling a cada 500ms (muito responsivo, mais consumo de rede)
LRealtime := TSimpleSupabaseRealtime.New(
  'https://abcdefghij.supabase.co',
  'eyJ...apiKey...',
  500
);

// Polling a cada 10 segundos (menos consumo, mais lento para detectar mudancas)
LRealtime := TSimpleSupabaseRealtime.New(
  'https://abcdefghij.supabase.co',
  'eyJ...apiKey...',
  10000
);

Apos criacao

// Alterar intervalo (antes de Connect)
LRealtime.PollInterval(3000); // 3 segundos

Recomendacoes de intervalo

IntervaloUso Recomendado
500msChat, colaboracao em tempo real (alta carga de rede)
1000ms - 2000msDashboards, notificacoes (padrao, bom equilibrio)
5000ms - 10000msSincronizacao de dados, background jobs (baixa carga)
30000ms+Verificacao periodica, tarefas agendadas

Token de autenticacao para Realtime

Se as tabelas monitoradas estao protegidas por RLS, passe o token de autenticacao:

var
  LAuth: iSimpleSupabaseAuth;
  LRealtime: iSimpleSupabaseRealtime;
begin
  LAuth := TSimpleSupabaseAuth.New(url, anonKey);
  LAuth.SignIn('user@email.com', 'pass');

  LRealtime := TSimpleSupabaseRealtime.New(url, anonKey);
  LRealtime.Token(LAuth.Token);

  LRealtime
    .Subscribe('tarefa')
    .OnInsert(
      procedure(aEvent: TSupabaseRealtimeEvent)
      begin
        Writeln('Nova tarefa: ', aEvent.NewRecord);
      end
    )
    .Connect;
end;

Referencia de API

TSimpleQuerySupabase

Driver de query que implementa iSimpleQuery. Traduz SQL para chamadas REST PostgREST.

Unit: SimpleQuerySupabase.pas

Construtores

MetodoParametrosRetornoDescricao
New aBaseURL, aAPIKey: String iSimpleQuery Cria driver com URL e API key. Token de autorizacao = API key.
New aBaseURL, aAPIKey, aToken: String iSimpleQuery Cria driver com JWT estatico para autorizacao.
New aBaseURL, aAPIKey: String; aAuth: iSimpleSupabaseAuth iSimpleQuery Cria driver com objeto auth. Token e obtido automaticamente com auto-refresh.

Metodos iSimpleQuery

MetodoRetornoDescricao
SQLTStringsRetorna o TStringList interno para definir o SQL.
ParamsTParamsRetorna os parametros da query.
ExecSQLiSimpleQueryExecuta INSERT/UPDATE/DELETE. Parseia SQL, converte para HTTP, envia requisicao.
DataSetTDataSetRetorna o TClientDataSet interno com resultados do ultimo Open.
Open(aSQL: String)iSimpleQueryDefine SQL e abre (executa SELECT). Resultado fica no DataSet.
OpeniSimpleQueryAbre com SQL ja definido via SQL.Text.
StartTransactioniSimpleQueryNo-op. REST e stateless.
CommitiSimpleQueryNo-op. REST e stateless.
RollbackiSimpleQueryNo-op. REST e stateless.
&EndTransactioniSimpleQueryDelega para Commit (no-op).
InTransactionBooleanSempre retorna False.
SQLTypeTSQLTypeRetorna TSQLType.MySQL (padrao).
RowsAffectedIntegerRetorna -1 (nao disponivel via REST).

Metodos adicionais

MetodoParametrosRetornoDescricao
Token aValue: String iSimpleQuery Define/altera o token JWT usado na autorizacao.

Metodos protegidos (parsing de SQL)

MetodoDescricao
ExtractTableNameExtrai nome da tabela do SQL (apos FROM, INTO, ou UPDATE).
DetectOperationDetecta operacao: INSERT, UPDATE, DELETE ou SELECT.
ExtractPKFieldNameExtrai o nome do campo PK da clausula WHERE.
ExtractPKValueExtrai o valor da PK dos parametros.
ExtractInsertFieldsExtrai nomes dos campos do INSERT (entre parenteses).
ExtractSelectFieldsExtrai campos do SELECT (entre SELECT e FROM).
ExtractWhereFiltersConverte WHERE SQL para filtros PostgREST (campo=eq.valor).
ExtractPaginationDetecta e extrai Skip/Take de qualquer dialeto SQL.
ExtractUpdateFieldsExtrai campos do SET no UPDATE.
BuildSupabaseURLMonta URL: baseURL + /rest/v1/ + tabela.
ParamsToJSONConverte TParams para JSON object, respeitando tipos.

TSimpleSupabaseAuth

Gerenciador de autenticacao. Implementa iSimpleSupabaseAuth.

Unit: SimpleSupabaseAuth.pas

Construtor

MetodoParametrosRetornoDescricao
New aBaseURL, aAPIKey: String iSimpleSupabaseAuth Cria instancia com URL do projeto e API key (tipicamente anon key).

Metodos de autenticacao

MetodoParametrosRetornoDescricao
SignIn aEmail, aPassword: String iSimpleSupabaseAuth Login com email/senha. Endpoint: /auth/v1/token?grant_type=password
SignUp aEmail, aPassword: String iSimpleSupabaseAuth Criar conta. Endpoint: /auth/v1/signup
SignOut (nenhum) iSimpleSupabaseAuth Logout. Limpa todos os tokens e dados do usuario. Endpoint: /auth/v1/logout
RefreshToken (nenhum) iSimpleSupabaseAuth Renova JWT via refresh token. Endpoint: /auth/v1/token?grant_type=refresh_token

Propriedades (somente leitura)

PropriedadeTipoDescricao
Token String Access token JWT. Chama auto-refresh se faltam < 30s para expirar.
User String Dados do usuario em formato JSON (retorno de user do Supabase Auth).
IsAuthenticated Boolean True se Token nao esta vazio E ExpiresAt > Now.
ExpiresAt TDateTime Data/hora de expiracao do token. Calculada via Now + expires_in da resposta.

Endpoints Supabase Auth utilizados

OperacaoMetodo HTTPEndpoint
SignInPOST/auth/v1/token?grant_type=password
SignUpPOST/auth/v1/signup
SignOutPOST/auth/v1/logout
RefreshTokenPOST/auth/v1/token?grant_type=refresh_token

TSimpleSupabaseRealtime

Monitoramento de mudancas via polling. Implementa iSimpleSupabaseRealtime.

Unit: SimpleSupabaseRealtime.pas

Construtor

MetodoParametrosRetornoDescricao
New aBaseURL, aAPIKey: String; aPollIntervalMs: Integer = 2000 iSimpleSupabaseRealtime Cria instancia com intervalo de polling (padrao 2 segundos).

Metodos de inscricao

MetodoParametrosRetornoDescricao
Subscribe aTable: String iSimpleSupabaseRealtime Inscreve para monitorar tabela. Ignora se ja inscrito. Thread-safe.
Unsubscribe aTable: String iSimpleSupabaseRealtime Cancela inscricao. Ignora se nao inscrito. Thread-safe.

Metodos de callback

MetodoParametrosRetornoDescricao
OnInsert aCallback: TSupabaseRealtimeCallback iSimpleSupabaseRealtime Callback global para INSERTs em qualquer tabela inscrita.
OnUpdate aCallback: TSupabaseRealtimeCallback iSimpleSupabaseRealtime Callback global para UPDATEs em qualquer tabela inscrita.
OnDelete aCallback: TSupabaseRealtimeCallback iSimpleSupabaseRealtime Callback global para DELETEs em qualquer tabela inscrita.
OnChange aTable: String; aCallback: TSupabaseRealtimeCallback iSimpleSupabaseRealtime Callback especifico para uma tabela (disparado em qualquer tipo de evento).

Metodos de controle

MetodoParametrosRetornoDescricao
Connect (nenhum) iSimpleSupabaseRealtime Inicia thread de polling. No-op se ja conectado.
Disconnect (nenhum) iSimpleSupabaseRealtime Para thread. Chama Terminate + WaitFor. No-op se nao conectado.
IsConnected (nenhum) Boolean Retorna True se a thread de polling esta ativa.
Token aValue: String iSimpleSupabaseRealtime Define token JWT para autorizacao nas requisicoes de polling.
PollInterval aMs: Integer iSimpleSupabaseRealtime Altera intervalo de polling em milissegundos.

Como o polling funciona internamente

EtapaDescricao
1Thread consulta cada tabela inscrita via GET /rest/v1/tabela?order=id.desc&limit=10
2Se LastKnownId existe, adiciona filtro &id=gt.lastId para buscar apenas novos
3Registros encontrados sao iterados em ordem cronologica (mais antigo primeiro)
4Para cada registro, cria TSupabaseRealtimeEvent e dispara callbacks via TThread.Queue
5Atualiza LastKnownId com o maior ID encontrado
6Dorme pelo intervalo de polling e repete

TSupabaseRealtimeEvent

Record que representa um evento realtime. Passado como parametro nos callbacks.

Unit: SimpleTypes.pas

Campos

CampoTipoDescricao
Table String Nome da tabela onde o evento ocorreu.
EventType TSupabaseEventType Tipo do evento: setInsert, setUpdate, ou setDelete.
OldRecord String JSON do registro antes da alteracao (vazio para INSERTs).
NewRecord String JSON do registro apos a alteracao (vazio para DELETEs).

TSupabaseEventType (enum)

ValorDescricao
setInsertNovo registro inserido.
setUpdateRegistro atualizado.
setDeleteRegistro removido.

TSupabaseRealtimeCallback (tipo)

TSupabaseRealtimeCallback = reference to procedure(aEvent: TSupabaseRealtimeEvent);

Procedimento anonimo que recebe um evento realtime. Usado em OnInsert, OnUpdate, OnDelete e OnChange.

Exemplos Completos

Exemplos compilaveis prontos para copiar e executar. Substitua URL e API key pelos seus valores.

App Console Basico — CRUD

Exemplo minimo de CRUD com Supabase. Nao usa autenticacao nem realtime.

program SupabaseBasico;

{$APPTYPE CONSOLE}

{$R *.res}

uses
  System.SysUtils,
  System.Generics.Collections,
  SimpleInterface,
  SimpleDAO,
  SimpleAttributes,
  SimpleQuerySupabase;

type
  [Tabela('produto')]
  TProduto = class
  private
    FId: Integer;
    FNome: String;
    FPreco: Double;
  published
    [Campo('id'), PK, AutoInc]
    property Id: Integer read FId write FId;
    [Campo('nome'), NotNull]
    property Nome: String read FNome write FNome;
    [Campo('preco')]
    property Preco: Double read FPreco write FPreco;
  end;

const
  SUPA_URL = 'https://abcdefghij.supabase.co';  // Altere para sua URL
  SUPA_KEY = 'eyJ...sua-service-role-key...';    // Altere para sua key

var
  LQuery: iSimpleQuery;
  LDAO: iSimpleDAO<TProduto>;
  LProduto: TProduto;
  LList: TObjectList<TProduto>;
  I: Integer;
begin
  try
    LQuery := TSimpleQuerySupabase.New(SUPA_URL, SUPA_KEY);
    LDAO := TSimpleDAO<TProduto>.New(LQuery);

    // === INSERT ===
    Writeln('--- INSERT ---');
    LProduto := TProduto.Create;
    try
      LProduto.Nome := 'Teclado Mecanico';
      LProduto.Preco := 299.90;
      LDAO.Insert(LProduto);
      Writeln('Inserido: ', LProduto.Nome);
    finally
      LProduto.Free;
    end;

    // === FIND ALL ===
    Writeln('');
    Writeln('--- FIND ALL ---');
    LList := LDAO.Find;
    try
      for I := 0 to LList.Count - 1 do
        Writeln(LList[I].Id, ' | ', LList[I].Nome, ' | R$ ', LList[I].Preco:0:2);
    finally
      LList.Free;
    end;

    // === UPDATE ===
    Writeln('');
    Writeln('--- UPDATE ---');
    LList := LDAO.Find;
    try
      if LList.Count > 0 then
      begin
        LProduto := LList[0];
        LProduto.Nome := LProduto.Nome + ' (atualizado)';
        LProduto.Preco := LProduto.Preco + 50;
        LDAO.Update(LProduto);
        Writeln('Atualizado: ', LProduto.Nome);
      end;
    finally
      LList.Free;
    end;

    // === PAGINACAO ===
    Writeln('');
    Writeln('--- PAGINACAO (5 por pagina) ---');
    LList := LDAO.SQL.Skip(0).Take(5).&End.Find;
    try
      Writeln('Pagina 1: ', LList.Count, ' registros');
    finally
      LList.Free;
    end;

    // === DELETE ===
    Writeln('');
    Writeln('--- DELETE ---');
    LList := LDAO.Find;
    try
      if LList.Count > 0 then
      begin
        LProduto := LList[LList.Count - 1];
        Writeln('Removendo: ', LProduto.Nome);
        LDAO.Delete(LProduto);
        Writeln('Removido com sucesso!');
      end;
    finally
      LList.Free;
    end;

    Writeln('');
    Writeln('Concluido!');
  except
    on E: Exception do
      Writeln('ERRO: ', E.Message);
  end;
  Readln;
end.

App com Autenticacao

Exemplo com SignIn, verificacao de token e CRUD com RLS.

program SupabaseAuth;

{$APPTYPE CONSOLE}

{$R *.res}

uses
  System.SysUtils,
  System.Generics.Collections,
  SimpleInterface,
  SimpleDAO,
  SimpleAttributes,
  SimpleQuerySupabase,
  SimpleSupabaseAuth;

type
  [Tabela('tarefa')]
  TTarefa = class
  private
    FId: Integer;
    FTitulo: String;
    FConcluida: Boolean;
    FUserId: String;
  published
    [Campo('id'), PK, AutoInc]
    property Id: Integer read FId write FId;
    [Campo('titulo'), NotNull]
    property Titulo: String read FTitulo write FTitulo;
    [Campo('concluida')]
    property Concluida: Boolean read FConcluida write FConcluida;
    [Campo('user_id')]
    property UserId: String read FUserId write FUserId;
  end;

const
  SUPA_URL = 'https://abcdefghij.supabase.co';
  ANON_KEY = 'eyJ...sua-anon-key...';

var
  LAuth: iSimpleSupabaseAuth;
  LQuery: iSimpleQuery;
  LDAO: iSimpleDAO<TTarefa>;
  LTarefa: TTarefa;
  LList: TObjectList<TTarefa>;
  I: Integer;
begin
  try
    // === AUTENTICACAO ===
    Writeln('--- AUTENTICACAO ---');
    LAuth := TSimpleSupabaseAuth.New(SUPA_URL, ANON_KEY);
    LAuth.SignIn('usuario@email.com', 'minhaSenha123');

    if not LAuth.IsAuthenticated then
    begin
      Writeln('Falha no login!');
      Readln;
      Exit;
    end;

    Writeln('Login OK!');
    Writeln('Token: ', Copy(LAuth.Token, 1, 40), '...');
    Writeln('User: ', LAuth.User);
    Writeln('Expira: ', DateTimeToStr(LAuth.ExpiresAt));

    // === CRIAR DAO COM AUTH (RLS ativo) ===
    LQuery := TSimpleQuerySupabase.New(SUPA_URL, ANON_KEY, LAuth);
    LDAO := TSimpleDAO<TTarefa>.New(LQuery);

    // === INSERT ===
    Writeln('');
    Writeln('--- INSERIR TAREFA ---');
    LTarefa := TTarefa.Create;
    try
      LTarefa.Titulo := 'Estudar SimpleORM + Supabase';
      LTarefa.Concluida := False;
      LDAO.Insert(LTarefa);
      Writeln('Tarefa criada: ', LTarefa.Titulo);
    finally
      LTarefa.Free;
    end;

    // === FIND (retorna apenas tarefas deste usuario via RLS) ===
    Writeln('');
    Writeln('--- MINHAS TAREFAS ---');
    LList := LDAO.Find;
    try
      Writeln('Total: ', LList.Count);
      for I := 0 to LList.Count - 1 do
      begin
        if LList[I].Concluida then
          Writeln('  [X] ', LList[I].Titulo)
        else
          Writeln('  [ ] ', LList[I].Titulo);
      end;
    finally
      LList.Free;
    end;

    // === VERIFICAR AUTO-REFRESH ===
    Writeln('');
    Writeln('--- TOKEN (com auto-refresh) ---');
    Writeln('Token atual: ', Copy(LAuth.Token, 1, 40), '...');
    Writeln('(Se faltavam < 30s, foi renovado automaticamente)');

    // === SIGNOUT ===
    Writeln('');
    Writeln('--- SIGNOUT ---');
    LAuth.SignOut;
    Writeln('Autenticado: ', LAuth.IsAuthenticated);  // False

  except
    on E: Exception do
      Writeln('ERRO: ', E.Message);
  end;
  Readln;
end.

App com Realtime

Exemplo que monitora mudancas em tabelas e exibe notificacoes no console.

program SupabaseRealtime;

{$APPTYPE CONSOLE}

{$R *.res}

uses
  System.SysUtils,
  SimpleInterface,
  SimpleTypes,
  SimpleSupabaseRealtime;

const
  SUPA_URL = 'https://abcdefghij.supabase.co';
  SUPA_KEY = 'eyJ...sua-api-key...';

var
  LRealtime: iSimpleSupabaseRealtime;
begin
  try
    Writeln('=== SimpleORM Supabase Realtime ===');
    Writeln('');

    LRealtime := TSimpleSupabaseRealtime.New(SUPA_URL, SUPA_KEY, 2000);

    // Callback global para INSERTs
    LRealtime.OnInsert(
      procedure(aEvent: TSupabaseRealtimeEvent)
      begin
        Writeln('[INSERT] ', aEvent.Table, ': ', aEvent.NewRecord);
      end
    );

    // Callback global para UPDATEs
    LRealtime.OnUpdate(
      procedure(aEvent: TSupabaseRealtimeEvent)
      begin
        Writeln('[UPDATE] ', aEvent.Table, ': ', aEvent.NewRecord);
      end
    );

    // Callback global para DELETEs
    LRealtime.OnDelete(
      procedure(aEvent: TSupabaseRealtimeEvent)
      begin
        Writeln('[DELETE] ', aEvent.Table, ': ', aEvent.OldRecord);
      end
    );

    // Callback especifico para tabela 'produto'
    LRealtime.OnChange('produto',
      procedure(aEvent: TSupabaseRealtimeEvent)
      begin
        Writeln('  >> Mudanca especifica em PRODUTO detectada!');
      end
    );

    // Inscrever em tabelas
    LRealtime
      .Subscribe('produto')
      .Subscribe('cliente')
      .Subscribe('pedido');

    // Iniciar monitoramento
    LRealtime.Connect;

    Writeln('Monitorando tabelas: produto, cliente, pedido');
    Writeln('Intervalo de polling: 2 segundos');
    Writeln('');
    Writeln('Faca INSERTs/UPDATEs/DELETEs no Supabase Dashboard');
    Writeln('e veja as notificacoes aqui.');
    Writeln('');
    Writeln('Pressione ENTER para encerrar...');
    Readln;

    // Parar monitoramento
    LRealtime.Disconnect;
    Writeln('Monitoramento encerrado.');

  except
    on E: Exception do
      Writeln('ERRO: ', E.Message);
  end;
end.

App Completa (CRUD + Auth + Realtime)

Exemplo completo que demonstra todos os 3 componentes trabalhando juntos.

program SupabaseCompleto;

{$APPTYPE CONSOLE}

{$R *.res}

uses
  System.SysUtils,
  System.Generics.Collections,
  SimpleInterface,
  SimpleTypes,
  SimpleDAO,
  SimpleAttributes,
  SimpleQuerySupabase,
  SimpleSupabaseAuth,
  SimpleSupabaseRealtime;

type
  [Tabela('produto')]
  TProduto = class
  private
    FId: Integer;
    FNome: String;
    FPreco: Double;
    FAtivo: Boolean;
  published
    [Campo('id'), PK, AutoInc]
    property Id: Integer read FId write FId;
    [Campo('nome'), NotNull]
    property Nome: String read FNome write FNome;
    [Campo('preco')]
    property Preco: Double read FPreco write FPreco;
    [Campo('ativo')]
    property Ativo: Boolean read FAtivo write FAtivo;
  end;

const
  SUPA_URL = 'https://abcdefghij.supabase.co';  // Altere
  ANON_KEY = 'eyJ...sua-anon-key...';            // Altere
  USER_EMAIL = 'admin@empresa.com';               // Altere
  USER_PASS  = 'senhaSegura123';                  // Altere

var
  LAuth: iSimpleSupabaseAuth;
  LQuery: iSimpleQuery;
  LDAO: iSimpleDAO<TProduto>;
  LRealtime: iSimpleSupabaseRealtime;
  LProduto: TProduto;
  LList: TObjectList<TProduto>;
  I: Integer;
begin
  try
    Writeln('========================================');
    Writeln('  SimpleORM + Supabase - App Completa');
    Writeln('========================================');
    Writeln('');

    // ============================================================
    // PASSO 1: Autenticacao
    // ============================================================
    Writeln('[1/4] Autenticacao...');
    LAuth := TSimpleSupabaseAuth.New(SUPA_URL, ANON_KEY);
    LAuth.SignIn(USER_EMAIL, USER_PASS);

    if not LAuth.IsAuthenticated then
    begin
      Writeln('  FALHA: Login nao realizado');
      Readln;
      Exit;
    end;
    Writeln('  OK - Logado como: ', USER_EMAIL);
    Writeln('  Token expira em: ', DateTimeToStr(LAuth.ExpiresAt));

    // ============================================================
    // PASSO 2: Configurar DAO com Auth
    // ============================================================
    Writeln('');
    Writeln('[2/4] Configurando DAO...');
    LQuery := TSimpleQuerySupabase.New(SUPA_URL, ANON_KEY, LAuth);
    LDAO := TSimpleDAO<TProduto>.New(LQuery);
    Writeln('  OK - DAO configurado com autenticacao');

    // ============================================================
    // PASSO 3: Configurar Realtime
    // ============================================================
    Writeln('');
    Writeln('[3/4] Configurando Realtime...');
    LRealtime := TSimpleSupabaseRealtime.New(SUPA_URL, ANON_KEY, 2000);
    LRealtime.Token(LAuth.Token);

    LRealtime.OnInsert(
      procedure(aEvent: TSupabaseRealtimeEvent)
      begin
        Writeln('  [REALTIME INSERT] ', aEvent.Table, ': ', aEvent.NewRecord);
      end
    );

    LRealtime.OnChange('produto',
      procedure(aEvent: TSupabaseRealtimeEvent)
      begin
        Writeln('  [REALTIME CHANGE] Produto mudou!');
      end
    );

    LRealtime
      .Subscribe('produto')
      .Connect;
    Writeln('  OK - Monitorando tabela produto');

    // ============================================================
    // PASSO 4: Operacoes CRUD
    // ============================================================
    Writeln('');
    Writeln('[4/4] Executando CRUD...');

    // INSERT
    Writeln('');
    Writeln('  --- INSERT ---');
    LProduto := TProduto.Create;
    try
      LProduto.Nome := 'Monitor 4K';
      LProduto.Preco := 2199.00;
      LProduto.Ativo := True;
      LDAO.Insert(LProduto);
      Writeln('  Inserido: ', LProduto.Nome);
    finally
      LProduto.Free;
    end;

    LProduto := TProduto.Create;
    try
      LProduto.Nome := 'Mouse Gamer';
      LProduto.Preco := 189.90;
      LProduto.Ativo := True;
      LDAO.Insert(LProduto);
      Writeln('  Inserido: ', LProduto.Nome);
    finally
      LProduto.Free;
    end;

    // FIND ALL
    Writeln('');
    Writeln('  --- FIND ALL ---');
    LList := LDAO.Find;
    try
      Writeln('  Total: ', LList.Count, ' registros');
      for I := 0 to LList.Count - 1 do
        Writeln('  ', LList[I].Id:4, ' | ', LList[I].Nome:30, ' | R$ ', LList[I].Preco:8:2);
    finally
      LList.Free;
    end;

    // PAGINACAO
    Writeln('');
    Writeln('  --- PAGINACAO ---');
    LList := LDAO.SQL.Skip(0).Take(3).&End.Find;
    try
      Writeln('  Primeiros 3: ', LList.Count, ' registros');
    finally
      LList.Free;
    end;

    // UPDATE
    Writeln('');
    Writeln('  --- UPDATE ---');
    LList := LDAO.Find;
    try
      if LList.Count > 0 then
      begin
        LProduto := LList[0];
        LProduto.Preco := LProduto.Preco * 0.9; // 10% desconto
        LDAO.Update(LProduto);
        Writeln('  Atualizado: ', LProduto.Nome, ' novo preco: R$ ', LProduto.Preco:0:2);
      end;
    finally
      LList.Free;
    end;

    // DELETE
    Writeln('');
    Writeln('  --- DELETE ---');
    LList := LDAO.Find;
    try
      if LList.Count > 0 then
      begin
        LProduto := LList[LList.Count - 1];
        Writeln('  Removendo: ', LProduto.Nome);
        LDAO.Delete(LProduto);
        Writeln('  Removido!');
      end;
    finally
      LList.Free;
    end;

    // ============================================================
    // RESULTADO FINAL
    // ============================================================
    Writeln('');
    Writeln('========================================');
    Writeln('  Operacoes concluidas!');
    Writeln('  Auth: ', LAuth.IsAuthenticated);
    Writeln('  Realtime: ', LRealtime.IsConnected);
    Writeln('========================================');
    Writeln('');
    Writeln('Pressione ENTER para encerrar...');
    Readln;

    // Cleanup
    LRealtime.Disconnect;
    LAuth.SignOut;

  except
    on E: Exception do
    begin
      Writeln('ERRO: ', E.Message);
      Readln;
    end;
  end;
end.

Troubleshooting

Erros HTTP comuns

CodigoMensagemCausaSolucao
401 Supabase HTTP 401: ... API key invalida ou token expirado Verifique a API key no Dashboard (Settings > API). Se usando auth, verifique se IsAuthenticated = True.
403 Supabase HTTP 403: ... RLS bloqueou o acesso Use service_role key (ignora RLS) ou configure politicas RLS corretas para o usuario.
404 Supabase HTTP 404: ... Tabela nao existe ou URL errada Verifique se a tabela existe no Dashboard. Verifique se o nome no [Tabela('nome')] esta correto (case-sensitive).
400 Supabase HTTP 400: ... Body JSON invalido ou campo inexistente Verifique se os nomes em [Campo('nome')] correspondem as colunas da tabela no Supabase.
409 Supabase HTTP 409: ... Conflito de chave unica (registro duplicado) O registro com essa PK ou constraint UNIQUE ja existe. Use Update em vez de Insert.

Problemas comuns

Transacoes nao funcionam

Esperado: O driver Supabase opera via REST/HTTP, que e stateless. StartTransaction, Commit e Rollback sao no-ops. InTransaction sempre retorna False. Cada operacao e executada individualmente. Se voce precisa de transacoes atomicas, use um driver local (FireDAC, UniDAC).

RLS bloqueando acesso — vejo 0 registros

Checklist de diagnostico:

VerificacaoComo resolver
Usando anon key (nao service_role)?RLS so se aplica com anon key. Com service_role, RLS e ignorado.
Usuario esta autenticado?Verifique LAuth.IsAuthenticated antes de usar o DAO.
Politica RLS existe para SELECT?No Dashboard, va em Authentication > Policies e verifique se ha uma politica para SELECT.
Coluna user_id esta preenchida?Inserts devem incluir user_id = auth.uid() na politica ou na aplicacao.

Token expirou e auto-refresh nao funcionou

O auto-refresh falha silenciosamente para nao quebrar a aplicacao. Possiveis causas:

CausaSolucao
Rede indisponivelO token atual e retornado. A proxima chamada tentara refresh novamente.
Refresh token invalido (usuario removido)Chame SignIn novamente para obter novos tokens.
Refresh token expirou (sessao muito longa)Supabase tem limite de vida para refresh tokens. Chame SignIn novamente.

Realtime nao detecta mudancas

CausaSolucao
Esqueceu de chamar ConnectSempre chame .Connect apos .Subscribe e configurar callbacks.
Tabela nao tem coluna idO polling usa ?order=id.desc. A tabela deve ter uma coluna id numerica.
RLS bloqueando leituraPasse o token via LRealtime.Token(LAuth.Token).
Intervalo muito longoReduza o polling: LRealtime.PollInterval(1000).
App console fecha imediatamenteAdicione Readln apos Connect para manter o programa aberto.

RowsAffected retorna -1

Esperado: A API PostgREST nao retorna contagem de registros afetados no formato que o driver consegue extrair. RowsAffected sempre retorna -1. Use Find apos operacoes para verificar o resultado.

Nomes de campos case-sensitive

Importante: O Supabase (PostgreSQL) usa nomes de coluna em lowercase por padrao. O driver converte nomes para lowercase automaticamente ao construir URLs e JSON (aTableName.ToLower, LParam.Name.ToLower). Certifique-se de que seus atributos [Campo('nome')] correspondam ao nome real da coluna no banco.

Erro de compilacao: Undeclared identifier 'TSimpleQuerySupabase'

Adicione SimpleQuerySupabase na clausula uses do seu programa. Se usando Boss, verifique se o pacote esta instalado: boss install academiadocodigo/SimpleORM.

Erro: Supabase Auth: no refresh token available

Voce chamou RefreshToken sem ter feito SignIn antes. O refresh token e obtido automaticamente durante o SignIn. Chame LAuth.SignIn(email, senha) primeiro.