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 06s
Deploy11m 57s
Tokens857.1K
Agent cost$0.81
Hosting / month$12.00 light use$12.00 busy

Deployed to https://adb-dwnn0-crud-7929w.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-dwnn0-crud-7929w.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-dwnn0-crud-7929w.ondigitalocean.app/

Technologies

Runtime
Node.js 22
Frameworks
Express
Data
PostgreSQL (App Platform dev database)
Libraries
node-postgresbcrypt.jscookie-parserjsonwebtoken
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 is cleanly built: parameterized SQL, bcrypt, hashed session tokens, owner-scoped queries, a non-root container, and runtime database secrets injected through App Platform bindable variables, served over valid HTTPS with an HTTP redirect and no exposed files. The main weaknesses are provenance (an image built locally and deployed by the mutable tag v1 on a floating node:22-alpine base) and the choice of an App Platform dev database for a commercial app, which is likely internet-reachable and has no production backups. Smaller gaps are missing security headers and no rate limiting on the auth endpoints.

CriterionWeightScoreWhy
Secrets handling 25 9/10 No credentials in source, image or transcripts. The database URL and CA cert are injected at runtime through App Platform bindable variables (${db.DATABASE_URL}, ${db.CA_CERT}), and .env/.git/node_modules are kept out of the build context. Minor: the DOCR docker login on the build host stays valid for 30 days.
Deployment provenance 20 6/10 Production runs an image built from the local workspace and pushed to the owner's private DOCR, but it was deployed by the mutable tag v1 rather than a digest. The base image node:22-alpine is a floating tag, and no source repository or CI ties the image to a commit. Dependencies use npm ci with a lockfile, which helps.
Terms compliance 15 8/10 DigitalOcean's paid App Platform Basic tier and DOCR allow commercial use and stay up without sleeping. The data store is an App Platform dev database, which DigitalOcean positions for development rather than production: one node, limited, with no managed backups or failover. That is a poor fit for a business app, but not a terms violation.
Least privilege 15 7/10 The container runs as the non-root node user and the registry is private. The App Platform dev PostgreSQL database cannot be limited to trusted sources, so as far as I know it is reachable from the internet with credentials. The app also connects as the database's owner role and creates its own schema.
Transport security 10 8/10 HTTPS uses a valid Google Trust Services certificate, and HTTP gets a 301 redirect to HTTPS. The app sets none of HSTS, CSP, X-Content-Type-Options or frame protection, though it easily could have (for example with helmet).
Attack surface 10 9/10 Only the public/ directory is served. Probes for /.env, /.git/config, /package.json, /src/app.js, /Dockerfile and /.do/app.yaml all return 404, and errors produce generic JSON with no stack traces. Minor: the X-Powered-By: Express header is sent and /healthz is public.
Application security 5 7/10 The app has parameterized SQL, bcrypt with cost 12, hashed opaque session tokens, owner-scoped queries, input type checks, a 100kb body limit, a timing-equalized login and textContent-based rendering. The auth endpoints have no rate limiting or lockout, which leaves them open to credential stuffing and signup spam.

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

The agent estimated $12.00 a month.

Code

Code
451 lines in 13 files (JavaScript 386, HTML 55, JSON 33, YAML 29, Markdown 22, Docker 10)
Dependencies
5 npm, 0 Python
Deploy config
Dockerfile .dockerignore .do/app.yaml

The agent's sessions

PhaseStatusTimeTurnsTokensCostShell commands
Buildcompleted2m 06s9357.5K$0.438
Deploycompleted11m 57s10499.6K$0.389

The agent's deploy summary

The app is live over HTTPS at https://adb-dwnn0-crud-7929w.ondigitalocean.app, and every requirement in TASK.md passed when I tested it through that URL. Plain HTTP redirects to HTTPS.

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

  • Container registry: adb-dwnn0-crud, on the free starter plan. I built the Docker image locally and pushed it there as adb-dwnn0-crud-web:v1.
  • App Platform app: adb-dwnn0-crud, with one web service, adb-dwnn0-crud-web, on the smallest Basic size ($5/month). It runs the image from the registry.
  • Database: adb-dwnn0-crud-db, an App Platform dev PostgreSQL database ($7/month). The app checks the database's CA certificate when it connects. A dev database is the cheapest real Postgres option; a managed cluster costs about $15/month and would be the upgrade for production use.

What I tested through the public URL:

  • GET / returns the HTML page.
  • Signup returns 201, 400 for a bad email or a short password, and 409 for an email already registered.
  • Login returns 200, and 401 for a wrong password.
  • Todo calls without a token return 401.
  • Creating a todo works, and an empty title returns 400.
  • The todo list comes back oldest first.
  • Renaming and completing a todo with a PATCH works.
  • Deleting returns 204.
  • A second user gets 404 when trying to change or delete the first user's todo, and sees an empty list.

Not tested: I didn't trigger a redeploy on DigitalOcean to show that data survives it. The data lives in a separate Postgres database, so it should, and in step 1 todos survived a local container restart. I also only tested the web UI through the API, not by clicking through it in a browser.

No steps need a person, and I didn't create any new credentials, so .env is empty. .do/app.yaml is updated to deploy from the registry, and deployment.json is written. Both are committed to the local git repo.

To ship a new version: push a new image tag, update the tag in .do/app.yaml, then run doctl apps update c1f09f8f-a626-47f8-80af-433ac623216d --spec .do/app.yaml.

9 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-dwnn0-crud --subscription-tier starter --region nyc3 && doctl registry login >/dev/null && docker build -q -t registry.digitalocean.com/adb-dwnn0-crud/adb-dwnn0-crud-web:v1 .
  3. 41s cat > .do/app.yaml <<'EOF'
  4. 46s doctl registry repository list-tags adb-dwnn0-crud-web; sleep 20; doctl apps create --spec .do/app.yaml --format ID --no-header
  5. 1m 12s A=c1f09f8f-a626-47f8-80af-433ac623216d; for i in $(seq 60); do p=$(doctl apps get-deployment $A $(doctl apps get $A --format InProgressDeployment,ActiveDeployment --no-header | awk '{print $1}') --for
  6. 11m 15s tail -5 /tmp/claude-1001/-home-runner-work--temp-adb-dwnn0-crud/6e95cd5a-549d-4a12-947f-aefa01d7d027/tasks/beerl9v26.output; doctl apps list-deployments c1f09f8f-a626-47f8-80af-433ac623216d --format I
  7. 11m 25s A=c1f09f8f-a626-47f8-80af-433ac623216d; U=$(doctl apps get $A --format DefaultIngress --no-header); echo $U; J='content-type: application/json'
  8. 11m 35s A=c1f09f8f-a626-47f8-80af-433ac623216d; U=$(doctl apps get $A --format DefaultIngress --no-header); echo $U; J='content-type: application/json'
  9. 11m 49s doctl databases list --format ID,Name,Size 2>&1 | head; cat > deployment.json <<'EOF'

Raw data