← The specification

Repo: lemonfiber-companion

Rendered from spec at76d46f3 (2026-10-25) · its source · Edit this page

Status: Draft

The mobile companion app. PHP on NativePHP, Hippocratic 3.0.

Implements: area N — the fourth surface, against the web API contract, by way of sdk-php.


What this repo is

A native mobile application for iOS and Android, built with NativePHP in SuperNative mode: Blade compiled to a binary representation that SwiftUI and Jetpack Compose render directly. There is no web view, no DOM and no JavaScript bridge (ADR-0017).

It is a fourth surface under G1, not a companion in the weaker sense of a viewer. The parity rule applies: an action reachable from a terminal is reachable here, subject to the two qualifications ADR-0017 records — reachability is a reported condition, and first-run setup does not cross the device boundary.

One application serves both audiences. The credential that signs in decides which (N3-R1).

What it does not contain

No API client. It consumes sdk-php, which owns that job (ADR-0013). A second client here would be a second source of truth for the contract, which ARCH-R58 forbids.

No way around the SDK. Not “prefers the SDK” — the app issues no HTTP request of its own at all (N1-R16). Where the SDK does not yet expose something a screen needs, the work stops and the gap is raised against the SDK and the contract (N1-R17). It is not worked around, not fetched directly “just this once”, and not approximated from a neighbouring endpoint.

That rule is load-bearing for anyone — person or agent — building here unsupervised. A blocked screen is a smaller problem than a fourth consumer the contract does not know it has, and being blocked is a finding worth reporting rather than a problem to solve locally.

No business logic. It renders the core’s answers. A decision made here that the core does not also make is the drift G1-R2 exists to prevent.

No second permission model. What a household member may do is the core’s answer, rendered (N3-R2).

No generated contract of its own. The envelope shapes arrive through the SDK, already generated from the contract/web-api/ directory (ADR-0014).

What it owns

Pairing, session exchange, and multi-stack handling N1
The operator’s screens N2
The household’s screens N3
Device permissions, secure storage, app lock, notifications N4
What the phone keeps between launches N27
The top bar, the tabs, the menu and the words on them N28
Keys for apps, minted as claims and proved by a device key N29

Standards

Held to the same bar as sdk-php, because it is the same language and the same project:

  • PHP 8.5, strict types throughout.
  • PHPStan at the level sdk-php runs, with the same strict, deprecation and ergebnis rule sets.
  • Pest, including its architecture plugin — the boundary rules above are tests, not conventions.
  • Pint for formatting, Rector for upgrades, composer-dependency-analyser for unused and shadow dependencies.
  • Git hooks under .githooks, installed by composer setting core.hooksPath. They run only the fast checks — a hook that takes a minute is one people turn off, and one that is off enforces nothing (OPS-R51).
  • A DCO sign-off on every commit and a Spec: citation naming a requirement, enforced by the shared governance workflow every repository in this project calls (spec-check).

How it is laid out

The application is modular: a composition root holding nothing but bindings, and modules under app-modules/ that each declare what kind they are — kernel, capability, design, surface, adapter, or stand-in for the development-only fakes in modules/dx. That declaration generates the module’s dependency rules, so a module added later is governed the moment it exists rather than when somebody remembers to write its test.

Each module is a composer package with its own manifest, which is what makes the SDK rule structural rather than advisory: modules/sdk and the development-only modules/dx are the only manifests requiring lemonfiber/sdk-php, so a screen that names the SDK is a shadow dependency and fails resolution. N1-R16 stops being a rule a reviewer applies.

A capability that keeps something between launches holds its store inside itself, walled off rather than split into a module of its own: the framework only in its src/Internal/Store and its database/migrations, the store reached only through a port the capability declares and the composition root binds, and its tables named for it and read by nobody else (ADR-0035).

The architecture document in that repository lists every rule beside the mechanism enforcing it, and a test reads that column and fails when the two disagree — in either direction. A rule table that can quietly go out of date is worse than none, because people stop reading it once they trust it.

The decisions that shape it

It is a fourth surface, reached over the network, rendering natively ADR-0017
A paired fingerprint decides which machine it will talk to ADR-0018
A screen paints what it knows before it reaches the stack ADR-0019
An action the stack did not receive did not happen ADR-0020
What the phone keeps: decided and stored by the capability it belongs to, its store walled inside it, sealed with a key the platform holds ADR-0035

It follows the main repos rather than gating them

This repo carries no version of its own yet, and locks no goal on the release train. Area N is specified in full because the specification leads everywhere in this project; the app is built against what the main repositories have already shipped rather than the other way round.

All twenty-six of its features are accepted. Every requirement in area N is citable, and the only ones a version locks are the stack’s side of pairing, which 0.17.0 locks: N1-R18, N1-R47 to N1-R49 and N1-R62. The rest of N1 is not scheduled, and every other feature in the area requires it. They wait in check_goal_coverage.AWAITING_A_VERSION until the app joins the train, beside the two outside area N the app keeps: G2-R14, a name this product gives rendered as declared in every locale, and G3-R16, a touch target the platform’s own minimum.

So it tracks main and takes no releases while it catches up. When it has, it is pinned with the rest of the repositories the way every other one is (pins), and from that point it moves on the train like everything else.

What this deliberately avoids: a version manifest locking N1–N4 would make 1.0.0 — “everything specced is built” — wait on an application that is following the core rather than leading it. The specification is not the thing holding anything up here, and a manifest would make it look like it was.

Distribution

Builds and signing run in Bifrost, NativePHP’s cloud build service, rather than on a machine holding distribution certificates.

That is a supply-chain surface this project did not previously have, and it is named here rather than left in a workflow file: a signing identity that lives in somebody else’s cloud is a thing an operator of this project’s own standards is entitled to know about. L1’s attestation requirements reach it.

Why PHP for a mobile app

The honest answer is that it is the same language as an SDK this project already maintains to a high standard, so the app is written against a client that is already tested rather than against a second one in a language nobody here is holding to that bar.

The less honest answer would be that PHP is the natural choice for mobile. It is not, and the decision is recorded with its trade-offs in ADR-0017 rather than presented as obvious.