Hosting
The two decisions, the proxy and the certificate, the two addresses that must match, and what to check after the first deploy.
Hosting this is two decisions rather than one, because the app and the backend are separate. Neither one is difficult, and doing them in the wrong order is what makes the first deployment feel hard.
On a laptop this is already done: pnpm dev serves the app on http://localhost:3000 and the same command
is what Installing it finishes with. Everything below is the move from there to an
address somebody else can visit.
Vercel
The shortest path: the app is a Next.js app, and the backend stays on Convex.
Cloudflare
Two roads, and the one that keeps the app on Node.
Your own server
A machine you control, a proxy, a certificate, and a restart policy.
| Piece | Runs where | Deployed with |
|---|---|---|
| The app | Anywhere that runs Node: a small VPS, a platform, a home machine | pnpm build, then pnpm start |
| The backend | Convex's own infrastructure | npx convex deploy |
There is no container to build, no queue to operate, no worker fleet, and no cron of yours to configure. The scheduled work is a function of the deployment.
The order that works
The backend first
npx convex deployThis creates or targets the production deployment and pushes the schema and the functions. Then set its
environment, either with --prod per name or from the Convex dashboard:
npx convex env set BETTER_AUTH_SECRET "..." --prod
npx convex env set SITE_URL "https://loomis.your-domain.com" --prod
npx convex env set ADMIN_EMAIL "you@your-domain.com" --prod
npx convex env set ADMIN_PASSWORD "a long one" --prod
npx convex run admin:bootstrap --prodThe app
pnpm install
pnpm build
pnpm startpnpm start listens on port 3000 by default. Whatever starts it should restart it if it dies, which on a
Debian-style host is a systemd unit and on a platform is a setting somewhere.
The proxy and the certificate
Put it behind whatever you already use, with TLS terminating there: nginx, Caddy, Traefik, or the platform's own. Two things matter and both are about honesty:
- Forward the original host and protocol, so the app knows it is behind HTTPS.
- Do not strip the path, because the app owns its routes.
server {
listen 443 ssl;
server_name loomis.your-domain.com;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}The upgrade headers are not optional: a streamed answer arrives over a long-lived request, and a proxy that buffers or closes it produces an answer that arrives all at once or not at all.
The two addresses, which have to agree
NEXT_PUBLIC_APP_URL and SITE_URL are the address people actually visit, including https://. Every
callback, every email link and every provider redirect is built from them. Setting one and not the other is
the classic half-configured installation: password sign in works and every email link points at localhost.
Check it
System → Status & health and confirm the four checks.Brand → Emails and read it on a phone.Insight → Overview for the spend appearing.What a production host should have set
| Name | Why it matters in production |
|---|---|
NEXT_PUBLIC_CONVEX_URL | The production deployment's address, not the development one |
NEXT_PUBLIC_APP_URL | The address people visit, with the scheme |
SETTINGS_SECRET | Your own value, not the one you tested with, because it seals real tokens |
ALLOW_SELF_UPDATE | Only if you want the panel to run the update commands itself |
`SETTINGS_SECRET` on a server is not the same key as on a laptop
If the laptop and the server point at the same deployment, they should share the secret, because sealed values have to be readable by both. If the server has its own deployment, it needs its own secret, and copying the laptop's is how two installations end up able to read each other's tokens.
Running it on a home machine or a small VPS
A machine with two gigabytes of memory runs this comfortably for a small installation. The practical list:
- Node 22.6 or newer, installed for the user the service runs as.
- The folder somewhere the service user can write, because a build and an update both write inside it.
- Port 3000 free, or an environment variable that moves it, with the proxy pointing at the right one.
- A restart policy, because a process that is not restarted is a product that is down until somebody notices.
- Disk space, mostly for
.nextand the packages, plus whatever your users store.
Moving it to another machine
The app is a folder and a file of environment names; the data is on the deployment. So:
Stop the old app, or put it in maintenance mode from System → Advanced.
Copy the folder to the new machine, without node_modules and without
.next.
.env.local across, which is the seven app names.pnpm install, then pnpm build.Point the proxy at the new machine, and move the certificate if the address changed.
Start it, then open /admin-login and check System → Status & health.
Nothing on the deployment needs to change unless the address did, in which case SITE_URL and the providers'
callback addresses move with it.

