Szzt — Architecture & Technology Inventory
Szzt publishes small static
sites and loose files to a user's own atproto PDS, then serves them
content-addressed and hash-verified at <name>.szzt.app. Because content lives
in the user's PDS and every byte is checked against its content ID (CID), the
gateway is a convenience and never an authority — the spec calls this credible
exit.
Naming note. Spec section references below (e.g. §9.2) point into
szzt-spec.mdat the repo root.ing.dasl.maslkeeps its own NSID because it is DASL's upstream Web Tiles record adopted as-is; every szzt-owned identifier isapp.szzt.*orszzt.
Repository layout
A pnpm workspace (pnpm-workspace.yaml, package.json — pnpm@10.33.2, Node
≥ 20, ESM throughout). Two globs: packages/* (libraries) and apps/*
(deployables). Every package is @szzt/*, "private": true, version 0.0.0,
built with tsup (esbuild under the hood), typechecked with tsc --noEmit, and
tested with vitest. tsc is typecheck-only — no package emits with it.
Authoritative references at the repo root: szzt-spec.md (normative),
conformance-audit.md, and roadmap.md. Several plan docs
(per-site-origins-plan.md, h12-verifier-sw-plan.md, deploy-provenance-plan.md,
moderation-labeler-research.md) capture specific subsystems.
apps/
gateway/ Node HTTP server — the composition root; serves + resolves + verifies
publisher/ Browser SPA — drag files, OAuth login, build, preview, publish
packages/
core/ Deterministic build: files -> MASL manifest + root CID (dCBOR42/DASL)
records/ atproto record shapes, AT-URI resolution, CID re-derivation (pure)
publish/ Write path: blob-first upload + atomic swap-guarded PDS record write
gateway/ PURE serving/verification engine + browser SW shell (transport injected)
server/ Node HTTP/TLS/ACME glue wrapping the pure engine; naming; RASL mirror
verify/ Reference verifier + CLI + the browser verifier service worker (sw.js)
frontend/ Browser front-end kernels: ingest/normalize/preview + publish-flow FSM
agent/ Headless publish facade (build -> verify root CID -> publish)
mcp/ stdio MCP server over the agent facade — publish/republish/read/name a site
moderation/ Label-based takedown enforcement + naming-allocation decision kernels
Dependency direction: core is the base (pure CID/build math); records depends
only on core; gateway, publish, frontend, agent, moderation build on
those; server wraps gateway; mcp wraps agent; the two apps compose everything.
Applications
apps/gateway — the serving composition root
The deployed Node process and the wiring hub for the whole read path. Entry point
apps/gateway/src/main.ts, symbol createServerFromEnv, reads env vars and
assembles the runtime graph, returning an unstarted http.Server (or https.Server
under the dormant app-TLS path). Highlights of what it wires:
- One SSRF-guarded outbound transport (
createGuardedFetch) shared by every attacker-influenced fetch — DID-document reads, PDS reads, and mirror races — which re-validates URL shape on every redirect hop and pins the connection to an address it has proven public (roadmap H15). - A cached DID-document fetcher (
cachingDidDocuments) feeding both the PDS resolver (HttpPdsResolver) and the signing-key resolver (HttpDidSigningKeyResolver), plus a multi-PDS reader (createMultiPdsReader) whose per-PDS reader is@szzt/verify'satpReader— so every record served is signature-proven. - Publisher service-auth (
ServiceAuthVerifier) split into per-endpoint, audience-bound verifiers (notify / slug / domain / pin / unpin), gated onSERVICE_DID. - Optional durable subsystems, each gated on a
*_DB_PATHenv var and backed by a SQLite store on a Railway volume (seeapps/gateway/src/sqlite-*.ts): the naming index (SqliteNameStore,NAME_DB_PATH), the content-only RASL mirror + pin graph (SqliteBlobStore,BLOB_DB_PATH), the labeler resume cursor, the abuse-report queue, and the TLS-certificate store. - The §17.5 moderation runtime (
startLabelerModeration) — a background labeler subscription started only at the real entry point, enabled by default toward the Bluesky mod service. - A single request listener that answers
/healthz(a deploy-provenance stamp read frombuild-info.json), serves the publisher SPA + its OAuthclient-metadata.jsonsame-origin (publisher-assets.ts), then hands everything else to the pure gateway handler.
Other app-local modules: sqlite-name-store.ts (the real §11 naming index — slug,
custom-domain, and version-pinned resolution), dns-txt-resolver.ts (custom-domain
proof lookups), and sqlite-cert-store.ts.
Build: tsup, platform: "node", ESM (apps/gateway/tsup.config.ts; note
removeNodeProtocol: false so node:sqlite survives). Runs as node dist/main.js.
apps/publisher — the browser publisher SPA
A self-contained single-page app the gateway serves at the apex. Entry
apps/publisher/src/main.ts — DOM + OAuth glue only, deliberately typecheck-only and
never unit-tested; all load-bearing logic lives in the pure packages it drives:
@szzt/core build(), @szzt/publish publish(), the publish-flow FSM
(@szzt/frontend reduce/canPublish), and the drag/zip ingest
(@szzt/frontend/browser). Login is fully in-browser atproto OAuth
(@atproto/oauth-client-browser); the client_id is the served
/client-metadata.json URL, so OAuth identity is bound to wherever the page is served.
App-local helpers: derive.ts (config/format helpers, incl. mirror-config
parsing), typeahead.ts (handle autocomplete), retention.ts (version pruning UI),
slug.ts (slug allocation UI + service-auth), build/docs.ts (build-time
Markdown→HTML rendering of the reference docs).
Build: tsup, platform: "browser", noExternal: [/.*/] — every dependency
(atproto OAuth/api + the @szzt packages) is inlined into one dist/main.js. The
onSuccess hook (apps/publisher/tsup.config.ts) copies the HTML shells (index.html,
about.html) and the shared stylesheet (styles.css) into dist/, and calls
generateDocs() from apps/publisher/build/docs.ts,
which renders the docs/*.md reference docs into styled HTML pages served at
/publish/architecture.html, /publish/content-spec.html,
/publish/gateway-conformance.html, and an index at /publish/docs (the standalone pages
about and docs are canonical without the extension; publisher-assets.ts serves those
and 301s the .html form to them). build/docs.ts
uses marked and is build-time only (Node, reachable only from the tsup config) —
src/main.ts never imports it, so it stays out of the browser bundle. A custom esbuild
plugin (noNodeBuiltinsPlugin) fails the build if any Node builtin is pulled into
the browser bundle — this guards against a real past outage where a stray fs import
shipped a page that was dead on load.
Styling has one owner: apps/publisher/styles.css. The app shell, the About page, and
every generated docs page link it; no page carries a <style> block or a style=
attribute. Rules that legitimately differ per page are scoped by a body class — about
on the About page, doc on the generated docs pages — rather than duplicated, so the
main max-width, h1, h2, code, and link-colour differences between the three
surfaces live in one file.
Packages
packages/core — deterministic build & CID math
Turns files + metadata into a MASL manifest (path → blob CID map) and a root CID.
src/cid.ts computes CIDs with @atcute/cbor (dCBOR42 / DRISL) and @atcute/cid
(CIDv1 base32) — explicitly not @ipld/dag-cbor, which produces different bytes.
This is the byte-exact CID root the whole system agrees on. Also: build.ts,
masl.ts, synthesize.ts (fills in missing pieces like icons), normalize.ts,
site-origin.ts, content sniffing. Pure/isomorphic; deps @atcute/cbor, @atcute/cid.
packages/records — atproto lexicons & resolution
The single source of truth for the three lexicon record shapes (src/types.ts,
src/resolve.ts COLLECTION), so the write/verify/serve paths cannot drift:
| NSID | Role (spec) |
|---|---|
app.szzt.site |
Stable site identity + active pointer (current strong-ref) — §4.1 |
app.szzt.deployment |
Immutable version; points at its manifest via tile — §4.2 |
ing.dasl.masl |
The inline MASL manifest record — §4.3 |
resolve.ts resolveTarget() follows site → deployment → manifest over an injected
RepoReader seam and does no integrity checks itself (the caller re-derives CIDs).
recompute.ts provides parseAtUri, recomputeRootCid, recomputeRecordCid,
recomputeBlobCid. Pure; depends only on @szzt/core.
packages/publish — the write path
Blob-first upload then an atomic, swap-guarded record write to the publisher's PDS
(src/publish.ts publish() → PublishResult; src/rollback.ts; src/blobs.ts
uploadBlobs with BlobCidMismatchError). src/client.ts (atpClient) is a thin
@atproto/api adapter over a minimal PdsClient seam. It also owns the gateway
credential: src/service-auth.ts ServiceAuthMinter (a one-method seam) and
atpServiceAuthMinter, which mints a §10.1 service-auth JWT via
com.atproto.server.getServiceAuth — scoped to one aud + lxm, and mintable from an
ordinary app-password session, so no OAuth is needed to authenticate to a gateway.
loginSessionWithAppPassword returns a PdsSession ({ client, minter }) from one login;
loginWithAppPassword is defined in terms of it and keeps returning just the client. Minting
lives here because this is the only package that depends on @atproto/api — a caller that
speaks to a gateway takes the seam instead. Also holds the mirror
producers (see Transports below): rasl-producer.ts (raslBucketProducer),
r2-bucket-put.ts (Cloudflare R2), ipfs-producer.ts + kubo-block-put.ts (Kubo/IPFS
block/put), plus retention/prune helpers. Node-targeted; deps @atproto/api,
@atproto/common-web.
packages/gateway — the pure serving/verification engine
The transport-agnostic heart: builds the CID→location index from signed state, routes,
and serves each resource by CID re-hashing every byte (fail closed). The network
transport is an injected seam, so the same engine runs server-side and in the browser SW.
Key symbols: src/serve.ts serve(); src/index-map.ts resolveIndex() →
SzztIndex; src/verify-cache.ts VerifyingCache (+ BoundedBlobStore,
MemoryBlobStore); src/mirror.ts MirrorFetcher (races mirror hints, output still
re-verified); src/zip.ts assembleZip and src/bundle.ts assembleBundle
(credible-exit export / transport bundle); src/types.ts GatewayError,
VerificationError. It also owns the DID/PDS resolution + SSRF primitives
(did-resolver.ts, pds-resolver.ts, did-cache.ts, ssrf.ts assertPublicHttps)
and a browser SW shell (sw.ts registerServiceWorker, exported at ./sw; plus
cache-storage.ts). Deps: only @szzt/core + @szzt/records — no external
runtime deps. Pure barrel is isomorphic; browser pieces are separate entries.
packages/server — Node HTTP/TLS/ACME glue
Wraps the pure engine with everything socket-shaped. src/dispatch.ts dispatch()
is the request orchestrator: it resolves the target AT-URI, builds the signed serving
index, serves through the pure engine, applies §12 caching, and enforces §17.5
moderation — and never throws (every failure is a defined HTTP response;
fail-closed 502 for integrity/upstream, 404 for naming misses, 451 for takedowns, 503
for the moderation readiness gate). src/http.ts is the Node socket/TLS glue (separate
./http entry, typecheck-only). Notable subsystems:
- ACME/TLS cert stack (custom-domain HTTPS, §11.2c):
acme.tsobtainCertificate(RFC 8555 order driver),acme-signer.tsWebCryptoAcmeSigner(P-256 account key, PKCS#10 CSRs),acme-renewal.ts(renewCertificates, renewal loop). Certs are persisted (Let's Encrypt rate limits make persistence mandatory). - Content mirror (§13):
mirror-serve.tsRaslMirrorserves + pins CIDs, backed bypin.ts/pin-graph.tsfor exact cross-restart unpin. - Publisher auth (§10.1):
publisher-auth.tsServiceAuthVerifierandsignature-verifier.tsAtprotoSignatureVerifier(via@atproto/crypto). - Naming seam (§11):
name-service.tsstubNameServiceis a resolution stub; the real durable index isSqliteNameStoreinapps/gateway.slug-alloc.ts,domain-bind.ts,domain-recheck.ts,apex.tsparseHost. Observed live against production: re-allocating a slug the requesting DID already owns succeeds and returns the same binding — a same-owner re-allocate is idempotent, not atakenrefusal. - Re-exports
HttpPdsResolver/HttpDidSigningKeyResolverfrom@szzt/gateway.
Deps: @atproto/crypto, cborg, undici, plus workspace core/gateway/records/
moderation. Node-targeted.
packages/verify — reference verifier + CLI + browser SW
The independent, trust-nothing verifier. src/car.ts verifyCarRecord() is the
crux: given a com.atproto.sync.getRecord proof CAR plus the repo's expected signing
key, it proves a record is really in the repo's signed tree — CID-checking every
block, parsing the v3 commit, verifying its signature (the load-bearing step), walking
the MST from the signed data root, and returning an unforgeable branded
VerifiedRecord. There is no boolean/skip variant: an unprovable record has no return
value. src/reader.ts atpReader is a RepoReader that runs every fetched record
through verifyCarRecord; src/resolve-reader.ts adds the DID-doc hops. src/cli.ts
ships the szzt-verify bin (inspect/diff/verify). src/sw-entry.ts is the
browser verifier service worker: it marries readerForTarget → atpReader → verifyCarRecord with the gateway's registerServiceWorker, so after first hit the
in-browser SW re-verifies every asset and the gateway is untrusted (§9.5, plan H12).
CBOR encoder boundary (load-bearing). Two CBOR encoders coexist here on purpose:
@atproto/repo's dag-cbor owns everything repo-shaped (CAR blocks, MST nodes, re-encoding the unsigned commit for signature verification — because that is what the PDS actually signed), while@atcute/cbor(@szzt/core) owns the szzt-computed CIDs.car.tsnever re-encodes a block to derive a CID, which is what keeps the two from ever having to agree. See the header comment incar.tsbefore touching it.
The SW ships as a self-contained classic sw.js IIFE — a second tsup/esbuild build
(platform: "browser", noExternal) with custom plugins that guard against Node
builtins, stub node:timers/promises (setImmediate → Promise.resolve), alias
@atproto/common to a no-op logger, and inject the buffer polyfill. apps/gateway
serves this bundle at /sw.js. Deps: @atproto/api, @atproto/repo, buffer; a
playwright real-browser harness in dev.
packages/frontend — front-end kernels + publish FSM
Browser kernels for ingest → normalize → build → preview → publish, with the
load-bearing logic pure and unit-tested. src/flow.ts reduce() is the explicit
publish-flow state machine: publish is reachable only from the previewed state (a
successful build and a rendered sandboxed preview) and only when authenticated
(canPublish) — illegal transitions return the state unchanged, so impossible states
are unrepresentable. Also ingest.ts (decompression-bomb ceiling guard),
preview.ts (a verifying preview that reuses the gateway serve engine rather than a
second verify path), exit-routes.ts (§11.4 credible-exit URLs). Framework-agnostic —
no React/Vue/Svelte; DOM/OAuth glue is isolated in the typecheck-only ./browser entry.
packages/agent — headless publish facade + the szzt CLI
An ergonomic, non-browser API over @szzt/core + @szzt/publish: build →
verify the root CID → publish (§15). src/build.ts buildSite, src/verify-build.ts
verifyBuild (re-checks the root CID before writing), src/auth.ts resolveAuth (and
loginSession, one login yielding both the PdsClient and a ServiceAuthMinter),
src/publish.ts publishSite/publishFiles. No external deps. Node-targeted.
Also ships the szzt bin (src/cli.ts, szzt publish <dir> and szzt slug <name>) —
the surface a headless agent drives. Supporting modules: src/files.ts readDirFiles
(directory tree → FileInput[]; POSIX-separated relative paths so the same tree yields the
same root CID on any OS, and .git/node_modules/symlinks skipped so nothing outside the
publish root can be published), src/urls.ts siteUrls (the derived addresses only —
per-site origin and web+rasl:// exit link; an allocated slug or custom domain is a separate
authenticated request and is never inferred), and src/cli-args.ts (the unit-tested pure
half: parseArgs, credentialsFromEnv, metaFromFlags, usage).
Slug claims (§11.1) are the one place this package talks to a gateway, so that a headless
publisher can hold a real name (https://<slug>.<apex>/) and not only a derived origin — the
only address a did:web author has at all. src/slug.ts is the pure half (slugCommandArgs,
normalizeSlug, slugInputError, slugOutcomeError, slugSiteUrl, slugAllocUrl,
serviceDidFor, plus SLUG_ALLOC_PATH/SLUG_ALLOC_LXM mirrored from @szzt/server
slug-alloc.ts, as apps/publisher/src/slug.ts mirrors them — no dependency exists between
the three, and a drifted lxm is a 401). src/claim-slug.ts claimSlug mints a token
through the ServiceAuthMinter seam and POSTs the allocation over plain fetch (injectable;
no HTTP dependency), raising SlugClaimError — with the gateway's HTTP status when it
answered, and without one when it could not be reached. A claim grants no authority over
content: the gateway still re-derives and hash-verifies every byte from the author's repo.
Only allocation is exposed; release permanently RETIRES a slug, which is not a thing a CLI
flag should put one keystroke away. szzt publish --slug <name> runs both steps on one
login, validating the name before the build so a typo costs no publish, and — because the
write has already landed by then — reports a failed claim as exactly that: the deployment
stands, its printed URLs work, and only the name is unclaimed (exit non-zero).
Credentials reach the CLI only through the environment — SZZT_IDENTIFIER,
SZZT_APP_PASSWORD, optional SZZT_PDS — never argv, which leaks into shell history,
ps, and agent transcripts. The gateway is not in this path: build is local, the root
CID is re-verified before any write, and the write goes straight to the author's PDS, so
agent-published content lives in the author's own repo and carries the same credible exit
as a browser publish.
packages/mcp — the stdio MCP server
The same headless publish path as the CLI, exposed to a model as tools. A stdio server
(src/bin.ts, bin szzt-mcp) built on @modelcontextprotocol/sdk, registering four tools
in src/server.ts: publish_site (new site), republish_site (replace content, keep the
URL — the only tool that does), get_site (read-only), and allocate_slug (claim a
readable <slug>.<apex> name). Depends on @szzt/agent, which is where the build/verify/
write actually happens.
Three of the four never touch a gateway — build is local, the write goes to the operator's
PDS. allocate_slug is the exception by definition, since a slug is an entry in one
gateway's naming index (tools.ts allocateSlugTool over @szzt/agent claimSlug). It
needs a session rather than a client, because the claim is authenticated with a
service-auth JWT that only a login can mint — hence the second seam, ToolDeps.session, and
SlugSessionUnavailableError when it is absent. The slug is normalized and charset-checked
before the request: a claim is permanent, so a typo must fail locally and for free.
Layered so the behaviour is testable without a transport: src/inputs.ts is the pure input
kernel (resolveSource — exactly one of dir / files; inlineFilesToFileInputs;
metaFromInput), src/tools.ts holds the tool bodies (publishTool, siteStatusTool) with
the credential seam injected as ToolDeps, src/result.ts shapes what comes back, and
src/server.ts is protocol wiring only. Tests drive the tool bodies end to end against the
in-memory PdsClient in test/fixtures.ts — no network, no transport.
Inline base64 is round-trip checked in inlineBytes because Node's decoder silently drops
out-of-alphabet input, which would publish a corrupt file under a root CID that commits to
the corruption. Path shape, ceilings and entry inference are deliberately not re-checked
here — @szzt/core normalize() owns those rules, and forking them would let the two
publish paths diverge.
Credentials are read from the server's own process environment (src/env.ts
credentialsFromEnv, mirroring the CLI's three variables) and are never tool parameters: a
tool parameter is written by the calling model and echoed back in the transcript, so exposing
one would let the model supply, read back, or exfiltrate the operator's app password. The
seam is a function resolved per call, so a missing variable is a readable tool error naming
it rather than a launch-time death behind an opaque transport failure. An MCP client
configures it like this:
{
"mcpServers": {
"szzt": {
"command": "node",
"args": ["/abs/path/to/szzt/packages/mcp/dist/bin.js"],
"env": {
"SZZT_IDENTIFIER": "you.example.com",
"SZZT_APP_PASSWORD": "xxxx-xxxx-xxxx-xxxx",
"SZZT_PDS": "https://bsky.social"
}
}
}
}
The absolute path to the built file is what a client launches: every workspace package is
"private": true, so the szzt-mcp bin the package declares is only on PATH inside the
workspace, and an MCP client starts the server from its own working directory. Run
pnpm -r build first — dist/ is gitignored.
SZZT_PDS is optional and defaults to https://bsky.social. SZZT_APP_PASSWORD is an app
password, never the account password — an app password is separately revocable and cannot
change the account's own credentials. Every publish is a real, public, externally visible
write with no unpublish, which is why the server's instructions and each tool description
say so: the model is the one deciding when to call, and it needs that in front of it.
packages/moderation — takedown enforcement + naming policy
Pure decision kernels over injected DNS/labeler seams; never in the write or auth path.
src/labels.ts LabelStore (ingests com.atproto.label labels, answers "what applies
to this target"), src/enforce.ts Enforcer (delist-only takedown decisions),
src/labeler-sync.ts LabelerSync (an explicit connection/readiness FSM:
connecting | backfilling | synced | reconnecting) + runLabelerSync() (reconnect
loop draining a label stream into a LabelStore). The actual
com.atproto.label.subscribeLabels WebSocket lives in @szzt/server. Also
slugs.ts / domains.ts / detection.ts / reports.ts. Depends only on
@szzt/records.
Technology inventory
| Concern | Technology / where |
|---|---|
| Monorepo | pnpm workspaces (pnpm-workspace.yaml), pnpm@10.33.2, Node ≥ 20, ESM |
| Build | tsup (esbuild) per package; tsc --noEmit for typecheck; vitest for tests |
| Content addressing | dCBOR42 / DRISL via @atcute/cbor + @atcute/cid (packages/core/src/cid.ts); DASL subset of IPFS |
| Records & identity | atproto lexicons app.szzt.site / app.szzt.deployment / ing.dasl.masl; @atproto/api, @atproto/repo, @atproto/crypto |
| Storage | User's own atproto PDS (blobs + signed records) |
| Auth (publish) | In-browser atproto OAuth (@atproto/oauth-client-browser); service-auth JWTs (ServiceAuthVerifier) |
| Verification | Signed-CAR proof (verify/src/car.ts verifyCarRecord); per-byte CID re-hash (gateway VerifyingCache) |
| Browser publisher | tsup browser bundle, single inlined dist/main.js, no-Node-builtin build guard; build also renders reference docs to HTML via marked (build-time only) |
| Verifier in browser | Classic-script service worker sw.js (IIFE), served at /sw.js (verify/src/sw-entry.ts) |
| Retrieval transports | RASL over HTTP (/.well-known/rasl/<cid>); mirror hints via CDN/R2 and IPFS/Kubo; PDS as backstop |
| Server transport | Node http/https, undici guarded fetch (SSRF-pinned), cborg |
| Custom-domain TLS | ACME / RFC 8555 (server/src/acme.ts), WebCryptoAcmeSigner (P-256, PKCS#10), Let's Encrypt |
| Moderation | atproto labelers (com.atproto.label), delist-not-erase Enforcer (packages/moderation) |
| Deploy | One command: pnpm ship (scripts/deploy.mjs → railway up --ci, provenance stamp). Railway builds the image remotely from apps/gateway/Dockerfile (node:24-slim) — no local Docker |
Retrieval transports (§13, "credible exit")
A resolved site can advertise mirror hints, and the gateway races them before falling
back to the owner PDS — but a mirror is never trusted: MirrorFetcher re-hashes
every mirror byte against the CID, so a hostile or stale mirror can only fail
verification and be skipped. Two mirror producers exist on the write side
(packages/publish): a Cloudflare R2 / CDN producer (r2BucketPut,
raslBucketProducer) and an IPFS/Kubo producer (ipfsProducer, kuboBlockPut).
The gateway itself can also act as a content-only RASL mirror (RaslMirror, gated on
BLOB_DB_PATH).
Current state vs. spec (flag). The publisher's mirror-configuration UI (
apps/publisher/index.html, the#r2-configand#ipfs-configsections) is currently hidden (class="hidden") — see the recent commit "hide CDN/IPFS mirror UI pending rework". The transports and their config parsing still exist in code, so the capability is present but not user-exposed right now.
Deployment & TLS posture
Deploying is one command from a clean tree: pnpm ship (=
node scripts/deploy.mjs, which runs railway up --ci under the hood). scripts/deploy.mjs
refuses a dirty tree, stamps apps/gateway/build-info.json so /healthz reports the exact
commit (and whether it was pushed), uploads, then polls /healthz until the new commit is
live. railway up
uploads the local working tree, not a git ref, so git push ships nothing to production —
the stamp is what keeps the running image traceable to a commit (see scripts/deploy.mjs
and deploy-provenance-plan.md).
Nobody runs Docker locally. Railway builds the image remotely from
apps/gateway/Dockerfile (node:24-slim): that build runs pnpm -r build, producing the
gateway dist/, the publisher dist/ (SPA + client-metadata.json + generated
reference-doc HTML pages), and the verifier sw.js — all served by the one gateway
process. By default the gateway binds plain HTTP behind Railway's edge TLS; its
own app-terminated TLS path (CUSTOM_DOMAIN_TLS, per-SNI certs from the cert store) is
implemented but dormant / off by default pending the Railway raw-TLS topology (see
the note in createServerFromEnv).
How a request flows (read path)
- A request arrives at
apps/gatewaymain.ts's request listener./healthzand the publisher SPA (on the apex) are handled first; everything else goes to the pure gateway handler. - The handler parses the host/path (
parseHost) into aGatewayRequest— a slug, custom domain, derived per-site origin, version-pinned root CID, a bare-apex/.well-known/rasl/<cid>mirror hit, an ACME challenge, or a bundle/export — and callspackages/serverdispatch.tsdispatch(). dispatchapplies the §17.5 readiness gate (503 until the label stream is synced), then a per-IP read limiter, then resolves the target AT-URI to a signed serving index (resolveSiteTarget→resolveIndex), following the signed hops site → deployment → manifest. Records are fetched throughatpReader, so each is proven against the repo's signed commit (verifyCarRecord).- Takedown enforcement (
guardServe/guardCid) refuses a labeled DID/URI/CID with - The pure
@szzt/gatewayengine (serve) fetches each resource blob — gateway cache → mirror hints (MirrorFetcher) → owner PDS — and re-hashes every byte against its CID (VerifyingCache). A mismatch is a fail-closed 502; it never serves unverified bytes. dispatchapplies §12 caching (immutable for version-pinned origins, short pointer TTL for named/current origins) and returns a defined HTTP response — it never throws.- On first hit, synthesized site content registers the verifier service worker
(
/sw.js); thereafter the in-browser SW re-verifies every asset, so the gateway is no longer trusted for integrity.
How a publish flows (write path)
- In the browser publisher (
apps/publisher/main.ts), the user signs in via atproto OAuth, then drops files/folder/zip. Ingest runs through@szzt/frontend/browser(with a decompression-bomb ceiling guard). @szzt/corebuild()normalizes the files, synthesizes anything missing, hashes each file to a CID, and assembles the MASL manifest whose CID is the site's root CID.- The publish-flow FSM (
@szzt/frontendreduce) advancesidle → building → ready. The site renders in a sandboxed preview iframe; only once it has rendered does the machine reachpreviewed. Publish is unreachable untilpreviewedand authenticated (canPublish) — the §14 preview-before-publish gate encoded as transitions, not ad-hoc flags. - On publish,
@szzt/publishpublish()uploads blobs first (each verified to its CID) and then does an atomic, swap-guarded write of theing.dasl.masl,app.szzt.deployment, andapp.szzt.siterecords to the user's PDS. Rollback is available if a swap guard trips. - Optionally the publisher pushes the new version as a hint to the gateway (§10) and/or
mirrors blobs to its own R2/IPFS (transports present but UI currently hidden). The
headless
@szzt/agentfacade performs the same build → verify-root-CID → publish sequence without a browser. - The user gets a URL (
<slug>.szzt.app, a derived per-site origin, or aweb+rasl://<rootCID>credible-exit link). The next read request resolves the newcurrentpointer and serves it verified.
Notes, gaps, and things to verify
- Naming layering.
packages/serverships onlystubNameService; the real durable §11 naming index (slug / custom-domain / version-pinned resolution) isSqliteNameStoreinapps/gateway, wired only whenNAME_DB_PATHis set. With it unset the gateway is read-only for naming and serves only derived per-site origins. - App-TLS is dormant. The ACME/cert stack is fully implemented but the app-terminated TLS listener is off by default; production relies on Railway edge TLS.
- Mirror UI hidden. As flagged above, the CDN/IPFS mirror UI is hidden pending rework — the only place code and shipped UX currently diverge.
- Moderation on by default. The gateway subscribes to the Bluesky mod service
labeler unless an operator explicitly opts out (
LABELER_ENDPOINT=""). - Per-subsystem invariants (SSRF pinning, the CBOR encoder boundary, cache-poisoning
defenses) are documented in load-bearing header comments in the relevant source files
and in
szzt-spec.md/conformance-audit.md.