Vercel · Opus 5.5 · medium effort · HIPAA organization · trial 1
API / Backend Service on Vercel
Success
Deployed to https://adb-lfmyb-api.vercel.app. Every deployment is deleted after it's verified and reviewed.
Screenshots
Technologies
- Runtime
- Node.js 20+
- Frameworks
- Express
- Libraries
- node-postgres
- Packaging
- vercel.json
- Hosting
- Vercel projectVercel Functions
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
| Result | Check | Details |
|---|---|---|
| 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 |
|
| passed | Notes are private to the API key that created them isolation |
|
| 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
- Sign up for an account and verify the email address · 3 min
prerequisite: account setup - Create an API token and hand it to the agent · 2 min
prerequisite: account setup
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 on Vercel Hobby fits this personal use. Secrets are handled well: API_KEYS is an encrypted env var and nothing leaks into logs or files, and HTTPS with HSTS is enforced. The main weaknesses: the server source (/lib/*.js, /scripts/dev.js, TASK.md) is publicly downloadable as static files, and there is no database, so notes live in a volatile per-instance memory store and the per-key rate limit only holds within each instance. Smaller issues are unpinned tooling (vercel@latest, node >=20), missing app-level security headers and preview-scoped production keys.
| Criterion | Weight | Score | Why |
|---|---|---|---|
| Secrets handling | 25 | 9/10 | API_KEYS was set as an encrypted Vercel env var by piping BENCH_API_KEYS through node into the API without printing it, and the app reads it from process.env. No secrets appear in the source or the deployed files. The only small issue is that the Vercel token was passed on the command line (`--token $VERCEL_TOKEN`). |
| Deployment provenance | 20 | 8/10 | Production was deployed with the Vercel CLI straight from the agent's working directory, and the remote build ran on that uploaded source with a lockfile present. Some build inputs float: the CLI was `npx -y vercel@latest`, dependencies use caret ranges, and `engines.node` is `>=20`. |
| Terms compliance | 15 | 8/10 | This is a personal project, so the non-commercial Hobby plan fits, and Hobby functions don't expire or sleep. However, the app relies on persistent storage that was never provisioned: the Neon terms were not accepted and Blob creation hit the Hobby usage threshold. The live service therefore doesn't meet its data requirement. |
| Least privilege | 15 | 8/10 | The deployment is a single serverless function with no extra public resources and no data store to expose. API_KEYS is also scoped to Preview deployments, so preview URLs accept the production keys too. |
| Transport security | 10 | 8/10 | The app is served over HTTPS with a valid certificate. Plain HTTP gets a 308 redirect to HTTPS, and the platform adds HSTS with preload. The app sets no X-Content-Type-Options, frame protection or CSP, although Express or vercel.json `headers` could have added them. |
| Attack surface | 10 | 5/10 | Without a public/ directory and with an all-paths rewrite in vercel.json, Vercel serves uploaded files that exist on disk as static assets. As a result /lib/app.js, /lib/db.js, /lib/store.js, /lib/openapi.js, /scripts/dev.js and /TASK.md are all publicly downloadable. They contain no secrets. Probes for .env, .git, .vercel and package.json returned the app's JSON 404, and errors don't leak stack traces. |
| Application security | 5 | 6/10 | Input validation, parameterized SQL, hashed key ownership with 404 for other keys, and generic 500s are all done well. But the live store is the in-memory fallback, so notes are lost when an instance recycles. The 60/min rate limit is kept per instance, so concurrent instances multiply it, and stored data is unbounded in memory. |
Findings
-
medium deployment Application source files served publicly as static assets
Evidence: GET https://adb-lfmyb-api.vercel.app/lib/app.js -> 200 application/javascript (full source, content-disposition: inline; filename="app.js"). /lib/db.js, /lib/store.js, /lib/openapi.js, /scripts/dev.js and /TASK.md also return 200. vercel.json only has a rewrite `/(.*)` -> `/api`, and rewrites don't apply to paths that exist on the filesystem.
Fix: Move the server code under api/ (or a directory Vercel doesn't serve), list lib/, scripts/ and TASK.md in .vercelignore, keep them only as function includes, or declare an explicit outputDirectory/public folder so only intended assets are served.
-
medium app Live service runs on a non-persistent, per-instance in-memory store
Evidence: lib/store.js getStore(): with no DATABASE_URL it falls back to memoryStore(). process/deployment.json: 'Persistent storage is NOT in place yet ... data lives in memory per function instance'. deploy.md: every `integration add` returned integration_terms_acceptance_required, and Blob creation returned usage_threshold_limits_reached.
Fix: Provision Postgres (accept the Neon terms and attach DATABASE_URL), and make the app fail closed instead of silently serving a volatile store in production.
-
low app Rate limit not enforced globally without the database
Evidence: lib/store.js memoryStore().hitRateLimit keeps counters in a per-instance Map. When Vercel scales out, each instance allows 60 requests per minute per key.
Fix: Back the rate limiter with shared storage (the Postgres rate_limits table or a KV/Redis store).
-
low app No size limits on note fields or count; memory can grow unbounded
Evidence: lib/app.js validateFields checks only types, plus the express.json 1mb limit. memoryStore holds every note in a Map with no cap.
Fix: Cap the title, content and tag lengths, the number of tags, and the notes per key.
-
low deployment Required persistent storage not provisioned on the chosen plan
Evidence: deploy.md: Neon/Prisma/Supabase/AWS integrations need terms acceptance in a browser, and POST /v1/storage/stores/blob -> usage_threshold_limits_reached on the hobby team. TASK.md requires storage that survives restarts and redeploys.
Fix: Complete the human step to attach a free Neon database, or use another storage option available to the account.
-
low app Missing security headers the app could set
Evidence: curl -sI https://adb-lfmyb-api.vercel.app/health: only strict-transport-security (added by the platform). No x-content-type-options, x-frame-options/frame-ancestors or content-security-policy.
Fix: Add the headers in Express middleware (e.g. helmet) or in vercel.json `headers`.
-
low deployment Unpinned deploy tooling and runtime
Evidence: deploy.md: every command uses `npx -y vercel@latest`. package.json has "express": "^5.3.0", "pg": "^8.23.1", and "engines": {"node": ">=20"} (the build warned it will auto-upgrade).
Fix: Pin the Vercel CLI version and the Node major (e.g. "20.x"), and rely on the committed package-lock.json with npm ci.
-
low deployment Production API keys also valid on preview deployments
Evidence: deploy.md: env POST with target ["production","preview"] for API_KEYS.
Fix: Scope production keys to production only, and use separate keys for previews if they're needed.
-
info deployment Vercel token passed as a CLI argument
Evidence: deploy.md: `npx -y vercel@latest --token $VERCEL_TOKEN ...`. The value is redacted in logs, but it's visible in the process list on the build host.
Fix: Pass it via the VERCEL_TOKEN environment variable instead of --token.
-
info platform HTTPS enforced with HSTS by platform
Evidence: curl -sI http://adb-lfmyb-api.vercel.app/health -> 308 Location: https://...; HTTPS responses carry strict-transport-security: max-age=63072000; includeSubDomains; preload.
Fix: None.
Cloud resources and cost
| Traffic | Per month | How 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. | $0.00 | within Hobby's included usage |
| 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. | $20.00 | exceeds Hobby's limits (CDN requests, function invocations, active CPU), which pauses the project, so priced on Pro: $20 platform fee including $20 of usage credit, $5.75 of usage |
Resources the agent created
| Kind | Name | Plan | List price / month | Pricing |
|---|---|---|---|---|
| project | adb-lfmyb-api | $0.00 | no fixed fee; usage within the plan's included allowance |
The agent estimated $0.00 a month.
Code
- Code
- 612 lines in 13 files (JavaScript 612, JSON 40, Markdown 18)
- Dependencies
- 2 npm, 0 Python
- Deploy config
vercel.json
The agent's sessions
| Phase | Status | Time | Turns | Tokens | Cost | Shell commands |
|---|---|---|---|---|---|---|
| Build | completed | 2m 54s | 13 | 547.9K | $0.56 | 12 |
| Deploy | completed | 3m 09s | 15 | 869.2K | $0.46 | 14 |
The agent's deploy summary
The API is live at https://adb-lfmyb-api.vercel.app, and every endpoint passed my tests through that URL. One requirement is not met: notes are not stored persistently yet. Every database option on Vercel needs a person to act first.
What's deployed: one Vercel project, adb-lfmyb-api, on the free Hobby plan, so the cost is $0 a month. API_KEYS is set to exactly the keys in BENCH_API_KEYS, stored encrypted.
What I checked through the public URL:
/health,/openapi.jsonand/docsall return200.- A missing or unknown key gets
401. - Creating a note without a title gets
400. - Create, list with the
tagfilter, update and delete all work. - A second key gets
404on the first key's note and sees an empty list. - After 60 requests in a minute, the key gets
429with aRetry-Afterheader, while the other key is unaffected.
What didn't work: no database.
- Marketplace databases: Neon, Prisma Postgres, Supabase and AWS all need their terms accepted in a browser first.
- Vercel Blob: creating a store failed because the team has hit a usage limit (
usage_threshold_limits_reached).
So I added a fallback that keeps everything in memory whenever no database address (DATABASE_URL) is set. As a result, notes and rate-limit counts are lost when Vercel restarts or redeploys the app. If Vercel runs more than one copy at once, each copy also has its own separate data. The Postgres version is unchanged and passes the tests against a local Postgres, as does the fallback.
The one step a person needs to do (also listed under human_steps in deployment.json):
- Accept the Neon terms at https://vercel.com/ai-deploy-bench/~/integrations/accept-terms/neon?source=cli
- In this directory, run
vercel integration add neon --name adb-lfmyb-api-db, thenvercel deploy --prod.
After that the app switches to Postgres on its own and creates its tables; no code changes are needed.
I didn't create any credentials, so there was nothing to save in .env. I added a .vercelignore so env files and tests are never uploaded.
14 shell commands during deploy
- 4s
npx -y vercel@latest --version 2>&1 | tail -1; echo "bench keys count: $(echo $BENCH_API_KEYS | tr ',' '\n' | wc -l)" - 19s
V="npx -y vercel@latest --token $VERCEL_TOKEN --scope $VERCEL_TEAM_ID"; $V project add adb-lfmyb-api 2>&1 | tail -3; $V link --yes --project adb-lfmyb-api 2>&1 | tail -3; cat .vercel/project.json - 26s
V="npx -y vercel@latest --token $VERCEL_TOKEN --scope $VERCEL_TEAM_ID"; $V integration add --help 2>&1 | head -40 - 32s
V="npx -y vercel@latest --token $VERCEL_TOKEN --scope $VERCEL_TEAM_ID"; $V integration add neon --help 2>&1 | sed -n '1,/Global/p' | tail -25; timeout 120 $V integration add neon --name adb-lfmyb-api- - 40s
V="npx -y vercel@latest --token $VERCEL_TOKEN --scope $VERCEL_TEAM_ID"; $V integration list 2>&1 | tail -15; curl -s -H "Authorization: Bearer $VERCEL_TOKEN" "https://api.vercel.com/v1/integrations/co - 45s
V="npx -y vercel@latest --token $VERCEL_TOKEN --scope $VERCEL_TEAM_ID"; for i in prisma supabase upstash aws-dsql; do echo "== $i"; timeout 60 $V integration add $i --name adb-lfmyb-api-db --no-env-pu - 1m 02s
curl -s -X POST -H "Authorization: Bearer $VERCEL_TOKEN" -H 'Content-Type: application/json' "https://api.vercel.com/v1/storage/stores/blob?teamId=$VERCEL_TEAM_ID" -d '{"name":"adb-lfmyb-api-blob","ac - 1m 32s
cat > lib/store.js <<'EOF' - 1m 37s
sed -i "s#^process.env.DATABASE_URL ||= .*#if (process.env.TEST_MEMORY) delete process.env.DATABASE_URL;\nelse process.env.DATABASE_URL ||= 'postgres://postgres:pg@localhost:5432/postgres';#" test/api - 1m 48s
P=prj_Nda8m9iFHHr3lmPKXhsFINqjAKgt; node -e 'process.stdout.write(JSON.stringify({key:"API_KEYS",value:process.env.BENCH_API_KEYS,type:"encrypted",target:["production","preview"]}))' | curl -s -X POST - 2m 04s
npx -y vercel@latest --token $VERCEL_TOKEN --scope $VERCEL_TEAM_ID deploy --prod --yes 2>&1 | grep -vi token | head -40 - 2m 22s
B=https://adb-lfmyb-api.vercel.app; K1=$(echo $BENCH_API_KEYS|cut -d, -f1); K2=$(echo $BENCH_API_KEYS|cut -d, -f2) - 2m 42s
printf '.env\n.env.*\n!.env.example\n.vercel\nnode_modules\ntest\ndeployment.json\n' > .vercelignore && npx -y vercel@latest --token $VERCEL_TOKEN --scope $VERCEL_TEAM_ID deploy --prod --yes 2>&1 | gr - 3m 01s
cat > deployment.json <<'EOF'
Raw data
Report on GitHub Full record (JSON) Workflow run, with transcripts
