Skip to content

Usage Examples

Learn how to implement CRUD operations with proper workspace and entity access control using handler calls.

CRUD Pattern for Sharable Entities

Document Management System

typescript
// convex/documents/mutations.ts
import { mutation } from "../_generated/server";
import { v } from "convex/values";
import { requireAuth } from "../utils/requireAuth";
import { workspaces } from "../workspaces";
import { checkEntityAccess, checkWritePermission } from "../utils/accessControl";

// Create document with new entity
export const createDocument = mutation({
  args: {
    workspaceId: v.id("workspaces"),
    title: v.string(),
    content: v.string(),
  },
  handler: async (ctx, args) => {
    await requireAuth(ctx);

    // Create entity in workspace
    const entityId = (await workspaces.createEntityHandler(ctx, {
      workspaceId: args.workspaceId,
    })) as Id<"entities">;

    // Create document
    const documentId = await ctx.db.insert("documents", {
      entityId,
      title: args.title,
      content: args.content,
    });

    return { entityId, documentId };
  },
});

// Create document for existing entity
export const createDocumentForEntity = mutation({
  args: {
    entityId: v.id("entities"),
    title: v.string(),
    content: v.string(),
  },
  handler: async (ctx, args) => {
    await requireAuth(ctx);

    // Check access to entity
    const { membership } = await checkEntityAccess(ctx, args.entityId);
    checkWritePermission(membership);

    // Create document
    const documentId = await ctx.db.insert("documents", {
      entityId: args.entityId,
      title: args.title,
      content: args.content,
    });

    return documentId;
  },
});

// Update document
export const updateDocument = mutation({
  args: {
    documentId: v.id("documents"),
    title: v.optional(v.string()),
    content: v.optional(v.string()),
  },
  handler: async (ctx, args) => {
    await requireAuth(ctx);

    const document = await ctx.db.get(args.documentId);
    if (!document) throw new Error("Document not found");

    // Check access to entity
    await checkEntityAccess(ctx, document.entityId);

    const { documentId, ...updates } = args;
    await ctx.db.patch(documentId, updates);
  },
});

// Delete document
export const removeDocument = mutation({
  args: {
    documentId: v.id("documents"),
  },
  handler: async (ctx, args) => {
    await requireAuth(ctx);

    const document = await ctx.db.get(args.documentId);
    if (!document) throw new Error("Document not found");

    // Check access to entity
    await checkEntityAccess(ctx, document.entityId);

    await ctx.db.delete(args.documentId);
  },
});

// Share document with workspace
export const shareDocumentWithWorkspace = mutation({
  args: {
    documentId: v.id("documents"),
    targetWorkspaceId: v.id("workspaces"),
    accessLevel: v.union(
      v.literal("admin"),
      v.literal("editor"),
      v.literal("viewer")
    ),
  },
  handler: async (ctx, args) => {
    await requireAuth(ctx);

    const document = await ctx.db.get(args.documentId);
    if (!document) throw new Error("Document not found");

    // Check access to source entity
    await checkEntityAccess(ctx, document.entityId);

    // Share entity with target workspace
    const result = (await workspaces.createEntityAccessHandler(ctx, {
      workspaceId: args.targetWorkspaceId,
      entityId: document.entityId,
      accessLevel: args.accessLevel,
    })) as Id<"entityAccess">;

    return result;
  },
});

// Share document with user (automatically finds their personal workspace)
export const shareDocumentWithUser = mutation({
  args: {
    documentId: v.id("documents"),
    targetUserId: v.id("users"),
    accessLevel: v.union(
      v.literal("admin"),
      v.literal("editor"),
      v.literal("viewer")
    ),
  },
  handler: async (ctx, args) => {
    await requireAuth(ctx);

    const document = await ctx.db.get(args.documentId);
    if (!document) throw new Error("Document not found");

    // Check access to source entity
    await checkEntityAccess(ctx, document.entityId);

    // Get target user's personal workspace
    const personalWorkspace = await workspaces.getPersonalWorkspaceByUserIdHandler(ctx, {
      userId: args.targetUserId,
    });
    
    if (!personalWorkspace) throw new Error("Personal workspace not found");

    // Share entity with personal workspace
    const result = (await workspaces.createEntityAccessHandler(ctx, {
      workspaceId: personalWorkspace._id,
      entityId: document.entityId,
      accessLevel: args.accessLevel,
    })) as Id<"entityAccess">;

    return result;
  },
});

Queries with Access Control

typescript
// convex/documents/queries.ts
import { query } from "../_generated/server";
import { v } from "convex/values";
import { requireAuth } from "../utils/requireAuth";
import { workspaces } from "../workspaces";
import { checkEntityAccess } from "../utils/accessControl";

// Get document by ID
export const getDocumentById = query({
  args: {
    documentId: v.id("documents"),
  },
  handler: async (ctx, args) => {
    await requireAuth(ctx);

    const document = await ctx.db.get(args.documentId);
    if (!document) return null;

    // Check access to entity
    await checkEntityAccess(ctx, document.entityId);

    return document;
  },
});

// Get documents by entity
export const getDocumentsByEntity = query({
  args: {
    entityId: v.id("entities"),
  },
  handler: async (ctx, args) => {
    await requireAuth(ctx);

    // Check access to entity
    await checkEntityAccess(ctx, args.entityId);

    return await ctx.db
      .query("documents")
      .withIndex("by_entity", (q) => q.eq("entityId", args.entityId))
      .collect();
  },
});

