Write a description that contains decisions
The highest-leverage thing you do in this workflow is decide things in the description. Compare "I need a backend for my task app" with:
A task manager. Users sign up with email.
Users create projects. Each project has many tasks.
A task has a title, a description, a status of
todo / in_progress / done, and a due date.
Users can only see their own projects and tasks.That names the entities, the relationships, the field types, the permitted status values, and the access rule. You do not need to know that the last line becomes a row-level security policy — but because you wrote it, it becomes one. What you leave out gets a default; what you state gets built as stated.
You can also skip the prose and hand your agent SQL. `apply_migration` takes ordinary PostgreSQL DDL — `CREATE TABLE`, `ALTER TABLE ADD COLUMN` / `RENAME COLUMN` / `ADD CONSTRAINT` / `ALTER COLUMN SET`|`DROP NOT NULL`, `CREATE [UNIQUE] INDEX` — multiple statements, semicolon-separated. Your DDL is applied as written: declared `NOT NULL`, `DEFAULT`, and nullability are honoured exactly.
What actually runs
Input
Your description, or DDL
Reaches the platform through `backend_chat` or `apply_migration` over MCP.
Plan
Intent becomes typed actions
A planner derives entities, relations, columns and policies, then compiles them into a sequence of governed actions. The model never writes SQL directly into your database — it produces a plan the executor runs.
Apply
One kernel, all-or-nothing
Every mutation goes through `executeAction`, whether it came from you, your agent, or an automated repair. Schema-mutating actions — creating a table, adding, dropping or renaming a column, adding a constraint or index, enabling auth, vector search or teams — snapshot the schema before they run, so the change can be rolled back to a saved version. An operation the parser cannot map is refused with the route forward, never silently dropped.
Verify
The backend is exercised, not assumed
Checks run against the live runtime over real HTTP and return their evidence with the result.
Output
A schema, endpoints, auth, and policies
Tables in your project's own PostgreSQL schema, REST endpoints resolved from the catalog, project-scoped JWT auth, and row-level security enforced in the database.
Three columns are provisioned for you on every table Backenly creates, and their naming is a known wart rather than a rule you can infer: `id` (uuid primary key), `"createdAt"` and `"updatedAt"` (camelCase, timestamptz), and `"deleted_at"` (snake_case, soft delete). Order by `createdAt`, filter soft deletes on `deleted_at`, quote the camelCase identifiers in raw SQL — unquoted they fold to lowercase and will not resolve. Declaring any of the three yourself is skipped and reported. Columns you declare exist exactly as you wrote them.
Read the verification, not the success message
The common failure mode of generated infrastructure is the confident lie: the system reports success and the backend does not work. After a build, Backenly runs checks against the live runtime and returns each one with its evidence, into your agent's reply and into the project's activity journal.
| Check | What it asserts |
|---|---|
| CRUD lifecycle | create → read → update → delete, then a post-delete read returns nothing |
| Auth flow | signup → token issuance → JWT verification → protected resource access |
| RLS two-user isolation | user A inserts a row; user B is signed in and sees none of it |
| Live HTTP endpoints | signup → JWT → GET list → POST create, over real HTTP rather than in-process |
| Trigger / function execution | a database event fires and the function's last-run timestamp advances |
| Webhook HMAC | an invalid signature is rejected with 401 |
The isolation check is the one to read on any backend holding private data. It is behavioural: a second user is created and signed in, and the assertion is that they receive zero rows — not that the policy text looks right. A wrong policy does not fail loudly on its own; it quietly shows one user another user's data.
Checks that do not apply are reported as skipped, and a skip is never counted as a pass. If verification cannot run at all, the reply says so rather than implying it passed.
Connect a frontend
What you have now is a standard REST API. The SDK is one install and a factory call:
import { createClient } from '@backenly/sdk'
const backend = createClient({ projectId, apiKey })
await backend.auth.signUp({ email, password })
await backend.auth.signIn({ email, password })
const open = await backend.tasks.list({
where: { status: 'todo' },
orderBy: 'due_date',
order: 'asc',
limit: 25,
})
const task = await backend.tasks.create({ title: 'Ship onboarding', status: 'todo' })
await backend.tasks.update(task.id, { status: 'in_progress' })
await backend.tasks.delete(task.id)
const remaining = await backend.tasks.count({ status: 'todo' })Because access control lives in the database, that `list` call is already scoped to the signed-in user. You never write "filter tasks by current user" logic, which means you cannot forget it on one screen. The full contract — both query grammars, the two authentication headers, and where the SDK is the wrong tool — is in the data API guide.
Changing a schema that has rows in it
Creating tables on an empty project is the easy half. The test is week six, when the change touches live data. Additive changes run without ceremony:
Add a comments table. Each task can have many comments.
A comment has a body and belongs to a user.Destructive ones do not. Ask to drop a table and the operation stops before anything runs. Over MCP it never even reaches a tool — destructive operations are absent from the advertised surface — so it is parked in the Review Queue with an approval id, and the dashboard card names the target, the live row count where it can be read, and whether the data is recoverable. A confirmation replays the exact stored call rather than re-deriving it from your words, and it expires: fifteen minutes for a named confirmation, two minutes for a bare "yes", so a forgotten approval cannot fire the next morning.
Backenly does
- Plans the change against the live schema and applies it through one governed kernel.
- Captures a pre-migration snapshot before creating a table.
- Runs the verification checks and reports each result with its evidence.
- Refuses to execute a destructive operation without an explicit human confirmation.
You own
- Read the plan before confirming it — that is the moment to add the field you forgot.
- Read the isolation check on anything holding private data.
- Decide whether a destructive change should happen at all.
- Own your frontend, your product decisions, and your data model's correctness.
Where this is the wrong tool
A described backend fits when your product has a recognisable shape: users, content, relations, permissions, files, scheduled work. It is the wrong tool when the backend is the product — a database engine, a system with microsecond latency budgets, or one whose regulation requires owning every line.
There is also a hard boundary worth knowing early: Backenly exposes no SQL functions, so there is no `rpc()` surface. Custom logic runs as a function attached to an event, a schedule, or an HTTP endpoint instead. If your business logic is hundreds of interlocking rules, you want engineers writing it, with the platform handling the infrastructure underneath them.
In short
Describe the backend with the decisions in it, read the plan, let the governed executor apply it, and judge the result by the verification evidence rather than the success message. Then keep iterating in plain English — additive changes run, destructive ones stop and ask, and every governed change is recorded.
Adarsh Chiriyamkandath Jose
Founder, Backenly · Updated August 29, 2026
Try it on a live project
One free project, no credit card. Connect your agent over MCP and read the verification evidence yourself.
Create a project