SimpleORM + Supabase NEW

Connect your Delphi application to Supabase with zero external dependencies.

Introduction

The Supabase integration for SimpleORM allows you to use your Supabase project as a backend database directly from Delphi, without installing any additional libraries. All communication happens via Supabase's PostgREST API using standard HTTP requests from System.Net.HttpClient.

The integration consists of 3 components:

Component Unit Description
TSimpleQuerySupabase SimpleQuerySupabase.pas Query driver implementing iSimpleQuery — translates SQL operations to PostgREST REST calls
TSimpleSupabaseAuth SimpleSupabaseAuth.pas Authentication via Supabase GoTrue API — SignIn, SignUp, SignOut, token refresh
TSimpleSupabaseRealtime SimpleSupabaseRealtime.pas Realtime change detection via polling — monitors INSERT, UPDATE, DELETE events
Zero external dependencies. The integration uses only native Delphi units (System.Net.HttpClient, System.JSON). No need to install DLLs, packages, or third-party components.

Installation

Via Boss (recommended)

boss install academiadocodigo/SimpleORM

Manual Installation

Add the src/ directory to your Delphi Library Path and include the required units:

uses
  SimpleInterface,
  SimpleDAO,
  SimpleQuerySupabase,      // Query driver for Supabase
  SimpleSupabaseAuth,       // Authentication (optional)
  SimpleSupabaseRealtime;   // Realtime (optional)

Basic Setup

TSimpleQuerySupabase offers 3 constructor variants for different scenarios:

1. URL + API Key (simplest)

var
  LQuery: iSimpleQuery;
begin
  LQuery := TSimpleQuerySupabase.New(
    'https://xyzproject.supabase.co',  // Your Supabase project URL
    'eyJhbGciOiJIUzI1NiIs...'         // API Key (anon or service_role)
  );
end;

2. URL + API Key + JWT Token

var
  LQuery: iSimpleQuery;
begin
  LQuery := TSimpleQuerySupabase.New(
    'https://xyzproject.supabase.co',  // Project URL
    'eyJhbGciOiJIUzI1NiIs...',        // API Key (apikey header)
    'eyJhbGciOiJIUzI1NiIs...'         // JWT Token (Authorization: Bearer)
  );
end;

3. URL + API Key + SQLType

var
  LQuery: iSimpleQuery;
begin
  LQuery := TSimpleQuerySupabase.New(
    'https://xyzproject.supabase.co',
    'eyJhbGciOiJIUzI1NiIs...',
    TSQLType.PostgreSQL                // SQL dialect for query generation
  );
end;

Where to find URL and API Key

In your Supabase Dashboard, go to Settings > API. You will find:

Project URL: https://xyzproject.supabase.co
anon (public) key: for client-side access with RLS
service_role key: for server-side access bypassing RLS
Security: The service_role key bypasses Row Level Security. Never expose it in client-side applications. Use the anon key combined with authentication for client apps.

Full CRUD

TSimpleQuerySupabase implements iSimpleQuery, so you use it with TSimpleDAO<T> exactly the same way as any other driver (FireDAC, UniDAC, etc.). The driver transparently translates SQL operations to PostgREST REST calls.

Insert

var
  LProduct: TProduct;
  LDAO: iSimpleDAO<TProduct>;
begin
  LProduct := TProduct.Create;
  try
    LProduct.Name := 'Keyboard';
    LProduct.Price := 149.90;

    LDAO := TSimpleDAO<TProduct>.New(
      TSimpleQuerySupabase.New(SUPABASE_URL, SUPABASE_KEY)
    );
    LDAO.Insert(LProduct);
  finally
    LProduct.Free;
  end;
end;

Generated HTTP Request

POST https://xyzproject.supabase.co/rest/v1/PRODUCTS
Headers:
  apikey: eyJ...
  Authorization: Bearer eyJ...
  Content-Type: application/json
  Prefer: return=representation
Body:
  {"NAME": "Keyboard", "PRICE": 149.90}

Find

Find All

var
  LList: TObjectList<TProduct>;
  LDAO: iSimpleDAO<TProduct>;
begin
  LList := TObjectList<TProduct>.Create;
  try
    LDAO := TSimpleDAO<TProduct>.New(
      TSimpleQuerySupabase.New(SUPABASE_URL, SUPABASE_KEY)
    );
    LDAO.Find(LList);

    for var LProduct in LList do
      Writeln(LProduct.Name, ' - $', LProduct.Price);
  finally
    LList.Free;
  end;
end;

Generated HTTP Request

GET https://xyzproject.supabase.co/rest/v1/PRODUCTS?select=*

Find by ID

var
  LProduct: TProduct;
  LDAO: iSimpleDAO<TProduct>;
