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

Database-Backed CRUD App on DigitalOcean

Success

Checks passed19/19
Human steps37 min of a person's time
Security & compliance78/100
Build2m 20s
Deploy11m 57s
Tokens864.4K
Agent cost$0.81
Hosting / month$12.00 light use$12.00 busy

Deployed to https://adb-jqmj7-crud-sgdds.ondigitalocean.app. Every deployment is deleted after it's verified and reviewed.

Screenshots

The web app before signing in
The web app before signing in · https://adb-jqmj7-crud-sgdds.ondigitalocean.app/
The todo list after signing in through the web UI as the test user
The todo list after signing in through the web UI as the test user · https://adb-jqmj7-crud-sgdds.ondigitalocean.app/

Technologies

Runtime
Node.js 22
Frameworks
Express
Data
PostgreSQL (App Platform dev database)
Libraries
node-postgresbcrypt.js
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 web app as HTML ui
passed POST /api/auth/signup creates an account and returns a token signup
passed Signup rejects an invalid email and a short password with 400 signup_validation
passed Signing up twice with the same email returns 409 signup_duplicate
passed POST /api/auth/login returns a token login
passed Login with a wrong password returns 401 login_wrong_password
passed /api/todos requires a valid token (401) auth_required
passed POST /api/todos creates a todo create
passed An empty title is rejected with 400 create_validation
passed GET /api/todos lists the caller's todos list
passed Todos are listed oldest first list_order optional
passed PATCH /api/todos/{id} renames and completes a todo update
passed Users can't see or modify each other's todos (404) isolation
passed DELETE /api/todos/{id} removes a todo delete
passed Data is still there a minute later, with a fresh login persistence
passed The web UI renders a login or signup form browser_ui optional

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 app was built carefully: secrets come from App Platform bindable variables, the container runs as non-root, queries are parameterized, passwords use bcrypt, per-user ownership is enforced, and probes found no exposed files or stack traces. The main weakness is provenance: the image was built locally from a base image not pinned by digest and deployed by a mutable tag with no source-control link. Smaller issues are missing security headers, no auth rate limiting, sessions that never expire, and a development-tier database with no backups behind a business app.

CriterionWeightScoreWhy
Secrets handling 25 9/10 No credentials in source, image or spec: DATABASE_URL and the CA cert come from App Platform bindable variables (${adb-jqmj7-crud-db.DATABASE_URL}) scoped to RUN_TIME. The only small issues are a 30-day registry login left on the build host and test-account tokens partly echoed in transcript output.
Deployment provenance 20 6/10 The image was built on the agent's machine and pushed to the owner's private DOCR, and npm ci with a lockfile pins dependencies. But the base image (node:22-alpine) isn't pinned by digest, the app points at a mutable tag (v1) instead of a digest, no source repo or CI ties the image to a commit, and doctl was downloaded without a checksum check.
Terms compliance 15 8/10 A business app on paid App Platform Basic plus a paid dev database fits DigitalOcean's standard commercial terms. However, DigitalOcean positions dev databases for development (no backups, no HA), which is a weak fit for a business's production data. The agent said so itself.
Least privilege 15 8/10 The container runs as the non-root node user, the registry is private, and only the web service is routed publicly. The dev database can't be restricted with trusted sources or a private network (as I understand App Platform dev DBs), and the app connects as the database's owner user.
Transport security 10 7/10 HTTPS works with a valid Google Trust Services certificate, and HTTP gets a 301 to HTTPS. The DB connection uses TLS and verifies the CA. The app sets no HSTS, CSP, X-Content-Type-Options or frame-protection headers, though it could easily have (e.g. helmet).
Attack surface 10 9/10 Probes of /.env, /.git/config, /package.json, /src/app.js, /Dockerfile and /.do/app.yaml all return 404. Errors return generic JSON without stack traces, and x-powered-by is disabled. Express's default 'Cannot GET' 404 page slightly reveals the framework.
Application security 5 7/10 All queries are parameterized, passwords use bcrypt with cost 12, ownership is enforced in every todo query, input types and lengths are checked, and the UI renders with textContent. There's no rate limiting on login or signup, session tokens never expire, and the UI keeps the bearer token in localStorage.

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-jqmj7-crudadb-jqmj7-crud-web: apps-s-1vcpu-0.5gb x 1, adb-jqmj7-crud-db: dev database$12.00adb-jqmj7-crud-web: apps-s-1vcpu-0.5gb x 1; adb-jqmj7-crud-db: dev database
registryadb-jqmj7-crudstarter$0.00Container Registry starter tier (one per account)

