GlassKit

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 deploy

The 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 at
  • https://<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:

Convex production environment variables
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.comAdd 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 wraps convex 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):

VariableValue
CONVEX_DEPLOY_KEYConvex 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_URLhttps://<prod>.convex.cloud
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEYproduction Clerk key (pk_live_...)
CLERK_SECRET_KEYsk_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 / _TEAMlive price_... IDs
GLASSES_APP_URLpublic 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_NAMEdisplay 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 build

This 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:

app/.env.production
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 runs convex 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.

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 []):

companion/vercel.json
{
  "$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

Pattern: main
Enable Require status checks to pass before merging
Required check: ci

Now 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 deploy from an older commit (Convex doesn't have a UI rollback button; deploying old code IS the rollback). For safety, tag releases with git 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.

On this page