begin
  LProduct := TProduct.Create;
  try
    LProduct.Id := 42;

    LDAO := TSimpleDAO<TProduct>.New(
      TSimpleQuerySupabase.New(SUPABASE_URL, SUPABASE_KEY)
    );
    LDAO.Find(LProduct);

    Writeln('Found: ', LProduct.Name);
  finally
    LProduct.Free;
  end;
end;

Generated HTTP Request

GET https://xyzproject.supabase.co/rest/v1/PRODUCTS?select=*&ID=eq.42

Update

var
  LProduct: TProduct;
  LDAO: iSimpleDAO<TProduct>;
begin
  LProduct := TProduct.Create;
  try
    LProduct.Id := 42;
    LProduct.Name := 'Mechanical Keyboard';
    LProduct.Price := 299.90;

    LDAO := TSimpleDAO<TProduct>.New(
      TSimpleQuerySupabase.New(SUPABASE_URL, SUPABASE_KEY)
    );
    LDAO.Update(LProduct);
  finally
    LProduct.Free;
  end;
end;

Generated HTTP Request

PATCH https://xyzproject.supabase.co/rest/v1/PRODUCTS?ID=eq.42
Headers:
  Content-Type: application/json
  Prefer: return=representation
Body:
  {"NAME": "Mechanical Keyboard", "PRICE": 299.90}

Delete

var
  LProduct: TProduct;
  LDAO: iSimpleDAO<TProduct>;
begin
  LProduct := TProduct.Create;
  try
    LProduct.Id := 42;

    LDAO := TSimpleDAO<TProduct>.New(
      TSimpleQuerySupabase.New(SUPABASE_URL, SUPABASE_KEY)
    );
    LDAO.Delete(LProduct);
  finally
    LProduct.Free;
  end;
end;

Generated HTTP Request

DELETE https://xyzproject.supabase.co/rest/v1/PRODUCTS?ID=eq.42

Pagination

var
  LList: TObjectList<TProduct>;
  LDAO: iSimpleDAO<TProduct>;
begin
  LList := TObjectList<TProduct>.Create;
  try
    LDAO := TSimpleDAO<TProduct>.New(
      TSimpleQuerySupabase.New(SUPABASE_URL, SUPABASE_KEY)
    );

    // Page 2 with 10 records per page
    LDAO.SQL
      .Skip(10)
      .Take(10)
    .&End
    .Find(LList);
  finally
    LList.Free;
  end;
end;

Generated HTTP Request

GET https://xyzproject.supabase.co/rest/v1/PRODUCTS?select=*&limit=10&offset=10

Batch Operations

Batch operations (InsertBatch, UpdateBatch, DeleteBatch) work the same way as with other drivers. Each item is sent as an individual REST request.

var
  LList: TObjectList<TProduct>;
  LDAO: iSimpleDAO<TProduct>;
begin
  LList := TObjectList<TProduct>.Create;
  // ... populate list ...

  LDAO := TSimpleDAO<TProduct>.New(
    TSimpleQuerySupabase.New(SUPABASE_URL, SUPABASE_KEY)
  );
  LDAO.InsertBatch(LList);
end;
Transactions: Supabase REST API is stateless, so transactions are no-ops. StartTransaction, Commit, and Rollback return Self without performing any action. InTransaction always returns False.

SQL to REST Mapping

The driver translates SQL operations generated by TSimpleDAO into PostgREST REST endpoints. Here is the complete mapping:

SQL Operation HTTP Method PostgREST Endpoint Notes
INSERT INTO table (...) VALUES (...) POST /rest/v1/table Body as JSON
SELECT * FROM table GET /rest/v1/table?select=* Returns JSON array
SELECT * FROM table WHERE id = :id GET /rest/v1/table?select=*&id=eq.value PostgREST filter
UPDATE table SET ... WHERE id = :id PATCH /rest/v1/table?id=eq.value Body with changed fields
DELETE FROM table WHERE id = :id DELETE /rest/v1/table?id=eq.value Filter in query string
LIMIT n OFFSET m - ?limit=n&offset=m Query string parameters
ORDER BY field ASC - ?order=field.asc Query string parameter
ORDER BY field DESC - ?order=field.desc Query string parameter

Authentication

TSimpleSupabaseAuth provides complete integration with the Supabase GoTrue authentication API. It supports email/password authentication, token management, and automatic token refresh.

API Key Only (service_role)

The simplest approach: use the service_role key directly. This bypasses all Row Level Security policies.

var
  LQuery: iSimpleQuery;
begin
  // service_role key bypasses RLS - use only on server-side
  LQuery := TSimpleQuerySupabase.New(
    'https://xyzproject.supabase.co',
    'eyJ_SERVICE_ROLE_KEY...'
  );
