Appbricx
Docs
PricingStart free

Start here

OverviewGetting startedProjects & editorBring your own key

Build with your agent

Coding agents quickstartAccess tokensMCP serverApp contractCustom UI & brandingBefore-write rulesDeveloper CLIDeveloper HTTP API

End-to-end walkthroughs

Lead intakeMulti-user TodoMulti-tenant SaaSSlack on signup

AI & keys

AI keys — how resolution worksBYOK deep dive

Integrations

Integrations catalog

Full-stack runtime

Runtime overviewNamed queriesAuth & RLSAuto CRUD REST APIWorkflowsWebhooks & schedulesTopics & CDCData templatesSecrets & env

Ship & own

Publish & domainsExport & GitHubSelf-host & deploy

Developers

Developer guide

Before-write rules

A rule is a small check that runs on the server before a row is created or updated. If it refuses, nothing is saved. It holds for every write through the app's data API, not just the ones your UI makes.

Why rules

  • UI checks can be bypassed. A disabled button stops nobody who calls the data API directly. A rule does.
  • Workflows run after the write. A workflow can react to a new row, but it cannot stop it being saved. A rule can.
  • Access is about rows, rules are about content. Use access and owner_refsfor who may touch which rows. Use a rule for conditions that depend on the data, like "who may send the first message".

Shape

"rules": [
  {
    "id": "women-first",
    "on": "create",
    "message": "Women make the first move.",
    "check": "<async function body>"
  }
]
KeyMeaning
idkebab-case, unique within the entity
on"create" (default), "update" or "both"
messageShown when the check returns false(default "That isn't allowed."; max 200 characters)
checkThe body of an async function over (row, ctx). Up to 4,000 characters. TypeScript syntax is accepted

The check returns true (or nothing) to allow, falseto refuse with the rule's message, or a string to refuse with that string.

What the check gets

NameValue
rowThe row as it would be written. On update: the current row merged with the patch. Server-stamped fields (owner columns, actor_ref) are already set
ctx.oldThe current row on update; null on create
ctx.user{ id, role } of the writer
await ctx.me("member")The writer's own row id in a persona table, or null
await ctx.db.query(sql, params)One read-only SELECT (or WITH … SELECT), run as the writer: row-level security applies. Returns { rows }
ctx.now()The current time as a Date

Because queries run as the writer, a rule only sees rows the writer may read. Make sure the access settings let the writer read what the rule needs.

Example: women message first

A dating app with member (with a gender field), match and message. The first message in a match must come from a woman. After that, anyone in the match can reply.

{
  "name": "message",
  "fields": [
    { "name": "match_id", "type": "ref", "ref": "match" },
    { "name": "sender_id", "type": "ref", "ref": "member" },
    { "name": "body", "type": "text" }
  ],
  "owner_refs": ["match_id"],
  "actor_ref": "sender_id",
  "rules": [
    {
      "id": "women-first",
      "on": "create",
      "message": "Women make the first move — she needs to message first.",
      "check": "const prior = await ctx.db.query('select id from message where match_id = $1 limit 1', [row.match_id]); if (prior.rows.length > 0) return true; const me = await ctx.db.query('select gender from member where id = $1', [row.sender_id]); return me.rows[0]?.gender === 'woman';"
    }
  ]
}

The same check, formatted:

const prior = await ctx.db.query(
  "select id from message where match_id = $1 limit 1",
  [row.match_id],
);
if (prior.rows.length > 0) return true;          // the conversation has started

const me = await ctx.db.query(
  "select gender from member where id = $1",
  [row.sender_id],                                // stamped by actor_ref
);
return me.rows[0]?.gender === "woman";            // false → the rule's message

On production this rule refused a man's first message sent straight to the API (422, nothing saved), and allowed a woman's first message and his reply after it (201 each).

Adding or changing rules

Put them in the contract and apply, or replace an entity's whole list with a patch op ([] or null removes them):

{ "op": "set_entity", "entity": "message", "rules": [ { "id": "women-first", ... } ] }

Limits

  • Up to 6 rules per entity. They run in order; the first refusal wins.
  • 3 seconds and 64 MB per check.
  • Each check runs in a sandbox: a separate process with no environment variables, no file writes, no network and no code generation.
  • No writes. ctx.db.query accepts one SELECT only.
  • Rules run on data-API creates and updates. Workflows (platform code acting for the app) don't run them.

Failure modes

A broken rule fails closed: the write is refused, never allowed.

  • Doesn't compile— the contract check reports an error and the contract can't be applied.
  • Throws, times out or runs out of memory— the write is refused with the rule's message.
  • Returns something other than true, false, nothing or a string — refused.
  • Bad id, bad on, empty check, duplicate id or a seventh rule — dropped, with a line in the report's fixes.

The response

A refused write answers 422 and saves nothing:

HTTP/1.1 422
{
  "ok": false,
  "error": {
    "code": "RULE_REFUSED",
    "message": "Women make the first move — she needs to message first."
  }
}

Show error.message to the user. A component calling app.create or app.updategets a rejected promise whose error message is the rule's message.

Next

  • App contract — entities, access, actor_ref
  • Workflows — what happens after a write
  • Custom components & branding
Appbricx

Full-stack AI app builder for teams. Hosted cloud or private deploy into your account — multi-tenant, sandboxed, credit-metered.

Product

For coding agentsHow it worksDemoCapabilitiesPricingPrivate cloudBYOKFAQ

Resources

DocumentationBlogFor freelancersFor agenciesSupport

Company

ContactPrivate cloud / agencyPrivacyTerms

© 2026 Appbricx. All rights reserved.

TermsPrivacyCookiesAcceptable Use