> ## Documentation Index
> Fetch the complete documentation index at: https://docs.phosra.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Core objects & data model

> The resource graph that runs Phosra — Family, Child, ChildPolicy, PolicyRule, EnforcementJob, EnforcementResult, and the Verdict — with cardinalities, lifecycle states, and links to every reference page.

Six objects carry every parental-controls integration, plus one more that lives on the
enforcement path. This page is the **map**: how they nest, how many of each you get, what
states they move through, and where each one's field-by-field reference lives. Read it once
and the rest of the API reference falls into place.

Every shape below is taken directly from the [OpenAPI spec](/openapi.yaml) — the same
document that generates the SDKs and the API reference. Where a field is an `enum`, the
allowed values are the real ones the census accepts.

## The resource graph

<Frame caption="The Phosra core object model. A Family owns Children and FamilyMembers; each Child owns ChildPolicies; each policy owns PolicyRules. Enforcing a policy creates an EnforcementJob that fans out to one EnforcementResult per platform. On the data plane, each rule compiles to a Verdict that the gatekeeper reads locally, and confirming it writes an EnforcementReceipt.">
  <img className="block dark:hidden" src="https://mintcdn.com/phosra/o0ansqUg4hqAQihW/images/data-model-light.svg?fit=max&auto=format&n=o0ansqUg4hqAQihW&q=85&s=59c8420d51f3848d102c6de0e36d5947" alt="Entity-relationship diagram: Family 1—many Child and 1—many FamilyMember; Child 1—many ChildPolicy; ChildPolicy 1—many PolicyRule and 1—many EnforcementJob; EnforcementJob 1—many EnforcementResult (one per platform); each PolicyRule compiles to a Verdict/CategoryEntry, which on confirm() produces an EnforcementReceipt." width="960" height="812" data-path="images/data-model-light.svg" />

  <img className="hidden dark:block" src="https://mintcdn.com/phosra/o0ansqUg4hqAQihW/images/data-model-dark.svg?fit=max&auto=format&n=o0ansqUg4hqAQihW&q=85&s=6174edf2db7cd2b7fdd82a315fa209c9" alt="Entity-relationship diagram: Family 1—many Child and 1—many FamilyMember; Child 1—many ChildPolicy; ChildPolicy 1—many PolicyRule and 1—many EnforcementJob; EnforcementJob 1—many EnforcementResult (one per platform); each PolicyRule compiles to a Verdict/CategoryEntry, which on confirm() produces an EnforcementReceipt." width="960" height="812" data-path="images/data-model-dark.svg" />
</Frame>

Read each edge as **"one ... has many ..."**:

| Relationship                       | Cardinality              | Foreign key                     | Meaning                                                                        |
| ---------------------------------- | ------------------------ | ------------------------------- | ------------------------------------------------------------------------------ |
| Family → Child                     | **1 — ∗**                | `child.family_id`               | A household holds one profile per kid.                                         |
| Family → FamilyMember              | **1 — ∗**                | `member.family_id`              | The guardians/parents on the account.                                          |
| Child → ChildPolicy                | **1 — ∗**                | `policy.child_id`               | A kid can have several policies (e.g. *School Day*, *Weekend*); one is active. |
| ChildPolicy → PolicyRule           | **1 — ∗**                | `rule.policy_id`                | A policy is a set of rules drawn from the 123 OCSS categories.                 |
| ChildPolicy → EnforcementJob       | **1 — ∗**                | `job.policy_id`, `job.child_id` | Each push of a policy to platforms is a job.                                   |
| EnforcementJob → EnforcementResult | **1 — ∗**                | `result.enforcement_job_id`     | One result row **per connected platform** the job fanned out to.               |
| PolicyRule → Verdict               | **1 — 1** *(derived)*    | `verdict.rule_ref`              | Each active rule compiles to one per-category decision in the signed profile.  |
| Verdict → EnforcementReceipt       | **1 — 1** *(on confirm)* | `receipt.rule_ref`              | Confirming an applied verdict writes one signed receipt.                       |

<Note>
  **Two planes, one graph.** The green objects (Family → PolicyRule) and the blue objects
  (EnforcementJob, EnforcementResult) live in the **[control/product plane](/concepts/architecture)** —
  normal REST resources you read and write with a `phosra_` key. The violet **Verdict** and
  **EnforcementReceipt** live on the **[data plane](/concepts/enforcement)**: they are compiled
  into a signed profile and evaluated *locally, in-process* by the gatekeeper — there is no
  hosted `POST /check`. The `rule_ref` field is the thread that stitches a product-plane
  `PolicyRule` to its data-plane `Verdict` and back to a receipt.
