A deployment gate for Node ESM functions, server-sent events, browser roles, and storage that disappears between serverless invocations.
A production build proved that my React bundle compiled. It did not prove that the deployed API could load.
I found that gap while deploying Trip Trace, a small TypeScript planner with three specialist agents. The Vercel build passed. The public functions then failed with ERR_MODULE_NOT_FOUND.
That failure changed the completion gate. I stopped treating the build as deployment evidence and tested each runtime boundary at the public URL.
test the function loader
Trip Trace uses Vite for the browser and separate TypeScript files under api/ for Vercel Functions. Those paths do not share one runtime.
The frontend bundler resolved extensionless imports during the build. Node loaded each serverless function as an ECMAScript module (ESM). At that boundary, imports such as this one failed:
import { orchestrate } from "../server/orchestrator";
The deployed function needed an explicit runtime extension:
import { orchestrate } from "../server/orchestrator.js";
I applied the same rule to every relative server import used by the functions. The local typecheck and build still passed. The public /api/health endpoint then returned a valid response.
A build can compile function source without exercising the loader that will run it after deployment.
verify the streamed protocol
The planner returns server-sent events (SSE). A request emits its identifier, agent status updates, the final result, and then closes the stream.
An HTTP 200 response is weak evidence for that contract. The handler can return 200 and still stop before the result, emit malformed JSON, or leave the connection open.
I sent a real brief to the production POST /api/plan endpoint and inspected the event sequence. The stream returned a request event, agent events, and one result event before it closed.
This check had already found a separate local bug. The Express handler treated the request's normal close event as a client disconnect after reading the body. The orchestration finished, but the response did not end. A protocol-level test exposed the fault because it waited for the complete stream.
test each user role at the destination
The application has a traveler view and an operator view. The traveler creates a plan. The operator sees redacted request counts, outcomes, durations, route mix, and run modes.
The deployment check therefore covered more than the homepage. It exercised these public paths:
- Request
/api/healthand confirm the runtime mode. - Request
/api/metricsand confirm the redacted schema. - Post a trip brief and read the full SSE stream.
- Open the traveler flow and create a plan.
- Open
/?role=operatorand confirm the role-specific surface.
The browser check matters because the role switch lives in client state. Fetching the HTML cannot prove that the correct React surface appears after hydration.
name the storage boundary
The local app writes audit records through a repository interface to a JSON file. The Vercel adapter points that repository at /tmp.
That path is writable, but it is not durable storage. A later invocation can run in another instance or region. The file can disappear at any time.
For the public demo, the browser keeps a session-scoped, redacted fallback so the traveler can see recent activity. The operator metrics endpoint can also report records available to its current function instance. Neither mechanism is a production audit ledger.
The repository documents PostgreSQL as the durable replacement. That is more accurate than describing /tmp as persistence. The interface is useful because the storage implementation can change without moving audit decisions into the UI or the agents.
make deployment evidence explicit
I now treat a green build as the start of deployment testing. I do not record the release as verified until the functions load, the stream ends, both roles render, and the storage claim matches the runtime.