[{"data":1,"prerenderedAt":364},["ShallowReactive",2],{"blog-blueprints/bolt-convex":3},{"id":4,"title":5,"body":6,"category":344,"date":345,"dateModified":345,"description":346,"draft":347,"extension":348,"faq":349,"featured":347,"headerVariant":350,"image":349,"keywords":349,"meta":351,"navigation":352,"ogDescription":353,"ogTitle":349,"path":354,"readTime":355,"schemaOrg":356,"schemaType":357,"seo":358,"sitemap":359,"stem":360,"tags":361,"twitterCard":362,"__hash__":363},"blog/blog/blueprints/bolt-convex.md","Bolt.new + Convex Security Blueprint",{"type":7,"value":8,"toc":326},"minimark",[9,20,24,39,44,47,110,114,119,122,136,145,160,164,168,171,180,189,193,202,206,215,219,228,232,237,240,243,246,249,252,255,258,261,274,296,315],[10,11,12],"blueprint-summary",{},[13,14,15,19],"p",{},[16,17,18],"strong",{},"To secure a Bolt.new + Convex stack,"," you need to: (1) mark administrative functions with internalMutation/internalQuery so they cannot be called from clients, (2) add authentication verification to all public functions using ctx.auth.getUserIdentity(), (3) ensure user IDs come from verified auth tokens rather than client arguments, and (4) scope all queries to the authenticated user. This blueprint covers function-level security unique to Convex.",[21,22],"blueprint-meta",{"time":23},"1-2 hours",[25,26,27],"tldr",{},[13,28,29,30,34,35,38],{},"Convex functions are your API-every exported function is callable from clients. Bolt-generated Convex code often exposes admin functions publicly and skips auth checks. After export: mark internal functions with ",[31,32,33],"code",{},"internalMutation","/",[31,36,37],{},"internalQuery",", add authentication checks to all public functions, and verify queries are scoped to the authenticated user.",[40,41,43],"h2",{"id":42},"convex-security-model","Convex Security Model",[13,45,46],{},"Convex's function-based approach requires careful visibility control:",[48,49,50,66],"table",{},[51,52,53],"thead",{},[54,55,56,60,63],"tr",{},[57,58,59],"th",{},"Function Type",[57,61,62],{},"Client Callable",[57,64,65],{},"Use Case",[67,68,69,81,91,101],"tbody",{},[54,70,71,75,78],{},[72,73,74],"td",{},"query",[72,76,77],{},"Yes",[72,79,80],{},"Public data reads",[54,82,83,86,88],{},[72,84,85],{},"mutation",[72,87,77],{},[72,89,90],{},"Public data writes",[54,92,93,95,98],{},[72,94,37],{},[72,96,97],{},"No",[72,99,100],{},"Server-only reads",[54,102,103,105,107],{},[72,104,33],{},[72,106,97],{},[72,108,109],{},"Server-only writes, admin ops",[40,111,113],{"id":112},"part-1-check-convex-function-visibility","Part 1: Check Convex Function Visibility",[115,116,118],"h3",{"id":117},"audit-bolt-generated-functions","Audit Bolt-Generated Functions",[13,120,121],{},"Search your exported convex folder for public functions that should be internal:",[123,124,126],"code-block",{"label":125},"DANGEROUS: Admin function publicly exposed",[127,128,133],"pre",{"className":129,"code":131,"language":132},[130],"language-text","// convex/admin.ts - Bolt might generate this\nimport { mutation } from \"./_generated/server\";\n\n// Anyone can call this from the browser!\nexport const deleteAllUsers = mutation({\n  handler: async (ctx) => {\n    const users = await ctx.db.query(\"users\").collect();\n    for (const user of users) {\n      await ctx.db.delete(user._id);\n    }\n  },\n});\n","text",[31,134,131],{"__ignoreMap":135},"",[123,137,139],{"label":138},"SECURE: Internal function",[127,140,143],{"className":141,"code":142,"language":132},[130],"// convex/admin.ts - Corrected\nimport { internalMutation } from \"./_generated/server\";\n\n// Only callable from other Convex functions\nexport const deleteAllUsers = internalMutation({\n  handler: async (ctx) => {\n    const users = await ctx.db.query(\"users\").collect();\n    for (const user of users) {\n      await ctx.db.delete(user._id);\n    }\n  },\n});\n",[31,144,142],{"__ignoreMap":135},[146,147,148],"warning-box",{},[13,149,150,153,154,156,157,159],{},[16,151,152],{},"Review all exports:"," Every function exported from convex/*.ts files using ",[31,155,85],{}," or ",[31,158,74],{}," is callable from any client. Check each one.",[40,161,163],{"id":162},"part-2-add-convex-authentication-checks","Part 2: Add Convex Authentication Checks",[115,165,167],{"id":166},"missing-auth-pattern","Missing Auth Pattern",[13,169,170],{},"Bolt often generates mutations without auth verification:",[123,172,174],{"label":173},"DANGEROUS: No auth check",[127,175,178],{"className":176,"code":177,"language":132},[130],"// convex/posts.ts\nexport const createPost = mutation({\n  args: { title: v.string(), content: v.string(), userId: v.string() },\n  handler: async (ctx, args) => {\n    // Trusts client-provided userId!\n    await ctx.db.insert(\"posts\", {\n      title: args.title,\n      content: args.content,\n      userId: args.userId,  // Anyone can set any userId\n    });\n  },\n});\n",[31,179,177],{"__ignoreMap":135},[123,181,183],{"label":182},"SECURE: Auth verified",[127,184,187],{"className":185,"code":186,"language":132},[130],"// convex/posts.ts\nexport const createPost = mutation({\n  args: { title: v.string(), content: 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    await ctx.db.insert(\"posts\", {\n      title: args.title,\n      content: args.content,\n      userId: identity.subject,  // From verified auth, not client\n    });\n  },\n});\n",[31,188,186],{"__ignoreMap":135},[115,190,192],{"id":191},"auth-helper-pattern","Auth Helper Pattern",[123,194,196],{"label":195},"convex/lib/auth.ts",[127,197,200],{"className":198,"code":199,"language":132},[130],"import { QueryCtx, MutationCtx } from \"./_generated/server\";\n\nexport async function requireAuth(ctx: QueryCtx | MutationCtx) {\n  const identity = await ctx.auth.getUserIdentity();\n  if (!identity) {\n    throw new Error(\"Not authenticated\");\n  }\n  return identity;\n}\n\nexport async function getUser(ctx: QueryCtx | MutationCtx) {\n  const identity = await requireAuth(ctx);\n\n  const user = await ctx.db\n    .query(\"users\")\n    .withIndex(\"by_token\", (q) =>\n      q.eq(\"tokenIdentifier\", identity.tokenIdentifier)\n    )\n    .unique();\n\n  if (!user) {\n    throw new Error(\"User not found\");\n  }\n\n  return user;\n}\n",[31,201,199],{"__ignoreMap":135},[40,203,205],{"id":204},"part-3-scope-queries-to-user","Part 3: Scope Queries to User",[123,207,209],{"label":208},"convex/posts.ts - Properly scoped queries",[127,210,213],{"className":211,"code":212,"language":132},[130],"import { query, mutation } from \"./_generated/server\";\nimport { v } from \"convex/values\";\nimport { getUser } from \"./lib/auth\";\n\n// Only return user's own posts\nexport const getMyPosts = query({\n  handler: async (ctx) => {\n    const user = await getUser(ctx);\n\n    return await ctx.db\n      .query(\"posts\")\n      .withIndex(\"by_user\", (q) => q.eq(\"userId\", user._id))\n      .collect();\n  },\n});\n\n// Update with ownership verification\nexport const updatePost = mutation({\n  args: { postId: v.id(\"posts\"), title: v.string() },\n  handler: async (ctx, args) => {\n    const user = await getUser(ctx);\n    const post = await ctx.db.get(args.postId);\n\n    if (!post) {\n      throw new Error(\"Post not found\");\n    }\n\n    if (post.userId !== user._id) {\n      throw new Error(\"Not authorized\");\n    }\n\n    await ctx.db.patch(args.postId, { title: args.title });\n  },\n});\n",[31,214,212],{"__ignoreMap":135},[40,216,218],{"id":217},"part-4-schema-definition","Part 4: Schema Definition",[123,220,222],{"label":221},"convex/schema.ts",[127,223,226],{"className":224,"code":225,"language":132},[130],"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  }).index(\"by_token\", [\"tokenIdentifier\"]),\n\n  posts: defineTable({\n    title: v.string(),\n    content: v.string(),\n    userId: v.id(\"users\"),\n    isPublic: v.boolean(),\n  }).index(\"by_user\", [\"userId\"]),\n});\n",[31,227,225],{"__ignoreMap":135},[40,229,231],{"id":230},"security-checklist","Security Checklist",[233,234,236],"h4",{"id":235},"post-export-checklist-for-bolt-convex","Post-Export Checklist for Bolt + Convex",[13,238,239],{},"Admin functions use internalMutation/internalQuery",[13,241,242],{},"All public mutations verify authentication",[13,244,245],{},"User ID from auth, not from client args",[13,247,248],{},"Queries scoped to authenticated user",[13,250,251],{},"Update/delete operations verify ownership",[13,253,254],{},"Schema defined with proper types",[13,256,257],{},"Indexes created for query patterns",[13,259,260],{},"Environment variables in Convex dashboard",[262,263,264,268],"stack-comparison",{},[115,265,267],{"id":266},"alternative-stacks-to-consider","Alternative Stacks to Consider",[127,269,272],{"className":270,"code":271,"language":132},[130],"      **Bolt.new + Supabase**\n      RLS-based security with PostgreSQL\n\n\n      **Bolt.new + Firebase**\n      Rule-based security with Firestore\n\n\n      **Bolt.new + MongoDB**\n      Document database alternative\n",[31,273,271],{"__ignoreMap":135},[275,276,277,284,290],"faq-section",{},[278,279,281],"faq-item",{"question":280},"Are Convex functions secure by default?",[13,282,283],{},"Convex functions run on Convex's servers, but every exported query/mutation is callable from any client. Security comes from your authentication checks and function visibility, not from the platform itself.",[278,285,287],{"question":286},"When should I use internalMutation?",[13,288,289],{},"Use internal functions for: admin operations, scheduled jobs, functions called only by other functions, and anything that shouldn't be directly triggered by clients.",[278,291,293],{"question":292},"How do I add admin functionality?",[13,294,295],{},"Create internal functions for admin operations, then expose them through HTTP actions that verify admin credentials, or trigger them from scheduled functions.",[297,298,299,305,310],"related-articles",{},[300,301],"related-card",{"description":302,"href":303,"title":304},"Similar stack with Cursor","/blog/blueprints/cursor-convex","Cursor + Convex",[300,306],{"description":307,"href":308,"title":309},"Deep dive into Convex","/blog/guides/convex","Convex Security Guide",[300,311],{"description":312,"href":313,"title":314},"RLS-based alternative","/blog/blueprints/bolt-supabase","Bolt + Supabase",[316,317,319,323],"cta-box",{"href":34,"label":318},"Start Free Scan",[40,320,322],{"id":321},"exported-a-bolt-convex-app","Exported a Bolt + Convex app?",[13,324,325],{},"Scan for exposed functions and missing auth checks.",{"title":135,"searchDepth":327,"depth":327,"links":328},2,[329,330,334,338,339,340,343],{"id":42,"depth":327,"text":43},{"id":112,"depth":327,"text":113,"children":331},[332],{"id":117,"depth":333,"text":118},3,{"id":162,"depth":327,"text":163,"children":335},[336,337],{"id":166,"depth":333,"text":167},{"id":191,"depth":333,"text":192},{"id":204,"depth":327,"text":205},{"id":217,"depth":327,"text":218},{"id":230,"depth":327,"text":231,"children":341},[342],{"id":266,"depth":333,"text":267},{"id":321,"depth":327,"text":322},"blueprints","2026-01-26","Security guide for Bolt.new + Convex stack. Configure function visibility, implement authentication, protect data access, and secure your Bolt-generated Convex app.",false,"md",null,"purple",{},true,"Complete security configuration for Convex apps built with Bolt.new.","/blog/blueprints/bolt-convex","10 min read","[object Object]","Article",{"title":5,"description":346},{"loc":354},"blog/blueprints/bolt-convex",[],"summary_large_image","ULhBgB2Dona_n2k0HTJ1ksVfKCCsC3fhkLhkKMtL2q4",1775843932958]