Bolt.new + Convex Security Blueprint

Share

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.

Setup Time1-2 hours

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 TypeClient CallableUse Case
queryYesPublic data reads
mutationYesPublic data writes
internalQueryNoServer-only reads
internalMutationNoServer-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:

DANGEROUS: Admin function publicly exposed
// 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);
    }
  },
});
SECURE: Internal function
// 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:

DANGEROUS: No auth check
// 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
    });
  },
});
SECURE: Auth verified
// 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

convex/lib/auth.ts
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

convex/posts.ts - Properly scoped queries
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

convex/schema.ts
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.

Exported a Bolt + Convex app?

Scan for exposed functions and missing auth checks.

Start Free Scan
Security Blueprints

Bolt.new + Convex Security Blueprint