> For the complete documentation index, see [llms.txt](https://docs.openg2p.org/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.openg2p.org/platform/platform-services/consent-management/design/data-model.md).

# Data model

The canonical entities the Consent Manager stores, and the JSON-LD documents it exchanges — consent object, auth context, consent artefact, and the Kantara/ISO-27560-aligned consent receipt.

The Consent Manager keeps two kinds of structures:

* **Stored entities** — relational records it owns (partners, policies, artefacts, receipts, logs).
* **Exchanged documents** — JSON-LD documents passed in/out (the consent object a partner embeds, the ID token from the IdP, and the artefact + receipt the CM issues).

All status transitions are **append-only**: a new status and timestamp are written, prior timestamps are never overwritten, and the decision log is immutable.

## Entity overview

```mermaid
erDiagram
  PARTNER ||--o{ PARTNER_POLICY : "has (versioned)"
  PARTNER ||--o{ CONSENT_ARTEFACT : "is audience of"
  CONSENT_REQUEST ||--o| AUTH_CONTEXT : "bound by (origination)"
  CONSENT_REQUEST ||--o| CONSENT_ARTEFACT : produces
  CONSENT_ARTEFACT ||--|| CONSENT_RECEIPT : "proven by"
  CONSENT_ARTEFACT ||--o{ REVOCATION_RECORD : "may be revoked by"
  CONSENT_ARTEFACT ||--o{ DECISION_LOG : "evaluated in"
```

### Partner (policy binding)

Partner **identity and signing keys live in the Partner Management (PM) service**, not in the CM. The CM's `Partner` record is a **thin policy binding** — it maps a PM partner to a CM authorization context and carries nothing more. There is **no local `PartnerKey` table** and the CM does **not** poll a `jwks_url`; at verification time the CM fetches the partner's public key (PEM) from PM by `partner_mgmt_id` + the object's `kid`. See [Partner Management Integration](/platform/platform-services/consent-management/design/partner-management-integration.md).

A partner (`audience`) can be bound to **several controllers**: one row per (`audience`, `controller_id`), unique on that pair, each with its own policy. All bindings of one audience share the same `partner_mgmt_id`.

| Field                     | Type     | Notes                                                                                                                                                          |
| ------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| partner\_id               | UUID     | Primary key (the CM-local binding id)                                                                                                                          |
| partner\_mgmt\_id         | str      | Reference to the partner in PM; inherited from the audience's other bindings, else falls back to `audience`. Changing it updates every binding of the audience |
| name                      | str      | Optional display name, for the admin UI only                                                                                                                   |
| status                    | enum     | `active`, `suspended` — whether this binding is active in the CM (per binding)                                                                                 |
| audience                  | str      | The `aud` value the partner's consent objects must carry (not unique on its own)                                                                               |
| controller\_id            | str      | The data controller (registry) this binding is for; unique per audience                                                                                        |
| created\_at / updated\_at | datetime |                                                                                                                                                                |

### PartnerPolicy

The data-share contract a partner was onboarded under. **Consent can never grant more than the policy.** Versioned; every decision records the version used, and the hot path only evaluates the `active` version. A **widening** change sits `pending` until approved via the [Approval Workflow Engine](/platform/platform-services/consent-management/design/approval-workflow-integration.md), then becomes `active` and supersedes the prior version; a rejected/cancelled request marks the new version `rejected` and the prior active version stays in force. At most one version per binding is `pending`; an approval that arrives after the active version changed ends `stale` (not applied), and a failed submission ends `failed` — see the [state machine](/platform/platform-services/consent-management/design/approval-workflow-integration.md#policy-version-state-machine).

| Field                       | Type       | Notes                                                                                                                  |
| --------------------------- | ---------- | ---------------------------------------------------------------------------------------------------------------------- |
| policy\_id                  | UUID       | Primary key                                                                                                            |
| partner\_id                 | UUID       | FK → Partner (the binding), so a policy is per (audience, controller)                                                  |
| version                     | int        | Monotonic; the active version is evaluated                                                                             |
| allowed\_data\_scopes       | list\[str] | Fields/registers the partner may ever receive                                                                          |
| allowed\_purposes           | list\[str] | Purpose codes the partner may assert                                                                                   |
| allowed\_subject\_id\_types | list\[str] | e.g. `national_id`, `farmer_id`                                                                                        |
| allowed\_signing\_algs      | list\[str] | Acceptable JWS algorithms                                                                                              |
| max\_validity\_duration     | duration   | ISO-8601 upper bound on consent validity (e.g. `P1Y`)                                                                  |
| fetch\_type                 | enum       | `oneshot`, `periodic` (DEPA-style)                                                                                     |
| max\_fetch\_frequency       | duration   | For `periodic` — minimum interval between fetches                                                                      |
| data\_life                  | duration   | How long the partner may retain data after fetch                                                                       |
| status                      | enum       | `pending`, `active`, `superseded`, `rejected`, `failed`, `stale` (at most one `pending` per binding)                   |
| awe\_request\_id            | str        | The AWE request id for the approval of a widening change (null for narrowing / AWE-disabled)                           |
| base\_version               | int        | Version that was active when this one was created (`0` = none); an approval applies only if it is still the active one |
| status\_reason              | text       | Why the version ended `failed` / `stale` / `rejected`                                                                  |
| effective\_from             | datetime   |                                                                                                                        |

### ConsentArtefact (CM-issued, canonical)

The CM's canonical representation of a consent decision — whether derived from the verified claims of a partner-embedded consent JWS or produced by the origination flow.

An **embedded** consent yields **one artefact per (`jti`, `data_controller`)** — a consent with grants for two registries, validated by both, has two artefacts (and two receipts), each revocable on its own. An **originated** consent yields one artefact per approved request; if the request carried grants, the approved grants are stored on it.

| Field                                   | Type       | Notes                                                                                                                                   |
| --------------------------------------- | ---------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| consent\_id                             | UUID       | Primary key                                                                                                                             |
| subject\_id\_type / subject\_id\_value  | str        | The data subject                                                                                                                        |
| controller\_id                          | str        | Data controller (registry). Null for an originated consent with several approved grants                                                 |
| partner\_id                             | UUID       | Audience / data recipient                                                                                                               |
| purpose                                 | json       | `{code, text}`                                                                                                                          |
| data\_scopes                            | list\[str] | Consented fields for this controller's grant (pre-policy-intersection)                                                                  |
| effective\_data\_scopes                 | list\[str] | `data_scopes ∩ policy.allowed_data_scopes` at issue time                                                                                |
| grants                                  | json       | Originated consents only: `[{data_controller, data_scopes, granted_scopes, effective_data_scopes, partner_binding_id, policy_version}]` |
| object\_jti                             | str        | The embedded consent's `jti`; unique together with `controller_id`                                                                      |
| valid\_from / valid\_until              | datetime   |                                                                                                                                         |
| fetch\_type                             | enum       | `oneshot`, `periodic`                                                                                                                   |
| auth\_context\_id                       | UUID       | FK → AuthContext (origination) or `null` (embedded, partner-attested)                                                                   |
| source                                  | enum       | `embedded` (partner-signed) or `originated` (CM-collected)                                                                              |
| policy\_version                         | int        | Policy version evaluated at issue                                                                                                       |
| status                                  | enum       | `active`, `revoked`, `expired`                                                                                                          |
| created\_at / revoked\_at / expired\_at | datetime   | Append-only                                                                                                                             |

### AuthContext (origination flow)

Created after the CM validates an ID token. The raw token is **never** persisted — only its hash.

| Field                | Type     | Notes                                          |
| -------------------- | -------- | ---------------------------------------------- |
| auth\_context\_id    | UUID     | Primary key                                    |
| consent\_request\_id | UUID     | FK → ConsentRequest                            |
| auth\_provider       | str      | e.g. `keycloak`                                |
| auth\_method         | str      | `otp`, `biometric`, … (from `amr`)             |
| auth\_timestamp      | datetime |                                                |
| issuer               | str      | `iss`                                          |
| id\_token\_hash      | str      | `sha256:…`                                     |
| token\_validated     | bool     |                                                |
| verified\_claims     | json     | `iss, sub, aud, amr, iat, exp, auth_time, kid` |

### ConsentReceipt

Kantara/ISO-27560-aligned, **signed with the CM's own `.p12` key**. The CM is self-verifying — it publishes its signing public keys at `GET /.well-known/jwks.json`, so any party can verify a receipt without PM. (The CM is **not** a PM partner.) See the JSON below.

| Field          | Type     | Notes                                                            |
| -------------- | -------- | ---------------------------------------------------------------- |
| receipt\_id    | UUID     | Primary key                                                      |
| consent\_id    | UUID     | FK → ConsentArtefact                                             |
| artefact\_hash | str      | `sha256:…` over the canonical artefact                           |
| algorithm      | enum     | `EdDSA` (ed25519), `ES256`, or `RS256` — the CM `.p12` key's alg |
| kid            | str      | CM key id used to sign                                           |
| signature      | text     | Detached/compact JWS over the artefact hash                      |
| version        | str      | Receipt schema version                                           |
| issued\_at     | datetime |                                                                  |

### ConsentRequest · RevocationRecord · DecisionLog

| Entity                            | Key fields                                                                                                                                                                                                                                                                 |
| --------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **ConsentRequest** (origination)  | request\_id, subject, controller\_id (null when several grants), partner\_id, requested\_scopes (union), grants (`[{data_controller, data_scopes, partner_binding_id}]` or null), purpose, status (`pending`/`approved`/`denied`/`expired`), timestamps                    |
| **RevocationRecord**              | revocation\_id, consent\_id, originated\_by (`subject`/`controller`/`partner`), reason, created\_at                                                                                                                                                                        |
| **DecisionLog** (immutable)       | decision\_id, consent\_id (nullable on deny), partner\_id, object\_jti, data\_controller (when known), request\_ctx\_hash, decision, reason\_code, policy\_version, receipt\_jti / receipt\_issuer (decisions on an Exchange consent token; null otherwise), evaluated\_at |
| **IssuedReceipt** (exchange role) | A consent token issued: jti, consent\_id (the artefact it was issued from), data\_controller, presenter, partner, expires\_at — see [Exchange consent tokens](/platform/platform-services/consent-management/design/exchange-receipts.md)                                  |

***

## Exchanged documents (JSON-LD)

### ID Token

Received from the identity provider after subject authentication (origination flow).

```json
{
  "@context": "https://openg2p.org/contexts/id_token.jsonld",
  "@type": "IDToken",
  "iss": "https://idp.example.org",
  "sub": "FARMER_1234",
  "aud": "my.registry.org",
  "amr": ["otp"],
  "iat": 1714555200,
  "exp": 1714558800,
  "auth_time": 1714555190,
  "kid": "key-2025-01",
  "signature": "BASE64URL(JWS_signature)"
}
```

### Consent object (partner-embedded, the primary flow)

The **partner signs this as a compact JWS** (RFC 7515) and embeds it in the registry request. The claims below are the **JWS payload**; the JWS protected header carries `alg` + `kid`. The CM recovers the claims from the payload and verifies the signature using the partner's public key **fetched from Partner Management** by `partner_mgmt_id` + the JWS `kid` (see [Partner Management Integration](/platform/platform-services/consent-management/design/partner-management-integration.md)). `jti` + `issued_at` give replay protection.

The data to share is given as **`grants`** — one consent, one grant per registry. The earlier single-registry form (`data_controller` + `data_scopes` at the top level) is still accepted and treated as one grant; a consent may not mix the two.

```json
// JWS payload (claims). Wire form: base64url(header).base64url(payload).base64url(signature)
{
  "@context": "https://openg2p.org/contexts/consent_object.jsonld",
  "@type": "ConsentObject",
  "jti": "b2f1...-unique-per-object",
  "subject_id": { "type": "national_id", "value": "FARMER_1234" },
  "partner_system": "PARTNER_SYSTEM_A",
  "aud": "PARTNER_SYSTEM_A",
  "purpose": { "code": "share_farm_profile", "text": "Share farmer profile with Partner A" },
  "grants": [
    { "data_controller": "farmer-registry",
      "data_scopes": ["farmer_profile.basic", "farmer_profile.crops", "farmer_profile.landholdings"] },
    { "data_controller": "crop-sown-registry", "data_scopes": ["crop_season", "crops_sown"] }
  ],
  "fetch_type": "oneshot",
  "validity": { "valid_from": "2025-05-01T12:00:00Z", "valid_until": "2026-05-01T12:00:00Z" },
  "issued_at": "2025-05-01T11:59:50Z"
}
```

There is no inline `signature` field — the JWS wrapper carries it (header `alg`/`kid` + the signature segment).

### Auth Context

Generated by the CM after validating the ID token (origination flow).

```json
{
  "@context": "https://openg2p.org/contexts/auth_context.jsonld",
  "@type": "AuthContext",
  "auth_provider": "keycloak",
  "auth_method": "otp",
  "auth_timestamp": "2025-05-01T12:01:22Z",
  "issuer": "https://idp.example.org",
  "subject_id": "FARMER_1234",
  "kid": "key-2025-01",
  "id_token_hash": "sha256:ab349faa12bc908d...",
  "token_validated": true,
  "verified_claims": {
    "iss": "https://idp.example.org", "sub": "FARMER_1234", "aud": "my.registry.org",
    "amr": ["otp"], "iat": 1714555200, "exp": 1714558800, "auth_time": 1714555190, "kid": "key-2025-01"
  }
}
```

### Consent Artefact

The CM's canonical decision document. Stored, and checked before any data is released.

```json
{
  "@context": "https://openg2p.org/contexts/consent_artefact.jsonld",
  "@type": "ConsentArtefact",
  "consent_id": "CONSENT-123456",
  "subject_id": "FARMER_1234",
  "data_controller": "farmer-registry",
  "partner_system": "PARTNER_SYSTEM_A",
  "source": "embedded",
  "purpose": { "code": "share_farm_profile", "text": "Share farmer profile with Partner A" },
  "data_scopes": ["farmer_profile.basic", "farmer_profile.crops", "farmer_profile.landholdings"],
  "effective_data_scopes": ["farmer_profile.basic", "farmer_profile.crops"],
  "fetch_type": "oneshot",
  "policy_version": 3,
  "validity": { "consent_timestamp": "2025-05-01T12:02:10Z", "expiry_timestamp": "2026-05-01T12:02:10Z" },
  "auth_context": { "...": "Auth Context (origination) or omitted for partner-attested embedded consent" }
}
```

> This is the Farmer Registry's artefact for the consent above: its grant only. `effective_data_scopes` excludes `farmer_profile.landholdings` because the partner's policy for this registry did not allow it — data minimisation enforced at the point of decision. An originated consent with several approved grants carries them under `grants` instead.

### Consent Receipt (Kantara / ISO 27560)

Generated alongside the artefact and **signed with the CM's own `.p12` key**. It is both cryptographic proof and a human-readable consent record for the subject, audit, and disputes.

```json
{
  "@context": "https://openg2p.org/contexts/consent_receipt.jsonld",
  "@type": "ConsentReceipt",
  "receipt_id": "RECEIPT-998877",
  "version": "1.1",
  "issued_at": "2025-05-01T12:02:12Z",
  "jurisdiction": "IN",
  "data_controller": {
    "id": "farmer-registry",
    "name": "National Farmer Registry",
    "contact": "dpo@registry.org",
    "dpo": "Data Protection Officer"
  },
  "data_controllers": [ { "id": "farmer-registry" } ],
  "subject_id": "FARMER_1234",
  "purposes": [
    { "code": "share_farm_profile", "text": "Share farmer profile with Partner A",
      "legal_basis": "consent", "retention": "P0D-after-fetch" }
  ],
  "data_categories": ["farmer_profile.basic", "farmer_profile.crops"],
  "sensitive": false,
  "third_parties": ["PARTNER_SYSTEM_A"],
  "withdrawal": { "method": "POST /consent/v1/consents/CONSENT-123456/revoke" },
  "consent_artefact": { "@id": "CONSENT-123456", "hash": "sha256:7b0a933e7cd398a..." },
  "signature": {
    "algorithm": "EdDSA", "kid": "registry-2025-01",
    "value": "BASE64URL(cm_private_key.sign(consent_artefact_hash))"
  }
}
```

`data_controllers` lists every controller the receipt covers: the one controller for an embedded consent (one receipt per registry), or one entry per approved grant — `{id, data_categories}` — for an originated consent with grants, where `data_controller` is null if there are several.

The CM publishes its signing public keys at `GET /.well-known/jwks.json` so any party can verify a receipt independently.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://docs.openg2p.org/platform/platform-services/consent-management/design/data-model.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
