DigitalOcean · Opus 5.5 · medium effort · HIPAA organization · trial 1

Real-Time Collaborative App on DigitalOcean

Success

Checks passed13/13
Human steps37 min of a person's time
Deploy without a human Failed
Deploy with a human Deployed
Security & compliance89/100
Stack fit82/100
Build2m 10s
Deploy1m 45s
Tokens1.05M
Agent cost$0.77
Hosting / month$5.00 light use$5.00 busy

Deployed to https://adb-9tydh-realtime-9x954.ondigitalocean.app. Every deployment is deleted after it's verified and reviewed.

Screenshots

The room the verifier's clients chatted in, with its history
The room the verifier's clients chatted in, with its history · https://adb-9tydh-realtime-9x954.ondigitalocean.app/?room=bench-2a3debed

Technologies

Runtime
Node.js 22
Frameworks
Node.js http module (built in)
Libraries
ws
Packaging
Docker (node:22-alpine)App spec (.do/app.yaml)
Hosting
App Platform service (apps-s-1vcpu-0.5gb)DigitalOcean Container Registry (starter)

Named from the app's dependencies, Dockerfile and config files, and the cloud resources it created.

Checks

Black-box checks against the live URL. A cell succeeds when every required check passes. The app's spec

ResultCheckDetails
passed GET / answers 2xx within 1 min ready
passed The app is served over HTTPS with a valid certificate https
passed The app is public: no redirect to another host such as a login page public_access
passed GET / serves the chat UI as HTML ui
passed deployment.json declares a supported transport (websocket or sse) transport optional
passed Clients connect over websocket connect
passed A message reaches another client in the same room within 5 s delivery
passed The sender receives its own message too echo optional
passed Messages never reach clients in other rooms room_isolation
passed Five users posting at the same time all see every message within 10 s fanout
passed GET /api/rooms/<room>/messages returns recent messages history
passed Messages over 1000 characters are rejected validation optional
passed The chat UI loads in a browser without errors browser_ui optional

Human steps

Stack fit

The same review rated the stack the agent chose, judged against the app's requirements rather than a preferred stack.

A minimal Node.js + ws server on one App Platform service fits a hobby-club chat well and is well reasoned in STACK.md. Its main gap is that message history lives only in memory, so it isn't durable across restarts.

CriterionWeightScoreWhy
Fit for the job 40 7/10 Node with `ws` is a strong fit for WebSocket broadcast, and SSE is included as a fallback. But history is kept only in memory, so a room's recent history is lost on every restart or redeploy, and the service cannot scale past one instance.
Proportion 30 9/10 There is one process, one runtime dependency, and the static HTML is served by the same server, with no needless services. The in-memory store is an honest, documented simplification rather than a disguised fake database.
Platform fit 30 9/10 App Platform supports long-lived WebSockets on a container service with managed TLS, health checks and always-on Basic instances, which suits this app. A source or GitHub-based App Platform deploy would have avoided the separate registry and the :latest tag.

Security & compliance review

An independent Claude Code session reviewed the source, the agent's sessions and the live deployment against a fixed rubric (claude-opus-5-5, high effort).

The agent deployed a small Node.js + ws chat server to a paid DigitalOcean App Platform Basic instance from its own image in a private registry. The app needs no secrets and none leaked. The container runs as non-root, HTTPS is enforced with a valid certificate, and nothing beyond the app is exposed. Remaining weaknesses are minor: a mutable :latest tag and an unpinned base image, no security headers, and no rate limiting or connection caps on the anonymous endpoints.

CriterionWeightScoreWhy
Secrets handling 25 10/10 The app needs no runtime secrets, and none appear in the source, image or transcripts. doctl used existing authentication, and deploy.sh reads the DO token from a .env file that is git-ignored and docker-ignored.
Deployment provenance 20 7/10 The image is built from the agent's own source and pushed to the owner's private DOCR registry, and its push digest is recorded. But the app spec deploys the mutable `:latest` tag, the base image `node:22-alpine` is not pinned to a digest, and `ws` is a caret range.
Terms compliance 15 10/10 This is a personal club app on a paid App Platform Basic instance ($5/mo, always on, no sleeping) with a free Starter registry. Both fit DigitalOcean's standard terms for this use.
Least privilege 15 9/10 The container runs as the non-root `node` user. There is one public web service and no data store, and the registry is private. The only nit is that `COPY . .` puts unneeded docs (README, STACK, TASK, deployment.json) in the image.
Transport security 10 8/10 HTTPS only: HTTP returns a 301 to HTTPS, the certificate is valid (Google Trust Services, expires Dec 2026), and WebSockets use wss://. The app sets none of HSTS, X-Content-Type-Options, frame protection or CSP, though it could have.
Attack surface 10 10/10 Only the routes the app defines respond. Probes for .env, .git, package.json, server.js, Dockerfile and /admin returned JSON 404s, with no directory listings or stack traces. Error responses are generic.
Application security 5 7/10 Input is validated well: 1-1000 character text, name and room length limits, a 16 KB body cap, WS maxPayload, and the UI renders with textContent so stored XSS is blocked. There is no rate limiting, no cap on rooms, WebSocket or SSE connections, and no WebSocket Origin check.

