Skip to main content

PostgREST store

The PostgREST store adapter connects to PostgreSQL via PostgREST or Supabase, for production-ready SQL storage.

Configuration​

Mission file​

Declare the store in your mission:

store items: postgrest("items")
store users: postgrest("users")

The string is the table name. A postgrest store needs connection options that aren't expressed in the DSL, so you wire them up programmatically (see below).

Programmatic setup​

There's no store-config CLI flag. Build a configured PostgREST adapter with createStore, then pass it in under the store's name. The adapter you supply overrides whatever the mission declared for that name:

import { createStore, fromFile } from 'reqon-dsl';

const items = createStore({
type: 'postgrest',
name: 'items', // table name
postgrest: {
url: 'https://your-project.supabase.co/rest/v1',
apiKey: process.env.SUPABASE_ANON_KEY!,
},
});

await fromFile('mission.reqon', {
// keyed by the store name from the mission (`store items: ...`)
stores: { items },
});

Options​

createStore's postgrest options map to the adapter:

OptionRequiredDefaultDescription
urlYes—Base URL of the PostgREST API (e.g. https://xxx.supabase.co/rest/v1)
apiKeyYes—API key, sent as both the apikey header and a bearer token
primaryKeyNoidPrimary-key column used to look up and upsert records
schemaNo—Postgres schema, sent as Accept-Profile/Content-Profile (e.g. public)
timeoutMsNo30000Per-request timeout; the request is aborted once it elapses
allowFullTableClearNofalseOpt-in guard for clear(), which issues a full-table delete

The table name comes from name (set from the store declaration), not from these options.

Supabase setup​

1. Create a Supabase project​

  1. Go to supabase.com
  2. Create a new project
  3. Note your project URL and anon key

2. Create tables​

CREATE TABLE items (
id TEXT PRIMARY KEY,
data JSONB NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE TABLE users (
id TEXT PRIMARY KEY,
email TEXT UNIQUE NOT NULL,
name TEXT,
metadata JSONB,
created_at TIMESTAMPTZ DEFAULT NOW()
);

3. Configure the adapter​

const items = createStore({
type: 'postgrest',
name: 'items',
postgrest: {
url: 'https://abc123.supabase.co/rest/v1',
apiKey: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
},
});

Self-hosted PostgREST​

Docker setup​

# docker-compose.yml
version: '3'
services:
db:
image: postgres:15
environment:
POSTGRES_PASSWORD: secret
volumes:
- pgdata:/var/lib/postgresql/data

postgrest:
image: postgrest/postgrest
environment:
PGRST_DB_URI: postgres://postgres:secret@db:5432/postgres
PGRST_DB_ANON_ROLE: anon
ports:
- "3000:3000"

volumes:
pgdata:

Configuration​

const items = createStore({
type: 'postgrest',
name: 'items',
postgrest: {
url: 'http://localhost:3000',
apiKey: 'your-jwt-or-role-key',
},
});

apiKey is always required by the adapter. For a self-hosted instance, pass the key your PostgREST is configured to accept.

Operations​

Write​

// Insert (upsert on primary key)
store response -> items { key: .id }

// Upsert
store response -> items { key: .id, upsert: true }

// Partial update (deep merge, same as upsert)
store response -> items { key: .id, partial: true }

set uses PostgREST's resolution=merge-duplicates, so a plain write also upserts on the primary key.

Read​

for item in items { }
for item in items where .status == "active" { }

Query mapping​

Where clauses are equality-only. Each field becomes a PostgREST equality filter:

ReqonPostgREST
.field == "value"?field=eq.value
.field == null?field=is.null

Other operators (not-equal, greater-than, less-than, and so on) aren't translated. Filter on what you can express as equality, then narrow further in your action logic.

Authentication​

The apiKey is sent as both the apikey header and an Authorization: Bearer token. For Supabase, use the anon key for row-level-security-scoped access or the service-role key for full access:

// Anon key — subject to RLS policies
const items = createStore({
type: 'postgrest',
name: 'items',
postgrest: { url: 'https://abc.supabase.co/rest/v1', apiKey: 'anon-key' },
});

// Service-role key — full access
const itemsAdmin = createStore({
type: 'postgrest',
name: 'items',
postgrest: { url: 'https://abc.supabase.co/rest/v1', apiKey: 'service-role-key' },
});

Clearing a table​

clear() issues a full-table delete and is disabled by default. Opt in explicitly:

const items = createStore({
type: 'postgrest',
name: 'items',
postgrest: {
url: 'https://abc.supabase.co/rest/v1',
apiKey: 'service-role-key',
allowFullTableClear: true,
},
});

Best practices​

Table design​

CREATE TABLE items (
-- Use text ID for compatibility
id TEXT PRIMARY KEY,

-- Store full record as JSONB
data JSONB NOT NULL,

-- Add common query fields
status TEXT,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);

-- Index common queries
CREATE INDEX items_status_idx ON items(status);
CREATE INDEX items_created_idx ON items(created_at);

Upsert with timestamps​

-- Add a trigger for updated_at
CREATE OR REPLACE FUNCTION update_updated_at()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER items_updated
BEFORE UPDATE ON items
FOR EACH ROW
EXECUTE FUNCTION update_updated_at();

Troubleshooting​

"Relation does not exist"​

The table hasn't been created:

CREATE TABLE your_table (...);

"Permission denied"​

Check your RLS policies:

CREATE POLICY "Allow all" ON items FOR ALL USING (true);

Connection issues​

Verify the URL and credentials:

curl -H "apikey: your-key" https://abc.supabase.co/rest/v1/items