end;
Warning: The service_role key has full access to all data. Never use it in client-side applications or expose it publicly.

JWT Token

Pass a JWT token for authenticated requests. The token is sent in the Authorization: Bearer header.

var
  LQuery: iSimpleQuery;
begin
  LQuery := TSimpleQuerySupabase.New(
    'https://xyzproject.supabase.co',
    'eyJ_ANON_KEY...',                // API Key
    'eyJ_USER_JWT_TOKEN...'           // JWT Token from authentication
  );
end;

SignIn / SignUp

var
  LAuth: TSimpleSupabaseAuth;
  LToken: string;
begin
  LAuth := TSimpleSupabaseAuth.Create(
    'https://xyzproject.supabase.co',
    'eyJ_ANON_KEY...'
  );
  try
    // Sign in with existing user
    LAuth.SignIn('user@email.com', 'password123');
    LToken := LAuth.AccessToken;
    Writeln('Authenticated! Token: ', Copy(LToken, 1, 20), '...');

    // Use the token with the query driver
    var LQuery := TSimpleQuerySupabase.New(
      'https://xyzproject.supabase.co',
      'eyJ_ANON_KEY...',
      LToken
    );
  finally
    LAuth.Free;
  end;
end;
var
  LAuth: TSimpleSupabaseAuth;
begin
  LAuth := TSimpleSupabaseAuth.Create(
    'https://xyzproject.supabase.co',
    'eyJ_ANON_KEY...'
  );
  try
    // Register a new user
    LAuth.SignUp('newuser@email.com', 'securepassword');
    Writeln('User created! Token: ', Copy(LAuth.AccessToken, 1, 20), '...');
  finally
    LAuth.Free;
  end;
end;

SignOut

var
  LAuth: TSimpleSupabaseAuth;
begin
  LAuth := TSimpleSupabaseAuth.Create(
    'https://xyzproject.supabase.co',
    'eyJ_ANON_KEY...'
  );
  try
    LAuth.SignIn('user@email.com', 'password123');
    // ... perform operations ...

    // Invalidate the session
    LAuth.SignOut;
    Writeln('Session terminated.');
  finally
    LAuth.Free;
  end;
end;

Refresh Token

Supabase JWT tokens expire after a default period (usually 1 hour). Use RefreshToken to obtain a new access token without re-entering credentials.

var
  LAuth: TSimpleSupabaseAuth;
begin
  LAuth := TSimpleSupabaseAuth.Create(
    'https://xyzproject.supabase.co',
    'eyJ_ANON_KEY...'
  );
  try
    LAuth.SignIn('user@email.com', 'password123');
    Writeln('Original token: ', Copy(LAuth.AccessToken, 1, 20), '...');

    // Manually refresh the token
    LAuth.RefreshToken;
    Writeln('Refreshed token: ', Copy(LAuth.AccessToken, 1, 20), '...');
  finally
    LAuth.Free;
  end;
end;

Auto-Refresh

AutoRefresh starts a background timer that automatically refreshes the token 30 seconds before it expires. This ensures uninterrupted access without manual management.

var
  LAuth: TSimpleSupabaseAuth;
begin
  LAuth := TSimpleSupabaseAuth.Create(
    'https://xyzproject.supabase.co',
    'eyJ_ANON_KEY...'
  );
  try
    LAuth.SignIn('user@email.com', 'password123');

    // Enable auto-refresh - token is refreshed 30s before expiry
    LAuth.AutoRefresh;
    Writeln('Auto-refresh enabled.');

    // The token is automatically renewed in the background
    // Use LAuth.AccessToken whenever you need the current token
  finally
    LAuth.Free;
  end;
end;
How it works: AutoRefresh reads the expires_in value from the authentication response and schedules a refresh 30 seconds before expiry. The timer runs in the background using TThread.CreateAnonymousThread.

Row Level Security (RLS)

When using the anon key with RLS enabled on your Supabase tables, requests are filtered based on the authenticated user's JWT token.

var
  LAuth: TSimpleSupabaseAuth;
  LQuery: iSimpleQuery;
  LDAO: iSimpleDAO<TTask>;
  LList: TObjectList<TTask>;
begin
  LAuth := TSimpleSupabaseAuth.Create(
    'https://xyzproject.supabase.co',
    'eyJ_ANON_KEY...'
  );
  try
    // Authenticate - the JWT contains the user_id
    LAuth.SignIn('user@email.com', 'password123');

    // Create query with anon key + JWT token
    LQuery := TSimpleQuerySupabase.New(
      'https://xyzproject.supabase.co',
      'eyJ_ANON_KEY...',
      LAuth.AccessToken
    );

    // RLS policy automatically filters data for this user
    LList := TObjectList<TTask>.Create;
    try
      LDAO := TSimpleDAO<TTask>.New(LQuery);
      LDAO.Find(LList);
      // LList contains only the authenticated user's tasks
    finally
      LList.Free;
    end;
  finally
    LAuth.Free;
  end;
