Data and tables
Every Appbricx app has its own Postgres database. You describe the tables (entities) and their fields in the contract, and Appbricx creates them, keeps them in step as the app changes, and gives your screens, workflows and outside tools a safe way to read and write them.
When to use it
Always: every app starts here. Add a table for each kind of thing your users work with (a class, a booking, a ticket) and one for each kind of person who signs in (a member, an instructor).
Step by step
- Name the tables. Names are
snake_caseand singular:session,booking. A table named exactly like a persona (member) holds one row per person with that role. - Add fields. Types:
text,number,boolean,date,timestamp,enum,ref,email,url,imageandattachment. Mark a field"optional": truewhen it may be empty. - Link tables with refs. A
reffield names another table:{ "name": "session_id", "type": "ref", "ref": "session" }. - Add uniqueness where it matters.
"unique": [["member_id", "session_id"]]stops the same member booking the same class twice. - Seed some rows so the preview and verify have data. Give each row your own
idlabel and use it in refs. - Check and apply. The database is created, or extended without losing rows.
Code
Two tables from the booking app:
"entities": [
{
"name": "session",
"label_field": "title",
"fields": [
{ "name": "title", "type": "text" },
{ "name": "style", "type": "enum", "enum_values": ["vinyasa", "pilates", "yin", "breathwork"] },
{ "name": "instructor_id", "type": "ref", "ref": "instructor" },
{ "name": "starts_at", "type": "timestamp" },
{ "name": "capacity", "type": "number" },
{ "name": "booked", "type": "number", "optional": true },
{ "name": "outdoor", "type": "boolean", "optional": true }
]
},
{
"name": "booking",
"label_field": "status",
"fields": [
{ "name": "member_id", "type": "ref", "ref": "member" },
{ "name": "session_id", "type": "ref", "ref": "session" },
{ "name": "status", "type": "enum", "enum_values": ["booked", "attended", "no_show", "cancelled"] }
],
"unique": [["member_id", "session_id"]]
}
],
"seeds": {
"session": [
{ "id": "s1", "title": "Sunrise Flow", "style": "vinyasa", "instructor_id": "i1",
"starts_at": "2026-10-13T06:30:00Z", "capacity": 14, "booked": 3, "outdoor": true }
]
}Read and write from a screen
const sessions = app.rows("session"); // re-renders as rows change
const { rows, total } = await app.query("session", {
where: { style: "pilates" }, sort: { field: "starts_at" }, limit: 20,
});
await app.create("booking", { session_id: id, member_id: app.me("member"), status: "booked" });
await app.transition("booking", bookingId, "status", "cancelled");Every call goes through your app's data API, so access rules and business rules apply. All calls are in the SDK reference. Outside tools use the REST API at /__appbricx/api/v1/<table> with a server key from Backend, API; see the API reference.
Change the data model later
[
{ "op": "add_field", "entity": "session", "field": { "name": "price", "type": "number" } },
{ "op": "update_field", "entity": "booking", "name": "status",
"enum_values": ["booked", "attended", "no_show", "cancelled", "waitlisted"] }
]Send these with patch_contract (or appbricx patch ops.json) instead of resending the whole contract.
Gotchas
- Never declare platform columns.
id,created_at,updated_at,user_id,org_idandcreated_byexist on every table already. - Don't name a table
users. That is the platform's account table. - Enum options can be added, never removed while rows use them.
- A removed field leaves the app, not the database. Its column and data stay until a destructive publish.
- Status enums are lifecycles. Move them with
app.transition; a plain update to the field is refused. - After the first apply, omitting
seedskeeps every row. - Seed images by description ("woman meditating on a wooden floor"), not by URL. Appbricx finds a licensed photo.
- Tables you created by hand can join the app with the
adopt_tablepatch op. See operating your app.