</Note>

## Objects

<AccordionGroup>
  <Accordion title="Family — the household root" icon="house" defaultOpen>
    The top of the graph. Everything else hangs off a family, directly or transitively.

    | Field        | Type        | Notes                          |
    | ------------ | ----------- | ------------------------------ |
    | `id`         | `uuid`      | Primary key.                   |
    | `name`       | `string`    | Display name of the household. |
    | `created_at` | `date-time` | RFC 3339.                      |

    ```json Family theme={null}
    {
      "id": "d3b07384-d9a0-4c9b-8f2e-2f1a6b0c1a20",
      "name": "The Rivera Family",
      "created_at": "2026-07-06T14:03:11Z"
    }
    ```

    **Reference:** [Create a family](/api-reference/families/create-family) ·
    [List families](/api-reference/families/get-families) ·
    concept → [Families](/concepts/families)
  </Accordion>

  <Accordion title="FamilyMember — a guardian on the account" icon="user">
    The people who can manage a family. Distinct from `Child`.

    | Field       | Type        | Notes                            |
    | ----------- | ----------- | -------------------------------- |
    | `id`        | `uuid`      | Primary key.                     |
    | `family_id` | `uuid`      | Foreign key → `Family`.          |
    | `user_id`   | `uuid`      | The underlying user.             |
    | `role`      | `enum`      | `owner` · `parent` · `guardian`. |
    | `joined_at` | `date-time` | RFC 3339.                        |

    ```json FamilyMember theme={null}
    {
      "id": "b8c9e1a2-3f45-4d67-89ab-cdef01234567",
      "family_id": "d3b07384-d9a0-4c9b-8f2e-2f1a6b0c1a20",
      "user_id": "6a1f0e2d-7c8b-4a9e-b0c1-d2e3f4a5b6c7",
      "role": "owner",
      "joined_at": "2026-07-06T14:03:11Z"
    }
    ```

    **Reference:** [Add a member](/api-reference/families/add-member) ·
    [List members](/api-reference/families/list-members)
  </Accordion>

  <Accordion title="Child — a kid's profile" icon="child">
    A profile within a family, keyed by birth date. Phosra derives the age group (and thus the
    age-appropriate rule defaults) from `birth_date`.

    | Field        | Type        | Notes                                                                    |
    | ------------ | ----------- | ------------------------------------------------------------------------ |
    | `id`         | `uuid`      | Primary key.                                                             |
    | `family_id`  | `uuid`      | Foreign key → `Family`.                                                  |
    | `name`       | `string`    | Display name.                                                            |
    | `birth_date` | `date`      | `YYYY-MM-DD`. Drives age-group defaults and the ≥13 write-notice branch. |
    | `avatar_url` | `string`    | Optional profile image.                                                  |
    | `created_at` | `date-time` | RFC 3339.                                                                |

    ```json Child theme={null}
    {
      "id": "a11ce0fa-0000-4000-8000-0000000000a1",
      "family_id": "d3b07384-d9a0-4c9b-8f2e-2f1a6b0c1a20",
      "name": "Mia",
      "birth_date": "2013-01-15",
      "avatar_url": null,
      "created_at": "2026-07-06T14:03:11Z"
    }
    ```

    **Reference:** [Add a child](/api-reference/children/add-child) ·
    [Get a child](/api-reference/children/get-child) ·
    [Age ratings](/api-reference/children/get-child-age-ratings) ·
    concept → [Children & Age Groups](/concepts/children-and-age)
  </Accordion>

  <Accordion title="ChildPolicy — a named set of rules" icon="shield">
    The container a child's rules live in. A child may own several; policies carry a lifecycle
    `status` and a `priority` for ordering.

    | Field        | Type        | Notes                                                          |
    | ------------ | ----------- | -------------------------------------------------------------- |
    | `id`         | `uuid`      | Primary key.                                                   |
    | `child_id`   | `uuid`      | Foreign key → `Child`.                                         |
    | `name`       | `string`    | Display name (e.g. *School Day*).                              |
    | `status`     | `enum`      | `draft` · `active` · `paused`. Only `active` policies enforce. |
    | `priority`   | `integer`   | Ordering weight when multiple policies apply; higher wins.     |
    | `version`    | `integer`   | Bumped on every change.                                        |
    | `created_at` | `date-time` | RFC 3339.                                                      |

    ```json ChildPolicy theme={null}
    {
      "id": "b011c1e5-0000-4000-8000-000000000b01",
      "child_id": "a11ce0fa-0000-4000-8000-0000000000a1",
      "name": "School Day",
      "status": "active",
      "priority": 10,
      "version": 3,
      "created_at": "2026-07-06T14:03:11Z"
    }
    ```

    **Reference:** [Create a policy](/api-reference/policies/create-policy) ·
    [Generate rules from age](/api-reference/policies/generate-policy-from-age) ·
    [Activate](/api-reference/policies/activate-policy) ·
    [Pause](/api-reference/policies/pause-policy) ·
    concept → [Policies & Rules](/concepts/policies-and-rules)
  </Accordion>

  <Accordion title="PolicyRule — one category decision" icon="list-check">
    A single rule inside a policy: which OCSS category, whether it's on, and its category-specific
    config. There are 123 categories — see the [Rule Categories reference](/concepts/policies-and-rules).

    | Field        | Type        | Notes                                |
    | ------------ | ----------- | ------------------------------------ |
    | `id`         | `uuid`      | Primary key.                         |
    | `policy_id`  | `uuid`      | Foreign key → `ChildPolicy`.         |
    | `category`   | `enum`      | One of the 123 OCSS rule categories. |
    | `enabled`    | `boolean`   | Whether this rule is active.         |
    | `config`     | `object`    | Category-specific configuration.     |
    | `created_at` | `date-time` | RFC 3339.                            |

    ```json PolicyRule theme={null}
    {
      "id": "c4f1e2d3-0000-4000-8000-0000000000c1",
      "policy_id": "b011c1e5-0000-4000-8000-000000000b01",
      "category": "content_rating",
      "enabled": true,
      "config": { "max_ratings": { "mpaa": "PG-13", "tv": "TV-Y7", "esrb": "E" } },
      "created_at": "2026-07-06T14:03:11Z"
    }
    ```

    **Reference:** [List policy rules](/api-reference/rules/list-policy-rules) ·
    [Bulk upsert rules](/api-reference/rules/bulk-upsert-rules) ·
    [Update a rule](/api-reference/rules/update-rule)
  </Accordion>

  <Accordion title="EnforcementJob — one push to platforms" icon="rocket">
    Created when you enforce a policy. It fans out to every connected platform in parallel.

    | Field          | Type                | Notes                                                       |
    | -------------- | ------------------- | ----------------------------------------------------------- |
    | `id`           | `uuid`              | Primary key.                                                |
    | `child_id`     | `uuid`              | Foreign key → `Child`.                                      |
    | `policy_id`    | `uuid`              | Foreign key → `ChildPolicy`.                                |
    | `trigger_type` | `enum`              | `manual` · `auto` · `webhook`.                              |
    | `status`       | `enum`              | `pending` · `running` · `completed` · `failed` · `partial`. |
    | `started_at`   | `date-time \| null` | Set when the job starts running.                            |
    | `completed_at` | `date-time \| null` | Set when it finishes.                                       |
    | `created_at`   | `date-time`         | RFC 3339.                                                   |

    ```json EnforcementJob theme={null}
    {
      "id": "e5a2b3c4-0000-4000-8000-0000000000e1",
      "child_id": "a11ce0fa-0000-4000-8000-0000000000a1",
      "policy_id": "b011c1e5-0000-4000-8000-000000000b01",
      "trigger_type": "manual",
      "status": "completed",
      "started_at": "2026-07-06T14:05:02Z",
      "completed_at": "2026-07-06T14:05:04Z",
      "created_at": "2026-07-06T14:05:02Z"
    }
    ```

    **Reference:** [Trigger enforcement](/api-reference/enforcement/post-children-enforce) ·
    [List jobs](/api-reference/enforcement/list-enforcement-jobs) ·
    [Job results](/api-reference/enforcement/get-enforcement-job-results) ·
    concept → [Enforcement](/concepts/enforcement)
  </Accordion>

  <Accordion title="EnforcementResult — per-platform outcome" icon="clipboard-check">
    One row **per connected platform** in a job. This is what you poll to verify enforcement —
    there is no push webhook on job completion.

    | Field                | Type                | Notes                                                                    |
    | -------------------- | ------------------- | ------------------------------------------------------------------------ |
    | `id`                 | `uuid`              | Primary key.                                                             |
    | `enforcement_job_id` | `uuid`              | Foreign key → `EnforcementJob`.                                          |
    | `compliance_link_id` | `uuid`              | Foreign key → the compliance link this result is attributed to.          |
    | `platform_id`        | `string`            | The target platform (e.g. `nextdns`).                                    |
    | `status`             | `enum`              | `pending` · `running` · `completed` · `failed` · `partial`.              |
    | `rules_applied`      | `integer`           | Rules successfully applied.                                              |
    | `rules_skipped`      | `integer`           | Rules the platform can't express (see [Platforms](/concepts/platforms)). |
    | `rules_failed`       | `integer`           | Rules that errored.                                                      |
    | `error_message`      | `string \| null`    | Detail on failure; `null` on success.                                    |
    | `details`            | `object`            | Free-form provider response payload captured for debugging.              |
    | `started_at`         | `date-time \| null` | RFC 3339 timestamp of when the result started running.                   |
    | `completed_at`       | `date-time \| null` | RFC 3339 timestamp of when it finished.                                  |

    ```json EnforcementResult theme={null}
    {
      "id": "f6b3c4d5-0000-4000-8000-0000000000f1",
      "enforcement_job_id": "e5a2b3c4-0000-4000-8000-0000000000e1",
      "compliance_link_id": "c7d8e9f0-0000-4000-8000-0000000000c1",
      "platform_id": "nextdns",
      "status": "completed",
      "rules_applied": 5,
      "rules_skipped": 2,
      "rules_failed": 0,
      "error_message": null,
      "details": { "provider_status": 200 },
      "started_at": "2026-07-06T14:05:02Z",
      "completed_at": "2026-07-06T14:05:04Z"
    }
    ```

    **Reference:** [Get job results](/api-reference/enforcement/get-enforcement-job-results) ·
    [Retry a job](/api-reference/enforcement/retry-enforcement-job)
  </Accordion>

  <Accordion title="Verdict — the gatekeeper's local decision" icon="scale-balanced">
    On the data plane, each active rule compiles into a **`CategoryEntry`** inside the signed
    §8.3.6 enforcement profile. The `@phosra/gatekeeper` SDK reads it with `gk.isAllowed({ category })`
    — **entirely locally, zero network** — and returns a **Verdict**.

    | Field       | Type     | Notes                                                                          |
    | ----------- | -------- | ------------------------------------------------------------------------------ |
    | `category`  | `slug`   | OCSS rule category.                                                            |
    | `decision`  | `enum`   | `allow` · `warn` · `block`.                                                    |
    | `fail_mode` | `enum`   | `open` · `closed`. `closed` = must block if the profile is missing/expired.    |
    | `rule_ref`  | `string` | Per-child opaque handle; echo it back to `confirm()`. `null` when fail-closed. |
    | `rule_slug` | `string` | Registry slug, for logging.                                                    |

    ```json Verdict (CategoryEntry) theme={null}
    {
      "category": "addictive_pattern_block",
      "decision": "block",
      "fail_mode": "closed",
      "rule_ref": "rr_9f3c...opaque",
      "rule_slug": "addictive_pattern_block"
    }
    ```

    <Info>
      **One field, two spellings.** The wire/profile names this `fail_mode` with values
      `open`/`closed`; the gatekeeper SDK surfaces the same signal as `failMode` with `allow`/`block`.
      `fail_mode:"closed"` ⇔ `failMode:"block"`. See [Platform integration](/integration/platform).
    </Info>

    **Reference:** [Fetch profile](/api-reference/data-plane/fetch-profile) ·
    [Platform integration](/integration/platform) · glossary → [Verdict](/glossary#verdict)
  </Accordion>

  <Accordion title="EnforcementReceipt — the signed apply attestation" icon="file-signature">
    After enforcing (or refusing) a verdict, the platform calls `verdict.confirm(...)`, which
    signs and POSTs a §8.3.8 `enforcement_result` receipt. It is **identity-free by construction**
    — no child ref, device id, or user id. The inner body is exactly five fields.

    | Field               | Type        | Notes                                                                                   |
    | ------------------- | ----------- | --------------------------------------------------------------------------------------- |
    | `envelope_ref`      | `string`    | Idempotency key of the carried envelope.                                                |
    | `rule_ref`          | `string`    | The same `rule_ref` from the Verdict — ties the receipt to the rule.                    |
    | `enforcement_state` | `enum`      | `applied` · `degraded` · `refused`.                                                     |
    | `applied_at`        | `date-time` | RFC 3339.                                                                               |
    | `method_class`      | `string`    | Mechanism of enforcement (e.g. `dns_block`, `platform_gate`). Non-empty when `applied`. |

    ```json EnforcementResultBody (signed inner body) theme={null}
    {
      "envelope_ref": "env_01JT...",
      "rule_ref": "rr_9f3c...opaque",
      "enforcement_state": "applied",
      "applied_at": "2026-07-06T14:05:04Z",
      "method_class": "dns_block"
    }
    ```

    **Reference:** [Submit confirmation](/api-reference/data-plane/submit-confirmation) ·
    glossary → [Receipt](/glossary#receipt)
  </Accordion>
</AccordionGroup>

## Lifecycle & state

Three objects in the graph are state machines. Knowing the transitions saves you a class of
"why isn't this enforcing?" bugs.

### ChildPolicy — `draft → active → paused`

<Steps>
  <Step title="draft" icon="pencil">
    Created but not enforcing. Build up rules here. `POST /policies` starts a policy in
    `draft` unless you activate it.
  </Step>

  <Step title="active" icon="circle-check">
    **The only status that enforces.** `POST /policies/{id}/activate`. When a child has
    multiple active policies, `priority` (higher wins) breaks the tie.
  </Step>

  <Step title="paused" icon="circle-pause">
    Temporarily off without deleting. `POST /policies/{id}/pause`. Re-activate any time.
  </Step>
</Steps>

### EnforcementJob — `pending → running → completed | failed | partial`

<Steps>
  <Step title="pending → running">
    The job is created (`202 Accepted`) and begins fanning out to platforms.
  </Step>

  <Step title="completed">
    Every `EnforcementResult` succeeded.
  </Step>

  <Step title="partial">
    Some platforms applied, some didn't — inspect `rules_skipped` / `rules_failed` per result.
    Common when a platform can't express a category (`manual_attested`; see [Platforms](/concepts/platforms)).
  </Step>

  <Step title="failed">
    The job as a whole failed. [Retry it](/api-reference/enforcement/retry-enforcement-job).
  </Step>
</Steps>

### Verdict standing — `allow · warn · block` (fail-closed)

A verdict is not a stored row that transitions; it is **recomputed locally on every**
`isAllowed()` call from the current signed profile. Its standing is one of `allow`, `warn`,
or `block`. If the cached profile is **missing or expired**, a `fail_mode: closed` category
resolves to `block` with `rule_ref: null` — enforcement never silently passes.

<Warning>
  Do not call `confirm()` on a fail-closed verdict (`rule_ref` is `null`) — there is nothing to
  attest. The SDK throws `RuleRefRequired`. See [Platform integration](/integration/platform).
</Warning>

## Prove the graph is live

The product-plane objects are served today. No key gives you a `401` (not a `404`), which is
the correct answer — the surface exists:

```bash theme={null}
# The control/product plane is live:
curl -fsS https://prodapi.phosra.com/health
# → 200 {"status":"ok"}

# Family (and every product object) requires a phosra_ key — 401 without one:
curl -s -o /dev/null -w "%{http_code}\n" https://prodapi.phosra.com/api/v1/families
# → 401
```

The data-plane objects (Verdict, EnforcementReceipt) are served by the census. The public
sandbox census is live and serves the signed Trust List every profile verifies against:

```bash theme={null}
curl -fsS https://phosra-api-sandbox-production.up.railway.app/health
# → 200

curl -fsS https://phosra-api-sandbox-production.up.railway.app/.well-known/ocss/trust-list | head -c 80
# → {"document":"{\"document_type\":\"trust_list_issue\",\"entries\":[ ...
```

<Note>
  The product-plane resources above require a `phosra_` key (mint one in the
  [Developer Console](https://dashboard.phosra.com)); see [Authentication](/authentication) and
  the [Quickstart](/quickstart) to create your first family → child → policy → rule → enforcement
  job end to end. There is no public unauthenticated sandbox for the product plane.
</Note>

## Where to go next

<CardGroup cols={2}>
  <Card title="Quickstart →" icon="bolt" href="/quickstart" horizontal>
    Create the whole graph in one `POST /setup/quick` call, then enforce it.
  </Card>

  <Card title="Architecture →" icon="sitemap" href="/concepts/architecture" horizontal>
    Why the product plane and the enforcement data plane are kept separate.
  </Card>

  <Card title="Policies & Rules →" icon="shield" href="/concepts/policies-and-rules" horizontal>
    How the 123 OCSS categories become a policy.
  </Card>

  <Card title="Enforcement →" icon="rocket" href="/concepts/enforcement" horizontal>
    Jobs, per-platform results, and the poll-don't-webhook contract.
  </Card>
</CardGroup>
