Concepts
Everything you need to reason about a LifeCare.ID integration. If you read one page before coding, read this one.
Decentralized identifiers (DIDs)
Every person on the network is identified by a DID — a stable, resolvable identifier that is not an email, a username, or a phone number, and is not owned by any application.
- Method (MVP):
did:web, anchored under the home node's domain:did:web:<node-domain>:u:<id>. The<id>is the first 16 hex characters of the RFC 7638 JWK thumbprint of the user's public key, so the DID is deterministically derived from the key. - Resolution: any party can fetch the DID document — the public record of
the user's verification keys and service endpoints — without authentication
(
GET /v1/identity/resolve/{did}, or the standardGET /u/{user_id}/did.json). - Method abstraction: the resolver interface hides the method, so a future
migration to
did:ion(🛣️) won't change how your app resolves a DID.
A DID document looks like this:
{
"@context": ["https://www.w3.org/ns/did/v1"],
"id": "did:web:node0.example:u:9f2a1c4d8b3e0f57",
"verificationMethod": [{
"id": "did:web:node0.example:u:9f2a1c4d8b3e0f57#key-1",
"type": "JsonWebKey2020",
"controller": "did:web:node0.example:u:9f2a1c4d8b3e0f57",
"publicKeyJwk": { "kty": "EC", "crv": "P-256", "x": "…", "y": "…" }
}],
"authentication": ["…#key-1"],
"assertionMethod": ["…#key-1"],
"service": [
{ "id": "#uma", "type": "UMAAuthorization", "serviceEndpoint": ".../v1/token" },
{ "id": "#consent", "type": "ConsentService", "serviceEndpoint": ".../v1/consent" },
{ "id": "#rev", "type": "RevocationList2020","serviceEndpoint": ".../v1/revocation/{id}" }
]
}
The service block is how a generic agent discovers where to ask for a token,
manage consent, and check revocation for a given identity.
Key custody — the device holds the key
The user's private signing key (P-256) never leaves their device. It is generated and stored in the platform secure element (Apple Secure Enclave, Android Keystore) and exercised through a FIDO2 / WebAuthn passkey, biometric-gated.
LifeCare.ID is the DID authority, not the key holder: it derives the DID, publishes the DID document, hosts the revocation registry, and coordinates recovery — but it can never sign as the user. This means:
- Onboarding is a single passkey ceremony — no passwords, no form fields.
- Your app never sees, stores, or transmits a private key.
- Account recovery is human-anchored, not a password reset (see below).
Identity recovery
Because there is no password, recovery rotates the user to a new device key while keeping the same DID (so their consents and data survive):
- Email magic link (beta default) — control of a verified email is the trust anchor. The user enrolls a recovery email at onboarding; on device loss they request a link, create a new passkey, and a single-use token authorizes a key rotation.
- Trust-partner anchored (production) — a medical practice (or, in the MVP,
the node operator) verifies identity and approves the rotation. No email
dependency. The rotation endpoint (
POST /v1/identity/rotate) is fail-secure: with no trust-partner approval configured, rotation is refused.
Recovery is an end-user / operator concern; your app does not implement it, but should expect that a user's verification key can change over the DID's lifetime (always resolve the DID document fresh when verifying signatures).
The authorization server (AS)
The AS is the consent + token engine. Its implementation base is D-001: a lean, standards-shaped custom service (chosen over Medplum and Keycloak+UMA for arm64 fit and minimal lock-in). It speaks two standards-aligned patterns:
- OAuth 2.0 (authorization code + PKCE) for who is this user → see Authentication.
- UMA 2.0 pattern for may this requester read this scope for this purpose → see Authorization & tokens.
These are two different tokens for two different jobs — don't conflate them:
| Token | Issued by | Means | Presented to |
|---|---|---|---|
Session / access token (lcs_…) |
POST /token (federation) |
"this session belongs to DID X" | LifeCare.ID (e.g. the panel API) |
Data-access token / RPT (lc_…) |
POST /v1/token (UMA) |
"bearer may read scope S for purpose P" | the data source / resource server |
Scopes — data categories, never values
Consent is expressed over scopes, which name categories of data, never the data itself. The MVP vocabulary:
cgm · daily · labs · body_composition · recovery · activity
The vocabulary is shared through the published contract; resource holders and the
AS agree on it. Requesting cgm says "I want continuous-glucose data"; it never
carries a glucose value. See Scopes & consent.
Consent — the dual artifact
Every grant produces two things at once:
- A plain-language ledger entry the user can read ("ProForta can read your cgm, daily data until 2026-09-14. Purpose: weekly synthesis.").
- A machine-readable consent receipt — ISO/IEC TS 27560:2023-shaped, DID-signed, with a W3C DPV purpose term — for controllers and auditors.
Consent is purpose-bound: a grant authorizes a scope for a declared purpose. A token only issues against a grant whose purpose matches the request, so the same scope can be granted to two consumers for two purposes without collision.
The ledger — hash-chained, tamper-evident
Every event — grant, token issuance, data access, revocation — is appended to a
per-subject, hash-chained, append-only ledger. Each entry hashes the previous
one, so any tampering is detectable: the ledger read returns a chain_valid
flag. This is the audit trail the user sees in the consent panel, and it is
exportable. Resource holders are expected to record each access here (
POST /v1/access/record) — this is what makes data use auditable end to end.
Revocation — fast and public
Revocation is one tap for the user. The targets:
- dependent access tokens invalidated in < 500 ms,
- the public, DID-anchored revocation registry updated in < 5 s.
Resource servers and token validators check the registry / introspection endpoint, so a revoked grant stops working everywhere quickly. See Authorization & tokens.
The zero-PHI invariant (binding on you too)
LifeCare.ID never receives, caches, relays, or logs a health-data value — and no field of any request or response you send it may carry one. Scopes name categories; purposes are short declared strings; free text must not smuggle values. Raw data flows directly between the data source and your app, never through LifeCare.ID. This is both the platform's guarantee and your contractual obligation — see Security & compliance.
Glossary
| Term | Meaning |
|---|---|
| DID | Decentralized identifier; the user's stable, resolvable identity |
| DID document | Public record of a DID's keys + service endpoints |
| RP / relying party | An app that authenticates users via LifeCare.ID |
| AS | Authorization server — LifeCare.ID's consent + token engine |
| PKCE | Proof Key for Code Exchange; secures the OAuth code flow |
| UMA | User-Managed Access; the pattern for purpose-bound data tokens |
| RPT | Requesting Party Token; the data-access token from /v1/token |
| DPV | W3C Data Privacy Vocabulary; types the purpose of a grant |
| Receipt | ISO 27560-shaped, signed record of a consent grant |
| Ledger | Hash-chained, append-only audit trail per DID |
| Trust partner | The human anchor that approves identity recovery |
| Scope | A data category (e.g. cgm) — never a data value |