Your First Governed Change

You have a running AuraBoot with the Showcase plugin in it — the quick start imported it. Ten minutes from here you will have changed that plugin, watched the platform stop you doing something you didn't declare, met the two gates every write passes, and read the audit record of everything that got through.

That is the whole product in one sitting. Nothing here is a toy: it is the same path a real feature takes.

Before you start — finish the Quick Start. You need a stack that is bootstrapped and has the plugins imported, and you need to be able to log in.

Every command below assumes a shell variable with an admin token:

BACKEND=http://localhost:3000   # the browser URL; only this port is published
ADMIN_EMAIL=[email protected]  # use the account configured during bootstrap
read -r -s -p "Administrator password: " ADMIN_PASSWORD; printf '\n'
JWT=$(AURA_CORE_BASE_URL="$BACKEND" ADMIN_EMAIL="$ADMIN_EMAIL" \
  ADMIN_PASSWORD="$ADMIN_PASSWORD" \
  python3 scripts/application/core-admin-session.py)

Run this from the cloned AuraBoot repository, using a version that includes scripts/application/core-admin-session.py. The helper logs in, selects exactly one business space through the real tenant-selection API and returns its token. A bootstrap administrator can also belong to the platform space, so a bare login token is not enough. If several business spaces exist, set AURA_ADMIN_TENANT_ID explicitly to the intended business tenant. Keep the token and password private.

1. Look at what you already have

Create a Showcase record and a monthly metric for it:

curl -s -X POST $BACKEND/api/meta/commands/execute/sc:create_showcase \
  -H "Authorization: Bearer $JWT" -H 'Content-Type: application/json' \
  -d '{"payload":{"sc_name":"Verify Widget","sc_description":"my first governed change",
                  "sc_quantity":10,"sc_price":99.5},"operationType":"CREATE"}'
{"code":"0","data":{"commandCode":"sc:create_showcase",
                    "phaseReached":"completed",
                    "data":{"recordPid":"01M3GK5TCVTRVQH66H8BS0ZE8E",
                            "showcase_all_fields_inserted":1},
                    "executionTimeMs":133}}

Copy data.data.recordPid — the metric below hangs off it. The list, its columns and its form all came from declarations — plugins/showcase/config/models.json plus fields, bindings and commands beside it. That is the first idea: the model is a file in Git, and the UI is derived from it. (The same pages render in Field Showcase → All Field Types if you prefer clicking to curling.)

Now record a monthly metric against it:

curl -s -X POST $BACKEND/api/meta/commands/execute/sc:create_monthly_metric \
  -H "Authorization: Bearer $JWT" -H 'Content-Type: application/json' \
  -d '{"payload":{"sc_showcase_pid":"<your recordPid>","sc_month":9,
                  "sc_revenue":1200,"sc_cost":800,"sc_units":40,
                  "sc_metric_remark":"baseline"},"operationType":"CREATE"}'

It works, and nobody wrote that page or that endpoint.

2. Add a field

Sales want to record a target next to each month's actuals. Open plugins/showcase/config/fields/sc_monthly_metric.json and append:

{
  "code": "sc_metric_target",
  "displayName:en": "Target",
  "displayName:zh-CN": "目标",
  "dataType": "decimal",
  "constraints": { "required": false },
  "feature": { "sortable": true }
}

A field that exists is not a field the model uses. Bind it — plugins/showcase/config/bindings/sc_monthly_metric.json:

{
  "modelCode": "sc_monthly_metric",
  "fieldCode": "sc_metric_target",
  "sequence": 7,
  "required": false,
  "visible": true,
  "editable": true,
  "displayConfig": { "sortable": true }
}

Re-import the plugin so the platform picks your files up. The path is where the backend sees the plugin directory — inside the Docker stack that is /app/plugins/showcase; running the backend on the host, it is <repo>/plugins/showcase:

curl -s -X POST $BACKEND/api/plugins/import/import-directory-sync \
  -H "Authorization: Bearer $JWT" -H 'Content-Type: application/json' \
  -d '{"path":"/app/plugins/showcase","conflictStrategy":"OVERWRITE"}'

The imported declaration changes the model metadata:

  • The database changed. mt_sc_monthly_metric has a new sc_metric_target numeric column. You wrote no migration.
  • The field is bound to sc_monthly_metric. It does not add Target to the separate showcase_all_fields form. This tutorial verifies the monthly metric through its command and persisted row; add an explicit page field declaration if you want to expose it in a custom page.

3. Watch the platform refuse to write it

Create a metric with a target:

curl -s -X POST $BACKEND/api/meta/commands/execute/sc:create_monthly_metric \
  -H "Authorization: Bearer $JWT" -H 'Content-Type: application/json' \
  -d '{"payload":{"sc_showcase_pid":"<your recordPid>","sc_month":8,
                  "sc_revenue":900,"sc_cost":700,"sc_units":30,
                  "sc_metric_target":777},"operationType":"CREATE"}'

The command reports success — "code":"0", "phaseReached":"completed". Look at the row:

SELECT sc_month, sc_revenue, sc_metric_target
FROM   mt_sc_monthly_metric ORDER BY created_at DESC LIMIT 1;
     8 | 900.00 | (empty)

The target is gone. Not an error. Not a warning. Dropped.

This is the second idea, and it is the one most platforms get wrong. The field exists on the model, but sc:create_monthly_metric never said it would accept it. Open plugins/showcase/config/commands/sc_monthly_metric.json:

{
  "code": "sc:create_monthly_metric",
  "type": "create",
  "inputFields": [
    "sc_showcase_pid", "sc_month", "sc_revenue",
    "sc_cost", "sc_units", "sc_metric_remark"
  ],
  "permissions": ["sc.showcase.manage"],
  ...
}

inputFields is a whitelist, not documentation. Undeclared fields do not enter the persistence mapping. It does not validate the range of declared values: whether sc_month may be 13 is the responsibility of field constraints and command validation.

Add "sc_metric_target" to inputFields, re-import, and create the metric again:

curl -s -X POST "$BACKEND/api/meta/commands/execute/sc:create_monthly_metric" \
  -H "Authorization: Bearer $JWT" -H 'Content-Type: application/json' \
  -d '{"payload":{"sc_showcase_pid":"<your recordPid>","sc_month":7,
                  "sc_revenue":1500,"sc_cost":700,"sc_units":30,
                  "sc_metric_target":777},"operationType":"CREATE"}'

Use a new month to avoid the unique constraint on showcase record and month. Inspect the row identified by this response’s metric recordPid:

     7 | 1500.00 | 777.00

Two declarations, two jobs. The model says what the thing is. The command says what may be written, by whom, and what the system fills in itself.

4. Get refused — at two different gates

Reading the rule is not the same as feeling it. Make two colleagues and watch the platform tell one of them no.

# Sam: the tenant baseline. No business grants.
curl -s -X POST $BACKEND/api/meta/commands/execute/admin:create_member \
  -H "Authorization: Bearer $JWT" -H 'Content-Type: application/json' \
  -d '{"payload":{"name":"Sam Reader","email":"[email protected]",
                  "password":"Test2026x","roleCodes":["tenant_member"]},
       "operationType":"CREATE"}'

# Rita: showcase administrator.
curl -s -X POST $BACKEND/api/meta/commands/execute/admin:create_member \
  -H "Authorization: Bearer $JWT" -H 'Content-Type: application/json' \
  -d '{"payload":{"name":"Rita Sales","email":"[email protected]",
                  "password":"Test2026x","roleCodes":["sc_admin"]},
       "operationType":"CREATE"}'

Log in as each of them and run the same command with the same payload:

SAM=$(AURA_CORE_BASE_URL="$BACKEND" ADMIN_EMAIL=[email protected] \
  ADMIN_PASSWORD=Test2026x python3 scripts/application/core-admin-session.py)
RITA=$(AURA_CORE_BASE_URL="$BACKEND" ADMIN_EMAIL=[email protected] \
  ADMIN_PASSWORD=Test2026x python3 scripts/application/core-admin-session.py)

curl -s -X POST $BACKEND/api/meta/commands/execute/sc:create_monthly_metric \
  -H "Authorization: Bearer $SAM" -H 'Content-Type: application/json' \
  -d '{"payload":{"sc_showcase_pid":"<your recordPid>","sc_month":6,
                  "sc_revenue":100,"sc_cost":50,"sc_units":5},
       "operationType":"CREATE"}'
curl -s -X POST "$BACKEND/api/meta/commands/execute/sc:create_monthly_metric" \
  -H "Authorization: Bearer $RITA" -H 'Content-Type: application/json' \
  -d '{"payload":{"sc_showcase_pid":"<your recordPid>","sc_month":6,
                  "sc_revenue":100,"sc_cost":50,"sc_units":5},
       "operationType":"CREATE"}'
Sam — tenant_member403 Access forbidden
Rita — sc_admin"code":"0", row inserted

Same command. Same payload. The difference is the role.

You actually walked past two gates on the way in, and it is worth being able to name both:

  1. The endpoint gate. Everything arrives at one place, /api/meta/commands/execute/..., and that endpoint itself requires meta.command.execute. Sam is refused here — the error's context names the missing permission. If you made a member with only a business role and no execute grant, you would see the same refusal; deny-by-default means a role passes this gate only because someone granted it (the Showcase plugin grants it to sc_admin in plugins/showcase/config/roles.json).
  2. The command gate. Past the endpoint, the command's own declaration applies: "permissions": ["sc.showcase.manage"]. This is the line written in the file you edited in step 3.

Neither is a UI-level check a determined caller can walk around. The endpoint is the only way to write. A person clicking Save, an automation, a BPM node, an AI agent calling a tool: all four arrive there, and all four meet the same two lines.

5. Read what you did

Every execution that gets past the gates leaves a record.

SELECT command_code, success, phase_reached, execution_time_ms
FROM   ab_command_audit_log
ORDER  BY created_at DESC
LIMIT  6;
 sc:create_monthly_metric | t | completion | 22
 admin:create_member      | t | completion | 109
 sc:create_monthly_metric | t | completion | 35

What the pipeline accepted, how long it took, how far it got. You did not turn this on. You cannot turn it off for one caller and not another, because there is only one caller — the pipeline.

Sam's 403 is not in there. The endpoint gate refuses him before the pipeline runs, so the pipeline's audit log never sees the attempt. Worth knowing before you go looking for it: today this table answers "what was done", not "what was attempted".

What you just proved

The model is a fileYou declared and bound a field; import added the database column. Page fields still require explicit declarations.
The command is a contractIt silently refused a value it never agreed to accept — and told the truth in the audit log about what it did take.
Writes pass two gatesThe endpoint's meta.command.execute, then the command's own permission. Both are declarations, not buttons.
The audit is not a featureIt is a property of there being exactly one way in — for everything that gets in.

Next