end;
Supabase RLS example policy:
CREATE POLICY "Users can view own tasks" ON tasks FOR SELECT USING (auth.uid() = user_id);

With this policy, the Find operation automatically returns only the rows belonging to the authenticated user.

Realtime

TSimpleSupabaseRealtime monitors changes in Supabase tables and triggers callbacks for INSERT, UPDATE, and DELETE events. It works via polling, periodically querying the tables and detecting differences.

Setup

var
  LRealtime: TSimpleSupabaseRealtime;
begin
  LRealtime := TSimpleSupabaseRealtime.Create(
    'https://xyzproject.supabase.co',
    'eyJ_ANON_KEY...'
  );
  try
    // Configure, subscribe, and connect...
  finally
    LRealtime.Free;
  end;
end;

Global Callbacks

Global callbacks are triggered for events on any subscribed table.

LRealtime
  .OnInsert(
    procedure(const aEvent: TSupabaseRealtimeEvent)
    begin
      Writeln('[INSERT] Table: ', aEvent.Table);
      Writeln('  New data: ', aEvent.NewData.ToJSON);
    end
  )
  .OnUpdate(
    procedure(const aEvent: TSupabaseRealtimeEvent)
    begin
      Writeln('[UPDATE] Table: ', aEvent.Table);
      Writeln('  Old data: ', aEvent.OldData.ToJSON);
      Writeln('  New data: ', aEvent.NewData.ToJSON);
    end
  )
  .OnDelete(
    procedure(const aEvent: TSupabaseRealtimeEvent)
    begin
      Writeln('[DELETE] Table: ', aEvent.Table);
      Writeln('  Deleted data: ', aEvent.OldData.ToJSON);
    end
  );

Per-Table Callbacks

Per-table callbacks are triggered only for events on a specific table.

LRealtime
  .OnChange('PRODUCTS',
    procedure(const aEvent: TSupabaseRealtimeEvent)
    begin
      Writeln('[', aEvent.EventType, '] on PRODUCTS');
      if Assigned(aEvent.NewData) then
        Writeln('  Data: ', aEvent.NewData.ToJSON);
    end
  )
  .OnChange('ORDERS',
    procedure(const aEvent: TSupabaseRealtimeEvent)
    begin
      Writeln('[', aEvent.EventType, '] on ORDERS');
    end
  );

Subscribe / Unsubscribe

Subscribe to one or more tables using a fluent interface. You can unsubscribe from individual tables at any time.

// Subscribe to multiple tables (fluent)
LRealtime
  .Subscribe('PRODUCTS')
  .Subscribe('ORDERS')
  .Subscribe('CUSTOMERS')
  .Connect;

// Unsubscribe from a specific table
LRealtime.Unsubscribe('ORDERS');

// Subscribe to more tables after initial connect
LRealtime.Subscribe('INVOICES');

Connect / Disconnect

Connect starts the polling loop. Disconnect stops it.

// Start monitoring
LRealtime.Connect;

// ... application runs ...

// Stop monitoring
LRealtime.Disconnect;
Thread safety: The polling runs in a background thread. Callbacks are invoked in the context of that thread. Use TThread.Synchronize or TThread.Queue inside callbacks if you need to update UI components.

Poll Interval

The default poll interval is 1000ms (1 second). Adjust it based on your needs:

// Check every 5 seconds (lower server load)
LRealtime.PollInterval(5000);

// Check every 500ms (faster detection)
LRealtime.PollInterval(500);

With Authentication Token

var
  LAuth: TSimpleSupabaseAuth;
  LRealtime: TSimpleSupabaseRealtime;
begin
  LAuth := TSimpleSupabaseAuth.Create(SUPABASE_URL, SUPABASE_KEY);
  try
    LAuth.SignIn('user@email.com', 'password123');

    LRealtime := TSimpleSupabaseRealtime.Create(SUPABASE_URL, SUPABASE_KEY);
    try
      LRealtime
        .Token(LAuth.AccessToken)       // Authenticate realtime requests
        .PollInterval(2000)
        .OnInsert(
          procedure(const aEvent: TSupabaseRealtimeEvent)
          begin
            Writeln('New record in ', aEvent.Table);
          end
        )
        .Subscribe('TASKS')
        .Connect;
    finally
      LRealtime.Free;
    end;
  finally
    LAuth.Free;
  end;