// Get all documents accessible to user
export const getUserAccessibleDocuments = query({
  args: {},
  handler: async (ctx, args) => {
    await requireAuth(ctx);

    // Get all accessible entities
    const accessibleEntities = await workspaces.getUserAccessibleEntitiesHandler(ctx);

    const documents: {
      _id: Id<"documents">;
      _creationTime: number;
      entityId: Id<"entities">;
      title: string;
      content: string;
      userRole: string;
      workspaceId: Id<"workspaces">;
    }[] = [];

    for (const entity of accessibleEntities) {
      const entityDocuments = await ctx.db
        .query("documents")
        .withIndex("by_entity", (q) => q.eq("entityId", entity._id))
        .collect();

      for (const document of entityDocuments) {
        documents.push({
          ...document,
          userRole: entity.userRole,
          workspaceId: entity.workspaceId,
        });
      }
    }

    return documents;
  },
});

// Get documents by workspace
export const getDocumentsByWorkspace = query({
  args: {
    workspaceId: v.id("workspaces"),
  },
  handler: async (ctx, args) => {
    await requireAuth(ctx);

    // Check workspace membership
    const membership = await workspaces.getCurrentUserMembershipHandler(ctx, {
      workspaceId: args.workspaceId,
    });
    if (!membership) throw new Error("Access denied");

    // Get all entities in workspace
    const entities = await workspaces.getEntitiesByWorkspaceHandler(ctx, {
      workspaceId: args.workspaceId,
    });

    const documents = [];
    for (const entity of entities) {
      const entityDocuments = await ctx.db
        .query("documents")
        .withIndex("by_entity", (q) => q.eq("entityId", entity._id))
        .collect();
      
      documents.push(...entityDocuments);
    }

    return documents;
  },
});

Key Patterns

1. Workspace Functions

Use workspace functions to interact with the system:

typescript
const entityId = await workspaces.createEntityHandler(ctx, {
  workspaceId: args.workspaceId,
});

const access = await workspaces.getUserEffectiveAccessHandler(ctx, {
  entityId,
});

2. Access Control Utilities

Use access control functions for consistent security:

typescript
// Check entity access (works for both workspace members and shared entities)
await checkEntityAccess(ctx, entityId);

// Check write permissions
const { membership } = await checkEntityAccess(ctx, entityId);
checkWritePermission(membership);

3. Personal Workspace Discovery

When sharing with users, find their personal workspace automatically:

typescript
const personalWorkspace = await workspaces.getPersonalWorkspaceByUserIdHandler(ctx, {
  userId: targetUserId,
});

Utility Functions Reference

Here are the key utility functions used in the examples:

Access Control Utilities

typescript
// convex/utils/accessControl.ts
import { Doc, Id } from "../_generated/dataModel";
import { workspaces } from "../workspaces";
import { MutationCtx, QueryCtx } from "../_generated/server";

export interface EntityAccessResult {
  entity: Doc<"entities">;
  membership: Doc<"memberships"> | null;
  effectiveAccess: string | null;
}

/**
 * Checks access to entity through workspace membership or shared access
 */
export async function checkEntityAccess(
  ctx: QueryCtx | MutationCtx,
  entityId: Id<"entities">
): Promise<EntityAccessResult> {
  const entity = (await ctx.db.get(entityId)) as Doc<"entities">;
  if (!entity) throw new Error("Entity not found");

  const membership = (await workspaces.getCurrentUserMembershipHandler(ctx, {
    workspaceId: entity.workspaceId as Id<"workspaces">,
  })) as Doc<"memberships"> | null;

  let effectiveAccess: string | null = null;
  if (!membership) {
    // Check access through shared entity
    effectiveAccess = await workspaces.getUserEffectiveAccessHandler(ctx, {
      entityId,
    });
    if (!effectiveAccess) throw new Error("Access denied");
  }

  return { entity, membership, effectiveAccess };
}

/**
 * Checks write permissions (not viewer)
 */
export function checkWritePermission(
  membership: Doc<"memberships"> | null
): void {
  if (membership?.userRole === "viewer") {
    throw new Error("Insufficient permissions");
  }
}

Workspace Functions

typescript
// Key functions from workspaces module:

// Entity management
workspaces.createEntityHandler(ctx, { workspaceId })
workspaces.getEntityByIdHandler(ctx, { entityId })
workspaces.getEntitiesByWorkspaceHandler(ctx, { workspaceId })
workspaces.removeEntityHandler(ctx, { entityId })

// Access control
workspaces.getCurrentUserMembershipHandler(ctx, { workspaceId })
workspaces.getUserEffectiveAccessHandler(ctx, { entityId })
workspaces.getUserAccessibleEntitiesHandler(ctx)

// Entity sharing
workspaces.createEntityAccessHandler(ctx, { workspaceId, entityId, accessLevel })
workspaces.removeEntityAccessHandler(ctx, { accessId })

// Personal workspace discovery
workspaces.getPersonalWorkspaceByUserIdHandler(ctx, { userId })

These functions are available as raw handlers and as full queries and mutations in your Convex application.