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

API / Backend Service on DigitalOcean

Partial stopped at the verify stage

Checks passed19/20
Human steps37 min of a person's time
Security & compliance84/100
Build2m 44s
Deploy8m 07s
Tokens941.2K
Agent cost$0.91
Hosting / month$12.00 light use$12.00 busy

Deployed to https://adb-r8zod-api-dwfel.ondigitalocean.app. Every deployment is deleted after it's verified and reviewed.

Screenshots

The API documentation at /docs
The API documentation at /docs · https://adb-r8zod-api-dwfel.ondigitalocean.app/docs

Technologies

Runtime
Node.js 22
Frameworks
Express
Data
PostgreSQL (App Platform dev database)
Libraries
node-postgres
Dev tools
SuperTest
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 /health 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 /health returns {status: ok} health
passed GET /openapi.json is an OpenAPI 3 document covering /v1/notes openapi
passed GET /docs serves human-readable documentation docs optional
passed Requests without an API key get 401 auth_missing
passed Requests with an unknown API key get 401 auth_invalid
passed POST /v1/notes creates a note create
passed Invalid notes are rejected with 400 create_validation
passed GET /v1/notes/{id} returns the note get
passed GET /v1/notes lists notes and filters by tag list
passed Notes are listed newest first list_order optional
passed PATCH /v1/notes/{id} updates only the given fields update
failed Notes are private to the API key that created them isolation GET /v1/notes/426b46a1-73ce-4d8e-b632-8ec85e31e180 returned 429, expected 404 or 403: {"error":"Rate limit exceeded; retry in 23 seconds"}
passed DELETE /v1/notes/{id} removes the note delete
passed Notes are still there a minute later persistence
passed Exceeding 60 requests/minute returns 429 rate_limit
passed 429 responses include a Retry-After header rate_limit_retry_after optional
passed One key hitting its limit doesn't throttle other keys rate_limit_per_key

Human steps

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 Notes API was built and deployed carefully. API keys live in an encrypted App Platform SECRET, nothing leaked into logs, files or the image, the container runs as non-root, the database uses CA-verified TLS, and the live app exposes only the specified endpoints with clean JSON errors over valid HTTPS. The weaknesses are minor: the base image and deployed tag aren't pinned by digest, the image was built and pushed from a local machine, there are no security headers, failed-auth attempts aren't rate limited, and the dev database can't be restricted to trusted sources.

CriterionWeightScoreWhy
Secrets handling 25 9/10 API_KEYS is stored as an encrypted App Platform SECRET env var, and DB credentials and the CA come from platform bindings. Keys were never echoed, written to source or baked into the image. The temporary spec holding them was written under umask 077 and then deleted. The only nit is a 30-day registry login left on the build host.
Deployment provenance 20 7/10 The image was built from the reviewed source with npm ci and a lockfile, and pushed to the owner's own DOCR registry. The base image (node:22-alpine) and the deployed image tag (v1) are mutable and not pinned by digest, and the doctl binary was downloaded without checksum verification.
Terms compliance 15 9/10 This is a personal side project on a paid Basic App Platform service with a paid dev Postgres database. As far as I know, DigitalOcean's terms don't restrict personal use and nothing here sleeps or expires. The dev database has no backups or HA, which is acceptable for this use.
Least privilege 15 8/10 The container runs as the non-root node user, the database is an App Platform-managed attached database used over CA-verified TLS, and only the HTTP service is public. Dev databases can't be restricted with trusted sources, so the DB endpoint is likely reachable from the internet, guarded only by its credentials (I'm not certain of DigitalOcean's current dev-DB network model).
Transport security 10 8/10 HTTPS uses a valid Google Trust Services certificate and HTTP returns 301 to HTTPS. Responses carry no HSTS, X-Content-Type-Options, frame protection or CSP, and the app could have set these itself.
Attack surface 10 10/10 Only the specified endpoints respond. Probes for .env, .git, package.json, source and admin/debug paths return a JSON 404, errors are generic JSON without stack traces, and x-powered-by is disabled.
Application security 5 8/10 Queries are parameterized, input is type- and length-checked, notes are scoped by a hashed key owner (other keys get 404), the docs HTML is escaped and per-key rate limiting works. Failed-auth attempts aren't rate limited, and the limiter is in memory only.

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.
$12.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.
$12.00fixed monthly prices; 49 GiB out of a 50 GiB allowance

