sereny / postgrest
A PostgREST connection driver that lets Laravel Eloquent models talk to a PostgREST server over HTTP.
Requires
- php: ^8.3
- guzzlehttp/guzzle: ^7.8
- illuminate/database: ^12.0|^13.0
- illuminate/support: ^12.0|^13.0
- spatie/laravel-package-tools: ^1.16
Requires (Dev)
- laravel/pint: ^1.18
- orchestra/testbench: ^10.0|^11.0
- pestphp/pest: ^3.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
A PostgREST connection driver for Laravel Eloquent. It lets your Eloquent models perform queries against a PostgREST server over HTTP, without a direct PostgreSQL/PDO connection from your app.
Under the hood it translates the Laravel Query Builder into PostgREST HTTP requests:
User::where('age', '>', 18)->orderBy('name')->limit(10)->get(); // GET /users?age=gt.18&order=name.asc&limit=10
Requirements
- PHP 8.3+
- Laravel 12 or 13 (via
illuminate/database&illuminate/support) - A running PostgREST server
Installation
composer require sereny/postgrest
The service provider is auto-registered via package discovery.
Configuration
1. Add the connection to config/database.php
'connections' => [ // ... 'postgrest' => [ 'driver' => 'postgrest', 'url' => env('PGREST_BASE_URI', 'http://localhost:3000'), 'schema' => env('PGREST_SCHEMA', 'public'), 'prefix' => env('PGREST_PREFIX', ''), 'token' => env('PGREST_AUTH_TOKEN'), 'headers' => [], ], ],
2. Publish the package config (optional)
php artisan vendor:publish --tag=postgrest-config
The published file (config/postgrest.php) holds client defaults
(base_uri, schema, prefix, token, headers, timeouts). Values in the
database.php connection entry override these defaults.
3. Environment variables
PGREST_BASE_URI=http://localhost:3000 PGREST_SCHEMA=public PGREST_AUTH_TOKEN=
Usage
Point your models at the PostgREST connection. The simplest way is to make it the application's default connection:
DB_CONNECTION=postgrest
Or set it per model:
use Illuminate\Database\Eloquent\Model; class User extends Model { protected $table = 'users'; }
// Create $user = User::create(['name' => 'John', 'age' => 30]); // Read $user = User::find(1); $users = User::where('age', '>=', 18)->orderBy('name')->limit(10)->get(); // Update $user->update(['age' => 31]); // Delete $user->delete(); // Count & paginate User::where('active', true)->count(); User::where('active', true)->paginate(15);
The raw Query Builder is also available through the connection:
DB::connection('postgrest') ->table('users') ->whereIn('role', ['admin', 'editor']) ->get();
Stored procedures (aggregates, custom logic) are available through rpc():
DB::connection('postgrest')->rpc('total_sales', ['year' => 2024]);
Many-to-many relationships use PostgREST resource embedding (instead of SQL
joins). Extend the bundled model (or override newBelongsToMany() yourself):
use Sereny\PostgREST\Eloquent\Model; class User extends Model { protected $connection = 'postgrest'; public function roles() { return $this->belongsToMany(Role::class, 'role_user', 'user_id', 'role_id'); } }
$roles = User::find(1)->roles; // lazy User::with('roles')->get(); // eager, one embedded request $roles = User::find(1)->roles()->withPivot('active')->get(); $user->roles()->attach($roleId); // writes to the pivot table $user->roles()->detach($roleId);
The pivot table must have foreign keys to both related tables (a requirement
of PostgREST resource embedding). attach/detach/sync write to the pivot
table over the same connection.
Three-level relationships (hasManyThrough) use nested embedding:
use Sereny\PostgREST\Eloquent\Model; class Country extends Model { protected $connection = 'postgrest'; public function posts() { return $this->hasManyThrough(Post::class, User::class); } }
Country::find(1)->posts; // GET /countries?select=*,users(country_id,posts(*)) Country::with('posts')->get(); // eager, one nested embedded request
Feature support
| Feature | Supported |
|---|---|
select / where (=, >, >=, <, <=, !=) |
✅ |
whereIn / whereNotIn |
✅ |
whereNull / whereNotNull |
✅ |
whereBetween |
✅ |
orWhere / nested where() groups |
✅ |
like / ilike (% is translated to *) |
✅ |
orderBy (multiple, asc/desc) |
✅ |
limit / offset / simplePaginate |
✅ |
count() (via Prefer: count=exact) |
✅ |
exists() |
✅ |
insert / insertGetId / update / delete |
✅ |
paginate() (count + slice) |
✅ |
| Soft deletes | ✅ |
hasOne / hasMany / belongsTo / morph relations |
✅ |
belongsToMany (via resource embedding) |
✅ |
hasManyThrough (via nested embedding) |
✅ |
sum / avg / min / max |
⚠️ via rpc() to a database function (or a view) |
groupBy / having / distinct |
❌ (not exposed by PostgREST — use a view) |
increment / decrement / raw expressions |
❌ (not exposed by PostgREST — use computed fields or RPC) |
| Transactions | ❌ (PostgREST runs one transaction per request; no multi-request transactions) |
| Schema builder / migrations | ❌ (run DDL directly on PostgreSQL) |
rpc() to call stored procedures |
✅ |
For anything marked ❌ or ⚠️, the idiomatic PostgREST approach is a database view, a function exposed as a route, or resource embedding for related resources.
Testing
composer test
License
MIT