S
supero.docs
Documentation/Reference apps/Sentinel's insurance claims portal ships tests for its own access rules

Sentinel's insurance claims portal ships tests for its own access rules

Sentinel is an open-source insurance claims portal for two insurers, with scripts that test its access rules. Source on GitHub, and it runs live as a demo.

  • apps
  • insurance
  • multi-tenant
  • access-control
  • workflows

Overview

For anyone building an insurance claims portal: this open-source one serves two insurers, and it is the one app the repo's verify/ scripts are written against.
On 2 October 2026 I ran one script from the repo against sentinel.supero.live. Two lines of its output:
text
    staff sees 24 fields, policyholder sees 22
  PASS  'fraud_score' present for staff (19) and ABSENT for the policyholder
Same claim, CLM-204455, fetched with two logins. The claims adjuster's response has fraud_score and internal_notes. In the policyholder's response both are absent from the response body, removed server-side. The demo's data is shared and can change, so the claim the script picks and the two counts may differ on your run.
The script is verify/02-rbac-enforcement.sh. It needs curl and python3, and no account. Of the 19 reference apps, Sentinel, whose own code is in apps/insurance/sentinel, is the only one the verify/ folder targets.

The rule under test

It lives in setup.py, in the policy for the tenant_user role:
python
PolicyRule(entity="claim", can_read=True, can_create=True, can_update=True,
           filter_field="owner_username", filter_match="$user.name",
           hidden_fields=["fraud_score", "internal_notes"]),
The middle line is policy-based row scoping: a policyholder gets only the claims whose owner_username is their own login. The last line names the two fields removed for that role. The claims team is tenant_admin, and its rule on claim has neither line.
In schemas.py the two fields are ordinary attributes on Claim, an integer and a text. Nothing in the schema marks them as special. The policy does that. And ui/app.js does not hide them either: the claims console draws a fraud badge that turns amber at 35 and red at 60, while the policyholder portal never asks for the score and instead prints a note telling the member that an internal score and review notes exist and are kept from their view.

Two insurers from one seed loop

config.py declares a tenant, its own sealed set of records, for each insurer: northwind-mutual is one, cascade-assurance the other. setup.py holds a dict called TENANT_BOOKS, and the seed function loops over it and passes tenant_name=tenant on every record it writes.
I counted the seed. Each insurer gets the same 8 coverage products. Northwind gets 6 policies and 8 claims with 6 supporting documents. Cascade gets 4 policies and 5 claims with 4 documents. The numbering differs on purpose: Cascade's records start POL-CA- and CLM-CA-, so a row from the wrong insurer would stand out in a list without anyone comparing UUIDs.
verify/03-tenant-isolation.sh takes a real Northwind claim UUID and requests it directly with a valid Cascade staff token.
On 2 October 2026 the answer was HTTP 403.
The message read "Access denied: this claim belongs to another tenant". The login page lists four demo accounts, a claims-team login and a policyholder login for each insurer, so you can repeat the comparison in a browser.

Through the insurance claims portal, from filed to paid

Three workflows sit in setup.py as plain dicts.
claim_intake has no button. It is bound to the event @create:sentinel:claim, so filing a claim is what triggers it: an acknowledgement email to the member and a message to the #claims Slack channel. The binding maps the recipient from user.email, not from a field on the claim, so the acknowledgement goes to the person who filed.
claim_decision is a saga, a workflow whose steps can name their own undo. Its first step sets claim_state to approved and writes amount_approved, and that step carries a compensate block returning the claim to under_review. claim_payout marks an approved claim paid and sends a receipt. In the console, the Approve and Pay buttons run those two workflows through a single helper that reports a failed run as failed. Afterwards the console re-reads the claim from the server instead of assuming the new state.
config.py enables four services, and one of them is ai. The public coverage pages use it for a plain-language explainer, with a written summary shown when the call does not return text.

What the three scripts check

01-policy.sh asserts nothing. It logs in as three accounts and prints, for each, the role on its token, how many claims it can read, and whether the two fields appear.
02 finds one seeded claim that both the claims team and the policyholder can see and compares the two responses, confirms that every claim the policyholder can read is one they own, then repeats the field check on POST /query and on a GET by UUID.
Both 02 and 03 are written to abort when the demo data cannot support the comparison. If the staff account saw no claim carrying the two fields, "absent for the policyholder" would mean nothing, and the script says so and exits.
The verify README lists what the scripts cover: the read paths they name, for this app's entities, on this deployment. Sentinel is a reference app for a claims workflow and an access model. It is not a compliance statement.

Make script 02 fail on your own domain

bash
git clone https://github.com/supero-platform/supero-apps
cd supero-apps/apps/insurance/sentinel
cp .env.example .env    # set SUPERO_DOMAIN and SUPERO_PASSWORD
./run.sh
You need no account for this, and the app from the source folder is running in about two minutes on http://localhost:5674.
Then delete "fraud_score" from hidden_fields, run ./run.sh again, and run SUPERO_VERIFY_DOMAIN=your-domain ./02-rbac-enforcement.sh from verify/. The README expects it to fail on that field. Want the browser version first? Open sentinel.supero.live and compare the two logins. When you have a claims model of your own to protect, start it at supero.dev.