Back to skills
SkillHub ClubWrite Technical DocsTech WriterSecurityTesting

fortknoxsecurityscanner

A battle-hardened security checklist and pattern library for Convex apps. Goes beyond basic auth checks to cover ownership verification, rate limiting with @convex-dev/ratelimiter, enumeration attack prevention, HTTP action hardening, and internal vs. public function architecture. Built with the latest Convex docs — not assumptions.

Packaged view

This page reorganizes the original catalog entry around fit, installability, and workflow context first. The original raw source lives below.

Stars
0
Hot score
74
Updated
March 20, 2026
Overall rating
C0.0
Composite score
0.0
Best-practice grade
N/A

Install command

npx @skill-hub/cli install loren-fortknoxsecurityscanner
SecurityConvex

Repository

loren/loren-fortknoxsecurityscanner

A battle-hardened security checklist and pattern library for Convex apps. Goes beyond basic auth checks to cover ownership verification, rate limiting with @convex-dev/ratelimiter, enumeration attack prevention, HTTP action hardening, and internal vs. public function architecture. Built with the latest Convex docs — not assumptions.

Open repository

Best for

Primary workflow: Write Technical Docs.

Technical facets: Tech Writer, Security, Testing.

Target audience: everyone.

License: Unknown.

Original source

Catalog source: SkillHub Club.

Repository owner: loren.

This is still a mirrored public skill entry. Review the repository before installing into production workflows.

What it helps with

  • Install fortknoxsecurityscanner into Claude Code, Codex CLI, Gemini CLI, or OpenCode workflows
  • Review https://www.skillhub.club/skills/loren-fortknoxsecurityscanner before adding fortknoxsecurityscanner to shared team environments
  • Use fortknoxsecurityscanner for security workflows

Works across

Claude CodeCodex CLIGemini CLIOpenCode

Favorites: 0.

Sub-skills: 0.

Aggregator: No.

Original source / Raw SKILL.md

---
name: "fortknoxsecurityscanner"
description: "A battle-hardened security checklist and pattern library for Convex apps. Goes beyond basic auth checks to cover ownership verification, rate limiting with @convex-dev/ratelimiter, enumeration attack prevention, HTTP action hardening, and internal vs. public function architecture. Built with the latest Convex docs — not assumptions."
category: "Security"
---

# fortknoxsecurityscanner

A battle-hardened security checklist and pattern library for Convex apps. Goes beyond basic auth checks to cover ownership verification, rate limiting with @convex-dev/ratelimiter, enumeration attack prevention, HTTP action hardening, and internal vs. public function architecture. Built with the latest Convex docs — not assumptions.

## Instructions

- Describe how this skill should be used

## Examples

```
// example usage
```


---

## Referenced Files

> The following files are included in this skill.

### convex-security-check_1.md

