Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/rjdellecese/confect/llms.txt

Use this file to discover all available pages before exploring further.

Node actions provide access to the full Node.js runtime, including the filesystem, crypto libraries, and other Node-specific APIs. They’re perfect for tasks that require capabilities beyond the standard Convex runtime.

What are Node Actions?

Node actions are a special type of action that runs in a Node.js environment instead of the standard Convex runtime. This gives you access to:

File System

Read and write files using Node’s fs module

Crypto

Advanced cryptographic operations

Native Modules

Use Node.js native modules and bindings

System APIs

Access operating system functionality

Defining Node Actions

Define a node action using FunctionSpec.nodeAction:
confect/api.ts
import { FunctionSpec } from "@confect/core";
import { Schema } from "effect";

export const processImageSpec = FunctionSpec.nodeAction({
  args: Schema.Struct({
    imageId: Schema.String,
    format: Schema.Literal("jpeg", "png", "webp"),
  }),
  returns: Schema.Struct({
    storageId: Schema.String,
    width: Schema.Number,
    height: Schema.Number,
  }),
});

Implementing Node Actions

Implement node actions with access to the NodeContext service:
confect/images.ts
import { FunctionImpl } from "@confect/server";
import { FileSystem, Path } from "@effect/platform-node";
import { Effect } from "effect";
import { api } from "./api";

export const processImage = FunctionImpl.make(
  api,
  "images",
  "process",
  ({ imageId, format }) =>
    Effect.gen(function* () {
      const fs = yield* FileSystem.FileSystem;
      const path = yield* Path.Path;
      const storage = yield* StorageActionWriter;
      
      // Download image from storage
      const blob = yield* storage.get(imageId);
      const buffer = Buffer.from(await blob.arrayBuffer());
      
      // Use Node.js APIs for image processing
      // (example with sharp library)
      const sharp = require("sharp");
      const processed = yield* Effect.tryPromise(() =>
        sharp(buffer)
          .resize(800, 600)
          .toFormat(format)
          .toBuffer()
      );
      
      // Upload processed image
      const processedBlob = new Blob([processed]);
      const storageId = yield* storage.store(processedBlob);
      
      // Get image metadata
      const metadata = yield* Effect.tryPromise(() =>
        sharp(processed).metadata()
      );
      
      return {
        storageId,
        width: metadata.width!,
        height: metadata.height!,
      };
    }).pipe(Effect.orDie)
);

Available Services

Node actions have access to all action services plus Node.js capabilities:

Effect Platform Node Services

FileSystem

Read and write files

Path

Work with file paths

Terminal

Execute shell commands

Process

Access process information

Confect Services

StorageActionWriter

Upload and download files

QueryRunner

Run database queries

MutationRunner

Run database mutations

Auth

Access authentication

Common Use Cases

File Processing

import { FileSystem } from "@effect/platform-node";
import { Effect } from "effect";

const processCSV = FunctionImpl.make(
  api,
  "data",
  "importCSV",
  ({ fileId }) =>
    Effect.gen(function* () {
      const fs = yield* FileSystem.FileSystem;
      const storage = yield* StorageActionWriter;
      const db = yield* MutationRunner;
      
      // Download CSV from storage
      const blob = yield* storage.get(fileId);
      const text = yield* Effect.promise(() => blob.text());
      
      // Parse CSV
      const csv = require("csv-parse/sync");
      const records = csv.parse(text, {
        columns: true,
        skip_empty_lines: true,
      });
      
      // Import records into database
      for (const record of records) {
        yield* db(refs.internal.records.create, record);
      }
      
      return { imported: records.length };
    }).pipe(Effect.orDie)
);

Cryptographic Operations

import { Effect } from "effect";
import * as crypto from "crypto";

const generateAPIKey = FunctionImpl.make(
  api,
  "auth",
  "generateKey",
  ({ userId }) =>
    Effect.gen(function* () {
      const db = yield* DatabaseWriter<typeof databaseSchema>();
      
      // Generate cryptographically secure API key
      const apiKey = yield* Effect.sync(() =>
        crypto.randomBytes(32).toString("base64url")
      );
      
      // Hash the key for storage
      const hashedKey = yield* Effect.sync(() =>
        crypto
          .createHash("sha256")
          .update(apiKey)
          .digest("hex")
      );
      
      // Store hashed key
      yield* db.table("apiKeys").insert({
        userId,
        hashedKey,
        createdAt: Date.now(),
      });
      
      // Return raw key (only time it's available)
      return { apiKey };
    }).pipe(Effect.orDie)
);

