Flow · verified · publish-flow

Page Publish

What happens from a user clicking Publish to the page being live at pages.fuzzycode.dev?

Last verified 2026-04-17 against code
Slug publish-flow
Format Mermaid sequence
Audience engineer

§1 The shape

Publish is a two-hop, browser-orchestrated flow. There is no server-to-server call from FuzzyCode to Pages. The browser first obtains an HMAC-signed attestation from FuzzyCode, then submits to Pages through a Cloudflare CORS-proxy Worker.

Pages makes two independent decisions after accepting a submit: how to persist the row (Branch A, controlled by duplication_strategy) and what status to return (Branch B, controlled by requested access_level). The two branches combine multiplicatively — any persistence outcome can pair with any status outcome.

sequenceDiagram autonumber participant U as 👤 Browser participant F as FuzzyCode
fuzzycode.dev participant W as CF Worker
proxy-cors participant P as Pages
pages.fuzzycode.dev participant DB as Postgres participant S3 as S3
aws.fuzzycode.dev rect rgb(237, 217, 192) Note over U,F: HOP 1 — Attest clean HTML U->>+F: POST /api/pages/attest-publish
html · title · project_id F->>F: PII firewall · Bedrock scan F->>F: HMAC-sign attestation F-->>-U: { claims, signature, html_hash, clean_state } end rect rgb(230, 169, 94) Note over U,P: HOP 2 — Submit via CORS-proxy Worker U->>+W: POST fuzzycode.dev/@pages/submit
html · attestation · screenshot · access_level Note right of W: Worker strips /@pages,
rewrites host to pages.fuzzycode.dev,
forwards cookies intact W->>-P: POST pages.fuzzycode.dev/submit end activate P P->>P: require JWT · resolve username
verify HMAC attestation P->>DB: duplicate-check by html_hash Note over P,DB: BRANCH A — persistence (duplication_strategy) alt 'update' P->>DB: UPDATE row (same hash_key) P->>S3: overwrite screenshot.webp
⚠ no Cloudflare purge else 'duplicate' (default) P->>DB: INSERT new row P->>S3: write screenshot.webp
direct boto3 (not via CDNBuddy) end Note over P,DB: BRANCH B — response status (requested access_level) alt public or unlisted P->>DB: open page_visibility_approval_request
store access_level='private', link_status=false P-->>U: HTTP 202 · { url, pending_approval } else private P-->>U: HTTP 200 · { url, hash_key, access_level } end deactivate P Note over U,S3: Page later served at pages.fuzzycode.dev/page/<hash>.
HTML returned verbatim · no asset URL rewriting.
Assets resolve directly to images./sounds./sprites./uploads. subdomains.
Hop 1 — Attestation (FuzzyCode-side PII firewall + HMAC)
Hop 2 — Submit via Cloudflare Worker
Note — behavior or gotcha
Pages-side processing

§2 Five things the diagram tells you that earlier docs did not