```md
---
name: Fort Knox Security Scanner
description: Comprehensive security audit for Convex applications covering authentication, authorization, function exposure, argument validation, row-level access control, rate limiting, HTTP action hardening, environment variables, and production security. Use when auditing, building, or hardening any Convex backend.
version: 2.0.0
author: Convex
tags: [convex, security, authentication, authorization, rate-limiting, checklist, production]
---

# Fort Knox Security Scanner

A comprehensive security audit for Convex applications. Covers every layer from auth to production hardening.

**Before implementing anything, fetch the latest docs — never assume:**

| Topic | URL |
|---|---|
| Authentication | https://docs.convex.dev/auth |
| Auth in Functions | https://docs.convex.dev/auth/functions-auth |
| Internal Functions | https://docs.convex.dev/functions/internal-functions |
| Argument Validation | https://docs.convex.dev/functions/validation |
| Best Practices | https://docs.convex.dev/understanding/best-practices |
| Production Security | https://docs.convex.dev/production |
| Environment Variables | https://docs.convex.dev/production/environment-variables |
| Authorization Guide | https://stack.convex.dev/authorization |
| Rate Limiting | https://stack.convex.dev/rate-limiting |
| Broader context | https://docs.convex.dev/llms.txt |

---

## Security Mindset

Convex deployment endpoints are **exposed to the open internet**. Every public function (`query`, `mutation`, `action`) can be called by anyone — including malicious actors probing your app. Your backend is your last and most important line of defense. Frontend checks are UX, not security.

**The security hierarchy:**
1. Authenticate: Who is making this request?
2. Authorize: Are they allowed to do this specific thing?
3. Validate: Is the data they sent the shape we expect?
4. Limit: Are they doing this too often?

Every public function should answer all four questions before touching the database.

---

## The Full Security Audit Checklist

### 1. Authentication

- [ ] Auth provider configured (Clerk, Auth0, WorkOS AuthKit, or Convex Auth)
- [ ] Every sensitive query and mutation calls `ctx.auth.getUserIdentity()` and throws if null
- [ ] Unauthenticated public access is **explicitly intentional** and documented where allowed
- [ ] HTTP actions validate the `Authorization` header with a Bearer JWT token
- [ ] Service-to-service calls use a shared secret checked against an environment variable
- [ ] Session tokens are never stored in the database or logged

### 2. Authorization (Row-Level Access Control)

- [ ] Users can only read their own data — verified with `tokenIdentifier` or user `_id`
- [ ] Users can only write/update/delete records they own — ownership checked before every mutation
- [ ] Admin-only functions verify the user's role before proceeding
- [ ] Shared resources (team data, org data) check membership before returning anything
- [ ] Functions are granular: `setTeamOwner` instead of `updateTeam` (enables precise checks)
- [ ] Authorization helpers (`requireAuth`, `requireAdmin`, `isTeamMember`) are shared, not duplicated
- [ ] No function trusts a client-provided user ID — always derive from `ctx.auth`

### 3. Function Exposure

- [ ] Every `query`, `mutation`, `action`, and `httpAction` has been audited
- [ ] Sensitive operations use `internalQuery`, `internalMutation`, `internalAction`
- [ ] No payment, credit, role, or permission logic is in a public function
- [ ] HTTP actions validate both the `Authorization` header and request origin
- [ ] Public functions cannot be used to enumerate other users' data

### 4. Argument Validation

- [ ] Every public function has explicit `args` validators — no exceptions
- [ ] Every public function has explicit `returns` validators
- [ ] `v.any()` is never used for args that touch the database
- [ ] ID validators use `v.id("tableName")` with the correct table name (prevents cross-table ID abuse)
- [ ] String inputs are bounded with length constraints where appropriate
- [ ] Enum-like values use `v.union(v.literal("a"), v.literal("b"))` — not bare `v.string()`
- [ ] Object validators list only allowed fields — no open-ended `v.object({})` for writes
- [ ] ESLint rule `@convex-dev/require-argument-validators` is enabled

### 5. Rate Limiting

- [ ] Expensive mutations (AI calls, emails, file uploads) have per-user rate limits
- [ ] Authentication failure paths (login, password reset) are rate limited per user/IP
- [ ] Free tier signup flows are rate limited globally to deter bots
- [ ] Rate limits are evaluated inside mutations (transactional) — not in actions before calling a mutation
- [ ] `@convex-dev/ratelimiter` component is used for consistent, type-safe limits
- [ ] Rate limit names are centrally defined for type safety and reuse

### 6. Environment Variables

- [ ] All API keys and secrets are in environment variables — never hardcoded
- [ ] `process.env` is only accessed inside `action` functions (not queries or mutations)
- [ ] Dev and production environments use different keys
- [ ] Missing required env vars throw a clear, descriptive error at runtime
- [ ] No secrets appear in schema definitions, function names, or log output
- [ ] `.env` files are in `.gitignore`

### 7. HTTP Actions

- [ ] All HTTP actions validate `Authorization: Bearer <jwt>` header
- [ ] Request body is validated with Zod or equivalent (not just TypeScript types)
- [ ] CORS headers are explicitly set and restrict allowed origins
- [ ] HTTP actions that trigger mutations call them as `internal` functions
- [ ] Response bodies never leak internal error details to the client

### 8. Production Hardening

- [ ] Schema enforcement is on — never turned off in production
- [ ] Function changes are backwards-compatible until old clients have cycled out
- [ ] Scheduled functions handle stale arguments from schema/function changes
- [ ] No `console.log` in production code that outputs sensitive data
- [ ] Dashboard access is restricted to necessary team members
- [ ] `CONVEX_DEPLOY_KEY` is stored securely in CI/CD — not in the repo

---

## Authentication Patterns

### The Auth Helper (Use Everywhere)

```typescript
// convex/lib/auth.ts
import { QueryCtx, MutationCtx } from "./_generated/server";
import { ConvexError } from "convex/values";

/**
 * Throws if the caller is not authenticated.
 * Use this at the top of every sensitive function.
 */
export async function requireAuth(ctx: QueryCtx | MutationCtx) {
  const identity = await ctx.auth.getUserIdentity();
  if (!identity) {
    throw new ConvexError({
      code: "UNAUTHENTICATED",
      message: "You must be logged in to perform this action.",
    });
  }
  return identity;
}

/**
 * Returns the full user record from the database.
 * Throws if not authenticated or if the user record doesn't exist.
 */
export async function requireUser(ctx: QueryCtx | MutationCtx) {
  const identity = await requireAuth(ctx);

  const user = await ctx.db
    .query("users")
    .withIndex("by_tokenIdentifier", (q) =>
      q.eq("tokenIdentifier", identity.tokenIdentifier)
    )
    .unique();

  if (!user) {
    throw new ConvexError({
      code: "USER_NOT_FOUND",
      message: "User profile not found. Please complete onboarding.",
    });
  }

  return user;
}

/**
 * Throws unless the authenticated user has the "admin" role.
 */
export async function requireAdmin(ctx: QueryCtx | MutationCtx) {
  const user = await requireUser(ctx);

  if (user.role !== "admin") {
    throw new ConvexError({
      code: "FORBIDDEN",
      message: "Admin access required.",
    });
  }

  return user;
}
```

### Secure Query Pattern

```typescript
// convex/users.ts
import { query } from "./_generated/server";
import { v } from "convex/values";
import { requireUser } from "./lib/auth";

export const getMyProfile = query({
  args: {},
  returns: v.object({
    _id: v.id("users"),
    name: v.string(),
    email: v.string(),
    role: v.union(v.literal("user"), v.literal("admin")),
  }),
  handler: async (ctx) => {
    // Always derive identity from ctx — never trust a client-provided ID
    const user = await requireUser(ctx);
    return user;
  },
});
```

### HTTP Action Auth Pattern

```typescript
// convex/http.ts
import { httpAction } from "./_generated/server";
import { ConvexError } from "convex/values";

export const webhookHandler = httpAction(async (ctx, request) => {
  // Validate Bearer token from Authorization header
  const identity = await ctx.auth.getUserIdentity();
  if (!identity) {
    return new Response("Unauthorized", { status: 401 });
  }

  // For webhook endpoints using a shared secret instead of JWT:
  const secret = process.env.WEBHOOK_SECRET;
  const provided = request.headers.get("x-webhook-secret");
  if (!secret || provided !== secret) {
    return new Response("Forbidden", { status: 403 });
  }

  // Validate the request body with Zod
  let body: unknown;
  try {
    body = await request.json();
  } catch {
    return new Response("Invalid JSON", { status: 400 });
  }

  // ... validate body shape with Zod here ...

  // Call an internal mutation — never a public one
  await ctx.runMutation(internal.webhooks.process, { body });

  return new Response(null, { status: 200 });
});
```

---

## Authorization Patterns

### Ownership Check Before Mutation

```typescript
// convex/tasks.ts
import { mutation } from "./_generated/server";
import { v } from "convex/values";
import { ConvexError } from "convex/values";
import { requireUser } from "./lib/auth";

export const updateTask = mutation({
  args: {
    taskId: v.id("tasks"),
    title: v.string(),
    // Only allow specific fields — never spread args.update into db.patch()
  },
  returns: v.null(),
  handler: async (ctx, args) => {
    const user = await requireUser(ctx);

    const task = await ctx.db.get(args.taskId);

    // Verify the task exists and belongs to this user
    // Use _id (database ID), not tokenIdentifier, for ownership comparisons
    if (!task || task.userId !== user._id) {
      // Return the same error for "not found" and "not authorized"
      // This prevents enumeration attacks
      throw new ConvexError({
        code: "NOT_FOUND",
        message: "Task not found.",
      });
    }

    await ctx.db.patch(args.taskId, { title: args.title });
    return null;
  },
});

export const deleteTask = mutation({
  args: {
    taskId: v.id("tasks"),
  },
  returns: v.null(),
  handler: async (ctx, args) => {
    const user = await requireUser(ctx);

    const task = await ctx.db.get(args.taskId);

    if (!task || task.userId !== user._id) {
      throw new ConvexError({ code: "NOT_FOUND", message: "Task not found." });
    }

    await ctx.db.delete(args.taskId);
    return null;
  },
});
```

### Team/Org Membership Check

```typescript
// convex/lib/teams.ts
import { QueryCtx, MutationCtx } from "./_generated/server";
import { Id } from "./_generated/dataModel";
import { ConvexError } from "convex/values";
import { requireUser } from "./auth";

export async function requireTeamMember(
  ctx: QueryCtx | MutationCtx,
  teamId: Id<"teams">
) {
  const user = await requireUser(ctx);

  const membership = await ctx.db
    .query("teamMembers")
    .withIndex("by_team_and_user", (q) =>
      q.eq("teamId", teamId).eq("userId", user._id)
    )
    .unique();

  if (!membership) {
    throw new ConvexError({ code: "FORBIDDEN", message: "Team access denied." });
  }

  return { user, membership };
}

export async function requireTeamAdmin(
  ctx: QueryCtx | MutationCtx,
  teamId: Id<"teams">
) {
  const { user, membership } = await requireTeamMember(ctx, teamId);

  if (membership.role !== "admin") {
    throw new ConvexError({
      code: "FORBIDDEN",
      message: "Team admin access required.",
    });
  }

  return { user, membership };
}
```

---

## Argument Validation Patterns

### What Good Validation Looks Like

```typescript
// convex/posts.ts
import { mutation } from "./_generated/server";
import { v } from "convex/values";
import { requireUser } from "./lib/auth";

// GOOD: Strict, explicit, no surprises
export const createPost = mutation({
  args: {
    title: v.string(),
    content: v.string(),
    // Enum with v.union + v.literal — not an open v.string()
    category: v.union(
      v.literal("tech"),
      v.literal("design"),
      v.literal("business")
    ),
    // IDs must reference the correct table — prevents cross-table injection
    tagId: v.optional(v.id("tags")),
  },
  returns: v.id("posts"),
  handler: async (ctx, args) => {
    const user = await requireUser(ctx);

    return await ctx.db.insert("posts", {
      ...args,
      authorId: user._id,       // Never trust a client-provided authorId
      createdAt: Date.now(),
    });
  },
});

// BAD: What not to do
export const createPostUnsafe = mutation({
  args: {
    data: v.any(), // ❌ Allows any shape — including system fields like _id
  },
  returns: v.id("posts"),
  handler: async (ctx, args) => {
    return await ctx.db.insert("posts", args.data); // ❌ Never do this
  },
});
```

### ID Validation — Why It Matters

```typescript
// v.id("tasks") does TWO things:
// 1. Validates the ID is structurally correct
// 2. Ensures it references the "tasks" table — not "users" or "payments"

// Without this, a malicious client could pass an ID from another table
// and your ownership check (task.userId === user._id) could pass unexpectedly

