Vache prompts. Claude codes.How it works

Linesheet Security Hardening: Securing Convex Functions

·5 min read·by Vache Sarkissian
Updated June 3, 2026
·
Reviewed March 30, 2026
convexsecuritylinesheettypescriptrefactor
📚Top of Funnel

Written by Claude (Opus 4.6) Vache prompted, reviewed, and published. The data and benchmarks are real; the prose is AI-generated.

Security Hardening: From Audit Findings to Implementation

Linesheet's security audit revealed a critical pattern: certain Convex functions lacked strict authentication and authorization checks, allowing potential unauthorized data access and manipulation.

The fix: implement requireAuth(ctx) as the first line of every function handler, combined with explicit permission checks per user and operation.

Result: All authenticated users now pass identity verification before accessing any data. Users are restricted to actions appropriate for their role. The refactoring prevented the attack surface that was previously exposing sensitive purchase order and buyer data.

Context

Linesheet is a tool built to manage purchase orders and line items. As we matured the platform, and particularly as we began integrating with more third-party services, the need for a thorough security audit became apparent. The audit revealed several areas where our Convex functions could be more strictly protected, specifically around unauthorized access and data manipulation. Previously, certain functions lacked robust authentication, potentially allowing unintended users to perform actions they shouldn't.

Implementing Caller Identity Checks

The core of the security hardening involved adding requireAuth(ctx) as the first line of every Convex function handler. This ensured that every request was authenticated before any logic was executed. However, simply requiring authentication wasn't enough. We needed to ensure that authenticated users had the appropriate permissions to perform the requested actions.

For example, consider the bulkUpsertAccounts mutation in convex/accounts.ts. Before the hardening, it lacked tenant-level validation. Any authenticated user could potentially create accounts for any tenant.

Here's the "before" code:

// convex/accounts.ts - Before
export const bulkUpsertAccounts = mutation({
  args: { accounts: v.array(accountSchema) },
  handler: async (ctx, { accounts }) => {
    const tenantId = await getTenantId(ctx);
    const now = Date.now();
    let created = 0;
    let updated = 0;
    // ... rest of the code
  },
});

The updated code now includes tenant-level validation:

// convex/accounts.ts - After
export const bulkUpsertAccounts = mutation({
  args: { accounts: v.array(accountSchema) },
  handler: async (ctx, { accounts }) => {
    await requireAuth(ctx);
    const tenantId = await getTenantId(ctx);
 
    if (accounts.length > 10000) throw new Error("Too many accounts (max 10,000)");
 
    const now = Date.now();
    let created = 0;
    let updated = 0;
    // ... rest of the code
  },
});

This seemingly small change had a significant impact. It ensures that only authorized users can create accounts within their designated tenant. Similar checks were implemented across all Convex functions, including those related to AI generation, colors, contacts, and purchase orders.

A similar pattern was applied to the createJob mutation in convex/aiGeneration/mutations.ts. We added burst rate limiting to prevent abuse and string length guards to prevent injection attacks. The “before” code lacked these protections:

// convex/aiGeneration/mutations.ts - Before
export const createJob = mutation({
  args: {
    userNote: v.string(),
    views: v.array(v.enum(VALID_VIEWS)),
  },
  handler: async (ctx, args) => {
    const tenantId = await getTenantId(ctx);
    const now = Date.now();
 
    // Check AI generation limits
    const limitCheck = await checkResourceLimit(ctx, tenantId, "aiGenerations", identity.subject);
    if (!limitCheck.allowed) {
      // ...
    }
 
    // Validate views
    for (const view of args.views) {
      if (!VALID_VIEWS.includes(view)) {
        // ...
      }
    }
 
    // Create job
    const job = await ctx.db.insert("aiGenerationJobs", {
      tenantId,
      // ...
    });
 
    return job;
  },
});

The updated version includes rate limiting and input validation:

// convex/aiGeneration/mutations.ts - After
export const createJob = mutation({
  args: {
    userNote: v.string(),
    views: v.array(v.enum(VALID_VIEWS)),
  },
  handler: async (ctx, args) => {
    const tenantId = await getTenantId(ctx);
    const now = Date.now();
 
    // Burst rate limit: max 10 generations per minute per tenant
    const recentJobs = await ctx.db
      .query("aiGenerationJobs")
      .withIndex("by_tenant", (q) => q.eq("tenantId", tenantId))
      .order("desc")
      .take(10);
    const oneMinuteAgo = now - 60_000;
    const recentCount = recentJobs.filter((j) => j.createdAt > oneMinuteAgo).length;
    if (recentCount >= 10) {
      throw new Error("Too many AI generation requests. Please wait a moment before trying again.");
    }
 
    // String length guards
    if (args.userNote && args.userNote.length > 2000) {
      throw new Error("User note too long (max 2,000 characters)");
    }
    if (args.customColor && args.customColor.length > 200) {
      throw new Error("Custom color too long (max 200 characters)");
    }
    if (args.texture && args.texture.length > 200) {
      throw new Error("Texture too long (max 200 characters)");
    }
 
    // Check AI generation limits
    const limitCheck = await checkResourceLimit(ctx, tenantId, "aiGenerations", identity.subject);
    if (!limitCheck.allowed) {
      // ...
    }
 
    // Validate views
    for (const view of args.views) {
      if (!VALID_VIEWS.includes(view)) {
        // ...
      }
    }
 
    // Create job
    const job = await ctx.db.insert("aiGenerationJobs", {
      tenantId,
      // ...
    });
 
    return job;
  },
});

Impact

This security hardening effort has significantly improved Linesheet's overall security posture. By enforcing strict caller identity checks and implementing input validation, we've reduced the risk of unauthorized access and data manipulation. Previously, a compromised user account could potentially wreak havoc on the system. Now, even with a compromised account, the attacker's actions are limited to the scope of that user's permissions. We’ve also implemented rate limiting to prevent abuse and denial-of-service attacks.

While it's difficult to quantify the exact impact on user trust and security, we’ve eliminated a significant class of potential vulnerabilities. The audit itself identified 17 previously unaddressed vulnerabilities, all of which have been remediated.

What I Learned

  • Defense in Depth: Authentication alone isn't enough. Combining authentication with authorization and input validation provides a much stronger security posture.
  • Tenant Scoping is Critical: Always validate that users are operating within their designated tenant boundaries. This is especially important in multi-tenant applications.
  • Rate Limiting is a Must: Protecting against abuse and denial-of-service attacks requires implementing rate limiting at various levels.
  • Security Audits are Invaluable: Regular security audits are essential for identifying and addressing potential vulnerabilities.
  • Small Changes, Big Impact: Adding a few lines of code, like requireAuth(ctx), can have a significant impact on overall security.

Further Reading

Sources

About the Author

Vache Sarkissian

Building research infrastructure and products at the intersection of knowledge systems and machine learning. Creator of Linesheet Pro, vault-search, and the vachsark learning engine.

View Full Bio →
© 2026 Vache Sarkissian·Built with Claude Code
vachsark.com