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 |
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
Project URL:
https://xyzproject.supabase.coanon (public) key: for client-side access with RLS
service_role key: for server-side access bypassing RLS
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;
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;
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;
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;
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;
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
| Cause | Solution |
|---|---|
| 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
| Cause | Solution |
|---|---|
| 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
| Cause | Solution |
|---|---|
| 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
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
| Cause | Solution |
|---|---|
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
| Cause | Solution |
|---|---|
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 |