export const getTask = query({
  args: { taskId: v.id("tasks") }, // ✅ Table-scoped ID validation
  returns: v.union(
    v.object({ _id: v.id("tasks"), title: v.string() }),
    v.null()
  ),
  handler: async (ctx, args) => {
    const user = await requireUser(ctx);
    const task = await ctx.db.get(args.taskId);

    if (!task || task.userId !== user._id) return null;
    return task;
  },
});
```

---

## Function Exposure Patterns

### Internal vs Public — The Hard Rule

```typescript
// convex/credits.ts
import { mutation, internalMutation } from "./_generated/server";
import { v } from "convex/values";

// PUBLIC mutation: Only handles what the user initiates directly
export const spendCredits = mutation({
  args: { amount: v.number() },
  returns: v.null(),
  handler: async (ctx, args) => {
    const user = await requireUser(ctx);

    if (user.credits < args.amount) {
      throw new ConvexError({ code: "INSUFFICIENT_CREDITS" });
    }

    // Call the internal mutation — never manipulate credits directly here
    // This ensures credit logic is centralized and auditable
    await ctx.runMutation(internal.credits._deductCredits, {
      userId: user._id,
      amount: args.amount,
    });

    return null;
  },
});

// INTERNAL: Credit manipulation logic — unreachable from clients
export const _deductCredits = internalMutation({
  args: {
    userId: v.id("users"),
    amount: v.number(),
  },
  returns: v.null(),
  handler: async (ctx, args) => {
    const user = await ctx.db.get(args.userId);
    if (!user) throw new ConvexError("User not found");

    await ctx.db.patch(args.userId, {
      credits: user.credits - args.amount,
    });

    return null;
  },
});
```

### What Must Always Be Internal

- Payment and credit adjustments
- Role and permission changes
- Admin operations triggered by automated logic (crons, webhooks)
- Any function called from a scheduled job or another internal function

---

## Rate Limiting Patterns

### Setup with `@convex-dev/ratelimiter`

```typescript
// convex/lib/rateLimits.ts
import { RateLimiter, MINUTE, HOUR } from "@convex-dev/ratelimiter";
import { components } from "./_generated/api";

// Define all rate limits centrally for type safety
export const rateLimiter = new RateLimiter(components.ratelimiter, {
  // Global: limit free signups to deter bot registration
  freeSignup: { kind: "fixed window", rate: 100, period: HOUR },

  // Per-user: prevent message spam
  sendMessage: { kind: "token bucket", rate: 10, period: MINUTE, capacity: 3 },

  // Per-user: lock out brute-force login attempts
  failedLogin: { kind: "token bucket", rate: 5, period: HOUR },

  // Per-user: limit expensive AI calls
  aiRequest: { kind: "fixed window", rate: 20, period: HOUR },
});
```

### Applying Rate Limits in Mutations (Not Actions)

```typescript
// convex/messages.ts
import { mutation } from "./_generated/server";
import { v } from "convex/values";
import { rateLimiter } from "./lib/rateLimits";
import { requireUser } from "./lib/auth";

export const sendMessage = mutation({
  args: {
    content: v.string(),
    channelId: v.id("channels"),
  },
  returns: v.id("messages"),
  handler: async (ctx, args) => {
    const user = await requireUser(ctx);

    // Rate limit check runs inside the mutation (transactional)
    // If the mutation fails and rolls back, the rate limit charge rolls back too
    await rateLimiter.limit(ctx, "sendMessage", {
      key: user._id,
      throws: true, // Throws ConvexError automatically if limit exceeded
    });

    return await ctx.db.insert("messages", {
      content: args.content,
      channelId: args.channelId,
      authorId: user._id,
      createdAt: Date.now(),
    });
  },
});
```

---

## Environment Variable Patterns

```typescript
// convex/actions/email.ts
"use node"; // Required for process.env access

import { action } from "./_generated/server";
import { v } from "convex/values";
import { internal } from "./_generated/api";

