| name | cursor-plugin-convex-rule-use-node-for-actions |
| description | Use \"use node\" directive in action files that need Node.js APIs. Cannot write queries or mutations in \"use node\" files. |
| metadata | {"version":"0.1.0"} |
Use "use node" for Node.js APIs in Actions
When you need Node.js APIs (fetch, crypto, Buffer, etc.) in Convex, you must use actions with the "use node" directive.
The Rule
Files with "use node" can ONLY contain:
- ✅
action functions
- ✅
internalAction functions
- ✅ Helper functions called by actions
- ❌ NEVER
query or mutation functions
Files without "use node" can contain:
- ✅
query functions
- ✅
mutation functions
- ✅
internalQuery and internalMutation functions
- ❌ Cannot use Node.js-specific APIs
When to Use "use node"
Use actions with "use node" when you need:
External API Calls
"use node";
import { action } from "./_generated/server";
import { v } from "convex/values";
export const fetchWeather = action({
args: { city: v.string() },
handler: async (ctx, args) => {
const response = await fetch(
`https://api.weather.com/weather?city=${args.city}`
);
const data = await response.json();
await ctx.runMutation(api.weather.store, {
city: args.city,
data: data,
});
return data;
},
});
AI/LLM Integrations
"use node";
import { action } from "./_generated/server";
import OpenAI from "openai";
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
export const generateSuggestion = action({
args: { prompt: v.string() },
handler: async (ctx, args) => {
const completion = await openai.chat.completions.create({
model: "gpt-4",
messages: [{ role: "user", content: args.prompt }],
});
return completion.choices[0].message.content;
},
});
Node.js Crypto
"use node";
import { action } from "./_generated/server";
import crypto from "crypto";
export const generateSecureToken = action({
handler: async (ctx) => {
const token = crypto.randomBytes(32).toString("hex");
await ctx.runMutation(api.tokens.store, { token });
return token;
},
});
Third-Party SDKs
"use node";
import { action } from "./_generated/server";
import Stripe from "stripe";
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
export const createPayment = action({
args: { amount: v.number() },
handler: async (ctx, args) => {
const paymentIntent = await stripe.paymentIntents.create({
amount: args.amount,
currency: "usd",
});
return paymentIntent.client_secret;
},
});
File Organization
❌ Wrong: Mixing in Same File
"use node";
import { action, mutation } from "./_generated/server";
export const create = mutation({
handler: async (ctx, args) => {
},
});
export const fetchData = action({
handler: async (ctx) => {
const data = await fetch("...");
return data;
},
});
✅ Correct: Separate Files
convex/tasks.ts (no "use node"):
import { query, mutation } from "./_generated/server";
export const list = query({
handler: async (ctx) => {
return await ctx.db.query("tasks").collect();
},
});
export const create = mutation({
args: { title: v.string() },
handler: async (ctx, args) => {
return await ctx.db.insert("tasks", { title: args.title });
},
});
convex/tasksActions.ts (with "use node"):
"use node";
import { action } from "./_generated/server";
import { api } from "./_generated/api";
export const generateTaskSuggestions = action({
args: { userId: v.id("users") },
handler: async (ctx, args) => {
const response = await fetch("https://ai-service.com/suggest", {
method: "POST",
body: JSON.stringify({ userId: args.userId }),
});
const suggestions = await response.json();
for (const suggestion of suggestions) {
await ctx.runMutation(api.tasks.create, {
title: suggestion.title,
});
}
return suggestions;
},
});
Common Pattern: Action → Mutation
Since actions can't directly modify the database in "use node" files, use this pattern:
"use node";
import { action } from "./_generated/server";
import { api, internal } from "./_generated/api";
export const syncFromExternalAPI = action({
handler: async (ctx) => {
const response = await fetch("https://api.example.com/data");
const data = await response.json();
await ctx.runMutation(internal.data.storeExternal, {
data: data,
});
},
});
import { internalMutation } from "./_generated/server";
export const storeExternal = internalMutation({
args: { data: v.any() },
handler: async (ctx, args) => {
await ctx.db.insert("externalData", args.data);
},
});
What Doesn't Need "use node"
These work in regular queries/mutations without "use node":
Convex Built-in fetch
import { action } from "./_generated/server";
export const fetchData = action({
handler: async (ctx) => {
const response = await fetch("https://api.example.com/data");
return await response.json();
},
});
However, if you need Node.js-specific features like:
- Custom headers with Node.js libraries
- Stream processing
- Node.js crypto
- File system operations
- Third-party SDKs that depend on Node.js
Then you need "use node".
Quick Reference
| Need | Use | Directive | Can Write |
|---|
| Database queries | query | No directive | queries only |
| Database writes | mutation | No directive | mutations only |
| External API | action | "use node" | actions only |
| Node.js APIs | action | "use node" | actions only |
| Third-party SDKs | action | "use node" | actions only |
Red Flags
Watch for these errors:
❌ Mutation in "use node" file
"use node";
export const create = mutation({ ... });
❌ Query in "use node" file
"use node";
export const list = query({ ... });
❌ Missing "use node" with Node APIs
import crypto from "crypto";
export const generate = action({
handler: async (ctx) => {
const token = crypto.randomBytes(32);
},
});
Checklist
When writing Convex functions: