Why Ulabase
Build your apps with AI agents
On Ulabase the backend of your app is a folder of setup files. A coding agent such as Claude Code can write and apply it, reading this documentation over MCP.
On Ulabase, AI agents have two roles. Your app can have an agent interface, so that AI agents such as Claude use it next to the screens: that is an agent-native app. And a coding agent, such as Claude Code, can build the app with you. This page is about the second role. The method is the same for an agent-native app and for an app with screens only.
An app usually has two parts: the frontend, the screens people use, and the backend, which stores the data and decides who may do what. The backend is usually code, written with Node, Spring Boot, RESTHeart or another framework, in its own repository or in a folder of a monorepo.
On Ulabase the backend is already running. What is left is its configuration: collections, schemas, permissions, aggregations, users, features. You can set it up by hand in the console. Or you keep it as a folder of setup files, and ulabase setup applies it to the service. A coding agent can write that folder for you, because it reads this documentation through its MCP server while it works.
What you get:
-
A backend you can hand to the agent. The agent writes the setup files and applies them to a development service. You review the changes and decide what goes to production.
-
A backend you can read. Every setting is a file in git, in the same repository as the screens.
-
Every change checked. A dry run shows what a change will do before it does it, and shows nothing left to do after.
Start in six steps
You do not need to read the whole page. These steps are enough to start; the agent follows the method described in the rest of the page.
-
Pick a starter. Clone the starter of your framework, React, Angular, Vue or ecommerce. It has the screens and a first setup file. See Getting started.
-
Create a free service for development, in the console.
-
Install the CLI and sign in, so the agent can apply the setup:
npm install -g ulabase ulabase login -
Connect your coding agent to the documentation. You need an agent that writes files and runs commands, such as Claude Code, Cursor or VS Code in agent mode. Add the
ulabase-docsMCP server. In Claude Code:claude mcp add --transport http ulabase-docs https://d0c5a1.ulabase.app/mcpIn the other tools, add
https://d0c5a1.ulabase.app/mcpas an HTTP MCP server in their settings. -
Give the agent its rules. Paste this in the instructions file of your repository,
CLAUDE.mdorAGENTS.md, with the id of your development service:## Backend The backend of this app is a folder of Ulabase setup files: ulabase.setup.ts at the root, the rest in backend/, one folder for each part of the app. `ulabase setup` applies them. - For anything about Ulabase, ask the ulabase-docs MCP server first, before reading code. - Follow the method of the page "Build your apps with AI agents" in ulabase-docs. - Every change: `ulabase setup --srv <dev-service-id> --dry-run`, then apply, then dry run again; the last dry run must report every step as done. - Run the setup on the development service only. For production, propose the command and wait. -
Ask for what you want, in plain words. For example: "Add orders: each user creates orders and reads only their own. Apply it to the development service." The agent writes the setup files, applies them, and shows you the last dry run.
When the app is ready, create the production service and apply the same folder to it. See One folder, two services.
The backend is a folder
A starter (see Getting started) gives you both parts: the screens, and a setup file, ulabase.setup.ts, that configures the service for them. As the backend grows, keep it in one folder, split the way the app is split:
ulabase.setup.ts the root file: every step, in the order they run
backend/
access.ts users, teams, guard rules
orders/ one folder for each part of the app
orders.schema.json
steps.ts its collection, indexes, aggregations, permissions
reports/
steps.ts
emails/ the HTML of the emails the service sends
lib/steps.ts the helpers that turn a folder into steps
src/ the frontend
The root file lists the steps of each folder:
import { defineSetup } from '@ulabase/cli';
import { accessSteps } from './backend/access';
import { ordersSteps } from './backend/orders/steps';
import { reportsSteps } from './backend/reports/steps';
import { retiredPermissionsStep } from './backend/lib/steps';
export default defineSetup('My app', [
...accessSteps,
...ordersSteps,
...reportsSteps,
// delete from the service the permissions you removed from the code
retiredPermissionsStep(['oldPermissionId']),
]);
A new part of the app is a new folder, with its own schemas, aggregations and permissions. The other folders do not change.
How the agent uses the documentation
The ulabase-docs server of step 4 has three tools: search finds pages by meaning, chunks reads a whole page, and ls lists the pages.
The rules in the instructions file, step 5 above, tell the agent to ask this server first and how to run the setup.
Ulabase runs on RESTHeart, which has its own documentation. On some points they differ: on Ulabase, for example, the email templates are part of the service configuration, and on a RESTHeart server they are files. The agent should ask ulabase-docs first.
Write every check to compare the content
Each step of the setup has a check and an apply. The agent writes a few helpers that turn what a folder declares into steps: one for schemas, one for permissions, one for aggregations. About a hundred lines in all.
Make each check compare the content. The check of a permission reads the permission saved on the service and compares it with the one in the code. When you change a permission in git, the next dry run shows it as work to do.
import { step } from '@ulabase/cli';
// one step per permission: done when the saved permission is the same as the one in the code
export const permissionStep = (id: string, doc: { roles: string[]; predicate: string; priority: number; mongo?: object }) =>
step(`permission ${id}`, {
async check({ service }) {
if (!(await service.permissionExists(id))) return false;
const saved = await (await service.fetch(`/acl/${id}`)).json();
return saved.predicate === doc.predicate
&& saved.priority === doc.priority
&& JSON.stringify(saved.roles) === JSON.stringify(doc.roles)
&& JSON.stringify(saved.mongo ?? null) === JSON.stringify(doc.mongo ?? null);
},
apply: ({ service }) => service.putPermission(id, doc),
});
Two kinds of steps check less, on purpose:
-
An index is checked only by name, because an index cannot be changed. See Indexes.
-
Initial data is written once. After that it belongs to the users of the app.
Two more rules keep the folders independent:
-
All the aggregations of a collection are in one list. The helper replaces only the entry with its own
uri, so two folders can add aggregations to the same collection. See Aggregations. -
A permission you delete from the code stays on the service, and keeps allowing what it allowed. List the deleted ones in one step that removes them from the service.
The starters end with a step that points the app at the service. Keep it last. If the setup stops halfway, the app keeps showing the "connect your service" screen, and no page calls a service that is not ready.
Work in short loops
Every change to the backend goes through the same loop:
-
the agent changes a folder: a schema, a permission, an aggregation;
-
ulabase setup --srv <dev-service-id> --dry-runshows what is missing, step by step; -
ulabase setup --srv <dev-service-id>applies it to the development service; -
a second
--dry-runmust report every step as done.
The loop takes seconds. The last dry run proves the change: a rule written wrong shows up as a step that is still not done.
The same tool reads the service. When the agent needs to see what the service holds, it writes a setup with one step whose check prints what it reads and returns true, runs it with --dry-run, and deletes it. It needs no other credential or tool.
One folder, two services
The same folder configures the free development service and the production service. The setup is TypeScript, so it can read environment variables, for example for the address of the app:
ulabase setup --srv <dev-service-id>
APP_URL=https://app.example.com ulabase setup --srv <prod-service-id>
APP_URL here is a name of your choice, read by your setup with process.env. Secrets, such as the password of an administrator, come in with fromEnv, and are needed only on the first run: on later runs the user already exists.
Access is part of the backend
Users, roles and rules are in the setup like the rest:
-
The first administrator is created with
createUser, with the password from the environment. See Users. -
Guard rules block what your app does not offer, such as creating teams.
-
Permissions can record who wrote a document and when, with
mergeRequest. See Permissions. -
The emails the service sends are HTML files in the folder. See The emails.
When the documentation does not answer
Ask the agent to write down exactly what is missing: which key, which shape, how to go back to the default, how long a change takes. Send it to us from the Support page of your service. The answer becomes part of this documentation, and the agent reads it over MCP the next time. Keys that the agent guessed never end up on the service.
The method, in short
-
Start from the starter of your framework.
-
Keep the backend as a folder of setup files, split like the app.
-
Give the agent the documentation over MCP, and the rules of the loop in your instructions file.
-
Make every check compare content, so every change shows up as work to do.
-
Work in short loops on a free service: change, dry run, apply, dry run.
-
Apply the same folder to production, with the addresses and the secrets from environment variables.
Related pages
-
Build your apps for AI agents: the other role of AI agents, as users of your app.
-
The
ulabaseCLI: commands, the setup file and how to force a step. -
Getting started: create a service and pick a starter.