end;

API Reference

TSimpleQuerySupabase

Query driver implementing iSimpleQuery for Supabase PostgREST.

Constructors

Method Parameters Description
New aBaseURL, aApiKey: string Creates driver with URL and API key
New aBaseURL, aApiKey, aToken: string Creates driver with URL, API key, and JWT token
New aBaseURL, aApiKey: string; aSQLType: TSQLType Creates driver with URL, API key, and SQL dialect

iSimpleQuery Methods

Method Return Description
SQL TStrings SQL text (parsed to determine HTTP method and endpoint)
Params TParams Query parameters (mapped to JSON body or query string)
ExecSQL iSimpleQuery Executes the SQL (sends HTTP request)
Open iSimpleQuery Executes SELECT and fills DataSet
Open(aSQL: string) iSimpleQuery Executes given SQL and fills DataSet
DataSet TDataSet Returns the result DataSet (TFDMemTable)
StartTransaction iSimpleQuery No-op (REST is stateless)
Commit iSimpleQuery No-op (REST is stateless)
Rollback iSimpleQuery No-op (REST is stateless)
&EndTransaction iSimpleQuery Delegates to Commit (no-op)
InTransaction Boolean Always returns False
SQLType TSQLType Returns the configured SQL dialect

Additional Methods

Method Parameters Return Description
Token aToken: string iSimpleQuery Sets or updates the JWT token for authenticated requests

TSimpleSupabaseAuth

Authentication client for Supabase GoTrue API.

Constructor

Method Parameters Description
Create aBaseURL, aApiKey: string Creates auth client with Supabase URL and API key

Authentication Methods

Method Parameters Description
SignIn aEmail, aPassword: string Authenticates with email and password
SignUp aEmail, aPassword: string Creates a new user account
SignOut - Invalidates the current session
RefreshToken - Obtains a new access token using the refresh token
AutoRefresh - Enables automatic token refresh 30 seconds before expiry

Properties

Property Type Description
AccessToken string Current JWT access token
RefreshTokenValue string Current refresh token (used by RefreshToken method)
ExpiresIn Integer Token expiry time in seconds

TSimpleSupabaseRealtime

Realtime change monitoring via polling.

Constructor

Method Parameters Description
Create aBaseURL, aApiKey: string Creates realtime client with Supabase URL and API key

Subscription Methods

Method Parameters Return Description
Subscribe aTable: string TSimpleSupabaseRealtime Subscribe to changes on a table
Unsubscribe aTable: string TSimpleSupabaseRealtime Unsubscribe from a table
Connect - TSimpleSupabaseRealtime Start the polling loop
Disconnect - TSimpleSupabaseRealtime Stop the polling loop

Callback Methods

Method Parameters Return Description
OnInsert aCallback: TProc<TSupabaseRealtimeEvent> TSimpleSupabaseRealtime Global callback for INSERT events
OnUpdate aCallback: TProc<TSupabaseRealtimeEvent> TSimpleSupabaseRealtime Global callback for UPDATE events
OnDelete aCallback: TProc<TSupabaseRealtimeEvent> TSimpleSupabaseRealtime Global callback for DELETE events
OnChange aTable: string; aCallback: TProc<TSupabaseRealtimeEvent> TSimpleSupabaseRealtime Per-table callback for any event

Configuration Methods

Method Parameters Return Description
Token aToken: string TSimpleSupabaseRealtime Sets JWT token for authenticated polling requests
PollInterval aMs: Integer TSimpleSupabaseRealtime Sets polling interval in milliseconds (default: 1000)

TSupabaseRealtimeEvent

Record containing realtime event data, passed to callbacks.

Field Type Description
Table string Name of the table where the event occurred
EventType string Event type: 'INSERT', 'UPDATE', or 'DELETE'
OldData TJSONObject Previous data (for UPDATE and DELETE events; nil for INSERT)
NewData TJSONObject New data (for INSERT and UPDATE events; nil for DELETE)
Timestamp TDateTime Timestamp when the event was detected

Complete Examples

Basic Console App

Complete example with setup and full CRUD operations.

program SupabaseBasicExample;

{$APPTYPE CONSOLE}

{$R *.res}

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

const
  SUPABASE_URL = 'https://xyzproject.supabase.co';
  SUPABASE_KEY = 'eyJ_YOUR_SERVICE_ROLE_KEY...';

type
  [Tabela('PRODUCTS')]
  TProduct = class
  private
    FId: Integer;
    FName: string;
    FPrice: Double;
  published
    [Campo('ID'), PK, AutoInc]
    property Id: Integer read FId write FId;
    [Campo('NAME'), NotNull]
    property Name: string read FName write FName;
    [Campo('PRICE')]
    property Price: Double read FPrice write FPrice;
  end;

