Web Publishing Design (Final)

CogOS provides the bridge. The cogent decides what to build.

Architecture

Request Flow
Browser
▼ Cloudflare (Access JWT + DNS)
▼ Web Gateway Lambda (per-cogent, Function URL, no VPC)
▼ Validate Cloudflare Access JWT
├─── Static: /*
│ ▼ map URL → web/{path}
│ ▼ lookup: web/{path} → web/{path}/index.html → 404
│ ▼ read from Postgres via RDS Data API
│ ▼ return content + Cache-Control: no-store
└─── Dynamic: /api/*
▼ append to io:web:request channel
▼ auto-delivery created, handler marked RUNNABLE
▼ push to SQS ingress (FIFO, with MessageGroupId)
▼ ingress Lambda → executor dispatches handler
▼ handler calls web.respond()
▼ writes to cogos_web_response table
▼ gateway polls table (exponential backoff, 30s timeout)
▼ return HTTP response (or 504 on timeout, 502 on error)

Three Layers

Layer 1: AWS Infrastructure (cogtainer CDK stack)
Gateway Lambda
Python 3.12, 512 MB, 60s timeout, Function URL, no VPC (RDS Data API). The only new AWS resource.
Cloudflare DNS
Points subdomain at Function URL. Access policy already exists.
IAM Role
RDS Data API (file store + channels + web_response table), SQS ingress push.
Response Table
cogos_web_response — lightweight request_id → response mapping. Rows TTL'd after 5 min.
Layer 2: CogOS Runtime (image boot)
web capability
publish(), unpublish(), respond(), list() — registered in init/capabilities.py like discord.
io:web:request
System channel for inbound HTTP requests. Created at boot like io:discord:dm.

Every cogent gets these. A cogent that doesn't use web pays nothing.

Layer 3: Cogent Application (optional)
Web cog
Defined in apps/website/init/cog.py. Daemon subscribed via handlers=["io:web:request"].
Handler prompt
Coglet's main.md — reads request, does work, calls web.respond(). Route logic lives here.
Published files
Any process with web capability calls web.publish("path", content). Stored at web/* in file store.

Discord Analogy

Concept Discord Web
IO Bridge Discord Bridge (Fargate) Web Gateway Lambda
Inbound Channel io:discord:dm io:web:request
Capability discord.send_message() web.respond() / web.publish()
Response mechanism Fire-and-forget (post to Discord) cogos_web_response table (sync)
Dispatch append → delivery → ingress → executor append → delivery → ingress → executor
Handler discord cog handler coglet web cog handler coglet

Response Mechanism

cogos_web_response table (not channels)
CREATE TABLE cogos_web_response (
request_id UUID PRIMARY KEY,
status INTEGER DEFAULT 200,
headers JSONB DEFAULT '{}',
body TEXT DEFAULT '',
error TEXT,
created_at TIMESTAMPTZ DEFAULT now()
);
Gateway polls by request_id (exponential backoff: 100ms, 200ms, 400ms...)
Handler writes via web.respond() → INSERT into table
Gateway reads row → returns HTTP → deletes row
Cleanup: dispatcher tick deletes rows older than 5 min

Concurrency (v1)

Serial handler. One daemon process, requests queue behind each other.

If handler is WAITING → first request dispatches it immediately.
If handler is RUNNING → new deliveries queue until it loops back.

Mitigations:

  • 30s gateway timeout → stale requests get 504'd
  • Handlers should respond fast + spawn child processes for expensive work (202 pattern)
  • Future: handler process pool for parallelism

v1 Limitations

  • Text-only static files — no binary (images, PDFs). Link to external assets.
  • Serial request handling — one handler, requests queue
  • 6 MB payload limit — Lambda Function URL constraint
  • Same-origin only — no CORS headers, frontend must be on same subdomain
  • No caching — every request hits Lambda + Postgres