Skip to contentNewLaunching soon on Product Hunt (opens in a new tab)

The API is your schema

Every table is served by PostgREST, straight from the PostgreSQL catalog. Nothing to regenerate: a table created a second ago is queryable now.

In practice

Every endpoint is testable from the dashboard the moment it exists. The APIs view lists each route per table, and an inline tester sends real requests to your live backend: type a JSON body into POST /auth/signup, send it, read the actual HTTP response, then open the users table and see the row it created. From code, the SDK mirrors the API: backend.tasks.list({ where: { status: 'todo' }, orderBy: 'due_date', limit: 25 }) for filtered queries, backend.projects.list({ include: ['tasks'] }) to resolve relations server-side in one request, and backend.tasks.count(...) when you need numbers without rows. Because row-level security lives in the database, all of these return only what the calling user may see. There is no authorization check to forget in client code.

What it is

Every table in your project is served by PostgREST, the same engine Supabase runs, reading directly from the PostgreSQL catalog. There is no separate API registry to generate, deploy or keep in sync. The API is the schema, so a table created a second ago is queryable immediately, and a column renamed a second ago is reflected without a rebuild. You get filtering, ordering, pagination, full-text search and embedded resources, across two surfaces that share one engine and one authorization path.

How it works

Backenly exposes two surfaces over the same engine. /api/v1/{projectId}/db/{table} is Backenly's stable REST contract: list, create, get, update and delete with typed responses. /api/v2/{projectId}/{table} passes PostgREST's native grammar through untouched: ?price=gte.100, ?or=(a.eq.1,b.eq.2), ?order=created_at.desc, and embedded resources, where ?select=*,author(*) returns a post and its author in one round trip. If you already know Supabase or PostgREST, you know this API.

Why it matters

Generated API layers drift. Once the code that serves your data is separate from the schema that defines it, the two can disagree, and that gap is where stale endpoints, forgotten authorization checks and "the table exists but the API does not" bugs live. Reading from the catalog removes the gap by construction. Authorization is enforced by PostgreSQL grants and row-level security rather than by application code, so a request for another tenant's rows fails on a missing database privilege instead of on a check somebody had to remember to write.

What you get

Two surfaces, one engine

/api/v1 is Backenly's stable REST contract, with typed responses. /api/v2 is PostgREST's native grammar, passed through untouched. Both read the same catalog and share one authorization path.

Embedded resources in one round trip

?select=*,author(*) returns a post and its author together. The relationship is resolved from the foreign key in the catalog, so there is nothing to configure and no N+1 to hand-optimize.

Authorization in the database

Grants and row-level security decide what a request can reach. A read for another tenant's rows, for the auth table, or through an embedded resource is refused by Postgres itself.

Typed clients and a drift gate

Generate an OpenAPI spec and a typed client from the CLI. backenly diff exits non-zero when committed types drift from the live schema, so contract drift fails CI instead of production.

Common questions

It is PostgREST, the same open-source engine Supabase runs, reading directly from your PostgreSQL catalog. On query capability Backenly is at parity with Supabase: same engine, same grammar, embedded resources included. If you know one, you know the other.

Try it on a live project

One free project, no card. Connect your agent over MCP and read the verification evidence yourself.