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. The product is "Szzt" and deploys at
szzt.app, but the code is internally named Mini-Site: every workspace package is@minisite/*, the root package isminisite, and the normative spec ismini-site-spec.mdat the repo root. "Szzt" and "Mini-Site" are the same system. Spec section references below (e.g. §9.2) point intomini-site-spec.md.
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 @minisite/*, "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: mini-site-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)
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; 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@minisite/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:
@minisite/core build(), @minisite/publish publish(), the publish-flow FSM
(@minisite/frontend reduce/canPublish), and the drag/zip ingest
(@minisite/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 @minisite packages) is inlined into one dist/main.js. The
onSuccess hook (apps/publisher/tsup.config.ts) copies the HTML shells (index.html,
about.html) 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.html. 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.
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) |
|---|---|
pub.mini.site |
Stable site identity + active pointer (current strong-ref) — §4.1 |
pub.mini.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 @minisite/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. 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() →
MiniSiteIndex; 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 @minisite/core + @minisite/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. - Re-exports
HttpPdsResolver/HttpDidSigningKeyResolverfrom@minisite/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 minisite-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(@minisite/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
An ergonomic, non-browser API over @minisite/core + @minisite/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,
src/publish.ts publishSite/publishFiles. No external deps. Node-targeted.
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 @minisite/server. Also
slugs.ts / domains.ts / detection.ts / reports.ts. Depends only on
@minisite/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 pub.mini.site / pub.mini.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
@minisite/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@minisite/frontend/browser(with a decompression-bomb ceiling guard). @minisite/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 (
@minisite/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,
@minisite/publishpublish()uploads blobs first (each verified to its CID) and then does an atomic, swap-guarded write of theing.dasl.masl,pub.mini.deployment, andpub.mini.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
@minisite/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
mini-site-spec.md/conformance-audit.md.