This is a modified version of the Row Level Security (RLS) helpers from convex-helpers. It simply passes the incoming update values to the Rules' modify
function.
The original helpers are not aware of the update value when checking for write access.
Copy the ./convex/rowLevelSecurity/
directory and its contents to your project.
// ./convex/rls.ts
import {
Rules,
wrapDatabaseReader,
wrapDatabaseWriter,
} from "./rowLevelSecurity";
import {
customCtx,
customMutation,
customQuery,
} from "convex-helpers/server/customFunctions";
import { DataModel } from "./_generated/dataModel";
import { mutation, query, QueryCtx } from "./_generated/server";
async function rlsRules(ctx: QueryCtx) {
const identity = await ctx.auth.getUserIdentity();
return {
users: {
read: async (ctx, user) => ...
insert: async (ctx, user) => ...
modify: async (ctx, user, updateValues) => ...
},
groups: {
read: async (ctx, groupBeingRead) => ...
insert: async (ctx, groupToInsert) => ...
modify: async (ctx, currentGroupDoc, updateValues) => {
// Example 1: Only allow updates to specific fields
const allowedFields = ['name', 'description', 'isPublic'];
const updateKeys = Object.keys(updateValues);
const hasUnauthorizedFields = updateKeys.some(key => !allowedFields.includes(key));
if (hasUnauthorizedFields) return false;
// Example 2: Prevent changing group ownership
if ('ownerId' in updateValues && updateValues.ownerId !== doc.ownerId) {
return false;
}
// Example 3: Only group members can update certain fields
if ('memberIds' in updateValues && !doc.memberIds.includes(ctx.userId)) {
return false;
}
// Example 4: Validate field values before allowing update
if ('isPublic' in updateValues && updateValues.isPublic === true) {
// Only admins can make groups public
if (!isAdmin(ctx)) return false;
}
// Example 5: Prevent removing all members from a group
if ('memberIds' in updateValues && Array.isArray(updateValues.memberIds)) {
if (updateValues.memberIds.length === 0) return false;
}
// Example 6: Rate limiting updates to group settings
if ('settings' in updateValues) {
const lastUpdate = doc.lastSettingsUpdate || 0;
const now = Date.now();
if (now - lastUpdate < 60000) { // 1 minute cooldown
return false;
}
}
return false;
},
},
} satisfies Rules<QueryCtx, DataModel>;
}
export const queryWithRLS = customQuery(
query,
customCtx(async (ctx) => ({
db: wrapDatabaseReader(ctx, ctx.db, await rlsRules(ctx)),
})),
);
export const mutationWithRLS = customMutation(
mutation,
customCtx(async (ctx) => ({
db: wrapDatabaseWriter(ctx, ctx.db, await rlsRules(ctx)),
})),
);