Deploying
Three deploy targets (Convex backend, Vercel companion site, and the static glasses app), and how they connect.
GlassKit is a monorepo with three deploy targets:
packages/backend/
Convex: pnpm exec convex deploy.
companion/
Vercel: a normal Next.js deploy, root dir set to companion/.
app/
Any HTTPS static host (Vercel works; so does Cloudflare Pages, Netlify, S3+CloudFront…).
They connect via the production Convex URL. Both apps point at the same deployment, and the Stripe + Resend webhooks land on the Convex side, not the Next.js side.
Two Vercel projects, one repo
Each Vercel project deploys exactly one app from the monorepo:
one for companion/ and one for app/. Both point at the same
GitHub repo with different Root Directory settings; Vercel
auto-detects Turborepo + pnpm.
Prefer another host? The companion is a stock Next.js 16 app and the glasses app is a static Vite bundle. Both deploy unchanged to Cloudflare Pages, Fly, Render, Railway, or self-hosted Node.
Step by step
Deploy the Convex backend (one-time setup)
cd packages/backend
pnpm exec convex deployThe CLI promotes your dev deployment to production (or links a new prod deployment on first run).
You only run this manually for first-time setup. Once the
Vercel project is wired up (Step 3 below), pnpm run vercel-build
runs convex deploy --cmd 'pnpm run build' on every Vercel
deploy, so the Convex push and Next build land atomically. You don't
run convex deploy from your laptop again unless you want to
push without rebuilding the companion.
It prints two URLs you'll need:
https://<prod>.convex.cloud: the API URL the apps point athttps://<prod>.convex.site: the HTTP routes URL (used for Stripe + Resend webhooks)
Set production env on the Convex dashboard
dashboard.convex.dev → your prod deployment → Settings → Environment Variables. Mirror your dev env, swapping test values for live:
CLERK_JWT_ISSUER_DOMAIN=https://your-app.clerk.accounts.dev # production Clerk instance
STRIPE_SECRET_KEY=sk_live_...
STRIPE_WEBHOOK_SECRET=whsec_... # from the LIVE Stripe webhook (Step 2 below)
STRIPE_PRICE_PRO=price_live_...
STRIPE_PRICE_TEAM=price_live_...
RESEND_API_KEY=re_...
RESEND_WEBHOOK_SECRET=whsec_...
EMAIL_FROM="Your Product <hello@yourdomain.com>"
APP_URL=https://your-app.com # final companion URL
AI_PROVIDER=anthropic
ANTHROPIC_API_KEY=sk-ant-...Reconfigure Stripe + Resend webhooks for prod
In the Stripe dashboard (live mode, not test):
- Developers → Webhooks → Add endpoint
- URL:
https://<prod>.convex.site/stripe/webhook - Subscribe to the same events as dev (see Payments add-on)
- Copy the new signing secret into the Convex env as
STRIPE_WEBHOOK_SECRET(overwriting the test one)
Point the delivery webhook at
https://<prod>.convex.site/resend-webhook and copy the secret
into the Convex env as RESEND_WEBHOOK_SECRET.
Deploy the companion site to Vercel
vercel.com → Add New → Project → import the repo.
Critical
Set Root Directory to companion/. Without this, Vercel
tries to build the whole monorepo.
Vercel picks up the shipped companion/vercel.json:
- Framework Preset: Next.js
- Build Command:
pnpm run vercel-build, which wrapsconvex deploy --cmd 'pnpm run build', so the Convex push and the Next.js build land atomically: if Convex deploy fails (env-var validation, schema break, etc.), the Next build never runs and Vercel keeps the previous deployment live. - Install Command: auto-detected
pnpm install. - Ignore Build Step:
npx turbo-ignore(Vercel skips this project's build when no files in its workspace dependency graph changed; see the callout below).
Set env vars in the import flow (everything from
companion/.env.example):
| Variable | Value |
|---|---|
CONVEX_DEPLOY_KEY | Convex prod deploy key: dashboard.convex.dev → your prod deployment → Settings → Deploy Keys → Generate Production Deploy Key. Required by vercel-build so Vercel pushes Convex functions on each deploy. |
NEXT_PUBLIC_CONVEX_URL | https://<prod>.convex.cloud |
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY | production Clerk key (pk_live_...) |
CLERK_SECRET_KEY | sk_live_... |
NEXT_PUBLIC_CLERK_SIGN_IN_URL / _UP_URL | /sign-in / /sign-up |
NEXT_PUBLIC_CLERK_SIGN_IN_FALLBACK_REDIRECT_URL / _UP_… | /dashboard |
NEXT_PUBLIC_STRIPE_PRICE_PRO / _TEAM | live price_... IDs |
GLASSES_APP_URL | public HTTPS URL of the deployed glasses app, used as a fallback if you don't use Connected Projects (Step 4). The companion's POST /api/pair/register route reads it server-side as the CORS allowlist origin, so only your glasses app can open pairing sessions. |
GLASSES_APP_NAME | display name shown under the icon on the glasses (e.g. MyApp) |
turbo-ignore: what it actually checks
npx turbo-ignore uses Turborepo's dependency graph to decide
whether to skip the build. It builds only when files in this
workspace or any workspace it depends on changed since the
previous deploy. Footgun: if a transitive dep changes (e.g.
packages/backend regenerates _generated/api.d.ts),
turbo-ignore will rebuild, which is correct, since the
companion's typecheck depends on those types. If a deploy
unexpectedly DIDN'T trigger, double-check that the change
actually touched files in this workspace's graph; an env-only
Vercel change won't (and shouldn't) trigger a build.
Deploy. The first build is ~2 min.
Use Vercel's Production / Preview / Development env scopes: live keys only in Production. Test keys in Preview keeps PR previews from charging real cards.
Custom domain
Project → Settings → Domains → Add Domain → your-app.com.
Vercel shows the DNS records to set at your registrar. HTTPS is
auto-provisioned.
After the domain is live, update the Convex env's APP_URL to
https://your-app.com so Stripe redirect URLs use the custom
domain.
Deploy the glasses app
The glasses app is a static Vite build; dist/ is uploadable to
anything that serves HTTPS.
pnpm --filter @glasskit/app buildThis produces app/dist/. The build expects two env vars to be
set at build time. Copy them into app/.env.production (or set
them in your host's UI) before building:
VITE_CONVEX_URL=https://<prod>.convex.cloud
VITE_CLERK_PUBLISHABLE_KEY=pk_live_...The simplest option: a second Vercel project for app/.
- Same repo, new project.
- Root Directory:
app/
Required setting
Settings → Build & Development → Include source files outside
of the Root Directory: ON. Required, because pnpm walks up to the
repo-root package.json to resolve the workspace:* dep on
@glasskit/backend, and without this toggle Vercel only uploads
app/ to the build container and install fails with
@glasskit/backend not found.
Vercel picks up the shipped app/vercel.json:
- Framework Preset: Vite
- Build Command:
pnpm run vercel-build, which runsconvex deploy --cmd 'pnpm run build'. Both the companion and the glasses-app projects deploy Convex from their build step, so a push that only redeploys one project still keeps the Convex backend current. The deploy is idempotent; pushing the same functions twice is a no-op. - Output Directory:
dist - Ignore Build Step:
npx turbo-ignore, which only rebuilds when this workspace or its deps changed. - Set the
VITE_*env vars +CONVEX_DEPLOY_KEY(same key as the companion project, see Step 3) in the project settings.
This gives you a https://<your-glasses-app>.vercel.app URL. That
URL is what you paste into the Meta Ray-Ban Display companion app
(or scan as a QR) to add it to the glasses.
Link the glasses-app project to the companion (Connected Projects): REQUIRED
The shipped vercel.json is intentionally inert
companion/vercel.json ships with relatedProjects: [], an
empty array. Until you populate it with your glasses-app
project ID, the companion will still need GLASSES_APP_URL set
manually as a regular env var. This is a required deploy
step, not an optional one if you want the auto-wiring the
README advertises.
Surface the glasses URL to the companion's build automatically,
with no manual GLASSES_APP_URL env-var copy after every redeploy:
Glasses-app project → Settings → General → copy the Project ID
(prj_…).
In the boilerplate, paste it into companion/vercel.json's
relatedProjects array (replacing the empty []):
{
"$schema": "https://openapi.vercel.sh/vercel.json",
"framework": "nextjs",
"buildCommand": "pnpm run vercel-build",
"ignoreCommand": "npx turbo-ignore --fallback=HEAD^1",
"relatedProjects": ["prj_paste_glasses_app_id_here"]
}Commit + redeploy the companion.
companion/env.ts pipes this through
@vercel/related-projects's
withRelatedProject, with the explicit GLASSES_APP_URL env var
as the fallback when the array is empty or the project name
doesn't match.
If you named your glasses-app Vercel project something other than
glasskit-app, set GLASSES_APP_VERCEL_PROJECT_NAME=<your-project-name>
in the companion's Vercel env (or edit the constant at the top of
companion/env.ts).
Manual-fallback path: skip Connected Projects entirely and set
GLASSES_APP_URL=https://<your-glasses-app>.vercel.app in the
companion's Vercel env. That works too; it just requires updating
the env var by hand whenever the glasses-app gets a new domain.
Manual fallback: set GLASSES_APP_URL in the companion's Vercel
project env vars directly. Used if you're not on Vercel for the
glasses app, or if Connected Projects didn't pick up the link.
app/dist/ is plain static files. Cloudflare Pages, Netlify,
S3+CloudFront, GitHub Pages, your own nginx all work. The only
requirement is public HTTPS (Meta's platform won't load HTTP
or self-signed URLs).
Add the app to the glasses
Once the glasses app is live:
Open the Meta Ray-Ban Display companion app on your phone.
Add Web App → paste your glasses URL (or scan a QR code with that URL).
The app appears in the glasses launcher.
Public publishing (to other people's glasses) isn't open yet. Meta's currently gating it to ~100 testers per app via a private URL. When Meta opens publishing, you're ready.
CI before deploy
GitHub Actions CI runs on every push / PR: typecheck + build
across every workspace. Until convex/_generated/ is committed
(i.e. you've run pnpm exec convex dev once), CI stays green with
a friendly notice instead of red errors. Once codegen is committed,
CI gates both Vercel projects.
Optional but recommended, protect main:
GitHub → Repo → Settings → Branches → Add branch protection rule
mainciNow main can't move without green CI, and Vercel only builds from
known-good commits.
Rollback
Per service, all instant:
- Companion site: Vercel → Deployments → ⋮ → Promote to Production on the last good deploy.
- Glasses app: same flow on the glasses Vercel project.
- Convex backend:
pnpm exec convex deployfrom an older commit (Convex doesn't have a UI rollback button; deploying old code IS the rollback). For safety, tag releases withgit tag.
Production checklist
- Custom domain live on the companion (HTTPS green).
- Stripe in live mode with live keys in Convex env.
- Live Stripe webhook pointed at
<prod>.convex.site/stripe/webhook, signing secret in Convex env. - Resend webhook pointed at
<prod>.convex.site/resend-webhook. - Convex production deployment populated with all env vars (see Step 1).
- Clerk production instance wired, JWT template still named
convex. - Glasses app deployed at a stable HTTPS URL; tested on-device.
- Test purchase with a real card → refund yourself; verify the dashboard's plan changes and the Stripe component synced the subscription.
- Welcome email arrives on sign-up (check spam).
-
convex/_generated/is committed → CI fully gated. - OG / favicon / robots.txt / sitemap.xml configured on the companion.
- Vercel Analytics (or Plausible) installed on both Vercel projects.
- Error tracking (Sentry, etc.) wired into Next.js.