A PostgREST and Supabase-compatible REST API powered by Elixir and Ecto.
Define Ecto schemas, get a full REST API that works with existing PostgREST clients like @supabase/postgrest-js. Queries are parsed into Ecto expressions — no raw SQL, no runtime code generation.
Why PgRest?
PostgREST and Supabase are fantastic battle-tested solutions, but they run as separate services. PgRest brings the same query language and client compatibility to Elixir — no sidecar process, no migration from Supabase, just add it to your existing Phoenix or Plug app.
- Drop-in client compatibility — Works with
@supabase/postgrest-jsand any PostgREST client. Same URL syntax, same operators, same response format. - Ecto-native — Schemas define your API resources. Filters become
whereclauses, embeds becomepreload, inner joins becomejoin. All type-safe through Ecto's query builder. - Scoped by default —
scope/2callbacks for tenant isolation and soft deletes. Customhandle_param/4for search, geo-queries, or any app-specific filter. - No extra infrastructure — It's a Plug. Runs in your existing BEAM process, uses your existing Ecto repo. No separate service to deploy or manage.
Demo App
See demo/ for an interactive query explorer built with Phoenix LiveView. It runs 18 example queries live against a seeded project/task database using @supabase/postgrest-js, showing the client code, generated SQL, JSON response, and timing for each.
The repo ships a Hivemind wrapper that serves the demo at
localhost:4042:
bin/pgrest start
bin/pgrest stop
bin/pgrest console
Requires a local Postgres reachable at localhost:5432 (mix ecto.setup
from demo/ creates the database). Or run the demo directly:
cd demo && mix setup && mix phx.server
Prerequisites
- Elixir 1.19+
- PostgreSQL with an Ecto repository
- A Plug-based application (Phoenix, Bandit, etc.)
Installation
Add pgrest to your dependencies in mix.exs:
def deps do
[
{:pgrest, "~> 0.1.0"}
]
endThen fetch dependencies:
mix deps.get
Quick Start
1. Define a Resource
A resource is an Ecto schema that uses PgRest.Resource:
defmodule MyApp.API.Tasks do
use Ecto.Schema
use PgRest.Resource
import Ecto.Query
schema "tasks" do
field :title, :string
field :status, :string
field :priority, :string
belongs_to :project, MyApp.API.Projects
timestamps()
end
# Optional: always-applied query scope (tenant isolation, soft deletes)
@impl PgRest.Resource
def scope(query, _context), do: where(query, [t], is_nil(t.deleted_at))
# Optional: custom URL params (?search=foo)
@impl PgRest.Resource
def handle_param("search", value, query, _context) do
where(query, [t], ilike(t.title, ^"%#{value}%"))
end
def handle_param(_, _, query, _), do: query
end2. Start the Registry
Add PgRest.Registry to your supervision tree. With :otp_app, it auto-discovers every module that has use PgRest.Resource:
children = [
MyApp.Repo,
{PgRest.Registry, otp_app: :my_app},
]Or pass an explicit list:
{PgRest.Registry, modules: [MyApp.API.Tasks, MyApp.API.Projects]}3. Mount the Plug
In your router, forward API requests to PgRest.Plug:
# Phoenix router
forward "/api", PgRest.Plug, repo: MyApp.Repo
# Plug.Router
forward "/api", to: PgRest.Plug, init_opts: [repo: MyApp.Repo]4. Query with Any PostgREST Client
import { PostgrestClient } from '@supabase/postgrest-js'
const api = new PostgrestClient('http://localhost:4000/api')
// Filter, order, paginate
const { data } = await api
.from('tasks')
.select('title,status,project(name)')
.eq('status', 'pending')
.order('due_date', { ascending: true })
.limit(10)Or with plain HTTP:
GET /api/tasks?status=eq.pending&select=title,status,project(name)&order=due_date.asc&limit=10How It Works
Every request flows through the same pipeline:
Base query (from schema)
|
scope/2 (tenant isolation, soft deletes — always runs)
|
URL filters (?status=eq.active&priority=eq.high)
|
handle_param/4 (custom params: ?search=, ?within_miles=)
|
select, order, limit, offset → Repo.all()URL parameters are parsed into an AST, then applied as Ecto query expressions. The parser handles the full PostgREST operator set: comparison (eq, neq, gt, gte, lt, lte), pattern matching (like, ilike, match), containment (in, cs, cd, ov), full-text search (fts, plfts, phfts, wfts), logical grouping (and, or, not), and relationship embedding via select.
Using the Query Builder Directly
The parser and filter modules work independently of HTTP. You can use them to build Ecto queries from PostgREST-style parameter maps anywhere — LiveView, GenServers, background jobs:
params = %{"status" => "eq.pending", "priority" => "eq.high", "order" => "due_date.asc", "limit" => "10"}
{:ok, parsed} = PgRest.Parser.parse(params, allowed_fields: MyApp.Tasks.__schema__(:fields))
{:ok, filters} = PgRest.TypeCaster.cast_filters(parsed.filters, MyApp.Tasks)
query =
MyApp.Tasks
|> PgRest.Filter.apply_all(filters)
|> PgRest.Order.apply_order(parsed.order)
|> Ecto.Query.limit(^parsed.limit)
Repo.all(query)The result is a composable %Ecto.Query{} — pipe it further, add your own clauses, or pass it to streams.
Plug Options
forward "/api", PgRest.Plug,
repo: MyApp.Repo, # Required — Ecto repo module
json: Jason, # JSON encoder (default: Jason)
max_limit: 1000, # Max rows per request (default: nil = no limit)
context_builder: &build/2 # Custom context from conn → mapResource Callbacks
| Callback | Purpose | Default |
|---|---|---|
scope/2 | Always-applied query filter (tenancy, soft deletes) | No-op |
handle_param/4 | Custom URL parameter handling | No-op |
changeset/3 | Create/update changeset | Casts all schema fields |
after_load/2 | Post-processing after DB load | Identity |
Supported Operations
| HTTP Method | PostgREST Equivalent | Description |
|---|---|---|
GET /resource | GET /table | List with filters, ordering, pagination |
GET /resource?select=a,b,rel(c) | Resource embedding | Preload associations |
GET /resource?rel.field=eq.x | Embedded filters | Filter on associated records |
GET /resource?select=*,rel!inner(*) | Inner join | Only return rows with matching associations |
POST /resource | POST /table | Create (single or bulk) |
PATCH /resource?filters | PATCH /table?filters | Update matching rows |
DELETE /resource?filters | DELETE /table?filters | Delete matching rows |
Telemetry
PgRest emits :telemetry events for all operations:
[:pg_rest, :query, :start] — %{resource: module, operation: atom, repo: module}
[:pg_rest, :query, :stop] — includes duration
[:pg_rest, :query, :exception] — on failureTesting
mix test
License
MIT