[{"data":1,"prerenderedAt":980},["ShallowReactive",2],{"blog-is-safe/convex-bk":3},{"id":4,"title":5,"body":6,"category":951,"date":952,"dateModified":952,"description":953,"draft":954,"extension":955,"faq":956,"featured":954,"headerVariant":965,"image":966,"keywords":967,"meta":968,"navigation":652,"ogDescription":969,"ogTitle":966,"path":970,"readTime":971,"schemaOrg":972,"schemaType":973,"seo":974,"sitemap":975,"stem":976,"tags":977,"twitterCard":978,"__hash__":979},"blog/blog/is-safe/convex-bk.md","Is the Convex Backend Safe? Security Review for AI App Builders (2026)",{"type":7,"value":8,"toc":940},"minimark",[9,13,16,22,27,83,87,90,208,211,214,395,401,415,422,434,539,545,549,552,563,566,779,783,793,799,803,806,809,813,816,854,857,905,924,936],[10,11,12],"p",{},"CheckYourVibe scans show roughly 40% of AI-built Convex apps have at least one public query that returns data to any caller without an authentication check. The function runs server-side, which sounds safe. But server-side only means the logic runs on Convex's infrastructure; it doesn't mean access is restricted. Any browser tab, any unauthenticated API client, any competitor's scraper can call that query and get whatever it returns.",[10,14,15],{},"The Convex backend is well-designed. The risks are in how AI tools generate Convex code, not in Convex itself.",[17,18,19],"tldr",{},[10,20,21],{},"Convex runs your queries and mutations server-side, which removes the Firebase security-rules footgun. But every exported query is publicly callable by default. You write authentication checks in TypeScript. Convex doesn't enforce them. AI tools frequently omit the getUserIdentity() call, leaving whole tables readable without a login. Add an auth check as the first line of every handler that touches non-public data.",[23,24,26],"h2",{"id":25},"our-verdict","Our Verdict",[28,29,30,35,62,66],"pros-cons",{},[31,32,34],"h4",{"id":33},"whats-good","What's Good",[36,37,38,42,45,56,59],"ul",{},[39,40,41],"li",{},"Server-side execution: no client-side security rules to misconfigure",[39,43,44],{},"TypeScript type safety catches bad argument shapes at compile time",[39,46,47,51,52,55],{},[48,49,50],"code",{},"internalMutation"," and ",[48,53,54],{},"internalQuery"," make backend-only logic truly unreachable from browsers",[39,57,58],{},"ACID transactions with automatic conflict resolution",[39,60,61],{},"Auth tokens come from verified providers (Clerk, Auth0), not forged client arguments",[31,63,65],{"id":64},"what-to-watch","What to Watch",[36,67,68,71,74,77,80],{},[39,69,70],{},"Every exported query is public: accessible without auth by default",[39,72,73],{},"No row-level security: ownership checks must be written inside each handler",[39,75,76],{},"HTTP actions handle webhooks without automatic signature verification",[39,78,79],{},"Scheduled functions run with no auth context",[39,81,82],{},"AI-generated Convex code frequently skips the getUserIdentity() call entirely",[23,84,86],{"id":85},"the-core-risk-public-queries-by-default","The Core Risk: Public Queries by Default",[10,88,89],{},"This is what an unsafe query from a Cursor or Bolt-generated Convex app looks like:",[91,92,94],"code-block",{"label":93},"convex/users.ts (unsafe)",[95,96,101],"pre",{"className":97,"code":98,"language":99,"meta":100,"style":100},"language-typescript shiki shiki-themes github-light github-dark","// Any visitor can call this and get every user in your database\nexport const getAllUsers = query({\n  handler: async (ctx) => {\n    return await ctx.db.query(\"users\").collect();\n  },\n});\n","typescript","",[48,102,103,112,137,165,196,202],{"__ignoreMap":100},[104,105,108],"span",{"class":106,"line":107},"line",1,[104,109,111],{"class":110},"sJ8bj","// Any visitor can call this and get every user in your database\n",[104,113,115,119,122,126,129,133],{"class":106,"line":114},2,[104,116,118],{"class":117},"szBVR","export",[104,120,121],{"class":117}," const",[104,123,125],{"class":124},"sj4cs"," getAllUsers",[104,127,128],{"class":117}," =",[104,130,132],{"class":131},"sScJk"," query",[104,134,136],{"class":135},"sVt8B","({\n",[104,138,140,143,146,149,152,156,159,162],{"class":106,"line":139},3,[104,141,142],{"class":131},"  handler",[104,144,145],{"class":135},": ",[104,147,148],{"class":117},"async",[104,150,151],{"class":135}," (",[104,153,155],{"class":154},"s4XuR","ctx",[104,157,158],{"class":135},") ",[104,160,161],{"class":117},"=>",[104,163,164],{"class":135}," {\n",[104,166,168,171,174,177,180,183,187,190,193],{"class":106,"line":167},4,[104,169,170],{"class":117},"    return",[104,172,173],{"class":117}," await",[104,175,176],{"class":135}," ctx.db.",[104,178,179],{"class":131},"query",[104,181,182],{"class":135},"(",[104,184,186],{"class":185},"sZZnC","\"users\"",[104,188,189],{"class":135},").",[104,191,192],{"class":131},"collect",[104,194,195],{"class":135},"();\n",[104,197,199],{"class":106,"line":198},5,[104,200,201],{"class":135},"  },\n",[104,203,205],{"class":106,"line":204},6,[104,206,207],{"class":135},"});\n",[10,209,210],{},"No login required. No token check. Every row in your users table, available to whoever calls it. This is the most common finding in our Convex scans.",[10,212,213],{},"The fix is one check at the top of the handler:",[91,215,217],{"label":216},"convex/users.ts (secure)",[95,218,220],{"className":97,"code":219,"language":99,"meta":100,"style":100},"export const getMyProfile = query({\n  handler: async (ctx) => {\n    const identity = await ctx.auth.getUserIdentity();\n    if (!identity) {\n      throw new Error(\"Unauthenticated\");\n    }\n    return await ctx.db\n      .query(\"users\")\n      .filter((q) => q.eq(q.field(\"userId\"), identity.subject))\n      .first();\n  },\n});\n",[48,221,222,237,255,275,288,307,312,322,337,375,385,390],{"__ignoreMap":100},[104,223,224,226,228,231,233,235],{"class":106,"line":107},[104,225,118],{"class":117},[104,227,121],{"class":117},[104,229,230],{"class":124}," getMyProfile",[104,232,128],{"class":117},[104,234,132],{"class":131},[104,236,136],{"class":135},[104,238,239,241,243,245,247,249,251,253],{"class":106,"line":114},[104,240,142],{"class":131},[104,242,145],{"class":135},[104,244,148],{"class":117},[104,246,151],{"class":135},[104,248,155],{"class":154},[104,250,158],{"class":135},[104,252,161],{"class":117},[104,254,164],{"class":135},[104,256,257,260,263,265,267,270,273],{"class":106,"line":139},[104,258,259],{"class":117},"    const",[104,261,262],{"class":124}," identity",[104,264,128],{"class":117},[104,266,173],{"class":117},[104,268,269],{"class":135}," ctx.auth.",[104,271,272],{"class":131},"getUserIdentity",[104,274,195],{"class":135},[104,276,277,280,282,285],{"class":106,"line":167},[104,278,279],{"class":117},"    if",[104,281,151],{"class":135},[104,283,284],{"class":117},"!",[104,286,287],{"class":135},"identity) {\n",[104,289,290,293,296,299,301,304],{"class":106,"line":198},[104,291,292],{"class":117},"      throw",[104,294,295],{"class":117}," new",[104,297,298],{"class":131}," Error",[104,300,182],{"class":135},[104,302,303],{"class":185},"\"Unauthenticated\"",[104,305,306],{"class":135},");\n",[104,308,309],{"class":106,"line":204},[104,310,311],{"class":135},"    }\n",[104,313,315,317,319],{"class":106,"line":314},7,[104,316,170],{"class":117},[104,318,173],{"class":117},[104,320,321],{"class":135}," ctx.db\n",[104,323,325,328,330,332,334],{"class":106,"line":324},8,[104,326,327],{"class":135},"      .",[104,329,179],{"class":131},[104,331,182],{"class":135},[104,333,186],{"class":185},[104,335,336],{"class":135},")\n",[104,338,340,342,345,348,351,353,355,358,361,364,367,369,372],{"class":106,"line":339},9,[104,341,327],{"class":135},[104,343,344],{"class":131},"filter",[104,346,347],{"class":135},"((",[104,349,350],{"class":154},"q",[104,352,158],{"class":135},[104,354,161],{"class":117},[104,356,357],{"class":135}," q.",[104,359,360],{"class":131},"eq",[104,362,363],{"class":135},"(q.",[104,365,366],{"class":131},"field",[104,368,182],{"class":135},[104,370,371],{"class":185},"\"userId\"",[104,373,374],{"class":135},"), identity.subject))\n",[104,376,378,380,383],{"class":106,"line":377},10,[104,379,327],{"class":135},[104,381,382],{"class":131},"first",[104,384,195],{"class":135},[104,386,388],{"class":106,"line":387},11,[104,389,201],{"class":135},[104,391,393],{"class":106,"line":392},12,[104,394,207],{"class":135},[10,396,397,400],{},[48,398,399],{},"identity.subject"," is the user ID from the auth provider's verified JWT. The client can't fake it. Because the filter is applied server-side, the query only ever returns data belonging to the caller.",[402,403,404],"warning-box",{},[10,405,406,407,410,411,414],{},"Never accept a userId as a function argument and trust it. If your function signature includes ",[48,408,409],{},"args: { userId: v.id(\"users\") }",", any caller can pass any user's ID. Always read the userId from ",[48,412,413],{},"ctx.auth.getUserIdentity().subject",". It comes from a signed token the client can't modify.",[23,416,418,421],{"id":417},"internal-functions-the-real-security-boundary",[48,419,420],{},"internal"," Functions: The Real Security Boundary",[10,423,424,425,427,428,430,431,433],{},"Convex's strongest security primitive is the ",[48,426,420],{}," prefix. Functions declared with ",[48,429,54],{}," or ",[48,432,50],{}," can't be called from any client, only from other server-side Convex functions:",[91,435,437],{"label":436},"convex/billing.ts",[95,438,440],{"className":97,"code":439,"language":99,"meta":100,"style":100},"// No browser can call this directly. Only server-side mutations can invoke it.\nexport const processCharge = internalMutation({\n  args: { userId: v.id(\"users\"), amountCents: v.number() },\n  handler: async (ctx, args) => {\n    // Stripe charge logic here\n    await ctx.db.insert(\"charges\", { userId: args.userId, amount: args.amountCents });\n  },\n});\n",[48,441,442,447,463,484,508,513,531,535],{"__ignoreMap":100},[104,443,444],{"class":106,"line":107},[104,445,446],{"class":110},"// No browser can call this directly. Only server-side mutations can invoke it.\n",[104,448,449,451,453,456,458,461],{"class":106,"line":114},[104,450,118],{"class":117},[104,452,121],{"class":117},[104,454,455],{"class":124}," processCharge",[104,457,128],{"class":117},[104,459,460],{"class":131}," internalMutation",[104,462,136],{"class":135},[104,464,465,468,471,473,475,478,481],{"class":106,"line":139},[104,466,467],{"class":135},"  args: { userId: v.",[104,469,470],{"class":131},"id",[104,472,182],{"class":135},[104,474,186],{"class":185},[104,476,477],{"class":135},"), amountCents: v.",[104,479,480],{"class":131},"number",[104,482,483],{"class":135},"() },\n",[104,485,486,488,490,492,494,496,499,502,504,506],{"class":106,"line":167},[104,487,142],{"class":131},[104,489,145],{"class":135},[104,491,148],{"class":117},[104,493,151],{"class":135},[104,495,155],{"class":154},[104,497,498],{"class":135},", ",[104,500,501],{"class":154},"args",[104,503,158],{"class":135},[104,505,161],{"class":117},[104,507,164],{"class":135},[104,509,510],{"class":106,"line":198},[104,511,512],{"class":110},"    // Stripe charge logic here\n",[104,514,515,518,520,523,525,528],{"class":106,"line":204},[104,516,517],{"class":117},"    await",[104,519,176],{"class":135},[104,521,522],{"class":131},"insert",[104,524,182],{"class":135},[104,526,527],{"class":185},"\"charges\"",[104,529,530],{"class":135},", { userId: args.userId, amount: args.amountCents });\n",[104,532,533],{"class":106,"line":314},[104,534,201],{"class":135},[104,536,537],{"class":106,"line":324},[104,538,207],{"class":135},[10,540,541,542,544],{},"Any sensitive operation (payments, role changes, data deletion, admin actions) should be an ",[48,543,420],{}," function. AI tools almost never use this pattern without an explicit prompt telling them to.",[23,546,548],{"id":547},"http-actions-and-webhook-security","HTTP Actions and Webhook Security",[10,550,551],{},"Convex HTTP actions are public HTTPS endpoints at URLs like:",[91,553,555],{"label":554},"example webhook URL",[95,556,561],{"className":557,"code":559,"language":560},[558],"language-text","https://yourproject.convex.cloud/api/webhooks/stripe\n","text",[48,562,559],{"__ignoreMap":100},[10,564,565],{},"Convex doesn't verify Stripe signatures, GitHub HMAC tokens, or any other webhook-specific authentication. You write that logic yourself. Without it, anyone can POST to your webhook URL and trigger whatever the handler does.",[91,567,569],{"label":568},"convex/http.ts (stripe webhook with verification)",[95,570,572],{"className":97,"code":571,"language":99,"meta":100,"style":100},"export const stripeWebhook = httpAction(async (ctx, request) => {\n  const signature = request.headers.get(\"stripe-signature\");\n  const body = await request.text();\n\n  // Throws ConvexError if signature is invalid\n  const event = stripe.webhooks.constructEvent(\n    body,\n    signature!,\n    process.env.STRIPE_WEBHOOK_SECRET!\n  );\n\n  await ctx.runMutation(internal.billing.processStripeEvent, {\n    type: event.type,\n    data: event.data.object,\n  });\n\n  return new Response(null, { status: 200 });\n});\n",[48,573,574,607,630,648,654,659,677,682,692,703,708,712,726,732,738,744,749,774],{"__ignoreMap":100},[104,575,576,578,580,583,585,588,590,592,594,596,598,601,603,605],{"class":106,"line":107},[104,577,118],{"class":117},[104,579,121],{"class":117},[104,581,582],{"class":124}," stripeWebhook",[104,584,128],{"class":117},[104,586,587],{"class":131}," httpAction",[104,589,182],{"class":135},[104,591,148],{"class":117},[104,593,151],{"class":135},[104,595,155],{"class":154},[104,597,498],{"class":135},[104,599,600],{"class":154},"request",[104,602,158],{"class":135},[104,604,161],{"class":117},[104,606,164],{"class":135},[104,608,609,612,615,617,620,623,625,628],{"class":106,"line":114},[104,610,611],{"class":117},"  const",[104,613,614],{"class":124}," signature",[104,616,128],{"class":117},[104,618,619],{"class":135}," request.headers.",[104,621,622],{"class":131},"get",[104,624,182],{"class":135},[104,626,627],{"class":185},"\"stripe-signature\"",[104,629,306],{"class":135},[104,631,632,634,637,639,641,644,646],{"class":106,"line":139},[104,633,611],{"class":117},[104,635,636],{"class":124}," body",[104,638,128],{"class":117},[104,640,173],{"class":117},[104,642,643],{"class":135}," request.",[104,645,560],{"class":131},[104,647,195],{"class":135},[104,649,650],{"class":106,"line":167},[104,651,653],{"emptyLinePlaceholder":652},true,"\n",[104,655,656],{"class":106,"line":198},[104,657,658],{"class":110},"  // Throws ConvexError if signature is invalid\n",[104,660,661,663,666,668,671,674],{"class":106,"line":204},[104,662,611],{"class":117},[104,664,665],{"class":124}," event",[104,667,128],{"class":117},[104,669,670],{"class":135}," stripe.webhooks.",[104,672,673],{"class":131},"constructEvent",[104,675,676],{"class":135},"(\n",[104,678,679],{"class":106,"line":314},[104,680,681],{"class":135},"    body,\n",[104,683,684,687,689],{"class":106,"line":324},[104,685,686],{"class":135},"    signature",[104,688,284],{"class":117},[104,690,691],{"class":135},",\n",[104,693,694,697,700],{"class":106,"line":339},[104,695,696],{"class":135},"    process.env.",[104,698,699],{"class":124},"STRIPE_WEBHOOK_SECRET",[104,701,702],{"class":117},"!\n",[104,704,705],{"class":106,"line":377},[104,706,707],{"class":135},"  );\n",[104,709,710],{"class":106,"line":387},[104,711,653],{"emptyLinePlaceholder":652},[104,713,714,717,720,723],{"class":106,"line":392},[104,715,716],{"class":117},"  await",[104,718,719],{"class":135}," ctx.",[104,721,722],{"class":131},"runMutation",[104,724,725],{"class":135},"(internal.billing.processStripeEvent, {\n",[104,727,729],{"class":106,"line":728},13,[104,730,731],{"class":135},"    type: event.type,\n",[104,733,735],{"class":106,"line":734},14,[104,736,737],{"class":135},"    data: event.data.object,\n",[104,739,741],{"class":106,"line":740},15,[104,742,743],{"class":135},"  });\n",[104,745,747],{"class":106,"line":746},16,[104,748,653],{"emptyLinePlaceholder":652},[104,750,752,755,757,760,762,765,768,771],{"class":106,"line":751},17,[104,753,754],{"class":117},"  return",[104,756,295],{"class":117},[104,758,759],{"class":131}," Response",[104,761,182],{"class":135},[104,763,764],{"class":124},"null",[104,766,767],{"class":135},", { status: ",[104,769,770],{"class":124},"200",[104,772,773],{"class":135}," });\n",[104,775,777],{"class":106,"line":776},18,[104,778,207],{"class":135},[23,780,782],{"id":781},"scheduled-functions","Scheduled Functions",[10,784,785,786,789,790,792],{},"Convex cron jobs and ",[48,787,788],{},"ctx.scheduler.runAfter()"," calls execute with no user auth context. That's expected. They're server processes. The risk appears when a scheduled function calls an exported (public) mutation instead of an ",[48,791,50],{},". If the mutation doesn't check auth, a client can call it directly and bypass whatever timing or rate logic the scheduler was protecting.",[10,794,795,796,798],{},"Use ",[48,797,50],{}," for anything the scheduler runs that shouldn't be directly callable from a browser.",[23,800,802],{"id":801},"file-storage","File Storage",[10,804,805],{},"Convex file storage returns public URLs. There's no auth layer in front of the storage URL itself. If your app stores user-uploaded documents or private photos, anyone with a direct URL can access them.",[10,807,808],{},"For private files: generate a short-lived access token server-side and return that to the authenticated user rather than the raw storage URL. This keeps file contents behind your auth check without exposing the underlying Convex storage endpoint.",[23,810,812],{"id":811},"what-checkyourvibe-finds-in-convex-apps","What CheckYourVibe Finds in Convex Apps",[10,814,815],{},"Our scanner identifies patterns in your client-side code and deployment configuration. In AI-built Convex apps, the most common issues in order of frequency:",[817,818,819,830,839,848],"ol",{},[39,820,821,825,826,829],{},[822,823,824],"strong",{},"Exported queries without auth checks (~40% of scanned Convex apps):"," the query is used in the client via ",[48,827,828],{},"useQuery",", which means the underlying function is callable without any session",[39,831,832,835,836],{},[822,833,834],{},"userId passed as a client argument (~25%):"," function signatures that accept userId from the caller rather than reading it from ",[48,837,838],{},"ctx.auth",[39,840,841,847],{},[822,842,843,844,846],{},"No ",[48,845,420],{}," prefix on admin mutations (~30%):"," admin-level operations callable from any browser console",[39,849,850,853],{},[822,851,852],{},"HTTP actions without signature verification (nearly all webhook handlers):"," webhook URLs accepting arbitrary POST requests",[10,855,856],{},"The Convex backend itself has no known vulnerabilities. The risks are all in application code, specifically code that AI tools write without auth scaffolding.",[858,859,860,871,877,889,895],"faq-section",{},[861,862,864],"faq-item",{"question":863},"Are Convex queries public by default?",[10,865,866,867,870],{},"Yes. Every exported Convex query is callable by any client without authentication unless you explicitly check ",[48,868,869],{},"ctx.auth.getUserIdentity()"," and throw on a null result. There is no equivalent of Supabase RLS. Access control lives in your TypeScript code.",[861,872,874],{"question":873},"Is the Convex backend safer than Firebase?",[10,875,876],{},"In one key way: yes. Convex functions run server-side, so there are no client-side security rules to misconfigure. But you still have to write auth checks yourself. An unprotected Convex query leaks data just as badly as an open Firebase rule.",[861,878,880],{"question":879},"How do I secure a Convex mutation from unauthorized writes?",[10,881,882,883,885,886,888],{},"Call ",[48,884,869],{}," at the top of the mutation handler and throw if the result is null. Then verify the ",[48,887,399],{}," (user ID) matches the record being modified. Never accept a userId as a function argument from the client; always read it from the verified auth token.",[861,890,892],{"question":891},"Does Convex have row-level security like Supabase?",[10,893,894],{},"No. Convex has no built-in row-level access control. You write per-row ownership checks in TypeScript inside each query and mutation. If you forget a check in one function, that function exposes every row in that table.",[861,896,898],{"question":897},"Can Convex HTTP actions be exploited?",[10,899,900,901,904],{},"Yes, if you don't validate the caller. HTTP actions are public endpoints that handle webhooks and external requests. Convex doesn't verify webhook signatures automatically. You must validate them yourself (for example, using ",[48,902,903],{},"stripe.webhooks.constructEvent"," for Stripe) before processing any payload.",[906,907,908,914,919],"related-articles",{},[909,910],"related-card",{"description":911,"href":912,"title":913},"Authentication patterns, argument validation, and internalMutation usage for Convex apps built with AI tools.","/blog/guides/convex","Convex Security Guide for Vibe Coders",[909,915],{"description":916,"href":917,"title":918},"Step-by-step security configuration for Bolt-generated Convex apps, including function visibility and auth setup.","/blog/blueprints/bolt-convex","Bolt.new + Convex Security Blueprint",[909,920],{"description":921,"href":922,"title":923},"General security analysis of the Convex platform covering the reactive backend model, pros, and cons.","/blog/is-safe/convex","Is Convex Safe? Security Overview",[925,926,929,933],"cta-box",{"href":927,"label":928},"/","Start Free Scan",[23,930,932],{"id":931},"using-convex-in-production","Using Convex in Production?",[10,934,935],{},"Scan your app for missing auth checks, public admin mutations, and unsigned webhook handlers.",[937,938,939],"style",{},"html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":100,"searchDepth":114,"depth":114,"links":941},[942,943,944,946,947,948,949,950],{"id":25,"depth":114,"text":26},{"id":85,"depth":114,"text":86},{"id":417,"depth":114,"text":945},"internal Functions: The Real Security Boundary",{"id":547,"depth":114,"text":548},{"id":781,"depth":114,"text":782},{"id":801,"depth":114,"text":802},{"id":811,"depth":114,"text":812},{"id":931,"depth":114,"text":932},"is-safe","2026-06-19","Is the Convex backend safe for production? Honest review covering public query exposure, auth patterns, HTTP actions, and what CheckYourVibe scanners find in AI-built Convex apps.",false,"md",[957,959,960,962,963],{"question":863,"answer":958},"Yes. Every exported Convex query is callable by any client without authentication unless you explicitly check ctx.auth.getUserIdentity() and throw on a null result. There is no equivalent of Supabase RLS. Access control lives in your TypeScript code.",{"question":873,"answer":876},{"question":879,"answer":961},"Call ctx.auth.getUserIdentity() at the top of the mutation handler and throw if the result is null. Then verify the identity.subject (user ID) matches the record being modified. Never accept a userId as a function argument from the client; always read it from the verified auth token.",{"question":891,"answer":894},{"question":897,"answer":964},"Yes, if you don't validate the caller. HTTP actions are public endpoints that handle webhooks and external requests. Convex doesn't verify webhook signatures automatically. You must validate them yourself (for example, using stripe.webhooks.constructEvent for Stripe) before processing any payload.","amber",null,"is convex backend safe, convex security, convex query security, convex authentication, convex backend security, convex production safety",{},"Public Convex queries expose all your data to any caller. Here's what our scanners find in AI-built Convex apps and the exact patterns to fix.","/blog/is-safe/convex-bk","7 min read","[object Object]","Article",{"title":5,"description":953},{"loc":970},"blog/is-safe/convex-bk",[],"summary_large_image","b8VBIOxZO0MfV0AxW1CVvgg7pL9qvVVcsvIJuhothyk",1782240238759]