var
  LQuery: iSimpleQuery;
  LDAO: iSimpleDAO<TProduct>;
  LProduct: TProduct;
  LList: TObjectList<TProduct>;

begin
  try
    LQuery := TSimpleQuerySupabase.New(SUPABASE_URL, SUPABASE_KEY);
    LDAO := TSimpleDAO<TProduct>.New(LQuery);

    // INSERT
    Writeln('--- INSERT ---');
    LProduct := TProduct.Create;
    try
      LProduct.Name := 'Wireless Mouse';
      LProduct.Price := 79.90;
      LDAO.Insert(LProduct);
      Writeln('Inserted: ', LProduct.Name);
    finally
      LProduct.Free;
    end;

    // FIND ALL
    Writeln('');
    Writeln('--- FIND ALL ---');
    LList := TObjectList<TProduct>.Create;
    try
      LDAO.Find(LList);
      for LProduct in LList do
        Writeln(SysUtils.Format('  ID: %d | Name: %s | Price: %.2f',
          [LProduct.Id, LProduct.Name, LProduct.Price]));
    finally
      LList.Free;
    end;

    // UPDATE
    Writeln('');
    Writeln('--- UPDATE ---');
    LProduct := TProduct.Create;
    try
      LProduct.Id := 1;
      LProduct.Name := 'Ergonomic Wireless Mouse';
      LProduct.Price := 129.90;
      LDAO.Update(LProduct);
      Writeln('Updated: ', LProduct.Name);
    finally
      LProduct.Free;
    end;

    // DELETE
    Writeln('');
    Writeln('--- DELETE ---');
    LProduct := TProduct.Create;
    try
      LProduct.Id := 1;
      LDAO.Delete(LProduct);
      Writeln('Deleted ID: ', LProduct.Id);
    finally
      LProduct.Free;
    end;

    Writeln('');
    Writeln('Done!');
  except
    on E: Exception do
      Writeln('Error: ', E.Message);
  end;

  Readln;
end.

App with Authentication

Authentication with email/password and CRUD with RLS.

program SupabaseAuthExample;

{$APPTYPE CONSOLE}

{$R *.res}

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

const
  SUPABASE_URL = 'https://xyzproject.supabase.co';
  SUPABASE_KEY = 'eyJ_YOUR_ANON_KEY...';

type
  [Tabela('TASKS')]
  TTask = class
  private
    FId: Integer;
    FTitle: string;
    FCompleted: Boolean;
    FUserId: string;
  published
    [Campo('ID'), PK, AutoInc]
    property Id: Integer read FId write FId;
    [Campo('TITLE'), NotNull]
    property Title: string read FTitle write FTitle;
    [Campo('COMPLETED')]
    property Completed: Boolean read FCompleted write FCompleted;
    [Campo('USER_ID')]
    property UserId: string read FUserId write FUserId;
  end;

var
  LAuth: TSimpleSupabaseAuth;
  LQuery: iSimpleQuery;
  LDAO: iSimpleDAO<TTask>;
  LTask: TTask;
  LList: TObjectList<TTask>;

begin
  LAuth := TSimpleSupabaseAuth.Create(SUPABASE_URL, SUPABASE_KEY);
  try
    try
      // Authenticate
      Writeln('Authenticating...');
      LAuth.SignIn('user@email.com', 'password123');
      Writeln('Authenticated! Token: ', Copy(LAuth.AccessToken, 1, 20), '...');
      LAuth.AutoRefresh;

      // Create query with auth token (RLS-aware)
      LQuery := TSimpleQuerySupabase.New(
        SUPABASE_URL, SUPABASE_KEY, LAuth.AccessToken
      );
      LDAO := TSimpleDAO<TTask>.New(LQuery);

      // INSERT a task
      Writeln('');
      Writeln('--- INSERT TASK ---');
      LTask := TTask.Create;
      try
        LTask.Title := 'Study SimpleORM';
        LTask.Completed := False;
        LDAO.Insert(LTask);
        Writeln('Task created: ', LTask.Title);
      finally
        LTask.Free;
      end;

      // FIND user's tasks (RLS filters automatically)
      Writeln('');
      Writeln('--- MY TASKS ---');
      LList := TObjectList<TTask>.Create;
      try
        LDAO.Find(LList);
        for LTask in LList do
          Writeln(SysUtils.Format('  [%s] %s',
            [IfThen(LTask.Completed, 'X', ' '), LTask.Title]));
        Writeln('Total: ', LList.Count, ' tasks');
      finally
        LList.Free;
      end;

      // Sign out
      LAuth.SignOut;
      Writeln('');
      Writeln('Session terminated.');

    except
      on E: Exception do
        Writeln('Error: ', E.Message);
    end;
  finally
    LAuth.Free;
  end;

  Readln;