The agent estimated $12.00 a month.

Code

Code
458 lines in 12 files (JavaScript 305, HTML 143, JSON 29, YAML 25, Markdown 19, Docker 10)
Dependencies
3 npm, 0 Python
Deploy config
Dockerfile .dockerignore .do/app.yaml

The agent's sessions

PhaseStatusTimeTurnsTokensCostShell commands
Buildcompleted2m 20s10406.4K$0.469
Deploycompleted11m 57s9458.0K$0.368

The agent's deploy summary

The app is live at https://adb-jqmj7-crud-sgdds.ondigitalocean.app, and every requirement in TASK.md passed when I tested it through that URL.

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

  • Container registry adb-jqmj7-crud on the free starter plan. It holds the app's Docker image, adb-jqmj7-crud-web:v1.
  • App Platform app adb-jqmj7-crud (id c4ae8ac2-bd2c-475d-a1d9-17af841ad0ed) in New York: one web service, adb-jqmj7-crud-web, on the smallest Basic size ($5/month). DigitalOcean provides the HTTPS on its default ondigitalocean.app address.
  • PostgreSQL adb-jqmj7-crud-db, an App Platform dev database ($7/month). The connection is encrypted and the app checks the database's certificate.

Tested through the public URL:

  • The web page loads.
  • Signup gives 201 with a token, and 400 for a bad email or a password under 8 characters.
  • Signing up twice with the same email gives 409; a wrong password at login gives 401.
  • Todos: create (400 for an empty title), list oldest first, rename and complete, then delete (204, and 404 for a second delete). Calls without a token get 401.
  • A second user sees an empty list and gets 404 when trying to change or delete the first user's todo.
  • I triggered two redeploys and the data was still there afterwards.

Limitation: the dev database has no backups, failover or scaling. For real use, switch it to a managed PostgreSQL cluster, which starts at about $15/month.

Nothing needed a person, so human_steps is empty. I didn't create any separate credentials, so there's no .env; the registry and app run off the existing access token. deployment.json is written, and the updated .do/app.yaml (now pointing at the image in the registry) is committed. To redeploy, push a new image tag, update .do/app.yaml, then run doctl apps update c4ae8ac2-bd2c-475d-a1d9-17af841ad0ed --spec .do/app.yaml.

8 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. 16s doctl registry create adb-jqmj7-crud --subscription-tier starter --region nyc3 && doctl registry login && docker build -q -t registry.digitalocean.com/adb-jqmj7-crud/adb-jqmj7-crud-web:v1 . && docker
  3. 51s cat > .do/app.yaml <<'EOF'
  4. 9m 34s U=https://adb-jqmj7-crud-sgdds.ondigitalocean.app; E="t$RANDOM$RANDOM@example.com"; j='content-type: application/json'
  5. 9m 47s doctl apps create-deployment c4ae8ac2-bd2c-475d-a1d9-17af841ad0ed --force-rebuild --wait >/dev/null 2>&1; doctl apps list-deployments c4ae8ac2-bd2c-475d-a1d9-17af841ad0ed --format ID,Phase | head -3;
  6. 10m 39s U=https://adb-jqmj7-crud-sgdds.ondigitalocean.app; j='content-type: application/json'
  7. 11m 39s doctl databases list --format ID,Name,Size 2>&1 | head; doctl apps get c4ae8ac2-bd2c-475d-a1d9-17af841ad0ed -o json | node -e 'const a=JSON.parse(require("fs").readFileSync(0))[0];console.log(a.tier_s
  8. 11m 49s cat > deployment.json <<'EOF'

Raw data