# KCP: how socialweb-computer uses kcp - kcp = the state store, API server and composition layer of EVERY application, not shared infra - companion of [SYSTEM.md](SYSTEM.md); decisions + measurements in [ARCHITECTURE.md](ARCHITECTURE.md) sections 0, 6, 12, 13 - source of truth for types: `api/*/v1alpha1`; for schemas and exports: generated files under `deploy/` ## kcp Is Part Of Each App - application instance = its own kcp + a manifest set + a profile - application state = kcp objects (ComputeVM, ComputeContract, NodeClaim, OpenBaoServer, HostProcess, component kinds, Service, Exposure, ...) - components run in process in the core; third-party binaries are HostProcess units; both reconciled from objects - apps talk to each other ONLY over the atproto market protocol. a shared kcp is composition + isolation, never a data channel - only exception: tenant Kubernetes clusters of the Gateway use kcp the traditional way (home workspaces, bound exports, tenant k3s behind kcp) ### Which kcp, by profile | profile | kcp | workspaces | |---|---|---| | prod gateway (`install gateway`) | strict kcp per app, blue/green slots | app root in `root`, provider in `root:socialweb-provider`, requester side in `root:app`, cluster homes | | prod ssh gateway (`run gateway ssh`, standalone) | its own strict kcp on the gateway host | app root in `root` | | prod bidder (`install bidder`, per handle) | strict kcp per handle, one slot | app root in `root` | | CLI requester (`run request` without `--network`) | fresh kcp per invocation, state dir persists | app root in `root` | | market network (`dev up --profile market-network`, CI, soak) | ONE kcp for every app | network in `root`, shared services `root:global` `root:relay` `root:alice` `root:bob`, each app `root:`, its components `root::app`, child cores in `root:dev:-` | | browser page (kcp fork, js/wasm) | in-tab kcp, memory storage | requester root | | iOS app (`pkg/embedded`) | in-process kcp + vault | requester root; snapshot bytes hold vault secrets in clear: store encrypted, never log | - one manifest set serves both shapes: in the network the applier rebases every phase workspace under `root:` (`kcpapply.Options.Base`); `root:*-provider` and sets of scope `kcp` stay shared - service names in manifests: `.default.{{.ServiceSuffix}}` - a child app in a network has NO kcp or kine of its own; its core is a HostProcess placed by the network's devsup, scoped to `root:` - children learn the network from values copied onto their root objects and `valueFrom` refs to other workspaces ## The kcp Runtime: Slots, Front, Kine, Postgres | piece | gateway | bidder | market network / dev | |---|---|---|---| | kcp binary | unpatched kcp-dev/kcp main, pinned asset (`versions.json` `assets.kcp`) | same | same | | slots | blue/green `socialweb-kcp-gw@a`, `@b` | one slot per handle `socialweb-kcp-@a` | one slot | | store | kine over postgres unit `socialweb-kcp-gw-pg` (unix socket) | kine over sqlite | kine | | front | own L4 front `socialweb-kcp-gw-front` on 127.0.0.1:6443 (caddy -> kcp.socialweb.computer) | port-0 front | front under devsup | | upgrade | roll: new slot beside old on same store, front flips after `/readyz`, old exits, Lease `kube-system/kcp-controllers` deleted so new slot leads at once; pg_dump before every roll | short restart | `dev roll kcp` | - slots run `--enable-leader-election` and `--shard-base-url` = the front - kcp release pairs whose storage versions or bootstrap schemas differ: planned short outage instead of a roll - caddy stays public TLS only, never reconfigured by a swap - `KcpInstallation` records the selected slot and the front backend - operator commands: `units roll kcp` (installed host), `dev roll kcp` (dev), `dev restart core` - memory caps: kcp slot 3 GiB (`kcpserver.SlotMemoryMax`), front 256 MiB, kine `kcpdatastore.KineMemoryMax` - packages: `internal/kcpserver` (slots, kubeconfigs, member tokens, roll), `internal/kcpdatastore` (kine), `internal/apiserverfront` (fronts), `internal/lease/local` (slots, ports, rolls) - tenant clusters reuse the pattern: per-cluster k3s slots a/b over kine over that cluster's postgres, behind a per-cluster front (`controlplane.json` records `activeSlot`) ## Workspaces, APIExports, APIBindings, WorkspaceTypes - provider workspace `root:socialweb-provider` (type `universal`) holds every APIResourceSchema and APIExport - apps bind the exports they need with APIBindings (`spec.reference.export.path: root:socialweb-provider`) - WorkspaceTypes give new workspaces default bindings ### Exports | export | resources (first ones) | bound by | |---|---|---| | `applications` | MarketNetwork, MarketBidder, MarketRequester, Gateway, SshGateway, component kinds, repos, leases, StateSync | `root` (`applications-binding.yaml`), `socialweb-application` type | | `clusters` | Cluster, Node, NodeClaim, ClusterLink, ClusterClientCertificate | `cluster` type, `socialweb-application` type | | `atprotoidentities` | AtprotoIdentity | `account` type, `socialweb-application` type | | `marketidentities` | MarketIdentity, AtprotoRecord, AtprotoRecordWatch | gateway `root:app` (`deploy/apps/gateway`), application type | | `computevms` | ComputeVM, FirecrackerVM, Bid, GuestIdentityGrant | `bidder` type, `root` (`computevms-binding.yaml`) | | `computecontracts` | ComputeContract, ContractEvent, Infra, EphemeralInfra, InfraCollection, EphemeralInfraCollection | requester and gateway bindings | | `hostprocesses` | HostProcess, Postgres, OpenBaoServer, RemoteOpenBao, Vault | `root` (`hostprocesses-binding.yaml`), `bidder` type | | `events` | EventStream, EventSource, Pds/Relay/JetstreamEventSource | `root` (`events-binding.yaml`) | | `exposures` | Service, Exposure, ComponentLink | `root` (`exposures-binding.yaml`) | | `pds` | RemotePDS | `root` (`pds-binding.yaml`) | | `policy` | PolicyEngine, PolicyWorkflow, PolicyWorkflowRun, PolicyTrigger | `root` (`policy-binding.yaml`) | ### WorkspaceTypes | type | default bindings | child type | used for | |---|---|---|---| | `account` | atprotoidentities | `cluster` | gateway account home (DID to shard map) | | `cluster` | clusters | | tenant cluster home workspace | | `socialweb-application` | all eleven exports | | an application workspace | | `bidder` | computevms, hostprocesses | | bidder workspace | | `dev-instances` / `dev-instance` | | `dev-instance` only | `root:dev` and `root:dev:` of a dev host | - never use kcp's shipped home (`~`): a bare LogicalCluster that can never be deleted ```mermaid flowchart TD subgraph ROOT["workspace root"] APPROOT["app root object: MarketBidder / Gateway / ..."] B1["APIBinding applications"] B2["APIBinding computevms"] B3["APIBinding hostprocesses, events, exposures, pds, policy"] WT["WorkspaceTypes: account, cluster, socialweb-application, bidder"] end subgraph PROV["workspace root:socialweb-provider"] ARS["APIResourceSchema v1alpha1-REV.plural.group (immutable)"] EXP["APIExports: applications, clusters, computevms, computecontracts, hostprocesses, marketidentities, atprotoidentities, events, exposures, pds, policy"] end subgraph ACC["account workspace (type account)"] AID["AtprotoIdentity"] subgraph CLW["cluster home (type cluster)"] CL["Cluster, NodeClaim, ClusterLink"] end end ARS --> EXP EXP -->|"bound by"| B1 EXP -->|"bound by"| B2 EXP -->|"bound by"| B3 WT -->|"defaultAPIBindings"| ACC WT -->|"defaultAPIBindings"| CLW ``` ### Manifest sets - `deploy/sets.yaml`: named sets of ordered phases; each phase = workspace + files + `after` waits (Workspace Ready, APIExport IdentityValid, APIBinding Ready) - sets: `provider` (scope kcp: provider workspace, schemas, exports, root bindings, WorkspaceTypes), `bidder`, `hostprocesses`, `dev-host`, grants - per-app sets: `deploy/apps/{gateway,market-bidder,market-requester,ssh-gateway,repo-pds}` (vault, RBAC, extra bindings, iroh dialer, association watch) - market network: `deploy/market-network/` (workspaces, network vault, peers RBAC) - applied by `socialweb-computer apply` / `internal/controllers/runtime/kcpapply` - every app root re-renders its set's HostProcesses and OpenBaoServers each pass; a missing or Lost one voids the applied-set shortcut and the set is applied again ## The Component Runtime - one core process per instance (`internal/stack/componentcore`) = controller manager + component runtime + HostProcess supervisor - component kinds (Bidder, Requester, Pds, Plc, Relay, GuestControlPlane, IngressProxy, DispatcherTLS, GatewayAPI, SshServer, PolicyEngine) are objects; `internal/controllers/component` reconciles each into a running instance in `internal/component` - a component never listens itself: it registers handlers; a Service names component + handler; an Exposure publishes the Service - local Exposure port comes from the core's port registry (`/ports/ports.json`, 64-port block of 20000-29999 by state dir), survives a core restart, reported in `status.address` - HostProcess = a systemd user unit (or proc backend) for a third-party binary; followed via D-Bus signals; adopted across core restarts by unit name + InvocationID - what a HostProcess runs: kcp slots, kine, postgres, OpenBao, fronts, dumbpipe/iroh dialer, firecracker via `socialweb-vmexec`, child cores (market network), deno for policy - suspend: root `spec.suspend` stops participation, never a process; per component `spec.components[].suspend` stops that process ```mermaid flowchart TD ROOT["App root: MarketBidder"] -->|"composes, SSA socialweb.computer/marketbidder"| MI["MarketIdentity bidder"] ROOT --> BAO["OpenBaoServer"] ROOT --> VAULT["Vault (abstract) backendRef -> OpenBaoServer"] ROOT --> PDS["Pds component"] ROOT --> BIDC["Bidder component"] ROOT --> GCP["GuestControlPlane component"] ROOT --> PE["PolicyEngine"] ROOT --> ING["IngressProxy"] ROOT --> ES["EventSource + EventStream"] ROOT --> LINKS["ComponentLinks"] BIDC -->|"creates"| BID["Bid"] BID -->|"owns"| CVM["ComputeVM (abstract)"] CVM -->|"backendRef"| FVM["FirecrackerVM"] FVM -->|"runs as"| HP1["HostProcess: vmexec + firecracker"] BAO -->|"runs as"| HP2["HostProcess: openbao"] PE -->|"runs"| HP3["deno sandbox"] BIDC --> SVC["Service"] SVC --> EXPO["Exposure: status.address"] ``` ## Controllers Pattern - one reconciler per kind: `internal/controllers/` on `internal/controllers/runtime` (the only home of the dynamic client and unstructured) - kcp multicluster: every core runs the manager for its own workspaces; a root reconciles only roots of its core's base workspace - no polling. driven by watches on own kind + every kind it refs - deadlines: delayed requeue - errors: rate-limited backoff - ticker only for an outbound obligation listed in `deploy/obligations.txt` (owner + period) - systemd units via D-Bus signals, processes via wait - writers rule (every write = server-side apply, field manager `socialweb.computer/`): - a spec is written only by its composer - a status only by the controller of its kind - a field another controller reports = its own status subfield + own field manager (e.g. ComputeVM `status.market.guest` by guestcontrolplane) - every child/intent object has an ownerReference to its composer or creator - no annotation carries a control signal (release = `spec.release`, restart = `spec.restartedAt`) - a child edited by another manager is re-applied with force and a `ChildEditReverted` Warning Event - cross-workspace children cannot carry ownerReferences: they carry root labels (`apps.socialweb.computer/root`, `root-kind`, `root-cluster`), pruned by label, torn down before the root's finalizer goes - abstract kinds (AtprotoRepo, Vault, ComputeVM, EventSource): `spec.backendRef` to one concrete kind with label `contracts.socialweb.computer/`; abstract status mirrors backend; conditions `Ready` + `BackendBound` - gate: `scripts/check-architecture.sh` (`hack/archcheck`) enforces layers, obligations, bus callers, no GVR literals outside `api/`, no annotation signals, `apigen:"userEndpoint"` on spec endpoints ### Bus and ComponentLinks - `bus.Resolve[T](ctx, b, from, to, capability)` -> typed contract handle - caller = the reconciled object or the component instance, never a controller; System only for `deploy/bus-bootstrap-callers.txt` - the callee holds credentials; a handle never exposes a token - a call with no Accepted ComponentLink fails `ErrLinkDenied` - ComponentLink lives in the target's workspace: `spec.from` (ref or selector), `spec.to`, `spec.capabilities` (name + scope), `spec.suspend` - capabilities: `xrpc.call`, `stream.dial`, `policy.run`, `vault.*`, `repo.*`, `pds.*`, `pds.serviceauth`, `events.*` - roots generate links from component refs in their desired specs (`internal/app/compose/links.go`) and wait for Accepted before calling - bound signals: `NotifyBound` (e.g. vault provider) wakes waiters without polling; binding fingerprint includes readiness ```mermaid sequenceDiagram autonumber participant K as kcp participant RT as controller runtime participant C as controller (kind X) participant BUS as bus participant L as ComponentLink participant T as target component K-->>RT: watch event (X or a ref'd kind) RT->>C: Reconcile(X) C->>K: read X + refs C->>BUS: Resolve[T](from=X, to=target, cap) BUS->>L: accepted link admits cap? alt no accepted link BUS-->>C: ErrLinkDenied else admitted BUS-->>C: typed handle C->>T: contract call (callee holds creds) T-->>C: result end C->>K: SSA status (own field manager) C-->>RT: done / requeue after deadline / backoff on error ``` ### StateSync and remote access - StateSync = durable outbox: source, sink, direction, payload, idempotencyKey, conflict + deletion policy (`Retain`, `DeleteRemote`, `AckThenDelete`), ack, retry - observe is the authority; Acked only when the observation matches the desired hash; status never holds secrets or payload bytes - examples: bidder `vm.terminated` before teardown (finalizer waits for Acked); market inbox syncs pruned at 10 min - RemoteOpenBao: vault access separate from a server's lifecycle; probes health/seal, authenticates, checks mounts; never writes the remote vault ## API Resource Catalog - every kind is cluster-scoped (kcp workspaces are the isolation; apigen writes `scope: Cluster`) - schema name: `v1alpha1-..`; rev = value in `resources.go` when this doc was written (source of truth: the file) - "composer" = who writes spec; status is always the kind's own controller unless noted - "operator-applied" = has a controller but no Go composer: applied from manifests or kubectl ### App roots (group `socialweb.computer`, export `applications`) | kind | rev | purpose | composer | |---|---|---|---| | MarketNetwork | 31 | a whole local market in one kcp: shared services + child roots | `dev up` | | MarketBidder | 43 | bidder app: profile, identity, comms, ingress, policy, guestNetwork, firecracker, kcp | `install bidder` / `run bidder` / network | | MarketRequester | 37 | requester app: identity, comms, secretsFallback, atircParentRef, ingress | `run request` / page / iOS / network | | Gateway | 38 | hosted k8s: webAppFront, clusterProvider, operator, marketNetwork | `install gateway` / network | | SshGateway | 35 | ssh as yourself: ssh options, owner sessions, per-user requesters | `run gateway ssh` / network | | ChildApplication | 2 | placement of a child app in a network (networkRef, app, profile) | operator-applied (no Go composer) | ### Components (`socialweb.computer`, export `applications`) | kind | rev | purpose | composer | |---|---|---|---| | Bidder | 24 | market bidder participant | MarketBidder | | Requester | 29 | contract runner (RFP, accept, secrets, atirc, release) | MarketRequester, Gateway, SshGateway | | GuestControlPlane | 11 | guest endpoints at 192.0.2.1 (events, downloads, identity proxy) | MarketBidder | | Pds | 12 | local PDS (localRepo identities, network PDSes) | roots | | Plc | 11 | local PLC directory | MarketNetwork | | Relay | 11 | local atproto relay | MarketNetwork | | IngressProxy | 12 | did:key / iroh / opentunnel ingress to the app's XRPC | roots | | DispatcherTLS | 11 | TLS front for the network dispatcher | MarketNetwork | | GatewayAPI | 16 | k8s.socialweb.computer web app + tenant API | Gateway | | SshServer | 17 | ssh server, web sign-in, keys API | SshGateway | | MarketPresence | 2 | bidder offering presence with TTL | Bidder; adopted by its root | ### Market and contracts | kind | group / export | rev | purpose | composer | |---|---|---|---|---| | ComputeContract | socialweb.computer / computecontracts | 31 | one rental: spec (vm, tunnels, secretsRef, agent, association, policy), status.contract flow state | CLI, page, Infra, ssh session, NodeClaim | | ContractEvent | / computecontracts | 5 | one guest/requester event (sealed signals, secrets), owned by its contract | CLI, page (outbound); Requester (inbound) | | Infra | / computecontracts | 13 | durable set of contracts + nodes + secrets, mirrored to `computer.socialweb.infra` | owner, NodeClaim controller | | EphemeralInfra | / computecontracts | 13 | Infra with TTL / endWhen | owner | | InfraCollection | / computecontracts | 1 | one Infra child per `computer.socialweb.infra` record in a repo | owner | | EphemeralInfraCollection | / computecontracts | 1 | same for `ephemeralInfra` | owner | | Bid | socialweb.computer / computevms | 9 | one bid per RFP (name = bid rkey); owns its ComputeVM; status by bidder | Bidder | | ComputeVM | / computevms | 17 | abstract VM: size, owner, requester, userData ref, contract refs, release | Bidder | | FirecrackerVM | / computevms | 7 | concrete firecracker backend (unit, network, image) | ComputeVM controller | | GuestIdentityGrant | / computevms | 3 | guest workload identity grant (entity, api, scopes, claims) | Bidder, GuestControlPlane | | StateSync | socialweb.computer / applications | 1 | durable outbox (vm.terminated, market inbox) | roles, controllers | ### Identity, repos, records | kind | group / export | rev | purpose | composer | |---|---|---|---|---| | MarketIdentity | socialweb.computer / marketidentities | 15 | app identity: mode (localRepo, pdsAccount, oauthQr, oauthBrowser, oauthDelegated, oauthSessionProxy), associations, pendingAction | roots, SshGateway session owners | | AtprotoRecord | / marketidentities | 7 | one record to keep in a repo (collection, rkey, swapCid, deletionPolicy) | roots, Infra | | AtprotoRecordWatch | / marketidentities | 7 | watch rkeys of a collection (associations, vouches) | app sets (`repo-pds`) | | AtprotoIdentity | socialweb.computer / atprotoidentities | 2 | DID to identity mapping in an account home | gateway | | AtprotoRepo | / applications | 4 | abstract repo: did, head, rev, writable, address | roots | | LocalRepo | / applications | 2 | repo on a Pds component (signer handle, no token) | roots | | RemoteRepo | / applications | 2 | repo on a RemotePDS via a MarketIdentity lease | roots | | AtprotoSession | / applications | 2 | a session of an account on a PDS | roots (`compose/repo.go`) | | PdsAccount | / applications | 2 | an account on a Pds component | roots | | PlcIdentity | / applications | 1 | a DID on the local Plc | roots | | RemotePDS | pds.socialweb.computer / pds | 3 | a remote PDS endpoint + auth + capabilities | roots | ### Vault and host | kind | group / export | rev | purpose | composer | |---|---|---|---|---| | HostProcess | socialweb.computer / hostprocesses | 4 | a host unit for a third-party binary; status.listeners | roots, controllers | | Postgres | / hostprocesses | 1 | embedded postgres (kcp store, cluster store, vault store) | gateway, cluster provider | | OpenBaoServer | / hostprocesses | 11 | local OpenBao: mounts, policies, clients; unseal keys in a 0600 file, root token revoked | roots | | RemoteOpenBao | / hostprocesses | 4 | access to an existing OpenBao (token / AppRole, https) | roots | | Vault | / hostprocesses | 1 | abstract vault: address, ready, caBundle | roots | | VaultCredential | / applications | 2 | a credential held in a vault for a repo account | roots (`compose/repo.go`) | | VaultCertificate | / applications | 2 | a PKI certificate (vault, mount, role, commonName) | operator-applied | | KcpInstallation | / applications | 1 | selected kcp slot + front backend | operator-applied | | HostInstall | / applications | 1 | installed account, version, profile | operator-applied | | HostRelease | / applications | 1 | installed release version + prune | operator-applied | | PortLease | / applications | 2 | a leased port (cluster, purpose, port) | MarketNetwork | | SubnetLease | / applications | 2 | a leased subnet (cluster, purpose, prefix) | operator-applied | ### Tenant clusters (export `clusters`) | kind | rev | purpose | composer | |---|---|---|---| | Cluster | 5 | mount object: status.URL, phase, Draining, nodeExpiry | cluster provider (from XRPC) | | Node | 2 | one node; the bound on leaks | node provider | | NodeClaim | 16 | one market node for a cluster; status.contract mirrors the ComputeContract | cluster provider (spec), NodeClaim controller (status) | | ClusterLink | 2 | iroh listener in front of a cluster apiserver or vault | cluster provider | | ClusterClientCertificate | 2 | client cert record per DID (serial, notAfter, revoked) | gateway | | ClusterRoll | 2 | control-plane roll of a cluster (strategy, maxUnavailable) | operator-applied | ### Exposure (group `exposure.socialweb.computer`, export `exposures`) | kind | rev | purpose | composer | |---|---|---|---| | Service | 2 | names a component + handler (+ protocol, auth) | roots | | Exposure | 5 | publishes a Service: local port, hostname/path, ingress; status.address | roots | | ComponentLink | 3 | admits bus calls from -> to with capabilities | roots (`compose/links.go`) | ### Events (group `events.socialweb.computer`, export `events`) | kind | rev | purpose | composer | |---|---|---|---| | EventSource | 1 | abstract source: cursor, lag, address | roots | | PdsEventSource | 1 | subscribeRepos of an AtprotoRepo | roots | | RelayEventSource | 1 | a relay subscribeRepos (Relay ref or URL) | roots | | JetstreamEventSource | 1 | a Jetstream (v1 or v2) | roots | | EventStream | 7 | filter (collections, dids) + cursor + sink over one EventSource; `spec.resume` Live / Recent | roots, requester composer | | RelayStatus | 2 (`socialweb.computer`) | status of a Relay | roots (`compose/repo.go`) | ### Policy (group `policy.socialweb.computer`, export `policy`) | kind | rev | purpose | composer | |---|---|---|---| | PolicyEngine | 3 | the engine component: limits, trust, cache, sandbox, bundled actions | roots | | PolicyWorkflow | 2 | a workflow (inline or record ref) + defaults | roots | | PolicyWorkflowRun | 3 | one decision run: perspective, self/subject/counterparty DIDs, rfp/bid/accept, cache | roles | | PolicyTrigger | 2 | run a workflow on matching source events | roots | ## Schema Revisions - kcp holds `APIResourceSchema.spec` IMMUTABLE: an update under the same name is Forbidden - so each schema is named `v1alpha1-..`; Revision per kind in `api/*/v1alpha1/resources.go` - `deploy/apiresourceschema-revisions.txt` = revision lock: every name ever generated + sha256 of its spec, append-only, `merge=union` - change a Go type without bumping Revision -> generation fails -> `deploy/check.sh` fails before any host sees it - migration = the manifest set applies the new schema, then the re-pointed export; already-bound APIBindings move to the new schema in seconds, identity hash unchanged, stored objects untouched. no binding deleted - old schemas stay in kcp, harmless ### Burned revisions - a revision applied to a prod kcp is taken forever, even if the branch that made it is reverted - reverted branch -> its lock line is gone -> the generator may hand out the same number with a different spec -> prod kcp refuses the apply -> core crash loop - known burned on prod: - Requester `v1alpha1-28` (lock now records 28 and 29 with the same spec) - MarketRequester `v1alpha1-38` (NOT in the lock: the next MarketRequester bump must be 39) - rule: before a bump, check the burned list; after a revert of a shipped schema, keep its lock line - fix for a crash loop: roll back the binary, bump to a fresh revision, roll forward ## apigen And Codegen ```mermaid flowchart LR T["api/GROUP/v1alpha1 Go types + resources.go"] --> G["go generate ./api/..."] G --> CG["hack/codegen.sh: zz_generated.deepcopy.go"] G --> AG["cmd/socialweb-apigen -root . (internal/apigen)"] AG --> S["deploy/KIND-apiresourceschema.yaml"] AG --> E["deploy/EXPORT-apiexport.yaml"] AG --> L["deploy/apiresourceschema-revisions.txt"] AG --> R["zz_generated.register.go, zz_generated.conditions.go"] S --> SET["deploy/sets.yaml phases"] E --> SET SET --> KCP["kcp: root:socialweb-provider"] L --> CHK["deploy/check.sh: regen + diff"] ``` - Go types are the ONLY source of every schema; nothing else writes the outputs - mapping (no kubebuilder markers: the no-comments rule): - struct layout from `encoding/json` tags; `required` = no `omitempty` unless `apigen:"optional"` - enums from source: typed string constants in declaration order; a named string type with no constants = error - struct tags: `apigen:"enum=a|b"`, `default=`, `minimum=`, `userEndpoint` (required on endpoint fields in spec) - `metav1.Condition` items open; unknown fields are an error in `apigen.Model.Validate` - abstract contracts: `APIResource.Contracts` adds the label `contracts.socialweb.computer/` to the concrete schema - printer columns from `APIResource.PrinterColumns` - checks: `deploy/check.sh --fast` regenerates into a scratch dir and diffs every file `socialweb-apigen -root . -list` names; `go generate ./...` must leave `api`, `internal/atproto/market`, `deploy` unchanged - rebase conflicts: `scripts/regen-after-rebase.sh`; never hand-merge generated output; resolve generator inputs by hand first - lexicons: `internal/lexgen` generates market record Go from `lexicons/` ## See Also - [SYSTEM.md](SYSTEM.md): the whole system - [ARCHITECTURE.md](ARCHITECTURE.md): section 0 (kcp per app), 6 (CRDs, generation, revisions), 12 (layout, bus, roots), 13 (invariants) - [../deploy/market-network/README.md](../deploy/market-network/README.md): the one-kcp network - `deploy/sets.yaml`, `deploy/workspacetype-*.yaml`, `deploy/*-apiexport.yaml`