Cursor + Convex Security Blueprint

Share

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.

Setup Time1-2 hours

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:

ConceptConvex ApproachTraditional Equivalent
Data AccessThrough functions onlyDirect DB queries + RLS
AuthorizationIn function codeRLS policies
Public APIExported queries/mutationsAPI routes
Internal LogicInternal functionsService 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:

convex/posts.ts - DANGEROUS: Admin function exposed
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);
    }
  },
});
convex/posts.ts - SECURE: Using internal functions
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

convex/users.ts - Auth-protected query
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

convex/lib/auth.ts - Reusable auth helper
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

convex/posts.ts - Properly scoped queries
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

convex/schema.ts - Type-safe schema
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
Security Blueprints

Cursor + Convex Security Blueprint