[{"data":1,"prerenderedAt":365},["ShallowReactive",2],{"blog-blueprints/cursor-convex":3},{"id":4,"title":5,"body":6,"category":345,"date":346,"dateModified":346,"description":347,"draft":348,"extension":349,"faq":350,"featured":348,"headerVariant":351,"image":350,"keywords":350,"meta":352,"navigation":353,"ogDescription":354,"ogTitle":350,"path":355,"readTime":356,"schemaOrg":357,"schemaType":358,"seo":359,"sitemap":360,"stem":361,"tags":362,"twitterCard":363,"__hash__":364},"blog/blog/blueprints/cursor-convex.md","Cursor + Convex Security Blueprint",{"type":7,"value":8,"toc":324},"minimark",[9,20,24,30,35,46,51,54,121,125,129,132,142,151,160,164,168,177,181,190,194,198,207,211,220,224,229,232,235,238,241,244,247,250,253,271,293,312],[10,11,12],"blueprint-summary",{},[13,14,15,19],"p",{},[16,17,18],"strong",{},"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.",[21,22],"blueprint-meta",{"time":23},"1-2 hours",[25,26,27],"tldr",{},[13,28,29],{},"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.",[31,32,34],"h3",{"id":33},"platform-guides-checklists","Platform Guides & Checklists",[36,37,42],"pre",{"className":38,"code":40,"language":41},[39],"language-text","      Cursor Security Guide\n\n\n\n      Convex Security Guide\n\n\n\n      TypeScript Security Guide\n\n\n\n      Pre-Launch Checklist\n","text",[43,44,40],"code",{"__ignoreMap":45},"",[47,48,50],"h2",{"id":49},"understanding-convex-security-model","Understanding Convex Security Model",[13,52,53],{},"Convex uses a different security model than traditional databases:",[55,56,57,73],"table",{},[58,59,60],"thead",{},[61,62,63,67,70],"tr",{},[64,65,66],"th",{},"Concept",[64,68,69],{},"Convex Approach",[64,71,72],{},"Traditional Equivalent",[74,75,76,88,99,110],"tbody",{},[61,77,78,82,85],{},[79,80,81],"td",{},"Data Access",[79,83,84],{},"Through functions only",[79,86,87],{},"Direct DB queries + RLS",[61,89,90,93,96],{},[79,91,92],{},"Authorization",[79,94,95],{},"In function code",[79,97,98],{},"RLS policies",[61,100,101,104,107],{},[79,102,103],{},"Public API",[79,105,106],{},"Exported queries/mutations",[79,108,109],{},"API routes",[61,111,112,115,118],{},[79,113,114],{},"Internal Logic",[79,116,117],{},"Internal functions",[79,119,120],{},"Service layer",[47,122,124],{"id":123},"part-1-function-visibility-convex","Part 1: Function Visibility Convex",[31,126,128],{"id":127},"public-vs-internal-functions-convex-cursor","Public vs Internal Functions Convex Cursor",[13,130,131],{},"Every exported function in Convex is callable from clients. Mark internal functions correctly:",[133,134,136],"code-block",{"label":135},"convex/posts.ts - DANGEROUS: Admin function exposed",[36,137,140],{"className":138,"code":139,"language":41},[39],"import { mutation } from \"./_generated/server\";\nimport { v } from \"convex/values\";\n\n// DANGEROUS: This is callable from any client!\nexport const deleteAllPosts = mutation({\n  handler: async (ctx) => {\n    const posts = await ctx.db.query(\"posts\").collect();\n    for (const post of posts) {\n      await ctx.db.delete(post._id);\n    }\n  },\n});\n",[43,141,139],{"__ignoreMap":45},[133,143,145],{"label":144},"convex/posts.ts - SECURE: Using internal functions",[36,146,149],{"className":147,"code":148,"language":41},[39],"import { mutation, internalMutation } from \"./_generated/server\";\nimport { v } from \"convex/values\";\n\n// Internal: Only callable from other Convex functions\nexport const deleteAllPosts = internalMutation({\n  handler: async (ctx) => {\n    const posts = await ctx.db.query(\"posts\").collect();\n    for (const post of posts) {\n      await ctx.db.delete(post._id);\n    }\n  },\n});\n\n// Public: Authenticated user can only delete their own post\nexport const deletePost = mutation({\n  args: { postId: v.id(\"posts\") },\n  handler: async (ctx, args) => {\n    const identity = await ctx.auth.getUserIdentity();\n    if (!identity) {\n      throw new Error(\"Not authenticated\");\n    }\n\n    const post = await ctx.db.get(args.postId);\n    if (!post || post.authorId !== identity.subject) {\n      throw new Error(\"Not authorized\");\n    }\n\n    await ctx.db.delete(args.postId);\n  },\n});\n",[43,150,148],{"__ignoreMap":45},[152,153,154],"warning-box",{},[13,155,156,159],{},[16,157,158],{},"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.",[47,161,163],{"id":162},"part-2-authentication-patterns-convex","Part 2: Authentication Patterns Convex",[31,165,167],{"id":166},"using-convex-auth-convex","Using Convex Auth Convex",[133,169,171],{"label":170},"convex/users.ts - Auth-protected query",[36,172,175],{"className":173,"code":174,"language":41},[39],"import { query, mutation } from \"./_generated/server\";\nimport { v } from \"convex/values\";\n\nexport const getMyProfile = query({\n  handler: async (ctx) => {\n    const identity = await ctx.auth.getUserIdentity();\n    if (!identity) {\n      return null;  // Or throw, depending on your UX\n    }\n\n    return await ctx.db\n      .query(\"users\")\n      .withIndex(\"by_token\", (q) => q.eq(\"tokenIdentifier\", identity.tokenIdentifier))\n      .unique();\n  },\n});\n\nexport const updateMyProfile = mutation({\n  args: { name: v.string() },\n  handler: async (ctx, args) => {\n    const identity = await ctx.auth.getUserIdentity();\n    if (!identity) {\n      throw new Error(\"Not authenticated\");\n    }\n\n    const user = await ctx.db\n      .query(\"users\")\n      .withIndex(\"by_token\", (q) => q.eq(\"tokenIdentifier\", identity.tokenIdentifier))\n      .unique();\n\n    if (!user) {\n      throw new Error(\"User not found\");\n    }\n\n    await ctx.db.patch(user._id, { name: args.name });\n  },\n});\n",[43,176,174],{"__ignoreMap":45},[31,178,180],{"id":179},"helper-function-for-auth-convex","Helper Function for Auth Convex",[133,182,184],{"label":183},"convex/lib/auth.ts - Reusable auth helper",[36,185,188],{"className":186,"code":187,"language":41},[39],"import { QueryCtx, MutationCtx } from \"./_generated/server\";\n\nexport async function getAuthenticatedUser(ctx: QueryCtx | MutationCtx) {\n  const identity = await ctx.auth.getUserIdentity();\n  if (!identity) {\n    throw new Error(\"Not authenticated\");\n  }\n\n  const user = await ctx.db\n    .query(\"users\")\n    .withIndex(\"by_token\", (q) => q.eq(\"tokenIdentifier\", identity.tokenIdentifier))\n    .unique();\n\n  if (!user) {\n    throw new Error(\"User not found in database\");\n  }\n\n  return user;\n}\n\n// Usage in mutations:\n// const user = await getAuthenticatedUser(ctx);\n",[43,189,187],{"__ignoreMap":45},[47,191,193],{"id":192},"part-3-data-access-patterns-convex","Part 3: Data Access Patterns Convex",[31,195,197],{"id":196},"scoping-queries-to-user-data-convex","Scoping Queries to User Data Convex",[133,199,201],{"label":200},"convex/posts.ts - Properly scoped queries",[36,202,205],{"className":203,"code":204,"language":41},[39],"import { query } from \"./_generated/server\";\nimport { v } from \"convex/values\";\nimport { getAuthenticatedUser } from \"./lib/auth\";\n\n// Get only the current user's posts\nexport const getMyPosts = query({\n  handler: async (ctx) => {\n    const user = await getAuthenticatedUser(ctx);\n\n    return await ctx.db\n      .query(\"posts\")\n      .withIndex(\"by_author\", (q) => q.eq(\"authorId\", user._id))\n      .order(\"desc\")\n      .collect();\n  },\n});\n\n// Get a specific post (with access check)\nexport const getPost = query({\n  args: { postId: v.id(\"posts\") },\n  handler: async (ctx, args) => {\n    const post = await ctx.db.get(args.postId);\n\n    if (!post) {\n      return null;\n    }\n\n    // Public posts are readable by anyone\n    if (post.isPublic) {\n      return post;\n    }\n\n    // Private posts require authentication and ownership\n    const identity = await ctx.auth.getUserIdentity();\n    if (!identity) {\n      return null;\n    }\n\n    const user = await ctx.db\n      .query(\"users\")\n      .withIndex(\"by_token\", (q) => q.eq(\"tokenIdentifier\", identity.tokenIdentifier))\n      .unique();\n\n    if (user?._id !== post.authorId) {\n      return null;\n    }\n\n    return post;\n  },\n});\n",[43,206,204],{"__ignoreMap":45},[47,208,210],{"id":209},"part-4-schema-validation-convex","Part 4: Schema Validation Convex",[133,212,214],{"label":213},"convex/schema.ts - Type-safe schema",[36,215,218],{"className":216,"code":217,"language":41},[39],"import { defineSchema, defineTable } from \"convex/server\";\nimport { v } from \"convex/values\";\n\nexport default defineSchema({\n  users: defineTable({\n    tokenIdentifier: v.string(),\n    name: v.string(),\n    email: v.string(),\n    role: v.union(v.literal(\"user\"), v.literal(\"admin\")),\n  })\n    .index(\"by_token\", [\"tokenIdentifier\"])\n    .index(\"by_email\", [\"email\"]),\n\n  posts: defineTable({\n    title: v.string(),\n    content: v.string(),\n    authorId: v.id(\"users\"),\n    isPublic: v.boolean(),\n  })\n    .index(\"by_author\", [\"authorId\"]),\n});\n",[43,219,217],{"__ignoreMap":45},[47,221,223],{"id":222},"security-checklist","Security Checklist",[225,226,228],"h4",{"id":227},"pre-launch-checklist-for-cursor-convex","Pre-Launch Checklist for Cursor + Convex",[13,230,231],{},"All admin functions marked as internalMutation/internalQuery",[13,233,234],{},"Auth check in every public mutation",[13,236,237],{},"Auth check in queries that return private data",[13,239,240],{},"Schema defined with proper types",[13,242,243],{},"Indexes created for query patterns",[13,245,246],{},"Environment variables in Convex dashboard",[13,248,249],{},".cursorignore excludes .env.local",[13,251,252],{},"Production deployment uses production project",[254,255,256,260],"stack-comparison",{},[31,257,259],{"id":258},"alternative-stack-configurations","Alternative Stack Configurations",[254,261,262,265],{},[13,263,264],{},"Cursor + Supabase + Vercel\nDatabase-first approach with PostgreSQL RLS instead of function-based security.",[36,266,269],{"className":267,"code":268,"language":41},[39],"      Cursor + Firebase + Vercel\n      Similar client-callable model with Firestore rules instead of function visibility.\n\n\n      Bolt.new + Convex\n      Same Convex security patterns with Bolt.new code generation approach.\n",[43,270,268],{"__ignoreMap":45},[272,273,274,281,287],"faq-section",{},[275,276,278],"faq-item",{"question":277},"Do I need RLS with Convex?",[13,279,280],{},"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.",[275,282,284],{"question":283},"Are Convex functions secure?",[13,285,286],{},"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.",[275,288,290],{"question":289},"How do I handle admin operations?",[13,291,292],{},"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.",[294,295,296,302,307],"related-articles",{},[297,298],"related-card",{"description":299,"href":300,"title":301},"RLS-based alternative","/blog/blueprints/cursor-supabase-vercel","Cursor + Supabase + Vercel",[297,303],{"description":304,"href":305,"title":306},"Deep dive into Convex","/blog/guides/convex","Convex Security Guide",[297,308],{"description":309,"href":310,"title":311},"Similar rules-based model","/blog/blueprints/cursor-firebase-vercel","Cursor + Firebase + Vercel",[313,314,317,321],"cta-box",{"href":315,"label":316},"/","Start Free Scan",[47,318,320],{"id":319},"building-with-convex-and-cursor","Building with Convex and Cursor?",[13,322,323],{},"Scan your functions for missing auth checks and exposed internals.",{"title":45,"searchDepth":325,"depth":325,"links":326},2,[327,329,330,333,337,340,341,344],{"id":33,"depth":328,"text":34},3,{"id":49,"depth":325,"text":50},{"id":123,"depth":325,"text":124,"children":331},[332],{"id":127,"depth":328,"text":128},{"id":162,"depth":325,"text":163,"children":334},[335,336],{"id":166,"depth":328,"text":167},{"id":179,"depth":328,"text":180},{"id":192,"depth":325,"text":193,"children":338},[339],{"id":196,"depth":328,"text":197},{"id":209,"depth":325,"text":210},{"id":222,"depth":325,"text":223,"children":342},[343],{"id":258,"depth":328,"text":259},{"id":319,"depth":325,"text":320},"blueprints","2026-01-30","Security guide for Cursor + Convex stack. Configure Convex functions securely, implement authentication, protect data access patterns, and deploy with confidence.",false,"md",null,"purple",{},true,"Complete security configuration for Convex apps built with Cursor.","/blog/blueprints/cursor-convex","11 min read","[object Object]","Article",{"title":5,"description":347},{"loc":355},"blog/blueprints/cursor-convex",[],"summary_large_image","nGAOgIe0R-KsVTx_kKcuLJyhK9S_Fg6BWdS-WagwyAU",1775843932747]