Build & deploy
What's in dist/ and vantage-manifest.json. Same-origin serving, and splitting the client and server across origins (S3+CloudFront ⇄ Lambda, etc.).
vantage build emits the production artifacts into dist/. From there, serving splits into two shapes:
serve the client and server together from the same origin, or split them — the client on static
hosting (S3+CloudFront / Cloudflare Pages) and the server on a separate origin (Lambda / a container).
For the split shape, Vantage provides an API base URL switch and CORS.
Build output
vantage build # → dist/
dist/
├── client/ Client (index.html + hashed assets, code-split per route)
├── server/index.mjs Only when server/ exists (esbuild / node20 / ESM bundle)
└── vantage-manifest.json Deploy contract
vantage-manifest.json is the one contract the host reads. Its fields:
| Field | Meaning |
|---|---|
schemaVersion |
Manifest schema version (currently 1) |
mode |
"spa" (client only) / "fullstack" (has server). Derived from server/ |
client |
Relative path to the client output ("./client") |
server |
Relative path to the server bundle ("./server/index.mjs"). Fullstack only |
spaFallback |
HTML for the SPA fallback ("./client/index.html") |
runtime |
Server runtime ("node") |
Preview locally
Before deploying, run the production build locally to verify it.
vantage preview
vantage preview reads mode from vantage-manifest.json: fullstack starts the Node server
(default :4173), spa serves a static preview.
Two deployment shapes
Same origin
One Node process serves both the client and /api. Minimal setup, and no CORS needed.
Split client & server
Client on S3+CloudFront etc., server on Lambda / a container. Needs the API base URL switch and CORS.
Deploy to a single origin
The simplest shape. Place all of dist/ on a host that runs Node (a container / VM) and start the
generated server. One process serves both static assets and /api/*, so there is nothing to switch
and no CORS to configure.
# on the host where dist/ lives
PORT=8080 node dist/server/index.mjs
The server listens on PORT (or 3000 if unset). The client calls relative /api/... paths on the
same origin.
Split the client and server (S3+CloudFront ⇄ Lambda)
Here the client lives on CDN static hosting and the server on a separate origin. A common pairing is
“dist/client on S3+CloudFront, dist/server/index.mjs on Lambda / a container”. Because the
origins differ, two things are required.
1. Switch the API base URL
Client API calls go through apiFetch / apiUrl from @squadbase/vantage/query. This is the one
seam that calls /api, and it’s where the base URL is swapped.
import { apiFetch, useQuery } from "@squadbase/vantage/query";
useQuery({
queryKey: ["monthly-analysis"],
// Keep the path as it maps in server/api (include /api)
queryFn: () => apiFetch("/api/monthly-analysis").then((r) => r.json()),
});
The base URL is read from PUBLIC_API_BASE_URL. Unset means empty, so requests stay same-origin
relative paths (/api/monthly-analysis). Set it and the value is prepended
(https://api.example.com/api/monthly-analysis). Two ways to set it:
(a) Override via env — put it in .env or a CI environment variable. Because of the PUBLIC_
prefix it is inlined into the client bundle.
# .env (or a CI env var)
PUBLIC_API_BASE_URL=https://api.example.com
(b) Specify at build time — pass --api-base-url to vantage build.
vantage build --api-base-url https://api.example.com
2. Allow CORS
Once the client and server are on different origins, the browser requires CORS. Setting CORS_ORIGIN
on the server enables CORS on /api (including the preflight OPTIONS response). Unset means CORS
is off, leaving same-origin serving unchanged.
# environment variables on the server (Lambda / container)
CORS_ORIGIN=https://app.example.com # a single origin
# CORS_ORIGIN=https://a.example.com,https://b.example.com # comma-separated for several
# CORS_ORIGIN=* # allow all (not usable with credentials)
CORS_CREDENTIALS=true # only if you allow cookies / credentials
| Variable | Role |
|---|---|
CORS_ORIGIN |
Allowed origin(s): * / a single value / comma-separated. Unset → CORS off |
CORS_CREDENTIALS |
When true, allow credentials (cookies, etc.). Not used with * |
The allowed methods are set automatically to the ones Vantage APIs can use
(GET,POST,PUT,PATCH,DELETE,OPTIONS). This behavior is shared by the dev server and the
production Node server, so you can verify CORS during development.
3. Put the client on static hosting
Upload dist/client/ to S3 (+ CloudFront) or Cloudflare Pages. As an SPA, unknown paths must fall
back to spaFallback (index.html).
- S3 + CloudFront: use a CloudFront “custom error response” to rewrite
403/404to/index.htmlwith status200. - S3 static website hosting: set the error document to
index.html. - Cloudflare Pages: SPA fallback is on by default.
4. Put the server on Lambda / a container
dist/server/index.mjs is a Node HTTP server that listens on PORT (the manifest’s runtime is
"node"). It runs anywhere a Node process runs.
- Container (Cloud Run / ECS·Fargate / Render / Fly, etc.): use
node dist/server/index.mjsas the start command and passPORT. - AWS Lambda: since it is a listening Node server, a container image + AWS Lambda Web Adapter (or another “run an HTTP server on Lambda” approach) is a good fit.
- Pass secrets (DB credentials, API keys) as server environment variables. They are only readable
via
ApiContext.envand never reach the client.
Deploy to AWS Lambda (a concrete walkthrough)
The generated dist/server/index.mjs is a Node HTTP server that listens on PORT. Lambda, on the
other hand, invokes a handler per event, so the natural way to run a listening server there is to put
the AWS Lambda Web Adapter (LWA) in front of it.
LWA proxies Function URL / API Gateway events to the HTTP server running locally. No Vantage code
changes are needed.
1. Run it as a container image (recommended)
esbuild bundles into a single file dist/server/index.mjs, so you don’t need to carry
node_modules.
# Dockerfile
FROM public.ecr.aws/docker/library/node:20-slim
# Bundle the Lambda Web Adapter as an extension (bump the version as needed)
COPY --from=public.ecr.aws/awsguru/aws-lambda-adapter:0.9.1 /lambda-adapter /opt/extensions/lambda-adapter
WORKDIR /var/task
# Copy all of dist/ (the server resolves ../client, so keep the layout)
COPY dist ./dist
# Keep LWA and the Node server on the same port (LWA defaults to 8080)
ENV PORT=8080
CMD ["node", "dist/server/index.mjs"]
vantage build # produce dist/
docker build -t vantage-api .
# Log in to ECR and push (<acct> / <region> are yours)
aws ecr get-login-password --region ap-northeast-1 \
| docker login --username AWS --password-stdin <acct>.dkr.ecr.ap-northeast-1.amazonaws.com
docker tag vantage-api <acct>.dkr.ecr.ap-northeast-1.amazonaws.com/vantage-api:latest
docker push <acct>.dkr.ecr.ap-northeast-1.amazonaws.com/vantage-api:latest
Create a Lambda function from this image and enable a Function URL to get an HTTPS endpoint. Use
it as the /api/* origin in CloudFront, or point the client’s PUBLIC_API_BASE_URL at it directly.
2. Set the Lambda environment variables
Pass server config and secrets as Lambda environment variables (they never reach the client).
| Variable | Example | Purpose |
|---|---|---|
CORS_ORIGIN |
https://d111111abcdef8.cloudfront.net |
Allow the client’s origin |
CORS_CREDENTIALS |
true |
Only if you use cookies / credentials |
SECRET_*, etc. |
sk_live_xxx |
Secrets read via ApiContext.env |
PORT is already set in the Dockerfile (matching LWA’s default 8080).
3. Point the client at Lambda
At client build time, set PUBLIC_API_BASE_URL to the Lambda (or the CloudFront in front of it)
origin.
vantage build --api-base-url https://d111111abcdef8.cloudfront.net
apiUrl("/api/monthly-analysis") then calls that origin with /api/monthly-analysis appended.
Production environment variables at a glance
The boundary is the prefix (see also Environment variables).
| Kind | Set where | When it applies | Example |
|---|---|---|---|
| Public client value | Build time (.env / --api-base-url) |
Baked into the build | PUBLIC_API_BASE_URL |
| Server config | Server environment variable | Read at startup | PORT, CORS_ORIGIN, CORS_CREDENTIALS |
| Secret | Server environment variable | Read at startup (ApiContext.env) |
SECRET_API_KEY, etc. |
Pre-deploy checklist
-
vantage checkpasses with no errors -
vantage build(with--api-base-urlfor a split setup, orPUBLIC_API_BASE_URLin.env) -
vantage previewverifies it locally - For a split setup, set
CORS_ORIGIN(the client’s origin) on the server - Configure SPA fallback (unknown paths →
index.html) on the static host - Keep secrets in server environment variables only (never expose to the client)
What to read next
- CLI reference — every command, including the
buildflags - Environment variables — the public / secret boundary