[{"data":1,"prerenderedAt":367},["ShallowReactive",2],{"blog-guides/convex":3},{"id":4,"title":5,"body":6,"category":340,"date":341,"dateModified":341,"description":342,"draft":343,"extension":344,"faq":345,"featured":343,"headerVariant":352,"image":353,"keywords":354,"meta":355,"navigation":356,"ogDescription":357,"ogTitle":353,"path":358,"readTime":353,"schemaOrg":359,"schemaType":360,"seo":361,"sitemap":362,"stem":363,"tags":364,"twitterCard":365,"__hash__":366},"blog/blog/guides/convex.md","Convex Security Guide for Vibe Coders",{"type":7,"value":8,"toc":314},"minimark",[9,13,17,32,37,40,43,47,50,55,65,78,82,85,89,95,99,105,109,112,116,122,126,132,136,139,143,149,155,159,162,168,172,210,214,220,224,227,233,261,265,268,288,295],[10,11,5],"h1",{"id":12},"convex-security-guide-for-vibe-coders",[14,15,16],"p",{},"Published on January 23, 2026 - 11 min read",[18,19,20],"tldr",{},[14,21,22,23,27,28,31],{},"Convex provides a reactive database with built-in functions, but security depends on how you write those functions. Always validate arguments using Convex's validators (",[24,25,26],"code",{},"v.string()",", etc.). Use the ",[24,29,30],{},"ctx.auth"," to check authentication. Implement authorization checks in every mutation. Remember that queries are public by default unless you add authentication checks.",[33,34,36],"h2",{"id":35},"why-convex-security-matters-for-vibe-coding","Why Convex Security Matters for Vibe Coding",[14,38,39],{},"Convex is a backend platform that combines a reactive database with serverless functions. When AI tools generate Convex code, they often create functional queries and mutations but miss important security patterns. Convex makes it easy to build features quickly, which means security oversights can happen just as quickly.",[14,41,42],{},"The reactive nature of Convex means data flows directly to clients. Without proper access controls, sensitive data can be exposed through subscriptions.",[33,44,46],{"id":45},"argument-validation","Argument Validation",[14,48,49],{},"Convex requires explicit argument validation using its validator system:",[51,52,54],"h3",{"id":53},"basic-validation","Basic Validation",[56,57,62],"pre",{"className":58,"code":60,"language":61},[59],"language-text","import { query, mutation } from \"./_generated/server\";\nimport { v } from \"convex/values\";\n\n// SECURE: Arguments validated\nexport const getDocument = query({\n  args: {\n    documentId: v.id(\"documents\"),\n  },\n  handler: async (ctx, args) => {\n    return await ctx.db.get(args.documentId);\n  },\n});\n\n// SECURE: Complex validation\nexport const createDocument = mutation({\n  args: {\n    title: v.string(),\n    content: v.string(),\n    isPublic: v.optional(v.boolean()),\n    tags: v.optional(v.array(v.string())),\n  },\n  handler: async (ctx, args) => {\n    // Validate string lengths\n    if (args.title.length > 200) {\n      throw new Error(\"Title too long\");\n    }\n    if (args.content.length > 50000) {\n      throw new Error(\"Content too long\");\n    }\n\n    return await ctx.db.insert(\"documents\", {\n      title: args.title,\n      content: args.content,\n      isPublic: args.isPublic ?? false,\n      tags: args.tags ?? [],\n      createdAt: Date.now(),\n    });\n  },\n});\n","text",[24,63,60],{"__ignoreMap":64},"",[66,67,68,72],"warning-box",{},[51,69,71],{"id":70},"convex-validators-dont-check-length","Convex Validators Don't Check Length",[14,73,74,75,77],{},"The ",[24,76,26],{}," validator confirms the type but doesn't limit length. Always add explicit length checks for strings that will be stored or displayed to prevent abuse.",[33,79,81],{"id":80},"authentication","Authentication",[14,83,84],{},"Integrate authentication and check it in your functions:",[51,86,88],{"id":87},"setting-up-auth-context","Setting Up Auth Context",[56,90,93],{"className":91,"code":92,"language":61},[59],"// convex/auth.ts\nimport { query, mutation, QueryCtx, MutationCtx } from \"./_generated/server\";\n\n// Helper to get authenticated user\nexport async function getUser(ctx: QueryCtx | MutationCtx) {\n  const identity = await ctx.auth.getUserIdentity();\n\n  if (!identity) {\n    return null;\n  }\n\n  // Get or create user in database\n  const user = await ctx.db\n    .query(\"users\")\n    .withIndex(\"by_token\", (q) => q.eq(\"tokenIdentifier\", identity.tokenIdentifier))\n    .unique();\n\n  return user;\n}\n\n// Helper that throws if not authenticated\nexport async function requireAuth(ctx: QueryCtx | MutationCtx) {\n  const user = await getUser(ctx);\n\n  if (!user) {\n    throw new Error(\"Authentication required\");\n  }\n\n  return user;\n}\n",[24,94,92],{"__ignoreMap":64},[51,96,98],{"id":97},"protected-functions","Protected Functions",[56,100,103],{"className":101,"code":102,"language":61},[59],"import { query, mutation } from \"./_generated/server\";\nimport { v } from \"convex/values\";\nimport { requireAuth, getUser } from \"./auth\";\n\n// Protected query - requires authentication\nexport const getMyDocuments = query({\n  args: {},\n  handler: async (ctx) => {\n    const user = await requireAuth(ctx);\n\n    return await ctx.db\n      .query(\"documents\")\n      .withIndex(\"by_owner\", (q) => q.eq(\"ownerId\", user._id))\n      .collect();\n  },\n});\n\n// Protected mutation\nexport const deleteDocument = mutation({\n  args: {\n    documentId: v.id(\"documents\"),\n  },\n  handler: async (ctx, args) => {\n    const user = await requireAuth(ctx);\n\n    const document = await ctx.db.get(args.documentId);\n\n    if (!document) {\n      throw new Error(\"Document not found\");\n    }\n\n    // Authorization check\n    if (document.ownerId !== user._id) {\n      throw new Error(\"Not authorized to delete this document\");\n    }\n\n    await ctx.db.delete(args.documentId);\n  },\n});\n",[24,104,102],{"__ignoreMap":64},[33,106,108],{"id":107},"authorization-patterns","Authorization Patterns",[14,110,111],{},"Beyond authentication, implement fine-grained authorization:",[51,113,115],{"id":114},"role-based-access","Role-Based Access",[56,117,120],{"className":118,"code":119,"language":61},[59],"// Check user role\nasync function requireRole(ctx: MutationCtx, role: \"admin\" | \"moderator\") {\n  const user = await requireAuth(ctx);\n\n  if (!user.roles?.includes(role)) {\n    throw new Error(`${role} access required`);\n  }\n\n  return user;\n}\n\n// Admin-only mutation\nexport const deleteAnyDocument = mutation({\n  args: {\n    documentId: v.id(\"documents\"),\n  },\n  handler: async (ctx, args) => {\n    await requireRole(ctx, \"admin\");\n\n    const document = await ctx.db.get(args.documentId);\n    if (!document) {\n      throw new Error(\"Document not found\");\n    }\n\n    await ctx.db.delete(args.documentId);\n  },\n});\n",[24,121,119],{"__ignoreMap":64},[51,123,125],{"id":124},"resource-based-access","Resource-Based Access",[56,127,130],{"className":128,"code":129,"language":61},[59],"// Check if user can access a workspace\nasync function canAccessWorkspace(\n  ctx: QueryCtx | MutationCtx,\n  workspaceId: Id\u003C\"workspaces\">\n) {\n  const user = await requireAuth(ctx);\n\n  const membership = await ctx.db\n    .query(\"workspaceMembers\")\n    .withIndex(\"by_user_workspace\", (q) =>\n      q.eq(\"userId\", user._id).eq(\"workspaceId\", workspaceId)\n    )\n    .unique();\n\n  return membership !== null;\n}\n\n// Workspace-scoped query\nexport const getWorkspaceDocuments = query({\n  args: {\n    workspaceId: v.id(\"workspaces\"),\n  },\n  handler: async (ctx, args) => {\n    if (!await canAccessWorkspace(ctx, args.workspaceId)) {\n      throw new Error(\"Not a member of this workspace\");\n    }\n\n    return await ctx.db\n      .query(\"documents\")\n      .withIndex(\"by_workspace\", (q) => q.eq(\"workspaceId\", args.workspaceId))\n      .collect();\n  },\n});\n",[24,131,129],{"__ignoreMap":64},[33,133,135],{"id":134},"query-visibility","Query Visibility",[14,137,138],{},"Convex queries are public by default. Be careful about what data you expose:",[51,140,142],{"id":141},"dangerous-exposing-all-data","Dangerous: Exposing All Data",[56,144,147],{"className":145,"code":146,"language":61},[59],"// DANGEROUS: Anyone can query all documents\nexport const getAllDocuments = query({\n  args: {},\n  handler: async (ctx) => {\n    return await ctx.db.query(\"documents\").collect();\n  },\n});\n",[24,148,146],{"__ignoreMap":64},[56,150,153],{"className":151,"code":152,"language":61},[59],"// SECURE: Only return public documents or user's own\nexport const getDocuments = query({\n  args: {},\n  handler: async (ctx) => {\n    const user = await getUser(ctx);\n\n    // Build query based on auth state\n    const documents = await ctx.db.query(\"documents\").collect();\n\n    // Filter to public docs or user's own\n    return documents.filter((doc) =>\n      doc.isPublic || (user && doc.ownerId === user._id)\n    );\n  },\n});\n\n// Better: Use an index for efficiency\nexport const getAccessibleDocuments = query({\n  args: {},\n  handler: async (ctx) => {\n    const user = await getUser(ctx);\n\n    // Get public documents\n    const publicDocs = await ctx.db\n      .query(\"documents\")\n      .withIndex(\"by_public\", (q) => q.eq(\"isPublic\", true))\n      .collect();\n\n    if (!user) {\n      return publicDocs;\n    }\n\n    // Get user's own documents\n    const userDocs = await ctx.db\n      .query(\"documents\")\n      .withIndex(\"by_owner\", (q) => q.eq(\"ownerId\", user._id))\n      .filter((q) => q.eq(q.field(\"isPublic\"), false))\n      .collect();\n\n    return [...publicDocs, ...userDocs];\n  },\n});\n",[24,154,152],{"__ignoreMap":64},[33,156,158],{"id":157},"internal-functions","Internal Functions",[14,160,161],{},"Use internal functions for operations that should never be called from clients:",[56,163,166],{"className":164,"code":165,"language":61},[59],"import { internalMutation, internalQuery } from \"./_generated/server\";\nimport { v } from \"convex/values\";\n\n// Internal function - not callable from client\nexport const processPayment = internalMutation({\n  args: {\n    userId: v.id(\"users\"),\n    amount: v.number(),\n  },\n  handler: async (ctx, args) => {\n    // This can only be called from other Convex functions\n    // Not from the client SDK\n    await ctx.db.insert(\"payments\", {\n      userId: args.userId,\n      amount: args.amount,\n      processedAt: Date.now(),\n    });\n  },\n});\n\n// Public mutation that uses internal function\nexport const subscribe = mutation({\n  args: {\n    planId: v.string(),\n  },\n  handler: async (ctx, args) => {\n    const user = await requireAuth(ctx);\n\n    // ... validate plan and calculate amount ...\n\n    // Call internal function\n    await ctx.scheduler.runAfter(0, internal.payments.processPayment, {\n      userId: user._id,\n      amount: planAmount,\n    });\n  },\n});\n",[24,167,165],{"__ignoreMap":64},[51,169,171],{"id":170},"convex-security-checklist","Convex Security Checklist",[173,174,175,183,186,189,192,195,198,201,204,207],"ul",{},[176,177,178,179,182],"li",{},"All functions have argument validation with ",[24,180,181],{},"v.*"," validators",[176,184,185],{},"String inputs have explicit length limits",[176,187,188],{},"Authentication checked in all non-public functions",[176,190,191],{},"Authorization verified before data access/modification",[176,193,194],{},"Queries filter data based on user permissions",[176,196,197],{},"Sensitive operations use internal functions",[176,199,200],{},"User-specific data indexed by owner for efficient filtering",[176,202,203],{},"Error messages don't leak sensitive information",[176,205,206],{},"Rate-sensitive operations have throttling",[176,208,209],{},"Scheduled functions validated for authorized callers",[33,211,213],{"id":212},"environment-variables","Environment Variables",[56,215,218],{"className":216,"code":217,"language":61},[59],"// Access environment variables securely\n// convex/config.ts\n\n// These are set in the Convex dashboard\n// Never hardcode secrets in code\n\nexport const getStripeKey = () => {\n  const key = process.env.STRIPE_SECRET_KEY;\n  if (!key) {\n    throw new Error(\"STRIPE_SECRET_KEY not configured\");\n  }\n  return key;\n};\n\n// Usage in mutation\nexport const createCheckout = mutation({\n  args: { priceId: v.string() },\n  handler: async (ctx, args) => {\n    const user = await requireAuth(ctx);\n    const stripe = new Stripe(getStripeKey());\n\n    // Create checkout session...\n  },\n});\n",[24,219,217],{"__ignoreMap":64},[33,221,223],{"id":222},"rate-limiting","Rate Limiting",[14,225,226],{},"Implement rate limiting for sensitive operations:",[56,228,231],{"className":229,"code":230,"language":61},[59],"// Simple rate limiting using database\nexport const sendMessage = mutation({\n  args: {\n    channelId: v.id(\"channels\"),\n    content: v.string(),\n  },\n  handler: async (ctx, args) => {\n    const user = await requireAuth(ctx);\n\n    // Check rate limit\n    const recentMessages = await ctx.db\n      .query(\"messages\")\n      .withIndex(\"by_author_time\", (q) =>\n        q.eq(\"authorId\", user._id).gt(\"createdAt\", Date.now() - 60000)\n      )\n      .collect();\n\n    if (recentMessages.length >= 30) {\n      throw new Error(\"Rate limit exceeded. Max 30 messages per minute.\");\n    }\n\n    // Validate content length\n    if (args.content.length > 2000) {\n      throw new Error(\"Message too long\");\n    }\n\n    return await ctx.db.insert(\"messages\", {\n      channelId: args.channelId,\n      authorId: user._id,\n      content: args.content,\n      createdAt: Date.now(),\n    });\n  },\n});\n",[24,232,230],{"__ignoreMap":64},[234,235,236,243,249,255],"faq-section",{},[237,238,240],"faq-item",{"question":239},"Are Convex queries secure by default?",[14,241,242],{},"No. Convex queries are public and can be called by anyone with your deployment URL. You must add authentication checks in your query handlers to protect sensitive data. Always filter query results based on user permissions.",[237,244,246],{"question":245},"How do I protect internal business logic?",[14,247,248],{},"Use\ninternalMutation\nand\ninternalQuery\nfor functions that should never be called from clients. These can only be called from other Convex functions, scheduled jobs, or HTTP actions.",[237,250,252],{"question":251},"Can users see my function code?",[14,253,254],{},"No, your server-side function code is not exposed to clients. However, clients can see the function names and argument schemas, so don't put secrets in function names or assume arguments are hidden.",[237,256,258],{"question":257},"How do I handle file uploads securely?",[14,259,260],{},"Use Convex's storage API with proper authentication. Generate upload URLs only for authenticated users, validate file types and sizes, and associate uploads with the authenticated user for access control.",[33,262,264],{"id":263},"what-checkyourvibe-detects","What CheckYourVibe Detects",[14,266,267],{},"When scanning your Convex project, CheckYourVibe identifies:",[173,269,270,273,276,279,282,285],{},[176,271,272],{},"Queries without authentication checks exposing data",[176,274,275],{},"Mutations without authorization verification",[176,277,278],{},"Missing argument validation or length limits",[176,280,281],{},"Sensitive operations not using internal functions",[176,283,284],{},"Environment variables accessed incorrectly",[176,286,287],{},"Missing rate limiting on abuse-prone endpoints",[14,289,290,291,294],{},"Run ",[24,292,293],{},"npx checkyourvibe scan"," to catch these issues before they reach production.",[296,297,298,304,309],"related-articles",{},[299,300],"related-card",{"description":301,"href":302,"title":303},"Security guide for Bolt.new apps. Learn how to secure your Bolt-generated app, especially Supabase database connections,","/blog/guides/bolt-new-security-guide","Bolt.new Security Best Practices",[299,305],{"description":306,"href":307,"title":308},"Complete security guide for Bolt.new. Learn to secure AI-generated full-stack applications, protect database credentials","/blog/guides/bolt","Bolt.new Security Guide: Protecting Full-Stack AI Apps",[299,310],{"description":311,"href":312,"title":313},"Security guide for Bubble.io users. Learn about privacy rules, API security, and protecting your no-code application fro","/blog/guides/bubble","Bubble Security Guide: No-Code App Protection",{"title":64,"searchDepth":315,"depth":315,"links":316},2,[317,318,323,327,331,334,337,338,339],{"id":35,"depth":315,"text":36},{"id":45,"depth":315,"text":46,"children":319},[320,322],{"id":53,"depth":321,"text":54},3,{"id":70,"depth":321,"text":71},{"id":80,"depth":315,"text":81,"children":324},[325,326],{"id":87,"depth":321,"text":88},{"id":97,"depth":321,"text":98},{"id":107,"depth":315,"text":108,"children":328},[329,330],{"id":114,"depth":321,"text":115},{"id":124,"depth":321,"text":125},{"id":134,"depth":315,"text":135,"children":332},[333],{"id":141,"depth":321,"text":142},{"id":157,"depth":315,"text":158,"children":335},[336],{"id":170,"depth":321,"text":171},{"id":212,"depth":315,"text":213},{"id":222,"depth":315,"text":223},{"id":263,"depth":315,"text":264},"guides","2026-01-19","Secure your Convex backend when vibe coding. Learn argument validation, authentication patterns, authorization rules, and best practices for the reactive database platform.",false,"md",[346,348,350],{"question":239,"answer":347},"No. Convex queries are public and can be called by anyone. You must add authentication checks in your query handlers.",{"question":245,"answer":349},"Use internalMutation and internalQuery for functions that should never be called from clients.",{"question":251,"answer":351},"No, server-side function code is not exposed. However, function names and argument schemas are visible to clients.","blue",null,"Convex security, vibe coding backend, Convex authentication, Convex authorization, serverless database security",{},true,"Secure your Convex backend with proper argument validation, authentication, and authorization patterns.","/blog/guides/convex","[object Object]","TechArticle",{"title":5,"description":342},{"loc":358},"blog/guides/convex",[],"summary_large_image","a4Tvf7bCA5PomY_7ptRffDbX49kMXdeRHJgR1yHOLt0",1775843930228]