1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
# Gateway architecture
Host readings on 2026-10-22 on the installed production gateway `socialweb.computer` (release binary
sha256 `30ab2e465a080bbe47ea16e085cdd99d10c640f4c90d438484363a443b31bd92`); the source claims were
rechecked against commit `1cad64b0` on 2026-10-26.
## Shape
One `socialweb-computer` core owns the gateway and its kcp. The gateway is an application root in
that kcp; its components run in the core and its infrastructure runs as HostProcess units. There is
no web-app DenoPod and no per-component systemd application unit.
```
public
k8s.socialweb.computer ──caddy──> 127.0.0.1:8788 (web-app front)
kcp.socialweb.computer ──caddy──> 127.0.0.1:6443 (kcp, no client cert)
└───> 127.0.0.1:8444 (client-certificate front, with cert)
systemd --user (account socialweb, uid 982), state /var/lib/socialweb-computer/socialweb
socialweb.slice
├─ socialweb-computer-gateway.service core: serve gateway kcp
├─ socialweb-kcp-gw@a.service / @b.service blue/green kcp slots
├─ socialweb-kcp-gw-kine@a / @b.service kine on the shared Postgres
├─ socialweb-kcp-gw-pg.service Postgres (unix socket)
├─ socialweb-kcp-gw-front.service L4 front for kcp
└─ socialweb-hp-gw-bao-root-vault.service OpenBao HostProcess
core goroutines
├─ devcore: kcp slot, bootstrap, HostProcess supervisor
├─ appctl: Gateway root, PDS, Requester, GatewayAPI, PolicyEngine,
│ MarketIdentity, Service, Exposure, ComponentLink
├─ componentcore: in-process handlers for every component
├─ cluster provider: Cluster, NodeClaim, tenant bootstrap, scale driver
└─ PolicyEngine and StateSync workers
process-local listeners
127.0.0.1:6443 kcp API
127.0.0.1:8443 TLS front for the gateway XRPC listener
127.0.0.1:8444 client-certificate front for kcp
127.0.0.1:8788 web-app front; its backend is the GatewayAPI local exposure
127.0.0.1:20000 Pds local Exposure
127.0.0.1:20001 Requester local Exposure
127.0.0.1:20002 GatewayAPI local Exposure
```
The local port numbers are leases and may move between component restarts. The stable entry points are
the fixed fronts 6443, 8443, 8444 and 8788. `gatewaykcp` resolves the 8788 backend from the
GatewayAPI local Exposure when the API-only gateway has no `socialweb-computer-k8s` component.
## Public front
`/etc/caddy/sites/k8s.socialweb.computer.caddy` sends the whole public origin to
`http://127.0.0.1:8788`. That front proxies to the GatewayAPI exposure, which serves:
- `POST /oauth/start`, `GET /oauth/callback`, `GET|DELETE /session`
- `GET /api/config`, `POST /api/clusters/{list,create,up,down,delete,kubeconfig}`
- `/xrpc/*` for the gateway server
- `/downloads/*`
`/etc/caddy/sites/kcp.socialweb.computer.caddy` keeps client-certificate-aware routing: a request
without a client certificate goes to kcp 6443; a request with one goes to the 8444 certificate front
with `X-Socialweb-Client-Certificate`, and Caddy authenticates to that front with the gateway's
edge-client certificate. Caddy trusts the gateway front certificate at
`/etc/caddy/socialweb-computer/gateway.crt`, the kcp serving certificate at
`/etc/caddy/socialweb-computer/kcp.crt`, and holds the edge client certificate at
`/etc/caddy/socialweb-computer/edge-client.{crt,key}`.
`scripts/deploy.sh` installs the binary, runs `install gateway`, copies those four Caddy files from
the new state, writes the sites, validates the Caddyfile and reloads Caddy. A fresh install that
regenerates state must run this certificate handoff; otherwise Caddy keeps the previous gateway and
edge-client certificates and the kcp front returns `bad certificate`.
The web root returns 404 because this tree is API-only. There is no SPA at `/`. OAuth is started by
`POST /oauth/start` with `{"handle":"..."}`; the response is an authorization URL. The browser flow
ends at `/oauth/callback`, which sets the `swc_session` cookie on `k8s.socialweb.computer`.
## kcp layout
Observed root workspaces:
| workspace | type | role | phase |
| --- | --- | --- | --- |
| `socialweb-provider` | `universal` | schemas, exports, provider service accounts | Ready |
| `app` | `socialweb-application` | gateway application objects and components | Ready |
| `2wjeqtgx` | `account` | Alice's account workspace (`did:plc:lpfuqerea3deuoyrn7ojser4`) | Ready |
The root workspace namespaces are `default` and `kcp-system`; the `app` logical cluster also has
`default` and `kube-system`. The account workspace is the tenant boundary for Alice's clusters and
contains the `clusters` child mount pointing at her cluster workspace.
Provider exports in `root:socialweb-provider`: `applications`, `atprotoidentities`, `clusters`,
`computecontracts`, `computevms`, `events`, `exposures`, `hostprocesses`, `marketidentities`, `pds`
and `policy`. The root bindings include `computecontracts` and `computevms` in addition to the
default application set. `socialweb-application` binds `computevms`, so the gateway set applies the
`computevms` resource schema, export and root binding before it creates `root:app`.
Main objects:
| kind/name | phase | note |
| --- | --- | --- |
| `Gateway/gateway` | Ready | profile `prod`, DID `did:plc:5ipkeflamx7tz2venxxgjupl` |
| `OpenBaoServer/root-vault` | Ready | root vault |
| `GatewayAPI/gateway-api` | Ready | `/api`, `/session`, `/oauth`, `/xrpc`, downloads |
| `Pds/pds` | Ready | app-owned PDS |
| `Requester/requester` | Ready | market requester |
| `PolicyEngine/policy-engine` | Running | in-process engine |
Services and exposures:
| Service | target | Exposure |
| --- | --- | --- |
| `svc-gatewayapi-gateway-api` | `GatewayAPI/gateway-api` | local `127.0.0.1:20002`; ingress through `IngressProxy/ingress` |
| `svc-pds-pds` | `Pds/pds` | local `127.0.0.1:20000`; ingress through `IngressProxy/ingress` |
| `svc-requester-requester` | `Requester/requester` | local `127.0.0.1:20001`; ingress through `IngressProxy/ingress` |
The prod profile defaults `spec.ingress` to `xrpc-subscription` with
`ingressProxyHost: xrpc.fedproxy.com` and the app `IngressProxy/ingress`, so the app PDS owns a
public URL. A gateway spec that sets `spec.ingress.opentunnel` selects `opentunnel` instead; a spec
that sets `spec.ingress` names its own provider. The `market-network` profile inherits the
dispatcher provider from `MarketNetworkValue.ingress`. Public `k8s` traffic still uses the local 8788
front, not an ingress Exposure.
## Gateway and requester identity
`Gateway/gateway` has `spec.profile: prod`, `spec.webAppFront: 127.0.0.1:8788`, and options:
```yaml
network:
plcDirectoryUrl: https://plc.directory
ingressProxyHost: xrpc.fedproxy.com
qrOrigin: ""
ingress:
provider: xrpc-subscription
ingressRef: {kind: IngressProxy, name: ingress}
xrpc:
ingressProxyHost: xrpc.fedproxy.com
options:
- {name: public-origin, value: https://k8s.socialweb.computer}
- {name: gateway-audience, value: did:web:k8s.socialweb.computer}
- {name: kcp-public-host, value: https://kcp.socialweb.computer}
- {name: node-policy, value: tangled-vouch}
- {name: node-policy-args, value: '{"firstFree":true}'}
```
The Gateway's app-owned PDS is created by appctl, its account session is stored in the root vault
at `identity/requester/pds-session`, and the requester reports its repo DID. The gateway DID above is
that requester repo DID, not a browser account DID.
Web sign-in uses the PDS as the OAuth authorization server, with the GatewayAPI as the OAuth client
and callback server. The browser session is a cookie held by the gateway; the PDS session is in the
root vault, not in the gateway's memory. Alice's observed session:
- handle `aliceoa.bsky.social`
- DID `did:plc:lpfuqerea3deuoyrn7ojser4`
- `GET /session` returns the DID after the OAuth callback
- `POST /api/clusters/list` returns the account's clusters
## Cluster API and kubeconfig
`/api/clusters/*` is the HTTP surface used by the browser and by scripts. The same operations are
also available as gateway XRPC (`com.publicdomainrelay.temp.cluster.*`) on the same handler.
For a Ready running cluster, `/api/clusters/kubeconfig` returns a client-certificate kubeconfig whose
server is:
```
https://kcp.socialweb.computer/clusters/<account>/<cluster>
```
On the `prod` profile the kubeconfig has no `certificate-authority-data`; it uses the system trust
store and therefore trusts Caddy's public certificate. A gateway on a loopback or self-signed front
keeps the gateway's `tls/tls.crt` as the CA unless the `kcp-public-ca-file` option names another file.
The client certificate is issued by the cluster's client CA and is presented through Caddy's 8444
certificate front.
A live check before the smoke cluster was deleted:
```bash
curl -sS -X POST -H "Cookie: $COOKIE" -H 'Origin: https://k8s.socialweb.computer' \
-H 'Content-Type: application/json' -d '{"name":"prod-smoke","ttlSeconds":3600}' \
https://k8s.socialweb.computer/api/clusters/kubeconfig
jq -r .kubeconfig response.json > prod-smoke.kubeconfig.yaml
kubectl --kubeconfig prod-smoke.kubeconfig.yaml get namespaces
# default, kube-node-lease, kube-public, kube-system, users
```
## Deploy and upgrade
The installed layout is:
- account `socialweb` (uid 982), state `/var/lib/socialweb-computer/socialweb`
- binary slot `bin/current -> bin/30ab2e...`
- config `/var/lib/socialweb-computer/socialweb/config.json`
- environment `/var/lib/socialweb-computer/socialweb/gateway.env` (mode 0600, owned by socialweb)
- spec `/var/lib/socialweb-computer/gateway-spec-socialweb.yaml`
- Caddy sites under `/etc/caddy/sites`, certificates under `/etc/caddy/socialweb-computer`
A routine upgrade is:
```bash
# as socialweb
CONFIG_PATH_SOCIALWEB_COMPUTER=/var/lib/socialweb-computer/socialweb/config.json \
/var/lib/socialweb-computer/socialweb/bin/current/socialweb-computer \
upgrade --binary /var/tmp/swc-release/socialweb-computer-<sha> --sha256 <sha>
sudo /var/lib/socialweb-computer/socialweb/bin/current/socialweb-computer install gateway \
--account socialweb --state-dir /var/lib/socialweb-computer/socialweb \
--gateway-spec-file /var/lib/socialweb-computer/gateway-spec-socialweb.yaml \
--cert-front-address 127.0.0.1:8444 --tls-front-address 127.0.0.1:8443 \
--kcp-front-address 127.0.0.1:6443 --host-backend systemd-user
```
Then run the Caddy certificate/site handoff from `scripts/deploy.sh` (or the script itself). An
upgrade preserves the kcp store, root vault, requester PDS, DID, account workspaces and clusters.
A fresh install is required when the existing `app` workspace type is the old `denoruntime`: the
WorkspaceType name is immutable. A fresh install removes the old state and requires the Caddy
certificate handoff and a new Alice sign-in.
## Live state
- Gateway Ready: root vault, GatewayAPI, Pds, Requester and PolicyEngine all Ready.
- Alice sign-in: `did:plc:lpfuqerea3deuoyrn7ojser4`, `swc_session` cookie.
- Account workspace `2wjeqtgx` Ready; the smoke cluster was created Ready and then deleted, leaving
no clusters.
- `kcp.socialweb.computer/healthz` 200; `k8s.socialweb.computer/api/config` 200; `/` 404 (API-only).
- `kcp-public-host` is `https://kcp.socialweb.computer`; kubeconfigs use the system CA on prod.
## Known gaps
- No static web UI. A browser sees 404 at `/`; sign-in is the JSON `/oauth/start` call.
- Ingress Exposures are Pending without an `IngressProxy`; the public path is the Caddy local front.
- A fresh install must refresh Caddy's gateway, edge-client and kcp CA files. `scripts/deploy.sh`
does this; a bare `install gateway` does not.
- The `app` workspace type is immutable; an old `denoruntime` install cannot be upgraded in place.
## Inspect
```bash
K=$HOME/.tmp/prod-kubeconfig-new # fetched from the host admin kubeconfig
kubectl --kubeconfig "$K" get workspaces,gateway,openbaoserver
kubectl --kubeconfig "$K" get gatewayapi,pds,requester,policyengine,service,exposure -o wide
kubectl --kubeconfig "$K" get apibinding -A
kubectl --kubeconfig "$K" get gateway gateway -o jsonpath='{.spec}{"\n"}{.status}{"\n"}'
```
On the host, `systemctl --user -M socialweb@ list-units` shows the core, kcp slots, Postgres, kine,
front and OpenBao units.