convex — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited convex (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Convex is a reactive database where queries are TypeScript functions. The sync engine (queries + mutations + database) is the heart of Convex — center your app around it.
| Type | DB Access | Deterministic | Cached/Reactive | Use For |
|---|---|---|---|---|
query | Read only | Yes | Yes | All reads, subscriptions |
mutation | Read/Write | Yes | No | All writes (transactions) |
action | Via ctx.run\* | No | No | External APIs, LLMs, email |
httpAction | Via ctx.run\* | No | No | Webhooks, custom HTTP |
Key rule: Queries and mutations cannot make network requests. Actions cannot directly access the database.
convex/
├── _generated/ # Auto-generated types (commit this)
├── schema.ts # Database schema
├── model/ # Helper functions (most logic lives here)
│ ├── users.ts
│ └── messages.ts
├── users.ts # Thin wrappers exposing public API
├── messages.ts
├── crons.ts # Cron job definitions
└── http.ts # HTTP action routes// convex/messages.ts
import { query, mutation, internalMutation } from './_generated/server';
import { internal } from './_generated/api';
import { v } from 'convex/values';
// PUBLIC query with validators (always validate public functions)
export const list = query({
args: { channelId: v.id('channels') },
handler: async (ctx, { channelId }) => {
return await ctx.db
.query('messages')
.withIndex('by_channel', (q) => q.eq('channelId', channelId))
.order('desc')
.take(50);
}
});
// PUBLIC mutation with validators and auth check
export const send = mutation({
args: { channelId: v.id('channels'), body: v.string() },
handler: async (ctx, { channelId, body }) => {
const user = await ctx.auth.getUserIdentity();
if (!user) throw new Error('Unauthorized');
await ctx.db.insert('messages', {
channelId,
body,
authorId: user.subject
});
}
});
// INTERNAL mutation (for scheduling, crons, actions)
export const deleteOld = internalMutation({
args: { before: v.number() },
handler: async (ctx, { before }) => {
const old = await ctx.db
.query('messages')
.withIndex('by_createdAt', (q) => q.lt('_creationTime', before))
.take(100);
for (const msg of old) {
await ctx.db.delete(msg._id);
}
}
});Most logic should live in helper functions, NOT in query/mutation handlers:
// convex/model/users.ts
import { QueryCtx, MutationCtx } from '../_generated/server';
import { Doc } from '../_generated/dataModel';
export async function getCurrentUser(
ctx: QueryCtx
): Promise<Doc<'users'> | null> {
const identity = await ctx.auth.getUserIdentity();
if (!identity) return null;
return await ctx.db
.query('users')
.withIndex('by_tokenIdentifier', (q) =>
q.eq('tokenIdentifier', identity.tokenIdentifier)
)
.unique();
}
export async function requireUser(ctx: QueryCtx): Promise<Doc<'users'>> {
const user = await getCurrentUser(ctx);
if (!user) throw new Error('Unauthorized');
return user;
}// convex/ai.ts
import { action, internalMutation } from './_generated/server';
import { internal } from './_generated/api';
import { v } from 'convex/values';
export const summarize = action({
args: { documentId: v.id('documents') },
handler: async (ctx, { documentId }) => {
// Read data via internal query
const doc = await ctx.runQuery(internal.documents.get, { documentId });
// Call external API
const response = await fetch('https://api.openai.com/v1/...', {...});
const summary = await response.json();
// Write result via internal mutation
await ctx.runMutation(internal.documents.setSummary, {
documentId,
summary: summary.text
});
}
});
// Trigger action from mutation (not directly from client)
export const requestSummary = mutation({
args: { documentId: v.id('documents') },
handler: async (ctx, { documentId }) => {
const user = await ctx.auth.getUserIdentity();
if (!user) throw new Error('Unauthorized');
await ctx.db.patch(documentId, { status: 'processing' });
// Schedule action (runs after mutation commits)
await ctx.scheduler.runAfter(0, internal.ai.summarizeInternal, {
documentId
});
}
});import { ConvexError } from 'convex/values';
export const assignRole = mutation({
args: { roleId: v.id('roles'), userId: v.id('users') },
handler: async (ctx, { roleId, userId }) => {
const existing = await ctx.db
.query('assignments')
.withIndex('by_role', (q) => q.eq('roleId', roleId))
.first();
if (existing) {
throw new ConvexError({
code: 'ROLE_TAKEN',
message: 'Role is already assigned'
});
}
await ctx.db.insert('assignments', { roleId, userId });
}
});internal. functions for all ctx.run*, ctx.scheduler, and cronsv.* validatorsctx.auth.getUserIdentity().withIndex() instead of .filter()no-floating-promises ESLint rule)ConvexError for user-facing errorsapi. functions for scheduling (use internal.).filter() on queries — use indexes or TypeScript filter.collect() on unbounded queries (use .take() or pagination)Date.now() in queries (breaks caching)ctx.runQuery/runMutation calls in actions (batch them)ctx.runAction unless switching runtimes (use helper functions)For detailed patterns, see:
Auth Provider Skills (add to project as needed):
convex-auth — Universal auth patterns, storing users, debuggingconvex-clerk — Clerk setup, webhooks, JWT configurationconvex-workos — WorkOS AuthKit setup, auto-provisioning~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.