export const sendWelcomeEmail = action({
  args: {
    userId: v.id("users"),
  },
  returns: v.object({ success: v.boolean() }),
  handler: async (ctx, args) => {
    // Always check for missing env vars — fail loudly, not silently
    const apiKey = process.env.RESEND_API_KEY;
    if (!apiKey) {
      throw new Error(
        "RESEND_API_KEY is not configured. Set it in your Convex dashboard environment variables."
      );
    }

    // Fetch user data via an internal query — never accept user data as action args
    const user = await ctx.runQuery(internal.users._getUserById, {
      userId: args.userId,
    });

    if (!user) throw new Error("User not found");

    const response = await fetch("https://api.resend.com/emails", {
      method: "POST",
      headers: {
        Authorization: `Bearer ${apiKey}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        from: "[email protected]",
        to: user.email,
        subject: "Welcome to YourApp",
        html: `<p>Hi ${user.name}, welcome!</p>`,
      }),
    });

    // Never surface raw API error details to callers
    if (!response.ok) {
      console.error("Email send failed:", response.status);
      return { success: false };
    }

    return { success: true };
  },
});
```

---

## Complete Secure Module Example

```typescript
// convex/projects.ts
import { query, mutation, internalMutation } from "./_generated/server";
import { v } from "convex/values";
import { ConvexError } from "convex/values";
import { requireUser, requireAdmin } from "./lib/auth";
import { requireTeamMember, requireTeamAdmin } from "./lib/teams";
import { rateLimiter } from "./lib/rateLimits";
import { internal } from "./_generated/api";

// PUBLIC: User lists their own projects only
export const listMyProjects = query({
  args: {},
  returns: v.array(
    v.object({
      _id: v.id("projects"),
      name: v.string(),
      teamId: v.id("teams"),
      createdAt: v.number(),
    })
  ),
  handler: async (ctx) => {
    const user = await requireUser(ctx);

    return await ctx.db
      .query("projects")
      .withIndex("by_owner", (q) => q.eq("ownerId", user._id))
      .collect();
  },
});

// PUBLIC: Create a project — rate limited, membership verified
export const createProject = mutation({
  args: {
    name: v.string(),
    teamId: v.id("teams"),
  },
  returns: v.id("projects"),
  handler: async (ctx, args) => {
    const { user } = await requireTeamMember(ctx, args.teamId);

    // Rate limit project creation per user
    await rateLimiter.limit(ctx, "createProject", {
      key: user._id,
      throws: true,
    });

    return await ctx.db.insert("projects", {
      name: args.name,
      teamId: args.teamId,
      ownerId: user._id,
      createdAt: Date.now(),
    });
  },
});

// PUBLIC: Delete — ownership check required
export const deleteProject = mutation({
  args: {
    projectId: v.id("projects"),
  },
  returns: v.null(),
  handler: async (ctx, args) => {
    const user = await requireUser(ctx);

    const project = await ctx.db.get(args.projectId);

    // Same error for "not found" and "not yours" — prevents enumeration
    if (!project || project.ownerId !== user._id) {
      throw new ConvexError({ code: "NOT_FOUND", message: "Project not found." });
    }

    // Use internal mutation to clean up related data atomically
    await ctx.runMutation(internal.projects._deleteProjectAndData, {
      projectId: args.projectId,
    });

    return null;
  },
});

// ADMIN ONLY: List all projects across all teams
export const adminListAllProjects = query({
  args: {
    limit: v.optional(v.number()),
  },
  returns: v.array(
    v.object({
      _id: v.id("projects"),
      name: v.string(),
      ownerId: v.id("users"),
    })
  ),
  handler: async (ctx, args) => {
    await requireAdmin(ctx);

    return await ctx.db
      .query("projects")
      .order("desc")
      .take(args.limit ?? 50);
  },
});

// INTERNAL: Cascading delete — never callable from clients
export const _deleteProjectAndData = internalMutation({
  args: { projectId: v.id("projects") },
  returns: v.null(),
  handler: async (ctx, args) => {
    // Delete all tasks belonging to the project
    const tasks = await ctx.db
      .query("tasks")
      .withIndex("by_project", (q) => q.eq("projectId", args.projectId))
      .collect();

    await Promise.all(tasks.map((t) => ctx.db.delete(t._id)));

    // Then delete the project itself
    await ctx.db.delete(args.projectId);

    return null;
  },
});
```

---

## Common Vulnerabilities and How to Prevent Them

| Vulnerability | How It Happens | Prevention |
|---|---|---|
| Unauthenticated data access | Forgot to call `getUserIdentity()` | Use `requireUser()` helper at the top of every sensitive function |
| Cross-user data access | Queried by ID without checking ownership | Always verify `record.userId === user._id` before returning or mutating |
| Privilege escalation | Client passes `role: "admin"` as an arg | Never trust client-provided roles — always read from the database |
| ID injection | Using `v.string()` instead of `v.id("table")` | Always use table-scoped `v.id("tableName")` for document IDs |
| Open writes | `v.any()` or spreading `args.data` into `db.insert()` | List every allowed field explicitly in validators |
| Enumeration attacks | Different errors for "not found" vs "forbidden" | Return identical errors for both cases on sensitive resources |
| Exposed internal logic | Sensitive ops in public functions | Move to `internalMutation` / `internalQuery` |
| Secrets in code | API keys hardcoded or in schema | Use `process.env` in actions only, set via dashboard |
| Unbounded mutations | No rate limits on expensive operations | Add `@convex-dev/ratelimiter` to all costly mutations |
| HTTP action abuse | No origin validation or auth on HTTP endpoints | Validate `Authorization` header and use Zod for request body validation |

---

## Security Audit Workflow

When auditing an existing Convex codebase:

1. **Find all public entry points.** Search for `query(`, `mutation(`, `action(`, `httpAction(` — these are all callable from the internet.

2. **Check each one for auth.** Does it call `requireUser()` or `ctx.auth.getUserIdentity()`? If not, is it intentionally public?

3. **Check each one for authorization.** After auth, does it verify the user can access the specific resource being requested?

4. **Check argument validators.** Does every function have `args: { ... }` with explicit validators? Install `@convex-dev/require-argument-validators` and fix any violations.

5. **Audit internal functions.** Search for `internalMutation`, `internalQuery`, `internalAction`. Anything that modifies credits, roles, payments, or cascades deletes should be here.

6. **Check for `v.any()`.** It should never appear in args for functions that touch the database.

7. **Review HTTP actions.** They need both JWT auth and body validation.

8. **Check rate limiting.** Anything a user can trigger repeatedly (messages, AI calls, uploads, signups) should have limits.

9. **Audit environment variables.** Search for any string that looks like a key or secret in source files.

10. **Review the production deployment.** Confirm schema enforcement is on, and that `CONVEX_DEPLOY_KEY` is only in CI/CD secrets — not in the repo.

---

## Absolute Rules (No Exceptions)

- Never run `npx convex deploy` unless explicitly instructed by the user
- Never run git commands unless explicitly instructed by the user
- Never trust a user ID, role, or permission level provided as a function argument
- Never use `v.any()` for args that touch the database
- Never surface raw internal errors or stack traces to API callers
- Never access `process.env` in a query or mutation — only in actions (`"use node"`)
- Never put API keys, secrets, or tokens in source code or schema definitions
- Never use the same error message to distinguish "not found" from "forbidden" on sensitive resources — use the same message for both to prevent enumeration

---

## References

| Resource | URL |
|---|---|
| Convex Documentation | https://docs.convex.dev |
| Auth in Functions | https://docs.convex.dev/auth/functions-auth |
| Internal Functions | https://docs.convex.dev/functions/internal-functions |
| Argument Validation | https://docs.convex.dev/functions/validation |
| Best Practices | https://docs.convex.dev/understanding/best-practices |
| Production Security | https://docs.convex.dev/production |
| Authorization Guide | https://stack.convex.dev/authorization |
| Rate Limiting Guide | https://stack.convex.dev/rate-limiting |
| Rate Limiter Component | https://www.convex.dev/components/rate-limiter |
| Convex LLMs.txt | https://docs.convex.dev/llms.txt |

```

fortknoxsecurityscanner | SkillHub