External Command Execution

import { Command } from "@effect/platform-node";
import { Effect } from "effect";

const generatePDF = FunctionImpl.make(
  api,
  "documents",
  "generatePDF",
  ({ html }) =>
    Effect.gen(function* () {
      const storage = yield* StorageActionWriter;
      
      // Use puppeteer or wkhtmltopdf via command
      const result = yield* Command.make("wkhtmltopdf", "-", "-").pipe(
        Command.stdin("inherit"),
        Command.stdout("pipe"),
        Command.start,
        Effect.flatMap((process) =>
          Effect.gen(function* () {
            // Write HTML to stdin
            yield* Effect.tryPromise(() => {
              process.stdin.write(html);
              process.stdin.end();
            });
            
            // Read PDF from stdout
            const chunks: Buffer[] = [];
            for await (const chunk of process.stdout) {
              chunks.push(chunk);
            }
            
            return Buffer.concat(chunks);
          })
        )
      );
      
      // Upload to storage
      const blob = new Blob([result], { type: "application/pdf" });
      const storageId = yield* storage.store(blob);
      
      return { storageId };
    }).pipe(Effect.orDie)
);

Native Database Connections

import { Effect } from "effect";
import { Pool } from "pg";

const syncFromPostgres = FunctionImpl.make(
  api,
  "sync",
  "importUsers",
  () =>
    Effect.gen(function* () {
      const db = yield* DatabaseWriter<typeof databaseSchema>();
      
      // Connect to external PostgreSQL database
      const pool = new Pool({
        host: process.env.PG_HOST,
        database: process.env.PG_DB,
        user: process.env.PG_USER,
        password: process.env.PG_PASSWORD,
      });
      
      const client = yield* Effect.tryPromise(() => pool.connect());
      
      try {
        const result = yield* Effect.tryPromise(() =>
          client.query("SELECT * FROM users WHERE synced = false")
        );
        
        // Import users into Convex
        for (const row of result.rows) {
          yield* db.table("users").insert({
            externalId: row.id,
            name: row.name,
            email: row.email,
            importedAt: Date.now(),
          });
        }
        
        return { imported: result.rows.length };
      } finally {
        client.release();
      }
    }).pipe(Effect.orDie)
);

File System Operations

Use Effect’s FileSystem service for file operations:
import { FileSystem, Path } from "@effect/platform-node";
import { Effect } from "effect";

const processDataFiles = FunctionImpl.make(
  api,
  "data",
  "process",
  ({ directory }) =>
    Effect.gen(function* () {
      const fs = yield* FileSystem.FileSystem;
      const path = yield* Path.Path;
      
      // Read directory
      const files = yield* fs.readDirectory(directory);
      
      // Process each file
      const results = yield* Effect.forEach(
        files.filter(f => f.endsWith(".json")),
        (file) =>
          Effect.gen(function* () {
            const filePath = path.join(directory, file);
            const content = yield* fs.readFileString(filePath);
            const data = JSON.parse(content);
            
            // Process data...
            return data;
          }),
        { concurrency: 5 }
      );
      
      return { processed: results.length };
    }).pipe(Effect.orDie)
);

Best Practices

1

Use for Node-Specific Tasks

Only use node actions when you need Node.js APIs. Standard actions are faster and scale better.
2

Handle Errors Properly

Wrap Node.js operations in Effect.tryPromise or Effect.sync for proper error handling.
3

Clean Up Resources

Always close file handles, database connections, and other resources.
4

Use Effect Services

Prefer Effect’s FileSystem and other services over raw Node.js APIs for better composability.
Node actions have longer cold start times than standard actions. Use them judiciously for operations that truly need Node.js capabilities.

Limitations

  • Node actions run in an isolated Node.js environment
  • They have access to the filesystem but not to your local development machine
  • Network access is allowed but subject to Convex’s security policies
  • Maximum execution time is 10 minutes (same as regular actions)

Next Steps

Storage

Learn about file storage

Actions

Back to functions overview

HTTP API

Build HTTP endpoints

Scheduling

Schedule node actions

Build docs developers (and LLMs) love