Skip to content

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.

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.

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.

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.

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>
)}

A pending outcome means the membrane could not complete the act with the current vessel’s signing posture. The Koerper should:

  • store the pendingAction ref,
  • 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.

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.

A Koerper does not interpret receipts. It stores them, displays the ref, surfaces the outcome, and forwards them when needed. The Geist owns interpretation.

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.

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.

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 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.

The customer is not a Gestalt actor. Use shop.prepareshop.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
});

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.

Walk the user through:

  1. Show period readiness from readPeriodCloseReadiness({ tenant, period }).
  2. Block the close button until durable obligations and open closure surfaces are resolved.
  3. Require explicit clearance evidence before the call.
  4. Render the refusal cleanly if economic_closure_surface_open.

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 lensDisclose rather than filtering raw commit.recent results 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.

  • 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.