Guide: building a Koerper
Status: design guidance. This guide describes how to build a Koerper that talks correctly to the Gestalt membrane today and remains correct when production admission lands.
A Koerper is the mutable operational body around a Geist. See concepts: Geist and Koerper. This guide gives you a working pattern for building one.
What kind of Koerper
Section titled “What kind of Koerper”The Koerper you build depends on who uses it:
- An operator console — internal back-office for staff.
- A customer-facing surface — a webshop, a self-service portal.
- An advisor console — a Steuerberater / Kanzlei interface.
- A vertical SaaS — agency-built for a specific industry niche.
- An agent workcell host — embeds an LLM and proposes acts.
- A native vessel — desktop / mobile app with secure signing.
Each has different requirements. Common discipline applies to all.
Common discipline
Section titled “Common discipline”1. Never write company truth locally
Section titled “1. Never write company truth locally”If a fact would be relied on by an audit, an advisor, a regulator, or a counterparty, it crosses the membrane. Period.
The Koerper may cache projections of company truth for fast UI. The
cache is invalidated by membrane projection updates (eventually) or
by re-fetching commit.recent and tenant-scoped queries. The Koerper
must not display cached data as if it were the source.
2. Cite capabilities, never operation strings
Section titled “2. Cite capabilities, never operation strings”Wrong:
await fetch("/api/issue-invoice", { method: "POST", body: ... });Right:
await client.economicInvoice({ tenant, amount_cents: ..., currency: "EUR", evidence: ["evidence_bundle:invoice_payload"],});The capability ref is what governs what the act means. A Koerper that calls operations by URL bypasses the capability discipline.
3. Surface refusals as findings
Section titled “3. Surface refusals as findings”A refused outcome is a structured finding, not an error toast.
Render the refusal:
{response.outcome === "refused" && ( <div className="refusal"> <h3>This action could not be admitted.</h3> <dl> <dt>Why</dt> <dd>{response.body.refusalReason}</dd> <dt>What's missing</dt> <dd>{response.body.missingEvidence?.join(", ") || "—"}</dd> <dt>How to fix</dt> <dd>{response.body.remedyHint || "—"}</dd> </dl> <Receipt receipt={response.receipt} /> </div>)}The user should always be able to see the receipt (or at least its ref) for any refusal.
4. Surface fixture status
Section titled “4. Surface fixture status”Until production admission lands, every receipt carries
fixture: true and most bodies carry productionAdmission: false.
The Koerper must surface this:
{response.receipt?.fixture && ( <Banner kind="warning"> Fixture rehearsal — this is not a binding admission. </Banner>)}5. Handle pending actions
Section titled “5. Handle pending actions”A pending outcome means the membrane could not complete the act
with the current vessel’s signing posture. The Koerper should:
- store the
pendingActionref, - show the user what is needed (higher signing, evidence, advisor intervention),
- allow them to switch device or request the missing intervention,
- poll or subscribe for completion.
6. Idempotency for effects
Section titled “6. Idempotency for effects”When you call effect.intent, you must supply an idempotency_key
that uniquely identifies the user-intended action. Reuse the same
key on retry; the membrane will refuse a colliding key with a
different payload but accept identical retries.
7. Treat receipts as opaque
Section titled “7. Treat receipts as opaque”A Koerper does not interpret receipts. It stores them, displays the ref, surfaces the outcome, and forwards them when needed. The Geist owns interpretation.
8. Assume replacement
Section titled “8. Assume replacement”The Koerper you build will be replaced. Future Koerpers must be able to take over without disturbing the Geist. Don’t:
- store the canonical version of any company fact in your Koerper’s database,
- couple your URL structure to the Geist’s atom refs,
- assume your Koerper is the only one talking to the membrane.
9. Use the read models for read surfaces
Section titled “9. Use the read models for read surfaces”For lists, dashboards, and detail panels that summarize durable membrane state, call the read models rather than reconstructing from raw atoms client-side:
const standings = await client.readActiveStandings({ tenant });const mandates = await client.readActiveMandates({ tenant });const closeReadiness = await client.readPeriodCloseReadiness({ tenant, period: "2026-04" });const evidenceGaps = await client.readConnectorEvidenceGaps({ tenant });const proofHistory = await client.readProofHistory({ tenant });These projections never expose raw DB rows, raw connector payloads, biometric material, or cross-tenant graphs. They give the Koerper a membrane-safe view that survives schema and atom-shape changes.
10. Request HumanAuth presence where required
Section titled “10. Request HumanAuth presence where required”Sensitive effects (signing an invoice atom, granting a mandate,
revoking standing) require fresh HumanAuth presence. The Koerper
requests presence approval through m13PresenceApproval and attaches
the resulting receipt ref to the proposing call:
const presence = await client.m13PresenceApproval({ tenant, actor: "human_person:anna", vessel: "vessel:fixture_sdk", human_presence_receipt: "human_presence_receipt:...", create_standing_from_presence: false,});create_standing_from_presence is always false. HumanAuth presence
proves who is at the keyboard right now — it is not standing, not a
mandate, not a session, not authority. Treat the four as four
different things in your data model.
11. Never mint authority locally
Section titled “11. Never mint authority locally”A Koerper does not create standing, grant mandates, mint package
trust, or sign authority package activations. These cross the
membrane through standing.*, mandate.*, and authority.package.*
routes and are governed there. A Koerper that synthesizes these refs
locally has bypassed the discipline.
The “minimum lawful Koerper”
Section titled “The “minimum lawful Koerper””The smallest possible useful Koerper exposes one button that crosses the membrane and one panel that shows projections.
function IssueInvoiceButton({ amount, currency, evidence }) { const [outcome, setOutcome] = useState(null);
async function handleClick() { const response = await client.economicInvoice({ tenant: "tenant_node:rheinwerk_calibration", amount_cents: amount, currency, evidence, }); setOutcome(response); }
return ( <> <button onClick={handleClick}>Issue invoice</button> {outcome && <OutcomePanel response={outcome} />} </> );}OutcomePanel renders:
- the outcome (admitted / refused / pending / projected),
- the body’s structured fields,
- the receipt ref + fixture marker.
This is the load-bearing pattern. Everything else is decoration.
Patterns for common surfaces
Section titled “Patterns for common surfaces”Webshop checkout
Section titled “Webshop checkout”The customer is not a Gestalt actor. Use shop.prepare →
shop.commit so a hosted operator delegate signs the invoice atom
on the tenant’s behalf.
const prepared = await client.prepareShop({ tenant, capability: "capability:issue_invoice_fixture_v1", action: "issue", evidence: ["evidence_bundle:invoice_payload"],});
const committed = await client.commitShop({ tenant, capability: "capability:issue_invoice_fixture_v1", action: "issue", evidence: ["evidence_bundle:invoice_payload"], // include prepared.body.prepareToken});Back-office invoice list
Section titled “Back-office invoice list”Render projections from commit.recent and the read models
(readPeriodCloseReadiness, readConnectorEvidenceGaps,
readProofHistory). For unresolved tensions specifically, the
membrane exposes /v1/tensions/query; the TS SDK does not have a thin
wrapper for it yet, so call it through the membrane directly when you
need it and surface the results in your tension panel.
Period close UI
Section titled “Period close UI”Walk the user through:
- Show period readiness from
readPeriodCloseReadiness({ tenant, period }). - Block the close button until durable obligations and open closure surfaces are resolved.
- Require explicit clearance evidence before the call.
- Render the refusal cleanly if
economic_closure_surface_open.
Steuerberater console
Section titled “Steuerberater console”The membrane exposes the lens, advisor, and intervention surfaces
today as staging-durable operations. A console should:
- record the engagement scope through
lensScope, - pull redacted disclosures through
lensDiscloserather than filtering rawcommit.recentresults client-side, - open a matter through
advisorOpenMatter, - record the Steuerberater’s decision through
advisorIssueOpinion, - request additional evidence through
advisorRequestEvidence, - escalate or attest through
interventionRequest/interventionIssue.
The advisor is not an admin; raw payloads, full tenant graphs, and biometric material remain refused on every one of these calls.
Anti-patterns
Section titled “Anti-patterns”- Re-implementing Gravity client-side. Don’t. Let the membrane refuse and surface the refusal.
- Hiding refusals as success. Don’t. A refused operation is a governed finding; show it.
- Caching without invalidation strategy. Don’t display stale state as canonical.
- Mixing fixture and real data. Don’t ever mark fixture receipts as real.
- Inventing capability refs. Don’t construct cap refs your Koerper hasn’t seen from the membrane / package status.
- Minting authority locally. Don’t synthesize
standing:*,mandate:*,authority_package:*, or proof refs in the Koerper — these belong to the membrane. - Equating presence with authority. A bearer session, a HumanAuth presence receipt, a standing record, and a mandate are four different things. Don’t model them as one.
Where to read next
Section titled “Where to read next”- Concepts: the membrane
- Concepts: Geist and Koerper
- Generated Koerper discipline — the rule set when the Koerper is produced by an AI generator.
- SDK: TypeScript
- Reference: outcomes
- Reference: refusal codes