Deploy the web app
The web app is a set of static files: the landing page, the sign-in page and the app itself. Your Convex address and Clerk publishable key are built into them, so you build the app for your own instance. The repository ships a script for Vercel; any other static host works if it supports the routing rules below.
Before you start, deploy/instance.json needs convexDeployment, appUrl and clerkPublishableKey
(see the instance settings file).
With Vercel
Section titled “With Vercel”-
Set up the Vercel CLI. Install the Vercel CLI and log in with
vercel login. -
Name the project. In
deploy/instance.json, setvercelProject(for examplexlr8) andvercelScope(your Vercel team slug). -
Build and deploy:
Terminal window node tooling/deploy-web.mjs -
Add your domain. In the Vercel dashboard, add
xlr8.example.comas a domain of the project and create the DNS record Vercel shows you.
What the script does:
- builds the app with the production values from your instance settings;
- checks the result and stops without uploading anything if the build contains the local test token or a local backend address, lacks your Convex address, or the landing page loads app code;
- copies the build to
deploy/web/and writes avercel.jsonwith the routing rules and headers; - links
deploy/web/to your Vercel project on first use and deploys it to production as a static site. Vercel builds nothing itself.
node tooling/deploy-web.mjs --build stops after building into deploy/web/ and uploads nothing. It
still expects all instance settings, including the two Vercel fields.
Hosting elsewhere
Section titled “Hosting elsewhere”-
Build the app with your values:
Terminal window VITE_CONVEX_URL=https://happy-otter-123.convex.cloud \VITE_CONVEX_SITE_URL=https://happy-otter-123.convex.site \VITE_CLERK_PUBLISHABLE_KEY=pk_live_… \XLR8_PUBLIC_URL=https://xlr8.example.com \VITE_DEV_TOKEN= \pnpm --filter @xlr8/app buildThe empty
VITE_DEV_TOKEN=matters if you have run XLR8 locally:pnpm devwrites a local test token intoapp/.env.local, and it must not end up in a production build. Alternatively, delete that file. -
Rename the app’s entry page:
Terminal window mv app/dist/index.html app/dist/app.htmlMany hosts, Vercel included, serve an
index.htmlat/before they apply any rewrite. That would skip the first rule below. -
Upload
app/dist/to your host. -
Set up the routing rules, in this order:
/without the cookiexlr8_signed_in→ serve/welcome.html(landing page)./sign-in→ serve/welcome.html(it shows the embedded Clerk form on this path).- Every other path → serve
/app.html. Deep links such as/issue/ACME-42must work. Files that exist in the build (/sw.js,/version.json, icons) are served as they are, and a missing file under/assets/…must return a 404, not the app.
-
Set the headers:
Path Header /assets/*Cache-Control: public, max-age=31536000, immutable/Cache-Control: no-cacheandVary: Cookie/app.html,/welcome.html,/sw.js,/version.json,/site.webmanifestCache-Control: no-cacheall responses (recommended) X-Content-Type-Options: nosniff,X-Frame-Options: DENY,Referrer-Policy: strict-origin-when-cross-origin,Permissions-Policy: camera=(), microphone=(), geolocation=(),Strict-Transport-Security
The Vercel version of these rules is in deploy/vercel.base.json; use it as a reference.
Check the result
Section titled “Check the result”- Open
https://xlr8.example.com/in a private window: the landing page appears. - Click Sign in and sign in with the user you created in Clerk. You land in the app.
- Open
https://xlr8.example.com/again: the app opens directly.
If you see the landing page although you are signed in, for example on the first visit after a deploy, click Sign in. Clerk recognizes the session and sends you to the app.
Troubleshooting
Section titled “Troubleshooting”“No access” or an empty app after sign-in
Section titled ““No access” or an empty app after sign-in”Both mean that the backend does not accept your sign-in. Work through these checks:
- The issuer.
CLERK_JWT_ISSUER_DOMAINon Convex must be exactly your Clerk Frontend API URL, for examplehttps://clerk.xlr8.example.com, without a trailing slash. If you changed it, redeploy the backend (node tooling/cloud.mjs deploy): Convex reads it only when the functions are pushed. - The JWT template. In Clerk, a template of type Convex must exist with the name
convex. - The owner list.
XLR8_OWNER_IDSmust contain exactly<CLERK_JWT_ISSUER_DOMAIN>|<your user id>: the same issuer spelling and the user id from your Clerk user page. A change here takes effect right away; reload the app. - After a change of your Clerk domain, the issuer changes, so update it in both
CLERK_JWT_ISSUER_DOMAINandXLR8_OWNER_IDS.
The No access screen (“You are signed in, but this account has no access to XLR8.”) has a Sign out button, so you can try another account.
“XLR8 is not set up”
Section titled ““XLR8 is not set up””The build has no Convex address or no sign-in method. Build again with VITE_CONVEX_URL and
VITE_CLERK_PUBLISHABLE_KEY set, or check convexDeployment and clerkPublishableKey in your instance
settings.
Deep links show a 404
Section titled “Deep links show a 404”The fallback rule to /app.html is missing. Check rule 3 above.
Next step
Section titled “Next step”Sign in and set up your workspace: First steps.