
backenly/backenly
Developer toolsAutonomous backend platform. Your coding agent builds it over MCP, Backenly keeps it running: Postgres, REST APIs, auth, storage, realtime, and functions, with every change governed, verified, and...
Overview
The autonomous backend platform for agentic coding. PostgreSQL, REST APIs, auth, storage, realtime, and functions, driven by your coding agent over MCP, with every change planned, verified, and reversible. https://github.com/user-attachments/assets/2b215d72-4b1b-4a4c-8290-a94553be192d ⭐ to follow releases and help other builders find Backenly. Most backend platforms hand you primitives and leave you owning schema design, API wiring, RLS policies, monitoring, and recovery. Newer agent-native backends hand an agent raw SQL and no safety net. Backenly does neither. You describe the product you're building through the coding agent you already use (Claude Code, Cursor, Codex) over an MCP server. Backenly derives the data model, generates the endpoints, writes the policies, applies the change, and then . A continuous autonomy loop keeps watching after that, repairing drift, missing indexes, broken triggers, and RLS gaps on its own.
README
⭐ Star the repo to follow releases and help other builders find Backenly.
What this is
Most backend platforms hand you primitives and leave you owning schema design, API wiring, RLS policies, monitoring, and recovery. Newer agent-native backends hand an agent raw SQL and no safety net.
Backenly does neither. You describe the product you’re building through the coding agent you already use (Claude Code, Cursor, Codex) over an MCP server. Backenly derives the data model, generates the endpoints, writes the policies, applies the change, and then verifies it against the live runtime. A continuous autonomy loop keeps watching after that, repairing drift, missing indexes, broken triggers, and RLS gaps on its own.
The distinction that matters: Backenly does not just generate backend
resources, it manages backend change safely. Every mutation, whether it comes
from an agent, from the dashboard, or from an automated repair, goes through one
typed action kernel with dry-run, audit, and rollback. There is deliberately no
raw-SQL path for mutating structure. Reads are standard SQL, and your data is
never locked in: direct Postgres connection strings and full pg_dump exports
are one command away.
How it works
- Intent-first. An LLM planner derives entities, relations, and actions from natural language. No table designer, no hand-written migrations.
- One governed kernel. Every mutation flows through
executeAction, so the agent’s model of the database and the database itself cannot silently diverge. - Closed-loop autonomy. The loop heals the reversible safe band by itself. Anything risky, such as auth, external credentials, or destructive and irreversible changes, always waits for a human.
- Multi-tenant by construction. Each project gets its own PostgreSQL schema
(
workspace_{projectId}). Isolation is enforced by Postgres grants and RLS, never by application-level string filtering. - PostgREST data plane. Tables are served through PostgREST, so the query grammar you already know works unchanged.
Core capabilities
| Capability | What you get |
|---|---|
| Database | PostgreSQL with a schema per project, served through PostgREST |
| Auth & Users | Email/password and social sign-in, JWT sessions, RLS-forced user tables |
| Storage | Public and private buckets with per-file access control |
| Realtime | Shared LISTEN/NOTIFY hub for table change subscriptions |
| Functions | Serverless route modules, validated before they ship and self-healed if they break |
| Integrations | One ctx.integrations..request() surface for third-party APIs |
| Autonomy | MAPE-K loop that observes, detects, proposes, applies, and verifies repairs |
| Monitoring | Request logs with stability and reliability scoring |
| Branches | Preview branches with their own sequences, plus diff and merge |
| Deploy | Governed rollout with restore points and an audit ledger |
What your agent can actually do
The MCP server advertises 20 tools. The catalog is capped deliberately, because tool-selection accuracy degrades as it grows, while the dispatcher stays wider so an agent pinned to an older manifest never gets a 404.
| Group | Tools |
|---|---|
| Understand | read_backend_state · get_table_schema · run_query · fetch_docs |
| Build | apply_migration · enable_auth · set_rls · create_bucket · generate_function · enable_realtime |
| Data | db_insert · db_update · db_delete |
| Operate | branch · create_api_key · set_env_var · get_database_credentials · check_approval · generate_types |
| Natural language | backend_chat, the fall-through for anything not named above |
Agents can also browse live project state as MCP resources (backenly://state,
tables, apis, buckets, triggers) instead of spending a tool call to ask.
Quick start
Cloud
Create a project at backenly.com, then point your agent at it. Nothing to install or operate.
Self-hosted
One deployment is one project.
git clone https://github.com/backenly/backenly.git
cd backenly
npm install
npm run selfhost
That is the whole install. It generates the secrets, starts the Compose stack, creates the tables, runs the superuser steps, issues the PostgREST credential and reconciles until the deployment reports ready. Then:
npm run dev # dashboard :3000 · runtime :3001
Then claim the deployment. The installer prints a claim link carrying a setup token. Open it and create your account: the first account to present the token becomes the administrator and takes ownership of this deployment’s single project in the same step. There is no second command to run. The signup page also has a Setup token field while the deployment is unclaimed, if you would rather paste the token than follow the link.
The token is in .env as BACKENLY_SETUP_TOKEN. It gates the claim because a
deployment is often reachable before its operator gets to it — an open port on
a VPS, a preview environment, a colleague pointed at the wrong host — and
without it the single administrator slot would go to whoever loaded the page
first. Once the deployment is claimed the token stops working, whatever it is
set to.
Claiming the deployment needs no email. Two things do: password-reset
codes, and the code a new account must enter before it exists if you open
registration with BACKENLY_ALLOW_PUBLIC_SIGNUP=true. Set any SMTP provider in
.env:
SMTP_HOST=smtp.resend.com # or SES, Mailgun, SendGrid, Gmail
SMTP_PORT=587 # STARTTLS; 465 is rewritten to 587
SMTP_USER=resend
SMTP_PASS=...
SMTP_FROM=Backenly # a domain your provider has verified
Without it, the reset page says email is unavailable rather than pretending a code was sent, and later signups are refused rather than created unverified. To reset a password without email, run this on the server:
npm run auth:reset-password -- --email [email protected] # prompts
npm run auth:reset-password -- --email [email protected] --generate # prints one
It ends every session for that account. Access to the machine is the authority, as it is for the setup token.
The four credentials
The install creates a separate PostgreSQL role for each job, rather than one that does everything:
| Role | Used by | Properties |
|---|---|---|
backenly_user |
the install scripts only | SUPERUSER. Creates roles, installs event triggers. Never in DATABASE_URL. |
backenly_app |
web and runtime | NOSUPERUSER NOBYPASSRLS. Owns the platform tables and workspace schemas, so governed typed actions keep their DDL rights. |
backenly_authenticator |
PostgREST | NOINHERIT; can only switch into anon / authenticated / service_role. |
backenly_backup |
pg_dump only |
NOSUPERUSER BYPASSRLS, with CONNECT, USAGE, SELECT and nothing else. |
backenly_app not being a superuser is the one that matters most. A superuser
bypasses row-level security — including FORCE ROW LEVEL SECURITY — so an
application running as one has its own policies applied only by convention.
Ownership plus FORCE is what keeps DDL working while leaving RLS in force.
Restore runs over the application connection, not the backup one, so the application keeps ownership of the schema it restored.
npm run selfhost is safe to rerun. It fills in what is missing and never
rotates a secret that already exists, so a rerun cannot sign out your sessions
or break a running PostgREST.
It does not hide the steps or replace them: it runs the same sequence
documented below, driven by the same module that
npm run bootstrap prints its guidance from. Read that section if you are
bringing your own database, if a step failed and you want to run it yourself,
or if you would simply rather see what is being done to your machine.
The installer generates everything except one choice: which AI provider
serves the natural-language tools (backend_chat, function generation). Set
ANTHROPIC_API_KEY or OPENAI_API_KEY in .env, or AI_PROVIDER=bedrock
with BEDROCK_REGION to use Claude through your AWS account. Without one,
those tools say AI is unavailable; the typed tools and the self-healing loop
need no model.
Doing it by hand
The same install, one step at a time. npm run bootstrap is the centre of it:
it provisions the project, and it is a reconciler rather than an installer, so
rerunning it repairs whatever is missing and changes nothing else. You will run
it at least twice, and that is the intended path, not a failure.
You install on the host:
- Node.js 20+ — the app, the scripts and the CLI all run here, not in a container
- Docker, with Compose
The Compose stack provides:
- PostgreSQL 15, with
pg_stat_statementspreloaded - Redis
- PostgREST, pinned — this is the
/db/*data plane
Docker does not provide Node. If you would rather run your own PostgreSQL or PostgREST, read Bringing your own database first; the steps below describe the Compose path.
Use a PostgreSQL cluster dedicated to this deployment.
PostgREST authenticates through cluster-global roles, and the list of schemas it serves is stored as a role setting with no
IN DATABASEscope. Two Backenly databases on one cluster therefore overwrite each other’s served-schema registry, and the loser’s data plane answersPGRST106on every table. This is a known limitation being redesigned, not something you can configure around. The Compose stack gives you a dedicated one.
1. Install
git clone https://github.com/backenly/backenly.git
cd backenly
npm install
cp .env.example .env
docker compose -f docker-compose.dev.yml up -d
postgrest will restart in a loop until step 4 issues its password. That is
expected, and it is deliberate that it serves nothing until then.
Then set these in .env, adding any line the template does not already carry —
today that is BACKENLY_PROJECT_ID:
BACKENLY_PROJECT_ID= # any UUID, e.g. `uuidgen`
JWT_SECRET=
POSTGREST_JWT_SECRET=
STORAGE_SECRET=
ANTHROPIC_API_KEY= # or OPENAI_API_KEY, or AI_PROVIDER=bedrock + BEDROCK_REGION
BACKENLY_EDITION is already single-tenant in the template and you can leave
it there. Backenly resolves an unset edition to single-tenant too, so the line
is not what makes this a self-host install — it is there because
scripts/deploy.sh composes through scripts/compose-cloud.sh, which refuses to
guess an edition rather than risk cloning a private repository you cannot read.
DATABASE_URL already matches the Compose stack, so leave it alone unless you
changed POSTGRES_USER or POSTGRES_PASSWORD.
BACKENLY_PROJECT_ID is the identity of this deployment and must never change
afterwards: it names the workspace_ schema your tables live in. Bootstrap
refuses to run against a database that already holds a different one. You may
leave it unset and pin the id bootstrap generates instead, but then step 4’s
command has to be copied from bootstrap’s output rather than from here.
JWT_SECRET signs every platform session. POSTGREST_JWT_SECRET is separate and
must match the value Compose passes to PostgREST, because the gateway mints
short-lived internal tokens with it. STORAGE_SECRET signs the access tokens for
stored files — without it every upload fails with a 500 whose only explanation is
in the server log. None of the three has a default, deliberately: a fallback
baked into a public repository is a key everybody already knows.
The AI provider key powers the natural-language tools; .env.example lists the
providers, per-purpose model overrides and monthly AI spending limits. See
.env.example for the rest.
2. Tell the database which role Backenly connects as
Skip this on the Compose path. The stack already uses backenly_user, which
is what the SQL defaults to.
It applies only if you brought your own database and the role in your
DATABASE_URL is something else, in which case run, as a superuser:
psql -c "ALTER DATABASE SET backenly.app_role = ''"
Skipping it there makes the next section fail with
role "backenly_user" does not exist.
3. Create the tables and bootstrap
npm run db:generate && npm run db:push
npm run bootstrap
db:pushbelongs to this step only — do not rerun it later.The PostgREST support objects are created by SQL in step 4, not by Prisma: a registry table and two event triggers that fire on schema DDL.
prisma db pushdoes not know about them, so on an installed deployment it treats them as objects to drop, which is exactly what--accept-data-lossauthorises. The registry is what the data plane reads to decide which schemas PostgREST serves, so losing it takes/db/*down.
npm run bootstrapis the command that is safe to rerun. It is a reconciler and repairs whatever is missing.
This first run is expected to exit 3. Bootstrap creates the project row, its workspace schema and its signing secret, then reports what it cannot install itself. Its exit code is the state, and deployment automation should read it:
| exit | meaning |
|---|---|
0 |
ready |
2 |
refused. The database is in a state bootstrap will not touch, either more than one project or a BACKENLY_PROJECT_ID that does not match what is already there. Nothing was written. |
3 |
core bootstrapped, prerequisites still unmet. Not ready: the /db/* data plane will answer PGRST106 until step 4 is done. |
4. Install the prerequisites bootstrap cannot install itself
These reach the database as a superuser. Bootstrap prints this same list, with your real project id filled in; run what it prints.
# 1. support objects: registry functions, DDL event triggers, and the
# anon / authenticated / service_role / backenly_authenticator roles.
bash scripts/postgrest-install.sh
# 2. passwords, role membership and the per-schema grants.
# Prints the connection string PostgREST authenticates with. Store it: it is
# not recoverable, and this command will not print it again.
npx tsx scripts/setup-postgrest-roles.ts --project --apply
# 3. put that password in .env as POSTGREST_AUTHENTICATOR_PASSWORD, then start
# the data plane. It has been restarting in a loop until now.
docker compose -f docker-compose.dev.yml up -d postgrest
# 4. optional. Only needed to hand out direct psql credentials to a project.
# Backenly is fully operational without it.
bash scripts/install-sql.sh scripts/setup-direct-access.sql
Those scripts talk to PostgreSQL through one explicit mechanism, chosen by
BACKENLY_DB_ADMIN, so the same command works wherever the database lives:
BACKENLY_DB_ADMIN |
how it connects |
|---|---|
docker (default) |
psql inside the Compose postgres service |
url |
psql -d "$BACKENLY_ADMIN_DATABASE_URL" — any reachable database |
local |
sudo -u postgres psql — a cluster installed on this host |
That order is not arbitrary and cannot be collapsed into one command. The first
has to create the roles, because the event triggers it installs grant to them,
and a CREATE SCHEMA that grants to a role nobody created aborts. The second
grants per workspace schema, so it needs the schema npm run bootstrap already
created above. This is why bootstrap runs, reports NOT ready, and is rerun: it is
a reconciler, and rerunning is the mechanism.
Then rerun:
npm run bootstrap
Exit 0 means ready. Anything else, read what it printed and fix that; it is safe to rerun as many times as you need.
5. Run it
npm run dev # dashboard :3000 · runtime :3001
npm run dev starts both processes together. There is no account yet, so sign up
in the dashboard, then run npm run bootstrap once more to issue the anon key
your frontend embeds.
Autonomy
The self-healing loop runs by itself. There is nothing to schedule.
instrumentation.ts starts an in-process scheduler when the app boots, and it
is active whenever VERCEL is unset — which is every self-hosted deployment. The
reconciler ticks every minute. .env.example already sets the two flags it
needs, so a .env copied from it has autonomy on:
ENABLE_AUTONOMY_RECONCILER=true # the loop runs at all
ENABLE_AUTONOMY_LIVE_EXECUTION=true # it may apply fixes, not just observe
Confirm it is ticking by watching the process output:
[AutonomyReconciler] Cron run complete — 1 projects, 0 fixes applied, 0 frozen
0 projects is normal on a new install, and does not mean it is broken. A
project is only reconciled once something has been built in it (a table other
than the built-in users table, a function, a bucket, a trigger, a webhook or
enabled auth) and it shows recent activity. A project that has only been named
has nothing to be right or wrong about, so the loop skips it and files nothing
against it. Create a table through your agent and the count becomes 1 on the
next tick.
CRON_SECRET is not required for any of the above. It guards one ad-hoc
endpoint, for forcing a tick without waiting for the minute:
CRON_SECRET= # add to .env, then restart
curl -H "x-cron-secret: $CRON_SECRET" http://127.0.0.1:3000/api/cron/autonomy
Nothing calls that URL on its own. The in-process scheduler is the mechanism; the endpoint is for operators.
Backups
Backups are a Backenly Cloud capability and the routes answer 404 on a self-hosted deployment. Nothing below is needed to run Backenly; it is here because the same code runs in both editions and the failure it describes is silent.
A backup is pg_dump of one project’s workspace_ schema, gzipped under
BACKUP_DIR. It carries tables, rows, indexes, constraints, RLS policies,
FORCE ROW LEVEL SECURITY and the end-user users table. It is not PITR
and not deployment-level disaster recovery: project secrets, API keys, storage
objects and function definitions all live outside that schema.
npm run selfhost creates the backup credential for you. It converges a
dedicated backenly_backup role and records BACKUP_DATABASE_URL in .env,
as the last step of the install — last because the objects it must be able to
read are created at different times: the platform tables by the migration
chain, the PostgREST registry and event triggers by the elevated SQL after it,
and the workspace schema by bootstrap after that.
This used to be a manual step, and the instructions here were incomplete: they
granted the workspace schema and not public, so an operator who followed them
exactly still could not produce a Deployment Recovery bundle. pg_dump --schema public takes a LOCK TABLE on every table in the schema and stopped at the
registry:
pg_dump: error: query failed:
ERROR: permission denied for table backenly_pgrst_schema_registry
The credential is needed because workspace tables are FORCE ROW LEVEL SECURITY, and under FORCE even the owner is subject to its policies, so
pg_dump over the application role aborts on the first protected table:
pg_dump: error: query failed: ERROR: query would be affected by
row-level security policy for table "users"
On production that meant every nightly backup failed for four days while the
pruner deleted the last good ones, ending at zero on disk. The Compose stack
hides it, because POSTGRES_USER there is the bootstrap superuser and
superusers bypass RLS. Managed Postgres does not hide it.
Do not fix this by granting BYPASSRLS to the application role — that
disables every RLS policy on every tenant at once. The installer creates a role
with exactly this much and no more:
LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE BYPASSRLS NOINHERIT
CONNECT on the database
USAGE + SELECT on public and on every workspace schema
no CREATE, no INSERT/UPDATE/DELETE/TRUNCATE, member of nothing, owns nothing
To converge it by hand on an existing deployment, or after adding a schema:
npx tsx scripts/setup-backup-role.ts --apply
Restore deliberately writes back over DATABASE_URL instead, so the
application keeps ownership of its own schema — restoring over the backup role
would re-own every table to it, and FORCE ROW LEVEL SECURITY keys on the
owner. tests/integration/backup-role-contract.spec.ts builds both roles for
real and proves the whole contract, including that the application role is
refused where the backup role succeeds.
Bringing your own database
The Compose stack is the supported path. If you already operate PostgreSQL, or run PostgREST under your own service manager, the differences are:
- Point
DATABASE_URLandDIRECT_URLat your database, and do step 2 above. - Run the step 4 scripts with
BACKENLY_DB_ADMIN=urlandBACKENLY_ADMIN_DATABASE_URLset to a superuser connection string, or withBACKENLY_DB_ADMIN=localif PostgreSQL is on this machine. - Start your own PostgREST against the connection string step 4.2 prints, with
POSTGREST_URLin.envpointing at it and its JWT secret set toPOSTGREST_JWT_SECRET. Use the version pinned indocker-compose.dev.yml: PostgREST reads its configuration out of the database, so a version difference changes what the data plane serves. - Enable
pg_stat_statementsyourself:
# postgresql.conf, then restart the server
shared_preload_libraries = 'pg_stat_statements'
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
It is how Backenly finds indexes that are missing by measurement — the columns Postgres is actually spending milliseconds filtering on — rather than only by schema shape. Without it that check reports itself as unchecked rather than passing, so nothing claims a guarantee it never evaluated. The Compose stack sets it already.
Connecting a coding agent
Backenly is built to be driven over MCP. Point your agent at the MCP server:
npx @backenly/mcp-server init
Works with Claude Code, Cursor, Codex, Cline, and Claude Desktop. Keys are scoped and revocable, and read-only keys serve a reduced tool set.
Restart your MCP host after installing. Tools stay absent until it reconnects, which looks like a broken install but is not one.
Verify the connection by asking your agent:
Call Backenly's read_backend_state tool and tell me what exists in this project.
Then describe what you want. The SDK is for the app you ship:
const backend = new BackenlyClient({ projectId, apiKey })
await backend.auth.signUp({ email, password })
await backend.posts.create({ title: 'Hello' })
await backend.posts.list({ filter: { published: true } })
Repository layout
| Path | What lives there |
|---|---|
app/ |
Next.js routes: dashboard UI, platform APIs, and the public /api/v1/* runtime |
lib/ai/ |
The Brain: planning, the tool loop, and executeAction, the governed mutation kernel |
lib/orchestration/ |
The nine-phase pipeline from intent to verified change |
lib/autonomy/ |
The MAPE-K loop that monitors and repairs running backends |
lib/execution/ |
Schema writes, migrations, and rollback beneath the kernel |
lib/tenant/ |
Schema isolation and the tenant boundary |
packages/ |
Client libraries: SDK, CLI, MCP server (MIT) |
prisma/ |
Platform schema: the public schema, not tenant data |
server/ |
Express runtime serving the end-user API |
scripts/ |
Operational tooling, probes, and the demo recording pipeline |
AGENTS.md is the deeper architectural guide, and is written for
coding agents working in this repo as much as for people.
Self-hosting vs Cloud
Self-hosting is free and complete. This repository is the whole platform: runtime, governance, and the full self-healing engine, not a stripped community edition. You bring the servers, the Postgres, and an AI provider key.
Backenly Cloud runs the same codebase, and
handles infrastructure, backups, upgrades, and the autonomy tokens. pg_dump
moves your data between the two in either direction.
Contributing
Pull requests are open and welcome. Bug reports, feature requests, questions,
and code all help. Good first places to look are open issues, the probe and
detector suite under lib/autonomy/, and client library ergonomics in
packages/. If a change is large or moves an architectural boundary, open an
issue first so we can agree the approach before you spend the time.
Before opening a PR:
npm run lint
npx tsc --noEmit
npm test
npx tsx scripts/preflight-oss.ts --tree # no credentials in what you committed
Two things worth knowing before you write code here. Tests run against a real
PostgreSQL instance, and the database is never mocked, because mocking it has
caused production incidents here before. And every schema mutation goes through
executeAction; a patch that writes DDL around the kernel will be sent back, no
matter how correct the SQL is.
See CONTRIBUTING.md for the full guide.
Security
Please do not open a public issue for a security problem. See SECURITY.md for the private reporting route.
License
The platform is licensed under the Apache License 2.0.
The client libraries under packages/, the SDK, CLI, and MCP
server, are MIT, so they impose nothing on the applications that embed them.
Trademark
Apache-2.0 §6 grants no rights to the Backenly name or logo, and this project does not grant them separately. You may fork, modify, self-host, and commercialise the software. Distributing it under the Backenly name requires written permission.
Describing your project as “built on Backenly” or “a fork of Backenly” is accurate and always welcome. See TRADEMARK.md for the full policy, including what you may do without asking.
Install
npx tsx scripts/setup-postgrest-roles.ts --project <PROJECT_ID> --apply