To secure a Cursor + Convex stack, you need to: (1) mark admin functions with internalMutation/internalQuery so they are not callable from clients, (2) add authentication checks (ctx.auth.getUserIdentity()) in every public mutation, (3) add auth checks in queries that return private data, (4) create reusable auth helper functions to ensure consistent security patterns, and (5) define a schema with proper types for input validation. This blueprint covers Convex function visibility, authentication patterns, and data access control.
TL;DR
Convex provides a TypeScript-first backend with built-in database, functions, and real-time sync. Security is enforced through function visibility and authentication checks. Key tasks: mark internal functions appropriately, validate auth in mutations, use Convex Auth or integrate with external providers, and never expose admin functions to clients. Convex functions run server-side, but AI-generated code often skips auth checks.
Platform Guides & Checklists
Cursor Security Guide
Convex Security Guide
TypeScript Security Guide
Pre-Launch Checklist
Understanding Convex Security Model
Convex uses a different security model than traditional databases:
| Concept | Convex Approach | Traditional Equivalent |
|---|---|---|
| Data Access | Through functions only | Direct DB queries + RLS |
| Authorization | In function code | RLS policies |
| Public API | Exported queries/mutations | API routes |
| Internal Logic | Internal functions | Service layer |
Part 1: Function Visibility Convex
Public vs Internal Functions Convex Cursor
Every exported function in Convex is callable from clients. Mark internal functions correctly:
import { mutation } from "./_generated/server";
import { v } from "convex/values";
// DANGEROUS: This is callable from any client!
export const deleteAllPosts = mutation({
handler: async (ctx) => {
const posts = await ctx.db.query("posts").collect();
for (const post of posts) {
await ctx.db.delete(post._id);
}
},
});
import { mutation, internalMutation } from "./_generated/server";
import { v } from "convex/values";
// Internal: Only callable from other Convex functions
export const deleteAllPosts = internalMutation({
handler: async (ctx) => {
const posts = await ctx.db.query("posts").collect();
for (const post of posts) {
await ctx.db.delete(post._id);
}
},
});
// Public: Authenticated user can only delete their own post
export const deletePost = mutation({
args: { postId: v.id("posts") },
handler: async (ctx, args) => {
const identity = await ctx.auth.getUserIdentity();
if (!identity) {
throw new Error("Not authenticated");
}
const post = await ctx.db.get(args.postId);
if (!post || post.authorId !== identity.subject) {
throw new Error("Not authorized");
}
await ctx.db.delete(args.postId);
},
});
AI code risk: Cursor often generates mutations without auth checks, assuming you'll add them later. Every public mutation should verify authentication before performing actions.
Part 2: Authentication Patterns Convex
Using Convex Auth Convex
import { query, mutation } from "./_generated/server";
import { v } from "convex/values";
export const getMyProfile = query({
handler: async (ctx) => {
const identity = await ctx.auth.getUserIdentity();
if (!identity) {
return null; // Or throw, depending on your UX
}
return await ctx.db
.query("users")
.withIndex("by_token", (q) => q.eq("tokenIdentifier", identity.tokenIdentifier))
.unique();
},
});
export const updateMyProfile = 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");
}
await ctx.db.patch(user._id, { name: args.name });
},
});
Helper Function for Auth Convex
import { QueryCtx, MutationCtx } from "./_generated/server";
export async function getAuthenticatedUser(ctx: QueryCtx | MutationCtx) {
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 in database");
}
return user;
}
// Usage in mutations:
// const user = await getAuthenticatedUser(ctx);
Part 3: Data Access Patterns Convex
Scoping Queries to User Data Convex
import { query } from "./_generated/server";
import { v } from "convex/values";
import { getAuthenticatedUser } from "./lib/auth";
// Get only the current user's posts
export const getMyPosts = query({
handler: async (ctx) => {
const user = await getAuthenticatedUser(ctx);
return await ctx.db
.query("posts")
.withIndex("by_author", (q) => q.eq("authorId", user._id))
.order("desc")
.collect();
},
});
// Get a specific post (with access check)
export const getPost = query({
args: { postId: v.id("posts") },
handler: async (ctx, args) => {
const post = await ctx.db.get(args.postId);
if (!post) {
return null;
}
// Public posts are readable by anyone
if (post.isPublic) {
return post;
}
// Private posts require authentication and ownership
const identity = await ctx.auth.getUserIdentity();
if (!identity) {
return null;
}
const user = await ctx.db
.query("users")
.withIndex("by_token", (q) => q.eq("tokenIdentifier", identity.tokenIdentifier))
.unique();
if (user?._id !== post.authorId) {
return null;
}
return post;
},
});
Part 4: Schema Validation Convex
import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";
export default defineSchema({
users: defineTable({
tokenIdentifier: v.string(),
name: v.string(),
email: v.string(),
role: v.union(v.literal("user"), v.literal("admin")),
})
.index("by_token", ["tokenIdentifier"])
.index("by_email", ["email"]),
posts: defineTable({
title: v.string(),
content: v.string(),
authorId: v.id("users"),
isPublic: v.boolean(),
})
.index("by_author", ["authorId"]),
});
Security Checklist
Pre-Launch Checklist for Cursor + Convex
All admin functions marked as internalMutation/internalQuery
Auth check in every public mutation
Auth check in queries that return private data
Schema defined with proper types
Indexes created for query patterns
Environment variables in Convex dashboard
.cursorignore excludes .env.local
Production deployment uses production project
Alternative Stack Configurations
Cursor + Supabase + Vercel Database-first approach with PostgreSQL RLS instead of function-based security.
Cursor + Firebase + Vercel
Similar client-callable model with Firestore rules instead of function visibility.
Bolt.new + Convex
Same Convex security patterns with Bolt.new code generation approach.
Do I need RLS with Convex?
No. Convex doesn't use traditional database access. All data access goes through your functions, so authorization is implemented in code. This gives you more flexibility but requires careful function design.
Are Convex functions secure?
Convex functions run on Convex's servers, not the client. However, any exported query or mutation is callable from clients. Security comes from properly checking authentication and authorization within each function.
How do I handle admin operations?
Use internalMutation for admin operations and trigger them from scheduled functions, HTTP actions with API key verification, or other internal functions. Never expose admin operations as regular mutations.
Building with Convex and Cursor?
Scan your functions for missing auth checks and exposed internals.
Start Free Scan