Files
2026-09-17 17:59:34 +02:00

8.5 KiB

Architecture and invariants

Components

Parent browser -> same-origin FastAPI service -> SQLite policy, pairing and reward records. Child SwiftUI app -> authenticated HTTPS API -> validated snapshot -> local Apple enforcement. DeviceActivity extension -> shared App Group state -> named ManagedSettings store.

The monitor extension does not contact the backend and does not receive its credentials. App/category tokens stay on the phone. Backend truth is limited to policies, submitted challenge results, awarded credit and device-reported status; it is not an independent observation of actual screen use.

Policy inheritance

effective_policy = family_policy + child.overrides (shallow merge of a validated, flat schema).

Empty overrides inherit everything. Clearing an override restores inheritance. Updating shared policy validates every child's resulting policy in the same transaction. A global monotonically increasing revision and expected_revision prevent silent lost updates. v0 invalidates outstanding challenges on any policy revision, including a change affecting another child; finer per-child revisions can be added later.

The timezone is family-wide. Days and bonus caps are computed by the server using that IANA timezone, not a device-supplied date. The native monitor uses the same timezone. Changes to the timezone after rewards exist are blocked until a proper ledger migration is implemented.

Default limits are configurable examples, not scientific or clinical recommendations.

Reward integrity

Each challenge contains generated question IDs and server-retained correct answers. The client receives prompts only. Submission must answer every question exactly once with strict integer values; the server checks those values rather than trusting a completed flag or client-supplied minutes.

An unfinished, unexpired challenge is reused instead of issuing parallel attempts. Attempts expire after ten minutes and at a family-day boundary or policy revision change. A wrong submission is finalized without credit; the child can start another challenge. Attempts are capped at 100 per device/day as a basic resource bound.

Submission uses a SQLite BEGIN IMMEDIATE transaction. A unique challenge_id on rewards plus a persisted challenge result makes retries and concurrent submissions idempotent. The awarded amount is min(reward_size, remaining_daily_cap). Existing grants are summed per child/day and clamped to the current cap in snapshots. There is no client endpoint for arbitrary credit issuance.

This verifies correctness, not who did the work. A calculator, another person, or a modified client can solve arithmetic too; v0 does not claim learning-outcome attestation.

Local usage enforcement

The app validates the snapshot, preserves usage observations for the current day, and installs a repeating allowed-hours monitor with cumulative usage events. The threshold ladder includes:

  1. Base allowance and each subsequent reward increment through the cap, including a partial final reward.
  2. The current earned offset and subsequent increments, so a mid-day reward-size change remains representable.

Both the ordinary and shifted ladders are registered. On a new day, the ordinary ladder remains valid even without a server connection. Bonus entitlement is only honored on its snapshot day. Observations roll over by policy timezone, not by a foreground-only timer.

A callback records the maximum reached threshold. Duplicate or out-of-order lower thresholds do not refund usage. The decision is shielded if outside allowed hours, allowance reached, or monitoring failed. A new ordinary reward just changes entitlement and reevaluates the shield; it does not restart the monitor. Policy/selection changes or lost registration install a new monitor with includesPastActivity: true. This is intended to preserve cumulative accounting; exact Apple behavior must be checked on the target OS.

Names contain an installation generation so obsolete monitor callbacks cannot overwrite the current plan. A file lock plus atomic JSON replacement serializes app/extension updates. Failed monitor registration leaves the selected apps shielded. Unreadable shared state does not clear existing ManagedSettings. This is defensive behavior, not a guarantee against an OS failing to deliver callbacks. Midnight, policy replacement, late callbacks and restarts are explicit hardware acceptance gates.

Only individual apps are supported initially. Whole categories/websites are deferred to avoid expanding enforcement and usage-accounting assumptions before the first physical tests. The app does not promise to override another Screen Time controller or a stricter native limit.

Authentication and synchronization

The bootstrap parent token lives in server configuration and is never sent to the child. Parent browser sessions are random, HttpOnly, SameSite=Strict cookies; their stored hash is keyed by the admin secret, so rotating it invalidates sessions. Cookie-authenticated mutations require the configured Origin. Parent API callers may use the bootstrap bearer directly.

Pairing/configuration codes carry 144 bits of randomness, expire in ten minutes, are stored hashed, and are consumed atomically. Device bearer tokens are child/device-scoped and hashed at rest on the server. The app stores its token and anchored server origin in Keychain, not UserDefaults or the extension's container. Parent-selection access lasts one short foreground setup session; Apple guardian authorization is requested separately with .child, never silently downgraded to individual/self-control authorization.

Remote updates apply at the next foreground/manual sync. status is a device acknowledgement with a revision, monitoring state, shield flag, bounded detail, and last threshold observation. Parent status deliberately says last reported, not independently verified or live. There is no push delivery or retry worker in v0.

Revoking pairing disables API access immediately but does not remotely remove local restrictions. The retired device retains its last cached rules. To replace a revoked pairing on the same phone, issue a new pairing code and use Parent setup -> Replace revoked pairing. The phone keeps the old server origin anchored. Server migration/decommissioning needs a designed guardian recovery flow before production use.

API surface

Role Endpoint Purpose
Parent POST/DELETE /v1/session Browser sign-in/sign-out
Parent GET /v1/parent/family Policy, children, overrides, pairing/status summaries
Parent PUT /v1/parent/policy Revision-checked shared policy replacement
Parent POST /v1/parent/children Create a child display-name profile
Parent PUT /v1/parent/children/{id}/overrides Replace sparse child overrides
Parent POST /v1/parent/children/{id}/pairing-code Issue a one-use enrollment code
Parent POST /v1/parent/devices/{id}/configuration-code Permit one selection-editing session
Parent DELETE /v1/parent/devices/{id} Revoke API access
Enrollment POST /v1/pair Consume code; return token once
Device POST /v1/device/configuration-unlock Consume a device-specific parent code
Device GET /v1/device/snapshot Validated effective policy and today's bonus
Device POST /v1/device/status Report local applied/error status
Device POST /v1/device/challenges Start/reuse generated challenge
Device POST /v1/device/challenges/{id}/submit Grade and issue bounded, idempotent reward

Snapshot v1 fields: schema_version, device_id, child_id, child_name, policy_revision, policy, day, earned_minutes, server_time (Unix seconds). JSON uses snake_case, with shared Swift encoder/decoder conventions. Policy fields are listed in backend/src/apc/models.py; packages/PolicyCore implements their native validation and semantics.

Submission result fields: challenge_id, passed, correct_count, required_count, awarded_minutes, nullable reward_id, and day. A credit being recorded is distinct from a subsequent successful snapshot application.

Planned extensions

The narrow first slice is deliberately not a general quest engine. Add manual chores/approval and an explicit grant ledger API after iPhone enforcement is validated. Each new reward source must preserve idempotency, cap handling and the exact threshold ladder; arbitrary reward denominations need extra accounting work. Then add background synchronization, richer schedules and safe recovery. Do not introduce per-device copies of the family policy as a shortcut.