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.
TL;DR
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 internalMutation/internalQuery, add authentication checks to all public functions, and verify queries are scoped to the authenticated user.
Convex Security Model
Convex's function-based approach requires careful visibility control:
| Function Type | Client Callable | Use Case |
|---|---|---|
| query | Yes | Public data reads |
| mutation | Yes | Public data writes |
| internalQuery | No | Server-only reads |
| internalMutation | No | Server-only writes, admin ops |
Part 1: Check Convex Function Visibility
Audit Bolt-Generated Functions
Search your exported convex folder for public functions that should be internal:
// convex/admin.ts - Bolt might generate this
import { mutation } from "./_generated/server";
// Anyone can call this from the browser!
export const deleteAllUsers = mutation({
handler: async (ctx) => {
const users = await ctx.db.query("users").collect();
for (const user of users) {
await ctx.db.delete(user._id);
}
},
});
// convex/admin.ts - Corrected
import { internalMutation } from "./_generated/server";
// Only callable from other Convex functions
export const deleteAllUsers = internalMutation({
handler: async (ctx) => {
const users = await ctx.db.query("users").collect();
for (const user of users) {
await ctx.db.delete(user._id);
}
},
});
Review all exports: Every function exported from convex/*.ts files using mutation or query is callable from any client. Check each one.
Part 2: Add Convex Authentication Checks
Missing Auth Pattern
Bolt often generates mutations without auth verification:
// convex/posts.ts
export const createPost = mutation({
args: { title: v.string(), content: v.string(), userId: v.string() },
handler: async (ctx, args) => {
// Trusts client-provided userId!
await ctx.db.insert("posts", {
title: args.title,
content: args.content,
userId: args.userId, // Anyone can set any userId
});
},
});
// convex/posts.ts
export const createPost = mutation({
args: { title: v.string(), content: v.string() },
handler: async (ctx, args) => {
const identity = await ctx.auth.getUserIdentity();
if (!identity) {
throw new Error("Not authenticated");
}
await ctx.db.insert("posts", {
title: args.title,
content: args.content,
userId: identity.subject, // From verified auth, not client
});
},
});
Auth Helper Pattern
import { QueryCtx, MutationCtx } from "./_generated/server";
export async function requireAuth(ctx: QueryCtx | MutationCtx) {
const identity = await ctx.auth.getUserIdentity();
if (!identity) {
throw new Error("Not authenticated");
}
return identity;
}
export async function getUser(ctx: QueryCtx | MutationCtx) {
const identity = await requireAuth(ctx);
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");
}
return user;
}
Part 3: Scope Queries to User
import { query, mutation } from "./_generated/server";
import { v } from "convex/values";
import { getUser } from "./lib/auth";
// Only return user's own posts
export const getMyPosts = query({
handler: async (ctx) => {
const user = await getUser(ctx);
return await ctx.db
.query("posts")
.withIndex("by_user", (q) => q.eq("userId", user._id))
.collect();
},
});
// Update with ownership verification
export const updatePost = mutation({
args: { postId: v.id("posts"), title: v.string() },
handler: async (ctx, args) => {
const user = await getUser(ctx);
const post = await ctx.db.get(args.postId);
if (!post) {
throw new Error("Post not found");
}
if (post.userId !== user._id) {
throw new Error("Not authorized");
}
await ctx.db.patch(args.postId, { title: args.title });
},
});
Part 4: Schema Definition
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(),
}).index("by_token", ["tokenIdentifier"]),
posts: defineTable({
title: v.string(),
content: v.string(),
userId: v.id("users"),
isPublic: v.boolean(),
}).index("by_user", ["userId"]),
});
Security Checklist
Post-Export Checklist for Bolt + Convex
Admin functions use internalMutation/internalQuery
All public mutations verify authentication
User ID from auth, not from client args
Queries scoped to authenticated user
Update/delete operations verify ownership
Schema defined with proper types
Indexes created for query patterns
Environment variables in Convex dashboard
Alternative Stacks to Consider
**Bolt.new + Supabase**
RLS-based security with PostgreSQL
**Bolt.new + Firebase**
Rule-based security with Firestore
**Bolt.new + MongoDB**
Document database alternative
Are Convex functions secure by default?
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.
When should I use internalMutation?
Use internal functions for: admin operations, scheduled jobs, functions called only by other functions, and anything that shouldn't be directly triggered by clients.
How do I add admin functionality?
Create internal functions for admin operations, then expose them through HTTP actions that verify admin credentials, or trigger them from scheduled functions.