Vercel
One command links the project, pushes your environment, deploys, and asks the deployment whether it came up configured.
npm run deploy is scripts/deploy-vercel.mjs inside your own project, driving the Vercel CLI.
There is no GitHub repo to connect and no dashboard to visit, and re-running it is the normal way
to ship an update — every step is written to be run again.
npm run deployIt finds the Vercel CLI, or fetches it
From this machine the deploy needs api.vercel.com, and registry.npmjs.org as well when the CLI
is not already installed. Nothing else: the build downloads what it needs on Vercel's side, and
telemetry upload is switched off for every call the script makes, so a blocked telemetry host
cannot stop a deploy. If something does not get through, the script names the host it could not
reach rather than blaming the build.
It signs you in
Through the CLI's own login.
It asks what to call the project, then links the directory
The name is the address (<name>.vercel.app when it is free), which is why it is asked rather than
taken from the directory name. If you already have a project by that name it says so and offers to
pick another — vercel link would otherwise link to it and deploy over whatever is live there.
Pass --project <name> or set VERCEL_PROJECT_NAME to skip the question.
It pushes the variables from .env
Each one goes up with vercel env add --force, to production and preview. --force
overwrites the value for that target instead of adding a second one. Development stays local.
Variables that exist on Vercel but not in your .env are left alone; the script never deletes.
It asks the deployment whether it came up configured
One request to /chatfuel/healthz, the proxy's health route reached through the rewrite. An
answer of ok prints the gate state and the deploy is done. A redirect means Deployment
Protection answered instead — the script reports where it was sent rather than following it. A
non-JSON answer means something other than the proxy replied, and it points at the routes in
vercel.json. A proxy that is up but misconfigured names its problems and tells you to check
vercel env ls and run the command again.
The URL it prints at the end is the public one. vercel deploy prints the deployment URL, which
under Vercel's default Deployment Protection sits behind an SSO wall — fine for you, a dead link
for everybody else — and which alias is which is not derivable from the name. So the script asks
each in turn and hands you the first that answers without the wall. Only if none of them do is
there anything to change in the dashboard, and it says so.
If you installed the auth module, one thing the script cannot do for you: Supabase has to be told
the deployed origin may receive auth redirects, or sign-in on that domain fails. It prints the URL
to add.
What goes up, and what stays home
.env is never uploaded (.vercelignore), so the Vercel project environment is the only source
of configuration there — a build cannot silently read a stale local file instead.
Sensitive — Vercel's default for production and preview, so they cannot be read back out of the
dashboard: CHATFUEL_TOKEN, SUPABASE_SERVICE_ROLE_KEY, PUBLISHING_SECRET, ADMIN_PASSWORD.
None of them carries a VITE_ prefix, so Vite cannot bake them into the client bundle even in
principle.
Readable in the dashboard — pushed with --no-sensitive: CHATFUEL_API_BASE,
CHATFUEL_WORKSPACE_ID, VITE_SUPABASE_URL, VITE_SUPABASE_ANON_KEY,
VITE_CHATFUEL_WORKSPACE_ID, VITE_APP_NAME, VITE_APP_LOGO, PUBLIC_URL. A workspace id or a
project URL that nobody can look at is a debugging tax for no security.
VITE_* values are read twice — baked into dist/ at build time and read again by the proxy at
runtime — which is why they are pushed as project variables rather than passed to the build alone.
What vercel.json does
{
"$schema": "https://openapi.vercel.sh/vercel.json",
"framework": "vite",
"buildCommand": "npm run build:client",
"outputDirectory": "dist",
"rewrites": [
{ "source": "/chatfuel/:cfpath*", "destination": "/api/chatfuel" },
{ "source": "/(.*)", "destination": "/index.html" }
],
"functions": { "api/chatfuel.ts": { "maxDuration": 300 } }
}The first rewrite puts every /chatfuel/* request on the proxy function. The second is the SPA
fallback that every host needs. maxDuration: 300 is the function's ceiling.
api/chatfuel.ts exports an http.Server, which is what lets Vercel Functions accept the
WebSocket upgrade for subscriptions. A plain rewrite straight to panel.chatfuel.com would be
simpler and is not usable: a rewrite forwards the browser's headers and cannot add one, so the
Chatfuel token would have to be sent by the client.
It is one static filename rather than a catch-all api/chatfuel/[...path].ts because Vercel's
zero-config api/ directory compiles a catch-all into a route that matches a single path segment —
/chatfuel/graphql survives that and /chatfuel/auth/provision does not. So the requested path
travels as the cfpath query parameter and is put back before the proxy core sees it. That
parameter name is a contract between vercel.json and api/chatfuel.ts; the two must not drift.
A WebSocket lives no longer than the function
A subscription's socket closes when the function hits its duration limit — 300 s on Hobby — and the client reconnects and refetches. Nothing is lost; that is what the reconnect refetch exists for. On your own server the socket is unbounded.
Root only
Vercel serves the app from the domain root. If you need https://example.com/app/, run the
bundled Node server yourself — see Serving from a sub-path.
Shipping an update
The same command. The link is reused, the project is never re-created, variables are overwritten rather than duplicated, and the production address moves to the new deployment. Local edits are not live until you run it.