Official Node.js/TypeScript SDK for Spooled Cloud - a modern job queue service for distributed applications.
Live Demo (SpriteForge) • Documentation
- Full TypeScript support with comprehensive type definitions
- ESM and CommonJS dual package support
- Automatic retries with exponential backoff and jitter
- Circuit breaker for fault tolerance
- Realtime events via WebSocket and SSE
- Worker runtime for processing jobs
- Workflow DAGs for complex job dependencies
- gRPC streaming for high-performance workers (with automatic limit enforcement)
- Tier-based limits with automatic enforcement across all endpoints
- Organization management with usage tracking
- Webhook management with automatic retries
- Dead Letter Queue (DLQ) operations
- API Key management with Redis caching (~28x faster authentication)
- Billing integration via Stripe
- Automatic case conversion between camelCase (SDK) and snake_case (API)
npm install @spooled/sdkimport { SpooledClient } from "@spooled/sdk";
const client = new SpooledClient({
apiKey: "sp_live_your_api_key",
// For self-hosted: baseUrl: 'https://your-spooled-server.com'
});
// Create a job
const { id } = await client.jobs.create({
queueName: "email-notifications",
payload: {
to: "user@example.com",
subject: "Welcome!",
body: "Thanks for signing up.",
},
priority: 5,
maxRetries: 3,
});
console.log(`Created job: ${id}`);
// Get job status
const job = await client.jobs.get(id);
console.log(`Status: ${job.status}`);| Guide | Description |
|---|---|
| Getting Started | Installation, setup, and first job |
| Configuration | All configuration options |
| Workers | Job processing with SpooledWorker |
| Workflows | Building DAGs with job dependencies |
| gRPC | High-performance streaming |
| Resources | Complete API reference |
See the examples/ directory for runnable code:
| Example | Description |
|---|---|
quick-start.ts |
Basic SDK usage |
worker.ts |
Processing jobs with SpooledWorker |
workflow-dag.ts |
Complex workflows with dependencies |
grpc-streaming.ts |
High-performance gRPC streaming |
realtime.ts |
Real-time event streaming |
schedules.ts |
Cron schedules |
error-handling.ts |
Error handling patterns |
If you want 5 copy/paste “real life” setups (Stripe → jobs, GitHub Actions → jobs, cron schedules, CSV import, website signup), see:
https://github.com/spooled-cloud/spooled-backend/blob/main/docs/guides/real-world-examples.md
Jobs are units of work with payloads, priorities, and retry policies:
const { id } = await client.jobs.create({
queueName: "my-queue",
payload: { data: "value" },
priority: 5, // -100 to 100
maxRetries: 3,
timeoutSeconds: 300,
scheduledAt: new Date(Date.now() + 60000),
idempotencyKey: "unique-key",
});Process jobs with the built-in worker runtime:
import { SpooledClient, SpooledWorker } from "@spooled/sdk";
const client = new SpooledClient({ apiKey: "sp_live_..." });
const worker = new SpooledWorker(client, {
queueName: "my-queue",
concurrency: 10,
});
worker.process(async (ctx) => {
console.log(`Processing job ${ctx.jobId}`);
await doSomething(ctx.payload);
return { success: true };
});
await worker.start();
// Graceful shutdown
process.on("SIGTERM", () => worker.stop());Orchestrate multiple jobs with dependencies:
const workflow = await client.workflows.create({
name: "ETL Pipeline",
jobs: [
{ key: "extract", queueName: "etl", payload: { step: "extract" } },
{
key: "transform",
queueName: "etl",
payload: { step: "transform" },
dependsOn: ["extract"],
},
{
key: "load",
queueName: "etl",
payload: { step: "load" },
dependsOn: ["transform"],
},
],
});Run jobs on a cron schedule:
const schedule = await client.schedules.create({
name: "Daily Report",
cronExpression: "0 9 * * *", // 9 AM daily; 5-field (min hour dom mon dow) or 6-field (leading seconds) both work
timezone: "America/New_York",
queueName: "reports",
payloadTemplate: { type: "daily" },
});Subscribe to real-time job events:
const realtime = await client.realtime();
realtime.on("job.completed", (data) => {
console.log(`Job completed: ${data.jobId}`);
});
await realtime.connect();
await realtime.subscribe({ queueName: "my-queue" });Manage your organization and track usage:
// Get current usage and limits
const usage = await client.organizations.getUsage();
console.log(
`Active jobs: ${usage.active_jobs.current}/${usage.active_jobs.limit}`,
);
console.log(`Plan: ${usage.plan.tier}`);
// Generate a unique slug for a new organization
const { slug } = await client.organizations.generateSlug("My Company");
// Check if a slug is available
const { available } = await client.organizations.checkSlug("my-company");Configure outgoing webhooks for job events:
// Create webhook
const webhook = await client.webhooks.create({
url: "https://your-app.com/webhooks/spooled",
events: ["job.completed", "job.failed"],
queueName: "my-queue",
secret: "webhook_secret_key",
});
// Retry a failed delivery
await client.webhooks.retryDelivery(webhookId, deliveryId);
// Get delivery history
const deliveries = await client.webhooks.getDeliveries(webhookId);Manage jobs that have exhausted all retries:
// List DLQ jobs
const dlqJobs = await client.jobs.dlq.list({ limit: 100 });
// Retry specific jobs from DLQ
await client.jobs.dlq.retry({
jobIds: ["job-1", "job-2"],
});
// Retry jobs by queue
await client.jobs.dlq.retry({
queueName: "my-queue",
limit: 50,
});
// Purge DLQ (requires confirmation)
await client.jobs.dlq.purge({
queueName: "my-queue",
confirm: true,
});Manage API keys programmatically:
// Create a new API key
const apiKey = await client.apiKeys.create({
name: "Production Worker",
queues: ["emails", "notifications"],
rateLimit: 1000,
});
console.log(`Save this key: ${apiKey.key}`); // Only shown once!
// List all API keys
const keys = await client.apiKeys.list();
// Revoke a key
await client.apiKeys.revoke(keyId);Manage billing via Stripe integration:
// Get billing status
const status = await client.billing.getStatus();
// Create customer portal session
const { url } = await client.billing.createPortal({
returnUrl: "https://your-app.com/settings",
});
// Redirect user to: urlEmail-based passwordless authentication:
// Start email login flow
await client.auth.startEmailLogin("user@example.com");
// User receives email with login link
// Check if email exists
const { exists } = await client.auth.checkEmail("user@example.com");
// Exchange API key for JWT
const { accessToken, refreshToken } = await client.auth.login({
apiKey: "sp_live_...",
});
// Use JWT for subsequent requests
const jwtClient = new SpooledClient({ accessToken });All operations automatically enforce tier-based limits:
| Tier | Active Jobs | Daily Jobs | Queues | Workers | Webhooks |
|---|---|---|---|---|---|
| Free | 10 | 1,000 | 5 | 3 | 2 |
| Starter | 100 | 100,000 | 25 | 25 | 10 |
| Enterprise | Unlimited | Unlimited | Unlimited | Unlimited | Unlimited |
Limits are enforced on:
- ✅ HTTP job creation (
POST /jobs,POST /jobs/bulk) - ✅ gRPC job enqueue
- ✅ Workflow creation (counts all jobs in workflow)
- ✅ Schedule triggers
- ✅ DLQ retry operations
- ✅ Worker registration
- ✅ Queue creation
- ✅ Webhook creation
When a plan quota or limit is exceeded, you'll receive an HTTP 429 response with
code: "QUOTA_EXCEEDED". The SDK reliably preserves the HTTP status, error code, message,
request ID, and rate-limit headers. error.details is populated only when the server nests
metadata under a JSON details field; quota fields sent at the top level of the server payload
(such as resource, current, limit, and plan) are not currently exposed by the SDK.
import { RateLimitError } from "@spooled/sdk";
try {
await client.jobs.create({
/* ... */
});
} catch (error) {
if (error instanceof RateLimitError && error.code === "QUOTA_EXCEEDED") {
console.log(`Plan limit: ${error.message}`);
console.log(`Request ID: ${error.requestId ?? "not provided"}`);
// Present only if this server response included a nested `details` object.
if (error.details) console.log("Details:", error.details);
}
}(Per-second rate limiting also returns 429, as a RateLimitError with the default RATE_LIMIT_EXCEEDED code.)
All errors extend SpooledError with specific subclasses:
import {
NotFoundError,
RateLimitError,
ValidationError,
AuthorizationError,
isSpooledError,
} from "@spooled/sdk";
try {
await client.jobs.get("non-existent");
} catch (error) {
if (error instanceof NotFoundError) {
console.log("Job not found");
} else if (error instanceof AuthorizationError) {
console.log(`Forbidden: ${error.message}`);
} else if (error instanceof RateLimitError) {
// 429: either a plan quota (code "QUOTA_EXCEEDED") or per-second rate limiting
console.log(`Retry after ${error.getRetryAfter()} seconds`);
} else if (isSpooledError(error)) {
console.log(`API error: ${error.code} - ${error.message}`);
}
}For high-throughput workers, use the native gRPC API with the included gRPC client:
import { SpooledGrpcClient } from "@spooled/sdk";
// Connect to gRPC server (Optimized for Cloudflare Tunnel)
// useTls: true if connecting to grpc.spooled.cloud:443 (Cloudflare terminates TLS)
// useTls: false if connecting to localhost or direct backend
const grpcClient = new SpooledGrpcClient({
address: "grpc.spooled.cloud:443",
apiKey: "sp_live_your_key",
useTls: true,
});
// Register worker
const { workerId } = await grpcClient.workers.register({
queueName: "emails",
hostname: "worker-1",
maxConcurrency: 10,
});
// Enqueue job (enforces plan limits automatically)
const { jobId } = await grpcClient.queue.enqueue({
queueName: "emails",
payload: { to: "user@example.com" },
priority: 5,
});
// Dequeue jobs in batch
const { jobs } = await grpcClient.queue.dequeue({
queueName: "emails",
workerId,
batchSize: 10,
leaseDurationSecs: 300,
});
// Process and complete. Echo leaseId when present so current servers can fence stale executions; legacy servers may omit it and then use worker-ID-only fencing.
for (const job of jobs) {
await processJob(job);
await grpcClient.queue.complete({
jobId: job.id,
workerId,
leaseId: job.leaseId ?? undefined,
result: { success: true },
});
}gRPC Features:
- ~28x faster than HTTP (with Redis cache: ~50ms vs 1400ms)
- Automatic plan limit enforcement on enqueue operations
- Batch operations for higher throughput
- Streaming support for real-time job processing
- Secure authentication via API key metadata
See gRPC Guide for complete documentation.
Full TypeScript support with comprehensive type definitions:
import type {
SpooledClientConfig,
Job,
JobStatus,
CreateJobParams,
Queue,
QueueStats,
Schedule,
CreateScheduleParams,
Workflow,
CreateWorkflowParams,
Worker,
WorkerStatus,
} from "@spooled/sdk";Before opening a pull request or tagging a release, run the same checks used by the package and publish workflow:
npm ci
npm run lint
npm run typecheck
npm run build
npm testProduction-safe API verification is separate and credentialed: npm run verify:production. Integration tests use npm run test:integration against a local or isolated test backend.
Maintainers release by pushing a matching vX.Y.Z tag. The tag workflow verifies all same-artifact version surfaces before publishing through npm Trusted Publishing (OIDC) with provenance.
This checklist records evidence and does not block ordinary commits, experiments, or compatibility research. A mismatch among values that identify the same npm artifact is a release error and cannot be waived for publication. Publishing the SDK is not a service deployment; consumers report new metadata only after upgrading and restarting or redeploying their applications.
Before pushing vX.Y.Z:
- Set
package.json.versiontoX.Y.Z. - Regenerate
package-lock.json; verify both top-levelversionandpackages[""].versionareX.Y.Z. - Set
SDK_VERSIONand the default User-Agent insrc/config.tstoX.Y.Z. - Confirm
SpooledWorkerderives its default registration version fromSDK_VERSION. - Add
## [X.Y.Z] - YYYY-MM-DDtoCHANGELOG.mdand review version-bearing worker documentation. - Run
npm ci, lint, typecheck, build, tests, andnpm pack --dry-run. - Review the packed file list and compiled output; confirm the package contains no local logs, caches, credentials, or unintended generated files.
- Record any cross-repository compatibility difference separately; backend, SDK, and application versions are not required to match numerically.
Publish and verify:
- Create one immutable
vX.Y.Ztag on the reviewed commit and never move or recreate a pushed tag. Fix forward with a new patch version after external visibility. - Record the tag commit and publish workflow URL.
- Verify
npm view @spooled/sdk@X.Y.Z version gitHead dist.integrity dist.attestations --jsonagainst the tag and commit. - Confirm
latestpoints toX.Y.Zwhen intended and inspect the published tarball'sSDK_VERSION, User-Agent, and worker registration default. - Confirm npm provenance names this repository,
.github/workflows/publish.yml,refs/tags/vX.Y.Z, and the release commit. - Record registry publication separately from consumer rollout, backend deployment, or documentation deployment.
Apache-2.0 - see LICENSE for details.