HOW TO SECURE CONVEX
Convex Security Context
Convex is a TypeScript-first backend with reactive queries, mutations, and actions. Unlike Supabase / Firebase, security is enforced at the function level — there is no row-level rule layer. Every query, mutation, and action you ship is publicly callable from any client unless you mark it internal. The recurring incident shape is “I wrote the function, it works, I never added the ctx.auth check, anyone can call it.”
Security Checklist
1. Define schemas strictly (Critical)
In convex/schema.ts: define every table with explicit field types and use v.string() / v.number() / v.id("table") validators. Strict schemas reject malformed writes at the framework boundary — the function never sees an unexpected shape.
export default defineSchema({
posts: defineTable({
authorId: v.id("users"),
title: v.string(),
body: v.string(),
}).index("by_author", ["authorId"]),
});
2. Use internal* for sensitive operations (Critical)
query / mutation / action are public — any client can call them. internalQuery / internalMutation / internalAction are server-only — they can only be called by other Convex functions or scheduled jobs. For anything that should not be reachable from the client (admin operations, batch jobs, data migrations), use the internal variant.
3. Implement auth checks on every public function (Critical)
Every public mutation and query that touches user data must check ctx.auth:
export const updateProfile = mutation({
args: { name: v.string() },
handler: async (ctx, args) => {
const identity = await ctx.auth.getUserIdentity();
if (!identity) throw new Error("Not authenticated");
const user = await ctx.db.query("users")
.withIndex("by_token", q => q.eq("tokenIdentifier", identity.tokenIdentifier))
.unique();
if (!user) throw new Error("User not found");
// ... use user._id, never trust args for ownership
},
});
The pattern that ships AI-generated bugs: trusting args.userId instead of deriving the user from ctx.auth.
4. Lock down HTTP actions (Critical)
httpAction exposes a Convex function as a public HTTP endpoint. Same auth and validation requirements as any API route — verify the JWT or API key in the handler before doing work. Add CORS to your origin, add rate limiting (Convex doesn’t ship one; track per-IP in a rate_limits table).
5. Validate all function arguments (Critical)
Convex’s argument validators (v.string(), v.number(), v.object({...})) reject invalid shapes. Use them for every arg, not just the obvious ones. The bug shape is args: { data: v.any() } — which accepts arbitrary input and the handler trusts it.
6. Configure environment variables in the dashboard
In Convex Dashboard → Settings → Environment Variables: add Stripe keys, OpenAI keys, third-party API tokens. Never hardcode in convex/ files. The dashboard secrets are server-side only and never reach the client bundle.
7. Audit reactive subscriptions
useQuery(api.posts.list) re-runs whenever the underlying data changes. The handler is called on the server with the user’s auth context — so the auth check applies — but the query result is cached client-side. Don’t return data the user shouldn’t have now on the assumption they’ll log out later.
8. Review scheduled functions
crons.ts defines scheduled jobs. Each runs without a user context — ctx.auth is undefined. They should be internalAction / internalMutation and operate at the system level. Audit for: jobs that read user data without an explicit user filter, jobs that perform bulk operations without batching.
9. Configure CORS for HTTP actions
In httpAction handlers, set Access-Control-Allow-Origin to your frontend’s origin only. The default in many examples is *, which with credentials becomes a credential-stuffing pivot. See CORS credentials misconfig.
10. Audit file storage access
ctx.storage.generateUploadUrl() returns a signed URL that anyone can upload to. Validate the upload metadata before storing the reference, restrict file size client-side and verify server-side, and never serve uploaded files via a public route without an auth check.
11. Review index definitions
Indexes don’t directly affect security but a missing index means an unindexed query, which scans the table — an attacker can DoS by triggering it. Define indexes for every query path your app uses.
12. Configure deployment environments
npx convex deploy --prod deploys to production. Use separate Convex projects for dev / staging / prod — never prod for testing AI-generated changes. The dev and prod schemas drift fast; running tests against prod is how you delete prod data by accident.
13. Audit AI-generated Convex functions
When Cursor / Claude Code / Copilot writes a Convex function, check: (a) is it mutation or internalMutation — should it be public? (b) does the handler call ctx.auth.getUserIdentity() before doing user-data work? (c) does it use ctx.auth-derived IDs, or does it trust args.userId?
14. Review team access controls
In Convex Dashboard → Team: review members. Anyone with admin access can read all data, deploy code, and reset env vars. Remove ex-team members same-day.
15. Monitor function execution logs
Dashboard → Logs: review weekly. Look for: spikes in calls to a single function from a single IP, repeated error responses (probing), calls to functions you don’t recognize.
16. Run a security scan
The full VibeEval scan tests your Convex HTTP actions and exposed function endpoints for missing auth, BOLA, and rate-limit gaps.
Related Resources
Free Self-Audit Suite
Five free scanners.
Vibe Coding Security Risk Guide
Full risk catalogue.
Supabase Guide
Comparison reference for the closest BaaS alternative.
Automate Your Security Checks
VibeEval scans your Convex deployment for missing auth checks, BOLA, and exposed internal functions that shouldn’t be public.
SCAN YOUR APP
14-day trial. No card. Results in under 60 seconds.