Trust model ✅
Four mechanisms carry every trust claim LifeCare.ID® makes, and each one has a
canonical reference page on lifecare.id that states it against the
implementation serving the network today, over a published, drift-tested
contract. Those pages are the source of truth and they move when the code moves.
This page is the developer's frame around them: what each mechanism means while
you build, which of your code paths it touches, and where the honest limit sits.
Nothing here is a second copy of a canonical page. Where you need the field table, the hash recipe or the sample payload, follow the link.
| Mechanism | What it answers | Canonical page |
|---|---|---|
| Consent receipt | who permitted what, for which purpose, until when | The consent receipt ↗ |
| Patient-held authorization server | may this specific request be served, right now | The patient-held authorization server ↗ |
| Zero egress | where the raw values are allowed to be while a model reads them | Zero-egress health AI ↗ |
| Node attestation | what the machine holding the data says it runs, and whose key signed that statement | Signed attestation ↗ · Verify a node ↗ |
The two halves. Consent decides who may touch the data; attestation lets you check what a node says it runs. Neither substitutes for the other, and the network runs both.
The ceiling, stated first
The attestation limit is the one you would otherwise find on your own in thirty seconds, so it goes at the top. From the verify page ↗, verbatim:
Three checks run here. The measurement recomputes from the response, so nothing in the claim is hidden. The signature over it is valid for the key the response carries, so the claim was not altered after that key signed it. And the key's id (its RFC 7638 thumbprint) is compared with the keys LifeCare.ID® lists for the node, as published by its operator: Verified, key known needs all three. A valid signature by a key that is not listed reads Signature valid, key unknown, because the quote may come from a different machine. This is the software floor: it is not yet hardware-rooted, so an operator in control of the machine could still publish a consistent claim that is not true; hardware-rooted attestation closes that gap and is designed in. The key list is what the node's operator declared; anchoring each key in the node's own decentralized identifier is the next step.
And from the attestation page ↗:
A pass does not prove that the machine is running what it claims. This is the software floor: the node attests what it reports about itself, and an operator who controls the machine could report a consistent lie.
Design accordingly. A verified attestation is a strong integrity and transparency signal (any change to code, models, parameters or twin recipe changes the measurement, visibly and under signature), and it is not yet a hardware proof. For a high-assurance flow, treat hardware-rooted attestation 🛣️ as the bar and say so in your own security documentation rather than borrowing more assurance than the mechanism gives.
1. Consent receipt: what your app actually holds
Every grant produces two artifacts from one event: a plain-language ledger line for the person, and a machine-readable receipt shaped to ISO/IEC TS 27560:2023 for you, for the resource holder, and for anyone auditing either.
What you see. POST /v1/consent/grant returns the receipt inline. Its fields
are consent_id, schema, subject_did, grantee, data_categories,
purpose (a dpv term and the text the person read), granted_at,
expires_at, revoked, revoked_at, proof, and revocation_endpoint.
Categories, never values: a full copy of every receipt ever issued names who
trusted whom for what, and not one measurement.
What that means in your code:
consent_idis your join key. Tokens issued under the grant reference it, revocation names it, and ledger lines carry it. Store the receipt with the record it authorizes.data_categoriesare scope strings, from the fixed vocabularycgm,daily,labs,body_composition,recovery,activity,insights,told_to_guardian. Do not parse them as anything richer, and do not put a value in one. The vocabulary grows by contract bump, so treat a value you do not recognise as a category you do not handle, not as an error.expires_atis real. Grants are time-boxed, one to 365 days, 90 by default. Plan the re-consent moment into your UX rather than discovering it as a403.revokedcan flip under you, at any instant, without a callback to you. Treat the receipt you stored as a record of what happened, never as a cached permission.proofis a reference to a verified device proof, ornull. A non-nullproofwas checked before the grant was stored. Absent is not "unchecked", it is "none presented"; whether a grant may be created without one is a deployment setting.
Honest limit. A W3C Verifiable Credential wrapping of the receipt is on the roadmap 🛣️ and is not what ships today.
Build detail (endpoints, the canonical grant payload you hash for a passkey challenge, listing grants, the ledger): Scopes & consent. Mechanism, shape and standards lineage: the consent receipt ↗.
2. Authorization server: the failure modes you must implement
The authorization server issues tokens against grants; the resource server holds the data and honours them. They are different parties by design, and the policy the authorization server applies is the person's own consent.
What that means in your code. A resource server never decodes a token. It
introspects and branches on token_type, failing closed on any shape it does not
implement. The four shapes and their exact fields are in
Authorization & tokens; here is how they behave
from the client side.
| What you see | What actually happened | What to do |
|---|---|---|
active: false |
expired, token revoked, its grant revoked, or unknown. Four causes, one answer, so a caller learns nothing from the difference | stop using the data, degrade the surface, re-consent if the person wants it back. Do not retry-storm: this is a normal state |
403 on POST /v1/token |
no active grant covering that scope with a matching purpose that you may use. A grant to another party counts as none: a token is issued only against a grant naming you as grantee, with one exception for the deployment's own first party (Authorization & tokens) | ask for consent. Do not retry with a different purpose string hoping for a hit: purpose is part of the lookup, not a check after it |
token_type: "user" vs "access" |
a signed-in person versus an app or agent holding a grant-backed token | user-self actions (granting or revoking that person's own consent) require a user token whose subject matches. An access token presented for the same action is refused with 403 |
token_type: "delegated" |
an actor exercising a subset of a person's authority | published so you can write the branch now. No endpoint mints one yet |
Three properties worth designing around rather than discovering:
- One scope per token. Reading two categories means two tokens, each revocable on its own.
- Time-boxed twice. A token expires at the earlier of twenty minutes from
issue and the grant's own expiry. A long grant never produces a long-lived
token, so cache to
expires_atand no further. - Revocation is not a notification. Every token under a revoked grant is invalidated in the same transaction, in under half a second. A resource server that introspects on every request needs no signal; one that caches decisions consults the public revocation registry under the person's identifier.
Honest limits. Caller authentication and the device proof on grants are implemented and verified whenever presented, but whether an unauthenticated call is refused is a deployment setting (see the rollout sequence in Authorization & tokens). Binding a token to its grantee rides on the same setting: with caller authentication off there is no proven identity to bind to. The delegated shape is in the published contract and no code path issues it yet.
Mechanism, the UMA 2.0 lineage and what was deliberately collapsed: the patient-held authorization server ↗.
3. Zero egress: what it means for a data-source integration
Zero egress means the models run where the raw health data lives, and what leaves is interpretation: a score, a band, a sentence with no number in it. It is enforced on three code paths, not promised in a policy: where models may run (loopback endpoints and declared model ids only), what an agent may read (a guarded accessor limited to declared scopes, every read token-backed and ledgered), and what may leave (a complete field whitelist per response, prose screened numeric-free).
What that means if you are building a data source or connector:
- Raw values travel provider to node, or device to node. A source with a cloud API is fetched by the node under the token the person granted; a source that lives only on the person's phone or scale is shipped by their own edge app straight to the node. Either way nothing transits LifeCare.ID®, and the node is the first and last place the raw value is stored.
- Expect interpreted outputs back, not your own values. An acknowledgement of a reading you submitted returns the day it was recorded and whether a related value was estimated, and not the reading.
- The declared action must be categorical. When an app contributes an
experience episode, the node refuses with
422anactionthat is empty or carries a digit. That is the only field the node screens for digits:contextandoutputare required by contract to carry derived signals only, never raw values, and the node does not check them today, so the discipline is yours. Design your enums before you send them. - There is no outcome field. An app contributes the decision, not its result, and no route is built for an app or agent to read back the outcome of its own episodes. That read-back is designed (the healthspannode outcome-substrate design, OC-8: a categorical verdict for the caller's own episodes) and not built.
- Raw provider payloads are buffered for twenty-four hours and purged; the derived series are what the node keeps. Do not assume the node can replay your raw history for you later.
- Erasure is key destruction. The person's key is destroyed and every payload encrypted under it becomes unreadable, on the node and in any backup. Your deletion story should match, not lag.
What zero egress does not mean. Not air-gapped: a node fetches its sources, serves interpreted outputs to the person's app, and talks to the authorization server about tokens, receipts and the registry. It is a statement about raw values, not about network traffic.
Honest limit, from the canonical page: the shared model server on the current node also lists a catalog entry that proxies to a cloud model, used by operator tooling. It sits outside the allowlist, so no agent can reach it, and moving it to a separate server so the allowlist becomes defence in depth is scheduled work.
Your obligations as an integrator: Security & compliance and Data sources & connectors. Mechanism and the three gates: zero-egress health AI ↗.
4. Attestation: verifying a node before you trust it
Every node publishes a signed claim about what it runs at GET {node}/v1/attestation,
unauthenticated on purpose: the point is that anyone can read it, including a
person who is not a member and a peer node that has never seen this one before.
Verify it programmatically, in five steps:
- Fetch
GET {node}/v1/attestation. No credential, no client registration. - Recompute the measurement over the published claim core as canonical JSON
(sorted keys, compact separators), with the
att-prefix. Only published fields are hashed, which is what makes recomputation possible; and every published claim is covered, which is what stops a relay from rewriting one. The exact recipe and a runnable snippet are on the canonical page. - Check the signature. ECDSA over SHA-256 with the node's P-256 key, whose
public half ships in the response as a JWK. Browser cryptography wants the raw
r||sform, so convert from the DER encoding the node emits. - Check whose key it is. Compute the key's RFC 7638 thumbprint (SHA-256
over
{"crv","kty","x","y"}, sorted, no whitespace, base64url without padding) and look it up inGET https://lifecare.id/v1/node-keys, the keys each node's operator has declared. Listed for the quote'snode_idas acurrentkey, or as aretiredkey with the quote'sgenerated_atbefore itsuntil, is key known. Arevokedkey fails outright, whatever date the quote carries:generated_atis the quote's own statement, outside the signature, and whoever holds a key can write any date on what it signs, sorevoked_fromis shown as information and opens no window. Anything else is key unknown. The zero-dependency library does this for you (classifyKey, andfetchAndVerifywith aregistryUrl). - Gate on the contents, not just the pass.
code_commit, themodelslist with their licences,params_versionandtwin_recipe_versionare the claim. A pass tells you the claim is intact and signed; you still decide whether that commit and those models are ones you will integrate with.
Do not skip step 4 into misplaced confidence. See the ceiling above. A pass with the key known proves internal consistency and that the key is the one the node's operator declared for it. It does not prove the machine runs what it claims, and the key list is the operator's declaration until node DIDs land 🛣️.
For your users, the browser verifier at lifecare.id/verify ↗
runs the same checks locally, with a "simulate tampering" switch that shows a
single changed character failing. Pass the node address (?node=<url>) for a
one-click link. The badge recipe is in Recipes; the endpoint and
the boundary are in
Security & compliance, under "Node transparency".
Mechanism, the published-fields rule, and why model licences sit inside the measurement: signed attestation ↗.
Why these pages, and not this one, are canonical
The four reference pages are maintained against the code that serves the network, republished with the site, and checked after every deploy; the API surface behind them is a published, drift-tested contract (openapi.json). A second description maintained by hand would drift from the first, and a trust document that has quietly drifted is worse than none. So this page frames and links, the canonical pages state, and the contract decides.
If a claim here and a claim there ever disagree, the canonical page wins and the disagreement is a bug. Report it through the channel your onboarding gave you.