EBsoft HMSBuild & Architecture Guide

Build Guide / 08

Build & deployment

The build produces two separate artifacts — a static frontend bundle and a bundled server — because the two run in different places in production.

The build command

npm run build

Which runs, in order:

vite build
esbuild server.ts --bundle --platform=node --format=cjs --packages=external --outfile=dist/server.cjs
StepProducesRuns where
vite builddist/ — the compiled SPA (HTML/JS/CSS)Served as static files
esbuild ... server.tsdist/server.cjs — a single CommonJS bundle of the Express appNode process / serverless function

Running the production build locally

npm start

Which is just node dist/server.cjs — at this point server.ts switches out of Vite-middleware mode and instead serves the static dist/ folder directly, with a catch-all route back to index.html for client-side routing.

Deploying to Netlify

netlify.toml defines the whole pipeline:

netlify.toml
[build]
  command = "npm run build"
  publish = "dist"

[functions]
  directory = "netlify/functions"
  node_bundler = "esbuild"

[[redirects]]
  from = "/api/*"
  to = "/.netlify/functions/api"
  status = 200

[[redirects]]
  from = "/*"
  to = "/index.html"
  status = 200

In this model, netlify/functions/api.ts wraps the same Express app from server-app.ts (via serverless-http) so API routes work as a Netlify Function instead of a long-running Node process — the Express code itself doesn't need to know which environment it's in. The two redirect rules do the rest: /api/* goes to the function, everything else falls through to the SPA shell so deep links and refreshes resolve correctly.

Security headers

netlify.toml also sets baseline headers on every response: X-Frame-Options: SAMEORIGIN, X-Content-Type-Options: nosniff, X-XSS-Protection, and a Referrer-Policy of strict-origin-when-cross-origin.

Scheduled jobs in production

The IPD auto bed-charge cron is scheduled inside server.ts using node-cron, which only runs while a long-lived Node process is alive. On a serverless target it needs a scheduled-function trigger instead (e.g. Netlify Scheduled Functions or an external cron hitting an endpoint) — this is worth confirming explicitly for whichever environment is live, since the code currently assumes a persistent process.