My App
Getting Started

Installation & Setup

Install and configure SPANE with Redis and PostgreSQL

Installation & Setup

Requirements

Before installing SPANE, ensure you have:

  • Redis 6.0+: Required for BullMQ queue management and event streaming
  • Node.js 18+ or Bun 1.0+: Runtime environment
  • PostgreSQL 14+ (optional): For persistent state storage
  • TypeScript 5+: For type safety

Installation

Install SPANE via npm, yarn, pnpm, or bun:

npm install @manyeya/spane
# or
yarn add @manyeya/spane
# or
pnpm add @manyeya/spane
# or
bun add @manyeya/spane

Peer Dependencies

SPANE requires TypeScript 5+ as a peer dependency:

npm install -D typescript@^5

Redis Setup

docker run -d -p 6379:6379 redis:7-alpine

Option 2: Local Installation

Follow the official Redis installation guide for your operating system.

Option 3: Managed Redis Service

Use a managed Redis service like:

Redis Configuration

import { Redis } from 'ioredis';

// Local Redis
const redis = new Redis();

// With connection options
const redis = new Redis({
  host: 'localhost',
  port: 6379,
  password: 'your-password', // optional
  db: 0, // Redis database number
  maxRetriesPerRequest: 3,
  retryDelayOnFailover: 100,
  lazyConnect: true,
});

// Using Redis Cloud URL
const redis = new Redis('rediss://:password@host:port');

PostgreSQL Setup (Optional)

PostgreSQL is optional but recommended for production deployments to persist workflow definitions and execution state.

docker run -d \
  -p 5432:5432 \
  -e POSTGRES_USER=spane \
  -e POSTGRES_PASSWORD=spane_password \
  -e POSTGRES_DB=spane \
  postgres:15-alpine

Option 2: Local Installation

Follow the official PostgreSQL installation guide for your operating system.

Database Schema

Set the DATABASE_URL environment variable:

# .env
DATABASE_URL="postgresql://spane:spane_password@localhost:5432/spane"

Run database migrations:

bun run db:push

Environment Variables

Create a .env file in your project root:

# Redis connection
REDIS_URL="redis://localhost:6379"
# or
SPANE_REDIS_URL="redis://localhost:6379"

# PostgreSQL connection (optional, for persistent state)
DATABASE_URL="postgresql://user:password@localhost:5432/spane"
# or
SPANE_DATABASE_URL="postgresql://user:password@localhost:5432/spane"

# Node environment
NODE_ENV="development" # or "production"

Basic Project Setup

Step 1: Create Node Executors

Create custom node executors by implementing the INodeExecutor interface:

// executors/http-executor.ts
import type { INodeExecutor, ExecutionContext, ExecutionResult } from 'spane';

export class HttpExecutor implements INodeExecutor {
  async execute(context: ExecutionContext): Promise<ExecutionResult> {
    const { url, method = 'GET', headers = {} } = context.nodeConfig || {};

    try {
      const response = await fetch(url, {
        method,
        body: method !== 'GET' ? JSON.stringify(context.inputData) : undefined,
        headers: {
          'Content-Type': 'application/json',
          ...headers
        }
      });

      const data = await response.json();

      if (!response.ok) {
        return {
          success: false,
          error: `HTTP ${response.status}: ${response.statusText}`
        };
      }

      return { success: true, data };
    } catch (error) {
      return {
        success: false,
        error: error instanceof Error ? error.message : 'Unknown error'
      };
    }
  }
}

Step 2: Initialize the Engine

// index.ts
import { Redis } from 'ioredis';
import { WorkflowEngine, NodeRegistry } from 'spane';
import { InMemoryExecutionStore } from 'spane/db/inmemory-store';
import { HttpExecutor } from './executors/http-executor';

// Connect to Redis
const redis = new Redis();

// Create node registry
const registry = new NodeRegistry();
registry.register('http', new HttpExecutor());

// Create state store (in-memory for development)
const stateStore = new InMemoryExecutionStore();

// Create engine
const engine = new WorkflowEngine(registry, stateStore, redis);

// Register workflow
await engine.registerWorkflow({
  id: 'example-workflow',
  name: 'Example Workflow',
  entryNodeId: 'fetch',
  nodes: [
    {
      id: 'fetch',
      type: 'http',
      config: { url: 'https://api.example.com/data' },
      inputs: [],
      outputs: []
    }
  ]
});

// Start workers
engine.startWorkers(5);

// Enqueue workflow
const executionId = await engine.enqueueWorkflow('example-workflow', { userId: 123 });
console.log('Execution started:', executionId);

TypeScript Configuration

Ensure your tsconfig.json includes:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "lib": ["ES2022"],
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true,
    "allowSyntheticDefaultImports": true,
    "declaration": true,
    "declarationMap": true,
    "outDir": "./dist",
    "rootDir": "./src"
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}

Production Considerations

Graceful Shutdown

Always handle graceful shutdown to prevent data loss:

process.on('SIGTERM', async () => {
  console.log('SIGTERM received, shutting down gracefully...');
  await engine.close();
  await redis.quit();
  process.exit(0);
});

process.on('SIGINT', async () => {
  console.log('SIGINT received, shutting down gracefully...');
  await engine.close();
  await redis.quit();
  process.exit(0);
});

Error Handling

Wrap your initialization in try-catch:

async function main() {
  try {
    const redis = new Redis();
    const engine = new WorkflowEngine(registry, stateStore, redis);

    await engine.registerWorkflow(workflow);
    engine.startWorkers(5);

    console.log('Engine started successfully');
  } catch (error) {
    console.error('Failed to start engine:', error);
    process.exit(1);
  }
}

main();

Health Checks

Implement health checks for your HTTP API:

import { Redis } from 'ioredis';

async function healthCheck(): Promise<boolean> {
  try {
    // Check Redis connection
    await redis.ping();

    // Check engine state
    const stats = await engine.getQueueStats();

    return true;
  } catch (error) {
    return false;
  }
}

Next Steps

On this page