Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Use custom functions for data protection - this is Convex's alternative to Row Level Security (RLS)
| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-02 | ✗→✓ | ▲ Improved | 62% | 0% |
| case-06 | ✗→✓ | ▲ Improved | 123% | 0% |
| case-08 | ✗→✓ | ▲ Improved | 115% | 0% |
| case-10 | ✗→✓ | ▲ Improved | 124% | 0% |
| case-13 | ✗→✓ | ▲ Improved | 161% | 0% |
Convex's approach to data protection: Instead of Row Level Security (RLS) like PostgreSQL, use custom functions to wrap all queries and mutations with automatic auth and access control.
Traditional databases (PostgreSQL):
Convex approach:
Instead of writing auth checks in every function:
typescriptexport const getTasks = query({ handler: async (ctx) => { // Repeated in every function! const identity = await ctx.auth.getUserIdentity(); if (!identity) throw new Error("Not authenticated"); const user = await getUser(ctx, identity); return await ctx.db.query("tasks") .withIndex("by_user", q => q.eq("userId", user._id)) .collect(); }, }); export const getProjects = query({ handler: async (ctx) => { // Same auth code again! const identity = await ctx.auth.getUserIdentity(); if (!identity) throw new Error("Not authenticated"); const user = await getUser(ctx, identity); return await ctx.db.query("projects") .withIndex("by_user", q => q.eq("userId", user._id)) .collect(); }, });
typescript// convex/lib/authFunctions.ts import { customQuery, customMutation } from "convex-helpers/server/customFunctions"; import { query, mutation } from "../_generated/server"; import { getCurrentUser } from "./auth"; // Authenticated query - user automatically in ctx export const authedQuery = customQuery(query, { args: {}, input: async (ctx, args) => { const user = await getCurrentUser(ctx); return { ctx: { ...ctx, user }, args }; }, }); // Authenticated mutation - user automatically in ctx export const authedMutation = customMutation(mutation, { args: {}, input: async (ctx, args) => { const user = await getCurrentUser(ctx); return { ctx: { ...ctx, user }, args }; }, }); // Now use everywhere: export const getTasks = authedQuery({ handler: async (ctx) => { // ctx.user automatically available and typed! return await ctx.db.query("tasks") .withIndex("by_user", q => q.eq("userId", ctx.user._id)) .collect(); }, }); export const getProjects = authedQuery({ handler: async (ctx) => { // Same pattern, no repeated code return await ctx.db.query("projects") .withIndex("by_user", q => q.eq("userId", ctx.user._id)) .collect(); }, });
typescriptexport const authedQuery = customQuery(query, { args: {}, input: async (ctx, args) => { const identity = await ctx.auth.getUserIdentity(); if (!identity) throw new Error("Not authenticated"); 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 { ctx: { ...ctx, user }, args }; }, });
typescript// Admin-only functions export const adminQuery = customQuery(query, { args: {}, input: async (ctx, args) => { const user = await getCurrentUser(ctx); if (user.role !== "admin") { throw new Error("Admin access required"); } return { ctx: { ...ctx, user }, args }; }, }); // Usage export const getAllUsers = adminQuery({ handler: async (ctx) => { // Only admins can call this return await ctx.db.query("users").collect(); }, });
typescriptexport const tenantQuery = customQuery(query, { args: { organizationId: v.id("organizations") }, input: async (ctx, args) => { const user = await getCurrentUser(ctx); // Verify user belongs to this organization const membership = await ctx.db .query("organizationMembers") .withIndex("by_org_and_user", q => q.eq("organizationId", args.organizationId) .eq("userId", user._id) ) .unique(); if (!membership) { throw new Error("Not a member of this organization"); } return { ctx: { ...ctx, user, organizationId: args.organizationId, role: membership.role }, args }; }, }); // Usage - automatically scoped to organization export const getOrgProjects = tenantQuery({ args: { organizationId: v.id("organizations") }, handler: async (ctx, args) => { // Only returns projects for this org return await ctx.db .query("projects") .withIndex("by_organization", q => q.eq("organizationId", ctx.organizationId) ) .collect(); }, });
typescriptexport const ownerQuery = customQuery(query, { args: { resourceId: v.id("resources") }, input: async (ctx, args) => { const user = await getCurrentUser(ctx); const resource = await ctx.db.get(args.resourceId); if (!resource) throw new Error("Resource not found"); if (resource.ownerId !== user._id) { throw new Error("You don't own this resource"); } return { ctx: { ...ctx, user, resource }, args }; }, }); // Usage export const getResourceDetails = ownerQuery({ args: { resourceId: v.id("resources") }, handler: async (ctx, args) => { // ctx.resource already verified as owned by user return ctx.resource; }, });
typescriptexport const teamQuery = customQuery(query, { args: { teamId: v.id("teams") }, input: async (ctx, args) => { const user = await getCurrentUser(ctx); const membership = await ctx.db .query("teamMembers") .withIndex("by_team_and_user", q => q.eq("teamId", args.teamId) .eq("userId", user._id) ) .unique(); if (!membership) { throw new Error("Not a member of this team"); } return { ctx: { ...ctx, user, teamId: args.teamId, permissions: membership.permissions }, args }; }, }); // Usage export const getTeamData = teamQuery({ args: { teamId: v.id("teams") }, handler: async (ctx) => { // Automatically scoped to team // ctx.permissions available for fine-grained checks return await ctx.db .query("teamData") .withIndex("by_team", q => q.eq("teamId", ctx.teamId)) .collect(); }, });
typescript// Read operations - more permissive export const viewerQuery = customQuery(query, { args: { teamId: v.id("teams") }, input: async (ctx, args) => { const user = await getCurrentUser(ctx); const member = await ctx.db .query("teamMembers") .withIndex("by_team_and_user", q => q.eq("teamId", args.teamId).eq("userId", user._id) ) .unique(); // Any team member can read if (!member) throw new Error("Not a team member"); return { ctx: { ...ctx, user, teamId: args.teamId }, args }; }, }); // Write operations - require specific role export const editorMutation = customMutation(mutation, { args: { teamId: v.id("teams") }, input: async (ctx, args) => { const user = await getCurrentUser(ctx); const member = await ctx.db .query("teamMembers") .withIndex("by_team_and_user", q => q.eq("teamId", args.teamId).eq("userId", user._id) ) .unique(); // Must be editor or admin to write if (!member || (member.role !== "editor" && member.role !== "admin")) { throw new Error("Editor access required"); } return { ctx: { ...ctx, user, teamId: args.teamId }, args }; }, });
typescript// Public query - no auth required export const publicQuery = query; // Private query - requires auth export const privateQuery = authedQuery; // Example: Blog posts export const listPublicPosts = publicQuery({ handler: async (ctx) => { return await ctx.db .query("posts") .withIndex("by_published", q => q.eq("published", true)) .collect(); }, }); export const listMyDrafts = privateQuery({ handler: async (ctx) => { return await ctx.db .query("posts") .withIndex("by_author", q => q.eq("authorId", ctx.user._id)) .filter(q => q.eq(q.field("published"), false)) .collect(); }, });
Recommended structure:
convex/
├── lib/
│ ├── auth.ts # getCurrentUser helper
│ └── customFunctions.ts # All custom wrappers
├── users.ts # Public functions
├── tasks.ts # Use authedQuery/authedMutation
├── admin.ts # Use adminQuery/adminMutation
└── organizations.ts # Use tenantQuery/tenantMutation| Aspect | Custom Functions (Convex) | Row Level Security (PostgreSQL) | |--------|---------------------------|----------------------------------| | Language | TypeScript | SQL | | Type Safety | Full | Limited | | Complexity | Medium | High | | Flexibility | Very High | Medium | | Testing | Easy (unit tests) | Hard (DB-level) | | Debugging | Standard debugging | DB logs | | Reusability | High (compose wrappers) | Medium |
bashnpm install convex-helpers
typescript// convex/lib/customFunctions.ts import { customQuery, customMutation } from "convex-helpers/server/customFunctions"; import { query, mutation } from "../_generated/server"; import { getCurrentUser } from "./auth"; // Base: Authenticated export const authedQuery = customQuery(query, { args: {}, input: async (ctx, args) => { const user = await getCurrentUser(ctx); return { ctx: { ...ctx, user }, args }; }, }); export const authedMutation = customMutation(mutation, { args: {}, input: async (ctx, args) => { const user = await getCurrentUser(ctx); return { ctx: { ...ctx, user }, args }; }, }); // Org-scoped export const orgQuery = customQuery(authedQuery, { args: { orgId: v.id("organizations") }, input: async (ctx, args) => { const member = await ctx.db .query("members") .withIndex("by_org_and_user", q => q.eq("orgId", args.orgId).eq("userId", ctx.user._id) ) .unique(); if (!member) throw new Error("Not a member"); return { ctx: { ...ctx, orgId: args.orgId, role: member.role }, args }; }, }); export const orgMutation = customMutation(authedMutation, { args: { orgId: v.id("organizations") }, input: async (ctx, args) => { const member = await ctx.db .query("members") .withIndex("by_org_and_user", q => q.eq("orgId", args.orgId).eq("userId", ctx.user._id) ) .unique(); if (!member) throw new Error("Not a member"); return { ctx: { ...ctx, orgId: args.orgId, role: member.role }, args }; }, }); // Admin-only (within org) export const orgAdminMutation = customMutation(orgMutation, { args: { orgId: v.id("organizations") }, input: async (ctx, args) => { if (ctx.role !== "admin") { throw new Error("Admin access required"); } return { ctx, args }; }, });
npm install convex-helpersconvex/lib/customFunctions.tsauthedQuery and authedMutationquery with authedQuery in functions needing authmutation with authedMutation in functions needing authOther measured skills in the registry, with their headline benchmark lift.