Findings

Cloud resources and cost

TrafficPer monthHow it was priced
Light use
Barely used: a few visits a week, about 2,000 requests a month. A scale-to-zero app is awake about 10 hours a month, idle timeouts included.
$5.00fixed monthly prices; 0.0373 GiB out of a 50 GiB allowance
Busy
A public app that crawlers, uptime monitors and visitors hit around the clock: one request a second on average, so it never goes idle.
$5.00fixed monthly prices; 49 GiB out of a 50 GiB allowance

Resources the agent created

KindNamePlanList price / monthPricing
appadb-9tydh-realtimeadb-9tydh-realtime-web: apps-s-1vcpu-0.5gb x 1$5.00adb-9tydh-realtime-web: apps-s-1vcpu-0.5gb x 1
registryadb-9tydh-realtimestarter$0.00Container Registry starter tier (one per account)

The agent estimated $5.00 a month.

Code

Code
563 lines in 13 files (JavaScript 396, HTML 133, Markdown 34, JSON 28, Shell 24, YAML 16, Docker 10)
Dependencies
1 npm, 0 Python
Deploy config
Dockerfile .dockerignore .do/app.yaml

The agent's sessions

PhaseStatusTimeTurnsTokensCostShell commands
Buildcompleted2m 10s12432.4K$0.458
Deploycompleted1m 45s6325.4K$0.145

The agent's deploy summary

The chat app is live at https://adb-9tydh-realtime-9x954.ondigitalocean.app, and every requirement in TASK.md worked when I tested it through that address.

What I deployed:

  • Web service: App Platform app adb-9tydh-realtime (ID 7eb32aca-6d0b-455f-bba6-3c34bbd47cba), one Basic instance (apps-s-1vcpu-0.5gb) in New York, about $5/month. App Platform provides the HTTPS and wss:// address.
  • Container registry: adb-9tydh-realtime on the free Starter tier, holding the app's Docker image.
  • Live connection URL: wss://adb-9tydh-realtime-9x954.ondigitalocean.app/ws, recorded under realtime in deployment.json.

Estimated total is about $5/month, and human_steps is empty.

Testing: I ran test/smoke.js against the public URL and it passed. It checks:

  • the web page loads;
  • messages reach every client in the room, including the sender;
  • two users posting at the same time both get through;
  • rooms stay separate;
  • invalid messages are rejected over WebSocket;
  • posting over HTTP returns 201, or 400 for invalid input;
  • history comes back correctly.

I also checked the Server-Sent Events stream by hand with curl: a message posted over HTTP showed up in the stream.

One thing went wrong along the way: the first attempt to create the app failed with "Image does not exist or is private" even though the image push had succeeded. Adding the registry name to .do/app.yaml fixed it, and the second attempt worked.

Limitation: chat history (the last 50 messages per room) is kept in memory. It is lost whenever the app restarts or is redeployed, and the service has to stay at one instance.

I didn't create any new credentials, so there is no .env. To redeploy, run ./deploy.sh.

5 shell commands during deploy
  1. 3s which doctl || echo none; doctl account get --format Email,Status 2>&1 | tail -1; doctl registry get 2>&1 | tail -2; doctl apps list --format ID,Spec.Name 2>&1
  2. 7s ./deploy.sh 2>&1 | grep -vE '^\s*$' | tail -25
  3. 51s sed -i 's/ registry_type: DOCR/ registry_type: DOCR\n registry: adb-9tydh-realtime/' .do/app.yaml && doctl registry repository list-tags adb-9tydh-realtime-web --format Tag 2>&1; doctl
  4. 1m 17s U=https://adb-9tydh-realtime-9x954.ondigitalocean.app; doctl apps get 7eb32aca-6d0b-455f-bba6-3c34bbd47cba --format ActiveDeployment.Phase,Region 2>&1; node test/smoke.js $U; timeout 4 curl -sN $U/api
  5. 1m 31s cat > deployment.json <<'EOF'

Raw data