end.

App with Realtime

Monitoring changes in real time with callbacks.

program SupabaseRealtimeExample;

{$APPTYPE CONSOLE}

{$R *.res}

uses
  System.SysUtils,
  System.JSON,
  SimpleSupabaseRealtime;

const
  SUPABASE_URL = 'https://xyzproject.supabase.co';
  SUPABASE_KEY = 'eyJ_YOUR_SERVICE_ROLE_KEY...';

var
  LRealtime: TSimpleSupabaseRealtime;

begin
  LRealtime := TSimpleSupabaseRealtime.Create(SUPABASE_URL, SUPABASE_KEY);
  try
    try
      Writeln('Starting realtime monitoring...');
      Writeln('Press ENTER to stop.');
      Writeln('');

      LRealtime
        .PollInterval(2000)  // Check every 2 seconds

        // Global callbacks
        .OnInsert(
          procedure(const aEvent: TSupabaseRealtimeEvent)
          begin
            Writeln(SysUtils.Format('[INSERT] %s - %s',
              [aEvent.Table, aEvent.NewData.ToJSON]));
          end
        )
        .OnUpdate(
          procedure(const aEvent: TSupabaseRealtimeEvent)
          begin
            Writeln(SysUtils.Format('[UPDATE] %s - %s',
              [aEvent.Table, aEvent.NewData.ToJSON]));
          end
        )
        .OnDelete(
          procedure(const aEvent: TSupabaseRealtimeEvent)
          begin
            Writeln(SysUtils.Format('[DELETE] %s - %s',
              [aEvent.Table, aEvent.OldData.ToJSON]));
          end
        )

        // Per-table callback
        .OnChange('PRODUCTS',
          procedure(const aEvent: TSupabaseRealtimeEvent)
          begin
            Writeln('*** Product changed! Type: ', aEvent.EventType);
          end
        )

        // Subscribe and connect
        .Subscribe('PRODUCTS')
        .Subscribe('ORDERS')
        .Connect;

      Writeln('Monitoring PRODUCTS and ORDERS...');
      Readln;  // Wait for user to press ENTER

      LRealtime.Disconnect;
      Writeln('Monitoring stopped.');

    except
      on E: Exception do
        Writeln('Error: ', E.Message);
    end;
  finally
    LRealtime.Free;
  end;
end.

Full App (Auth + CRUD + Realtime)

Complete application combining authentication, CRUD operations, and realtime monitoring.

program SupabaseFullExample;

{$APPTYPE CONSOLE}

{$R *.res}

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

const
  SUPABASE_URL = 'https://xyzproject.supabase.co';
  SUPABASE_KEY = 'eyJ_YOUR_ANON_KEY...';

type
  [Tabela('PRODUCTS')]
  TProduct = class
  private
    FId: Integer;
    FName: string;
    FPrice: Double;
  published
    [Campo('ID'), PK, AutoInc]
    property Id: Integer read FId write FId;
    [Campo('NAME'), NotNull]
    property Name: string read FName write FName;
    [Campo('PRICE')]
    property Price: Double read FPrice write FPrice;
  end;

var
  LAuth: TSimpleSupabaseAuth;
  LRealtime: TSimpleSupabaseRealtime;
  LQuery: iSimpleQuery;
  LDAO: iSimpleDAO<TProduct>;
  LProduct: TProduct;
  LList: TObjectList<TProduct>;