1. There is no server-to-server hop. The browser orchestrates both requests. fuzzycode.dev/@pages/* is a Cloudflare CORS-proxy Worker, not a FuzzyCode backend route. Prior docs framed this as FuzzyCode → Pages → CDN → UGC proxy; that pipeline does not exist.
2. Attestation is a prerequisite, not a suggestion. Pages rejects any /submit that can't present a valid HMAC over {html_hash, clean_state, clean_basis, title_hash}. The attestation is created by FuzzyCode, stored client-side briefly, and forwarded on the second hop.
3. The screenshot goes direct to S3. Not via CDNBuddy. CDNBuddy only serves /resolve and is not on the publish path at all.
4. Public/unlisted are aspirational at publish time. Pages always stores new rows as access_level='private', link_status=false when the request is public or unlisted — an approval request is opened, HTTP 202 is returned, and the row is "promoted" only if a parent approves. There is no explicit status column; state is encoded across four fields plus the approval-request reference.
5. Re-publish does not invalidate cache. duplication_strategy='update' overwrites both the Postgres row and the S3 screenshot at the same keys. Nothing in code calls Cloudflare's cache purge API. Until TTL expires (~2h default), edges may serve the stale HTML.

§3 Error handling — server emits 15 paths, client handles one

The single biggest source of publish-related incidents (including the 2026-04-06 Fire HD issue) is that all non-401 errors are silently swallowed by the client. The diagram above hides this for clarity; the table below does not.

# Where Failure HTTP Client handling Code
1Hop 1Missing html_content400silently swallowedapi.py:1666
2Hop 1PII detected in prompt or HTML400silently swallowedapi.py:1696
3Hop 1PII firewall unavailable503silently swallowedapi.py:1702
4Hop 1Post-cancellation archive window403silently swallowedapi.py:1673
5Hop 2Missing JWT401handled — toast "Missing User Token"jwt_required
6Hop 2Username missing from JWT (Fire HD bug)400silently swallowedmain.py:2602
7Hop 2Archive read-only403silently swallowedmain.py:2608
8Hop 2Attestation HMAC mismatch400silently swallowedmain.py:2209-2223
9Hop 2Duplicate hash (prevent)409handled — handleDuplicationWarningmain.py:2716
10Hop 2Postgres read timeout (5s)503silently swallowedmain.py:2700
11Hop 2Postgres insert timeout (5s)503silently swallowedmain.py:2874
12Hop 2Postgres verify timeout (3s)503silently swallowedmain.py:2868
13Hop 2DB insert failure500silently swallowedmain.py:2899
14Hop 2Unhandled submit exception500silently swallowedmain.py:3005
15asyncBackground screenshot failurelogged only; screenshot=NULL forevermain.py:2959-2976

§4 Environment state

Publish itself is identical UAT vs prod. Auth layer feeding into publish is not.

UAT · 2026-04-17

Active-context on. Sibling verifiers fetch JWKS from FuzzyCode. Parent-child context tokens in play. Attestation path identical.

PROD · 2026-04-17

Active-context off (ACTIVE_CONTEXT_ENABLED="false" in every service .replit). Legacy JWT-claims gating. Media siblings (Images/Sounds/Sprites) still block child-direct sessions that lack SUBSCRIPTION:ACTIVE / CREATE_IMAGE:FULL.

§5 What state does a page end up in?

There is no status column on the pages table. Lifecycle is encoded as the combination of four fields: access_level, link_status, deleted, and the presence of pending_approval_request_id. Two artifacts below: a minimal transitions diagram (what can move where), and a state-encoding matrix (what each state looks like in the database).

Transitions

flowchart LR classDef private fill:#eaf3ff,stroke:#1d58b1,color:#111 classDef pending fill:#fff4d6,stroke:#a47a3a,color:#111 classDef public fill:#e8f6ec,stroke:#2f7a3e,color:#111 classDef deleted fill:#f0f0f0,stroke:#6b6455,color:#555 Start((submit)) -->|request private| A[Private
visible to owner] Start -->|request public / unlisted| B[Awaiting
parent approval] B -->|parent approves| C[Public
or Unlisted] B -->|parent denies /
request cancelled| A A -->|re-submit requesting
public or unlisted| B C -->|visibility change
requested| B A -->|/delete_page| D[Soft-deleted] B -->|/delete_page| D C -->|/delete_page| D class A private class B pending class C public class D deleted

State encoding — what each state looks like in the DB

Reading this matrix is the only way to correctly query for a given state. A query like WHERE access_level='public' alone does not return public pages — it also returns pages awaiting approval that were originally public.

Field Private
visible
Awaiting
approval
Public /
Unlisted
Soft-
deleted
access_level private private (forced) public or unlisted any (unchanged)
link_status true false true any
deleted false false false true
pending_approval_request_id NULL <uuid> NULL any
active_approval_request_id NULL NULL <uuid> any
HTTP at submit 200 202 n/a (approval path) n/a
Servable by /page/<hash> yes (owner) yes (owner) yes (per ACL) no

Transition and encoding anchors: db/migrations/0001_baseline_prod_schema.sql:320-331, 0004_add_pages_visibility_approval_columns.sql, handler transitions at FuzzycodePagesFlaskServer/main.py:2812-2828 (approval open), main.py:3231 (approval grant), main.py:2771 (cancel), main.py:4383 (delete).

§6 Verification pointers

Every claim in the diagram and on this page is anchored to code. Regenerate confidence by re-reading these:

§7 How this page was built

Everything above comes from two files:

This HTML page is the human-facing render. It's static — no build step, no tooling install required. Open it in any browser. Mermaid rendering happens client-side via CDN.

When the underlying code shifts (say, a new step in Hop 2 or a changed Worker route), the maintenance agent updates source.mmd and description.md, bumps last_verified, and this page re-renders automatically. No drift between agent-facing text and human-facing visual, by construction.