Security issues: email karl@xube.io. Please do not open public GitHub issues for exploitable bugs.
All secrets live in one of two places, never both:
| Secret | Location |
|---|---|
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY |
.env.local (client-safe by design) |
CLERK_SECRET_KEY |
.env.local (server-only, never prefix with NEXT_PUBLIC_) |
NEXT_PUBLIC_CONVEX_URL |
.env.local |
CONVEX_DEPLOY_KEY |
.env.local (dev) / Vercel env (prod) |
CLERK_JWT_ISSUER_DOMAIN |
Convex Dashboard — Settings → Environment Variables |
CLERK_WEBHOOK_SECRET |
Convex Dashboard — Settings → Environment Variables |
.env.local is in .gitignore (line 34: .env*). Never commit it. gitleaks runs in CI to catch regressions (.github/workflows/ci.yml).
If a key is exposed (or suspected exposed), rotate all of them as a batch:
- Clerk publishable + secret key — Clerk Dashboard → API Keys → regenerate. Update
.env.localand Vercel env. Any live user sessions are invalidated. - Clerk webhook signing secret — Clerk Dashboard → Webhooks → the Convex endpoint → Rotate Signing Secret. Paste the new value into Convex Dashboard env vars (NOT
.env.local— webhook verification runs on Convex's runtime). - Convex deploy key —
bunx convex dev --configureregenerates. Update Vercel env.
After rotation, confirm git history stayed clean:
git log --all --full-history -- .env.localExpected output: empty. If anything shows, the file was committed at some point — rewrite history (git filter-repo) or treat as permanently compromised.
convex/http.ts verifies Svix signatures on every Clerk webhook. Signature failures return 401 and are logged (via Sentry if configured). Do not disable verification; the only acceptable change is to add more rigor (IP allow-listing, per-id rate limiting).
- Server-side enforcement is the only authority.
proxy.tsdoes UX routing (redirect-to-sign-in); it is not the security boundary. Every Convex public function that touches user data must callgetCurrentUserOrThroworassertAdminfromconvex/helpers.ts. - Per-resource ownership check. Mutations that edit a doc must verify
doc.userId === user._idbefore.patch()/.delete(). Seeconvex/itineraries.tsfor the pattern. internal.*for server-to-server. Never useapi.*inside Convex server code — that's the client-facing API and bypasses internal-only enforcement.
Public mutations that write user content are rate-limited via convex/rateLimit.ts. Keys MUST be keyed on authenticated identity (user._id) — never on user-supplied strings. A cron in convex/crons.ts prunes expired rows.
Photo uploads are double-validated: client checks MIME + size for UX; server checks ctx.storage.getMetadata for size, content-type, and rejects SVG (JS execution risk). Upload ownership is tracked in tip_photo_uploads (tips) and photos (destinations) — tips may only attach photos the current user uploaded.