begin
  LAuth := TSimpleSupabaseAuth.Create(SUPABASE_URL, SUPABASE_KEY);
  try
    LRealtime := TSimpleSupabaseRealtime.Create(SUPABASE_URL, SUPABASE_KEY);
    try
      try
        // 1. AUTHENTICATE
        Writeln('=== AUTHENTICATION ===');
        LAuth.SignIn('admin@company.com', 'admin123');
        LAuth.AutoRefresh;
        Writeln('Authenticated as admin@company.com');
        Writeln('');

        // 2. START REALTIME MONITORING
        Writeln('=== REALTIME ===');
        LRealtime
          .Token(LAuth.AccessToken)
          .PollInterval(2000)
          .OnInsert(
            procedure(const aEvent: TSupabaseRealtimeEvent)
            begin
              Writeln('[REALTIME INSERT] ', aEvent.Table, ': ',
                aEvent.NewData.ToJSON);
            end
          )
          .OnUpdate(
            procedure(const aEvent: TSupabaseRealtimeEvent)
            begin
              Writeln('[REALTIME UPDATE] ', aEvent.Table, ': ',
                aEvent.NewData.ToJSON);
            end
          )
          .OnDelete(
            procedure(const aEvent: TSupabaseRealtimeEvent)
            begin
              Writeln('[REALTIME DELETE] ', aEvent.Table, ': ',
                aEvent.OldData.ToJSON);
            end
          )
          .Subscribe('PRODUCTS')
          .Connect;
        Writeln('Monitoring PRODUCTS table...');
        Writeln('');

        // 3. CRUD OPERATIONS
        LQuery := TSimpleQuerySupabase.New(
          SUPABASE_URL, SUPABASE_KEY, LAuth.AccessToken
        );
        LDAO := TSimpleDAO<TProduct>.New(LQuery);

        // Insert
        Writeln('=== INSERT ===');
        LProduct := TProduct.Create;
        try
          LProduct.Name := '4K Monitor';
          LProduct.Price := 2499.90;
          LDAO.Insert(LProduct);
          Writeln('Inserted: ', LProduct.Name);
        finally
          LProduct.Free;
        end;
        Writeln('');

        // Find All
        Writeln('=== FIND ALL ===');
        LList := TObjectList<TProduct>.Create;
        try
          LDAO.Find(LList);
          for LProduct in LList do
            Writeln(SysUtils.Format('  #%d %s - $%.2f',
              [LProduct.Id, LProduct.Name, LProduct.Price]));
          Writeln('Total: ', LList.Count, ' products');
        finally
          LList.Free;
        end;
        Writeln('');

        // Find with Pagination
        Writeln('=== PAGINATION (first 5) ===');
        LList := TObjectList<TProduct>.Create;
        try
          LDAO.SQL
            .Skip(0)
            .Take(5)
          .&End
          .Find(LList);
          for LProduct in LList do
            Writeln(SysUtils.Format('  #%d %s', [LProduct.Id, LProduct.Name]));
        finally
          LList.Free;
        end;
        Writeln('');

        // Update
        Writeln('=== UPDATE ===');
        LProduct := TProduct.Create;
        try
          LProduct.Id := 1;
          LProduct.Name := 'Ultrawide 4K Monitor';
          LProduct.Price := 3299.90;
          LDAO.Update(LProduct);
          Writeln('Updated ID 1: ', LProduct.Name);
        finally
          LProduct.Free;
        end;
        Writeln('');

        // Delete
        Writeln('=== DELETE ===');
        LProduct := TProduct.Create;
        try
          LProduct.Id := 1;
          LDAO.Delete(LProduct);
          Writeln('Deleted ID: 1');
        finally
          LProduct.Free;
        end;
        Writeln('');

        // Wait to see realtime events
        Writeln('Waiting 5 seconds for realtime events...');
        Sleep(5000);

        // 4. CLEANUP
        LRealtime.Disconnect;
        LAuth.SignOut;

        Writeln('');
        Writeln('All done! Session terminated.');

      except
        on E: Exception do
          Writeln('Error: ', E.Message);
      end;
    finally
      LRealtime.Free;
    end;
  finally
    LAuth.Free;
  end;

  Readln;
end.

Troubleshooting

401 Unauthorized

CauseSolution
Invalid or missing API key Verify the apikey in your Supabase Dashboard > Settings > API
Expired JWT token Use RefreshToken or enable AutoRefresh
Wrong key type Use service_role for server-side or anon + auth for client-side

404 Not Found

CauseSolution
Table does not exist Verify the table name in [Tabela('TABLE_NAME')] matches your Supabase table
Wrong project URL Check the URL format: https://xyzproject.supabase.co
Table not exposed via API In Supabase Dashboard, check that the table is in the public schema

400 Bad Request

CauseSolution
Column name mismatch Ensure [Campo('COLUMN')] matches the exact column name in Supabase (case-sensitive)
Data type mismatch Verify that Delphi property types match the PostgreSQL column types
Not-null constraint violation Provide values for all required columns

Transactions Not Working

Expected behavior: Supabase REST API is stateless. Transaction methods (StartTransaction, Commit, Rollback) are no-ops. InTransaction always returns False. If you need transaction support, use Supabase Edge Functions or direct database connections.

RLS Blocking All Requests

CauseSolution
Using anon key without authentication Authenticate with SignIn and pass the JWT token to the query driver
Missing RLS policy Create appropriate policies in Supabase Dashboard > Authentication > Policies
Testing: need to bypass RLS Use service_role key (server-side only)

Realtime Not Detecting Changes

CauseSolution
Not calling Connect Call .Connect after configuring subscriptions
Poll interval too high Reduce PollInterval for faster detection
Table not subscribed Verify .Subscribe('TABLE_NAME') was called with the correct table name