Resources the agent created

KindNamePlanList price / monthPricing
appadb-r8zod-apiadb-r8zod-api-api: apps-s-1vcpu-0.5gb x 1, adb-r8zod-api-db: dev database$12.00adb-r8zod-api-api: apps-s-1vcpu-0.5gb x 1; adb-r8zod-api-db: dev database
registryadb-r8zod-apistarter$0.00Container Registry starter tier (one per account)

The agent estimated $12.00 a month.

Code

Code
574 lines in 13 files (JavaScript 564, JSON 32, YAML 31, Markdown 23, Docker 10)
Dependencies
3 npm, 0 Python
Deploy config
Dockerfile .dockerignore .do/app.yaml

The agent's sessions

PhaseStatusTimeTurnsTokensCostShell commands
Buildcompleted2m 44s12511.3K$0.5511
Deploycompleted8m 07s8429.9K$0.357

The agent's deploy summary

The Notes API is live at https://adb-r8zod-api-dwfel.ondigitalocean.app, and I tested every requirement in TASK.md through that URL.

What's running (about $12/month):

  • Container registry adb-r8zod-api, on the free starter tier. It holds the app's Docker image.
  • App adb-r8zod-api on DigitalOcean App Platform, running one instance of the smallest paid size ($5/month). HTTPS comes on the default ondigitalocean.app address.
  • Database adb-r8zod-api-db, an App Platform PostgreSQL 16 "dev database" ($7/month). This was cheaper than a standalone managed Postgres database (about $15/month).
  • API keys: API_KEYS is set to exactly the keys in BENCH_API_KEYS, stored as an encrypted secret. The key values aren't in any file; the temporary config file that held them during deployment has been deleted.

Results through the public URL:

  • /health, /openapi.json and /docs each return 200.
  • A missing or unknown key gets 401.
  • Create, read, update, delete and the tag filter all work, and an empty title gets 400.
  • Another key's note gets 404.
  • A note was still there after I forced a full rebuild and redeploy.
  • A burst of 62 requests from one key started getting 429s after the limit, with a Retry-After header.

Limitation: the rate-limit counts are held in the server's memory. That's exact with the single instance, but the counts reset whenever the app restarts or redeploys, and running several instances would make it a limit per instance.

Nothing needed a person, so human_steps in deployment.json is empty. I didn't create any new credentials, so .env is empty. I also updated .do/app.yaml to deploy from the registry image instead of a GitHub repo; to ship a code change, push a new image tag and update it in that file.

7 shell commands during deploy
  1. 6s which doctl || (cd /tmp && curl -sL https://github.com/digitalocean/doctl/releases/download/v1.146.0/doctl-1.146.0-linux-amd64.tar.gz | tar xz && sudo mv doctl /usr/local/bin/); doctl version; doctl a
  2. 12s doctl registry create adb-r8zod-api --subscription-tier starter --region nyc3 && doctl registry login && docker build -q -t registry.digitalocean.com/adb-r8zod-api/adb-r8zod-api-api:v1 . && docker pus
  3. 50s cat > .do/app.yaml <<'EOF'
  4. 59s A=8a789613-c40b-450e-8f92-87050f396990; for i in $(seq 1 40); do P=$(doctl apps get $A -o json | python3 -c 'import json,sys;a=json.load(sys.stdin)[0];print((a.get("active_deployment") or {}).get("pha
  5. 6m 35s B=https://adb-r8zod-api-dwfel.ondigitalocean.app; K1=$(echo $BENCH_API_KEYS|cut -d, -f1); K2=$(echo $BENCH_API_KEYS|cut -d, -f2)
  6. 6m 45s A=8a789613-c40b-450e-8f92-87050f396990; B=https://adb-r8zod-api-dwfel.ondigitalocean.app; K2=$(echo $BENCH_API_KEYS|cut -d, -f2)
  7. 8m 00s cat > deployment.json <<'EOF'

Raw data