Enforce in a backend API
Load the bundle at start-up, evaluate every state-changing operation on the server channel, answer 422 on deny, persist, then run commands once. Java, Node.js, Go and Python.
The backend holds the decision. Whatever the browser reported, every state-changing operation is evaluated again on the server channel, with facts only the server has.
Before you start
- A bundle compiled in CI (ship a rule change).
- The runtime for your language installed from the repository: Java, Node.js, Go, Python.
- Each API operation tagged with
x-rule-cascade: { entity, operation }in its OpenAPI description (OpenAPI contract example).
Steps
Load at start-up; refuse to start on a load error
@SuppressWarnings("unchecked")
RuleSet rules = RuleSet.fromBundle((Map<String, Object>) Json.parse(Files.readString(bundlePath)));
// LoadException propagates out of the @Bean method and stops the application.Verify the custom operators
A manifest lists the operators its rules need. Refuse to start when one is not registered.
| Runtime | The operators the server manifest needs |
|---|---|
| Java | rules.requiredOperators(Channel.SERVER) |
| Node.js | missingOperators(rules.manifest('server'), operators) returns those not in operators |
| Go | manifest.Get("operators") on the result of rules.Manifest("server") |
| Python | rules.manifest("server").get("operators", []) |
Evaluate every state-changing operation from the server's own facts
| Member | Source |
|---|---|
entity, operation | The x-rule-cascade tag of the API operation, never the caller |
data | The body after shape validation (400 before any rule runs), plus the fields the server assigns |
original | The stored record, for every operation except create |
actor | id and roles from authentication, never from the payload or a caller-set header |
ctx.now | The server clock |
resolutions | The resolutions member of the body |
Deny with 422, or persist with the computed values and the commands
From TransferController
in the Spring Boot example, which CI builds and tests:
EvaluationRequest.Builder builder = EvaluationRequest.builder(ENTITY, operation)
.data(data)
.original(original)
.actor(actorId, roles);
EvaluationResult result = rules.evaluate(request);
if (!result.allowed()) {
throw new RuleViolationException(result); // ApiExceptionHandler: 422 problem details
}EvaluationResult result = check("create", transfer, null, actorId, roles, body);
applyComputedValues(transfer, result);
store.put((String) transfer.get("id"), transfer);
run(result); // each command once per idempotencyKey()
return ResponseEntity.status(HttpStatus.CREATED).body(envelope(transfer, result));store, log, caller, operators and newID stand for your persistence with its outbox, your
logger, the authenticated principal, your custom operators and your id generator. The four listings
are those of enforcement guide step 4.
Run each command once, after the commit
A relay publishes the outbox after the transaction commits and de-duplicates on idempotencyKey.
The engine returns commands only for an allowed server evaluation, so a denied operation never
produces one. See post-save commands.
Test the server-only path
Write a test that sends a request a server-only rule denies (in the payments example, a
transfer to a blocked country) and asserts 422 and that nothing was stored. The Spring Boot
example's TransferApiTest.aBlockedCountryIsRefusedWithProblemDetails asserts the 422 and the
finding through MockMvc; add the "nothing stored" assertion against your own store.
Done when no state-changing endpoint can persist anything before an allowed server evaluation;
a request denied by a server-only rule returns 422 and changes nothing; every decision is logged
with ruleset id, version and checksum; and a load error or a missing operator stops start-up.
Audit the result against the backend checklist.
Add rule enforcement to a React form
Fetch the client manifest, evaluate on every change with useRuleEvaluation, draw field state, computed values and findings, and send resolutions with the submit.
Run rules from any other language
Ruby, PHP, Rust, C#, PowerShell, shell or anything else - through the rule server over HTTP, or the command or WebAssembly module over the engine protocol.