Spooled Backend
Job queue + worker coordination service built with Rust
Live Demo (SpriteForge) β’ Documentation β’ Website
Spooled is a high-performance, multi-tenant job queue system designed for reliability, observability, and horizontal scalability.
β¨ Features
- π High Performance: Built on Rust + Tokio + PostgreSQL with Redis caching (~28x faster auth)
- β‘ Optimized gRPC: HTTP/2 keepalive, TCP optimizations, and connection pooling for ~3x faster throughput
- π Multi-Tenant: PostgreSQL Row-Level Security (RLS) for data isolation
- π Observable: Prometheus metrics, Grafana dashboards, optional OpenTelemetry export (
--features otel) - π Reliable: At-leastβ¦
Spooled Backend
Job queue + worker coordination service built with Rust
Live Demo (SpriteForge) β’ Documentation β’ Website
Spooled is a high-performance, multi-tenant job queue system designed for reliability, observability, and horizontal scalability.
β¨ Features
- π High Performance: Built on Rust + Tokio + PostgreSQL with Redis caching (~28x faster auth)
- β‘ Optimized gRPC: HTTP/2 keepalive, TCP optimizations, and connection pooling for ~3x faster throughput
- π Multi-Tenant: PostgreSQL Row-Level Security (RLS) for data isolation
- π Observable: Prometheus metrics, Grafana dashboards, optional OpenTelemetry export (
--features otel) - π Reliable: At-least-once processing with leases + retries (use idempotency keys for exactly-once effects)
- β‘ Real-Time: WebSocket + SSE for live job/queue updates
- π Secure: Bcrypt API keys with Redis caching, JWT auth, HMAC webhook verification
- π Scalable: Stateless API nodes (Kubernetes-friendly) + DB-backed locking (
FOR UPDATE SKIP LOCKED) - ποΈ Scheduling: Cron-based recurring jobs with timezone support
- π Workflows: Job dependencies with DAG execution
- π Dual Protocol: REST API (
:8080) + real gRPC (GRPC_PORT, default:50051) with streaming support - π Tier-Based Limits: Automatic enforcement across all endpoints (HTTP, gRPC, workflows, schedules)
- π Dead Letter Queue: Automatic retry and purge operations for failed jobs
- π Webhooks: Outgoing webhook delivery with automatic retries and status tracking
- π³ Billing: Stripe integration for subscriptions and usage tracking
π³ Quick Start with Docker
Pull and Run
# Pull the multi-arch image (supports amd64 and arm64)
docker pull ghcr.io/spooled-cloud/spooled-backend:latest
# Run with Docker Compose
curl -O https://raw.githubusercontent.com/spooled-cloud/spooled-backend/main/docker-compose.prod.yml
curl -O https://raw.githubusercontent.com/spooled-cloud/spooled-backend/main/.env.example
cp .env.example .env
# Generate secure secrets
export JWT_SECRET=$(openssl rand -base64 32)
export POSTGRES_PASSWORD=$(openssl rand -base64 16)
sed -i "s/your-jwt-secret-minimum-32-characters-long/$JWT_SECRET/" .env
sed -i "s/your_secure_password/$POSTGRES_PASSWORD/g" .env
# Start services
docker compose -f docker-compose.prod.yml up -d
# Verify
curl http://localhost:8080/health
Environment Variables
| Variable | Required | Default | Description |
|---|---|---|---|
DATABASE_URL | β | - | PostgreSQL connection string |
JWT_SECRET | β | - | 32+ char secret for JWT signing |
ADMIN_API_KEY | β | - | Key for admin portal access |
REDIS_URL | β | redis://localhost:6379 | Redis for pub/sub & caching |
RUST_ENV | β | development | development/staging/production |
REGISTRATION_MODE | β | open | open/closed - controls public registration |
PORT | β | 8080 | REST API server port |
GRPC_PORT | β | 50051 | gRPC API server port |
GRPC_TLS_ENABLED | β | true (prod) | Enable TLS for gRPC (required for Cloudflare Tunnel) |
GRPC_TLS_CERT_PATH | β | /certs/grpc-cert.pem | Path to TLS certificate (PEM) |
GRPC_TLS_KEY_PATH | β | /certs/grpc-key.pem | Path to TLS private key (PEM) |
METRICS_PORT | β | 9090 | Prometheus metrics port |
METRICS_TOKEN | β | - | If set, requires Authorization: Bearer <token> for /metrics |
Plan Limits via Environment Variables (Self-Hosted)
Spooled ships with sensible built-in plan defaults (Free/Starter/Pro/Enterprise), but you can override every plan limit via env vars.
Limits resolution order (lowest β highest precedence):
- Built-in defaults
SPOOLED_PLAN_LIMITS_JSON(global per-tier JSON map)SPOOLED_PLAN_<TIER>_LIMITS_JSON(tier-specific JSON)SPOOLED_PLAN_<TIER>_<FIELD>(tier-specific individual fields)- Organization
custom_limits(DB, per-org override)
JSON overrides
SPOOLED_PLAN_LIMITS_JSON: JSON object mapping tier β overrides (same keys asorganizations.custom_limits)SPOOLED_PLAN_FREE_LIMITS_JSON,SPOOLED_PLAN_STARTER_LIMITS_JSON,SPOOLED_PLAN_PRO_LIMITS_JSON,SPOOLED_PLAN_ENTERPRISE_LIMITS_JSON
Example:
{
"free": { "max_jobs_per_day": 5000, "max_payload_size_bytes": 131072 },
"starter": { "max_active_jobs": 2000 },
"enterprise": { "max_jobs_per_day": null }
}
Notes:
- For optional limits (like
max_jobs_per_day),nullmeans unlimited.
Per-field overrides
You can override individual fields per tier with env vars:
-
Limits (support
unlimited/none/null/-1): -
SPOOLED_PLAN_<TIER>_MAX_JOBS_PER_DAY -
SPOOLED_PLAN_<TIER>_MAX_ACTIVE_JOBS -
SPOOLED_PLAN_<TIER>_MAX_QUEUES -
SPOOLED_PLAN_<TIER>_MAX_WORKERS -
SPOOLED_PLAN_<TIER>_MAX_API_KEYS -
SPOOLED_PLAN_<TIER>_MAX_SCHEDULES -
SPOOLED_PLAN_<TIER>_MAX_WORKFLOWS -
SPOOLED_PLAN_<TIER>_MAX_WEBHOOKS -
Sizes / rates / retention:
-
SPOOLED_PLAN_<TIER>_MAX_PAYLOAD_SIZE_BYTES -
SPOOLED_PLAN_<TIER>_RATE_LIMIT_RPS -
SPOOLED_PLAN_<TIER>_RATE_LIMIT_BURST -
SPOOLED_PLAN_<TIER>_JOB_RETENTION_DAYS -
SPOOLED_PLAN_<TIER>_HISTORY_RETENTION_DAYS
Where <TIER> is one of: FREE, STARTER, PRO, ENTERPRISE.
π§ Local Development
Prerequisites
- Rust 1.85+
- Docker & Docker Compose
- PostgreSQL 16+ (or use Docker)
- Redis 7+ (optional, for pub/sub)
Setup
# Clone repository
git clone https://github.com/spooled-cloud/spooled-backend.git
cd spooled-backend
# Start dependencies
docker compose up -d postgres redis
# Run migrations and start server
cargo run
# Run tests
cargo test
API Endpoints
Core Job Management
| Method | Endpoint | Description |
|---|---|---|
GET | /health | Health check |
POST | /api/v1/jobs | Create a job (enforces plan limits) |
GET | /api/v1/jobs | List jobs |
POST | /api/v1/jobs/bulk | Bulk enqueue jobs (enforces plan limits) |
POST | /api/v1/jobs/claim | Claim (lease) jobs for worker processing |
POST | /api/v1/jobs/{id}/complete | Mark a job completed (worker ack) |
POST | /api/v1/jobs/{id}/fail | Mark a job failed (worker nack) |
POST | /api/v1/jobs/{id}/heartbeat | Extend a job lease (long-running jobs) |
GET | /api/v1/jobs/stats | Get job statistics |
Dead Letter Queue (DLQ)
| Method | Endpoint | Description |
|---|---|---|
GET | /api/v1/jobs/dlq | List jobs in dead letter queue |
POST | /api/v1/jobs/dlq/retry | Retry jobs from DLQ (enforces plan limits) |
POST | /api/v1/jobs/dlq/purge | Purge jobs from DLQ |
Organizations
| Method | Endpoint | Description |
|---|---|---|
POST | /api/v1/organizations | Create organization (returns initial API key) |
GET | /api/v1/organizations/usage | Get plan usage and limits |
GET | /api/v1/organizations/check-slug | Check if slug is available |
POST | /api/v1/organizations/generate-slug | Generate unique slug from name |
Schedules & Workflows
| Method | Endpoint | Description |
|---|---|---|
POST | /api/v1/schedules | Create cron schedule |
POST | /api/v1/schedules/{id}/trigger | Manually trigger schedule (enforces plan limits) |
POST | /api/v1/workflows | Create workflow/DAG (enforces plan limits) |
Webhooks
| Method | Endpoint | Description |
|---|---|---|
POST | /api/v1/outgoing-webhooks | Configure outgoing notifications |
GET | /api/v1/outgoing-webhooks/{id}/deliveries | Get delivery history |
POST | /api/v1/outgoing-webhooks/{id}/retry/{delivery_id} | Retry failed delivery |
POST | /api/v1/webhooks/{org_id}/custom | Incoming webhook (ingestion β creates jobs) |
Real-Time
| Method | Endpoint | Description |
|---|---|---|
GET | /api/v1/ws | WebSocket for real-time |
GET | /api/v1/events | SSE stream of all events |
GET | /api/v1/events/queues/{name} | SSE stream of queue updates |
GET | /api/v1/events/jobs/{id} | SSE stream of job updates |
Authentication
| Method | Endpoint | Description |
|---|---|---|
POST | /api/v1/auth/login | Exchange API key for JWT |
POST | /api/v1/auth/refresh | Refresh JWT token |
POST | /api/v1/auth/email/start | Start email-based login |
POST | /api/v1/auth/email/verify | Verify email login code |
Billing
| Method | Endpoint | Description |
|---|---|---|
GET | /api/v1/billing/status | Get billing status |
POST | /api/v1/billing/portal | Create Stripe customer portal session |
Admin API (requires X-Admin-Key header)
| Method | Endpoint | Description |
|---|---|---|
GET | /api/v1/admin/organizations | List all organizations |
POST | /api/v1/admin/organizations | Create organization with plan tier |
GET | /api/v1/admin/organizations/{id} | Get organization details |
PATCH | /api/v1/admin/organizations/{id} | Update organization (plan, status) |
DELETE | /api/v1/admin/organizations/{id} | Delete organization (soft or hard) |
POST | /api/v1/admin/organizations/{id}/api-keys | Create API key for organization |
POST | /api/v1/admin/organizations/{id}/reset-usage | Reset daily usage counters |
GET | /api/v1/admin/stats | Platform-wide statistics |
GET | /api/v1/admin/plans | List available plans with limits |
Quick Examples
Create Your First Job
# 1. Create an organization (returns initial API key - save it!)
RESPONSE=$(curl -s -X POST http://localhost:8080/api/v1/organizations \
-H "Content-Type: application/json" \
-d '{"name": "My Company", "slug": "my-company"}')
echo "$RESPONSE"
# Save the api_key from the response - it's only shown once!
API_KEY=$(echo "$RESPONSE" | jq -r '.api_key')
# 2. Create a job using the API key
curl -X POST http://localhost:8080/api/v1/jobs \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $API_KEY" \
-d '{
"queue_name": "emails",
"payload": {"to": "user@example.com", "subject": "Hello!"},
"priority": 0,
"max_retries": 3
}'
Create a Cron Schedule (Recurring Jobs)
# Run daily sales report every day at 9 AM
curl -X POST http://localhost:8080/api/v1/schedules \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "daily-sales-report",
"cron_expression": "0 0 9 * * *",
"timezone": "America/New_York",
"queue_name": "reports",
"payload_template": {"report_type": "daily_sales"}
}'
Create a Workflow (Job Dependencies)
# User onboarding: create account β send email β setup defaults
curl -X POST http://localhost:8080/api/v1/workflows \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "user-onboarding",
"jobs": [
{
"name": "create-account",
"queue_name": "users",
"payload": {"email": "user@example.com"}
},
{
"name": "send-welcome",
"queue_name": "emails",
"depends_on": ["create-account"],
"payload": {"template": "welcome"}
},
{
"name": "setup-defaults",
"queue_name": "users",
"depends_on": ["create-account"],
"payload": {"settings": {}}
}
]
}'
Configure Outgoing Webhooks (Notifications)
# Get notified in Slack when jobs fail
curl -X POST http://localhost:8080/api/v1/outgoing-webhooks \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Slack Alerts",
"url": "https://hooks.slack.com/services/YOUR/WEBHOOK/URL",
"events": ["job.failed", "queue.paused"],
"secret": "your-hmac-secret"
}'
π gRPC API
Spooled provides a real gRPC API using HTTP/2 + Protobuf for high-performance worker communication.
Endpoints
- Spooled Cloud (TLS):
grpc.spooled.cloud:443 - Self-hosted / local:
localhost:50051(or whateverGRPC_PORTis set to)
gRPC TLS (Cloudflare Tunnel)
When using Cloudflare Tunnel with HTTPS origin, gRPC TLS is required because HTTP/2 needs TLS at the origin.
The production docker-compose includes:
- TLS enabled by default (
GRPC_TLS_ENABLED=true) - Self-signed certificates in
./certs/(10-year validity) - Performance Optimized: HTTP/2 keepalives, TCP_NODELAY, and tuned connection windows
Cloudflare Tunnel Configuration:
- Service Type:
HTTPS - URL:
backend:50051 - HTTP2 Connection:
ON - No TLS Verify:
ON(required for self-signed certs)
Note: Cloudflare Tunnel requires HTTPS for HTTP/2 (gRPC). You cannot use plaintext HTTP with gRPC through Cloudflare.
To disable TLS for local development (without Cloudflare):
GRPC_TLS_ENABLED=false cargo run
Proto Definition
The service definitions are in proto/spooled.proto:
service QueueService {
rpc Enqueue(EnqueueRequest) returns (EnqueueResponse);
rpc Dequeue(DequeueRequest) returns (DequeueResponse);
rpc Complete(CompleteRequest) returns (CompleteResponse);
rpc Fail(FailRequest) returns (FailResponse);
rpc RenewLease(RenewLeaseRequest) returns (RenewLeaseResponse);
rpc GetJob(GetJobRequest) returns (GetJobResponse);
rpc GetQueueStats(GetQueueStatsRequest) returns (GetQueueStatsResponse);
// Server-side streaming for continuous job delivery
rpc StreamJobs(StreamJobsRequest) returns (stream Job);
// Bidirectional streaming for real-time job processing
rpc ProcessJobs(stream ProcessRequest) returns (stream ProcessResponse);
}
service WorkerService {
rpc Register(RegisterWorkerRequest) returns (RegisterWorkerResponse);
rpc Heartbeat(HeartbeatRequest) returns (HeartbeatResponse);
rpc Deregister(DeregisterRequest) returns (DeregisterResponse);
}
gRPC Features
- β‘ ~28x faster than HTTP (with Redis cache: ~50ms vs 1400ms per operation)
- π‘οΈ 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 (x-api-key header)
Note: The default gRPC port is
50051. If this port is in use (e.g., by Multipass on macOS), setGRPC_PORT=50052or another available port. See gRPC Server Guide for details.
gRPC Quick Start
# Test with grpcurl (install: brew install grpcurl)
# List services (reflection enabled)
grpcurl -plaintext localhost:50051 list
# Enqueue a job
grpcurl -plaintext \
-H "x-api-key: sp_live_your_key" \
-d '{
"queue_name": "emails",
"payload": {"to": "user@example.com"},
"priority": 0,
"max_retries": 3
}' \
localhost:50051 spooled.v1.QueueService/Enqueue
# Dequeue jobs
grpcurl -plaintext \
-H "x-api-key: sp_live_your_key" \
-d '{"queue_name": "emails", "worker_id": "worker-1", "batch_size": 10}' \
localhost:50051 spooled.v1.QueueService/Dequeue
# Stream jobs (server streaming)
grpcurl -plaintext \
-H "x-api-key: sp_live_your_key" \
-d '{"queue_name": "emails", "worker_id": "worker-1", "lease_duration_secs": 300}' \
localhost:50051 spooled.v1.QueueService/StreamJobs
gRPC Features
| Feature | Description |
|---|---|
| Health Check | Standard gRPC health protocol (grpc.health.v1.Health) |
| Reflection | Service discovery for debugging tools |
| Streaming | Server + bidirectional streaming for efficient workers |
| Compression | gzip compression supported |
| Auth | x-api-key or authorization: Bearer metadata |
When to Use gRPC vs REST
| Use Case | Recommended |
|---|---|
| Web/mobile apps | REST API |
| Dashboard/admin | REST API |
| High-throughput workers | gRPC |
| Streaming job delivery | gRPC StreamJobs |
| Language with gRPC SDK | gRPC |
π Plan Limits & Tiers
Spooled enforces tier-based limits automatically across all endpoints to prevent abuse and enable fair multi-tenancy.
Available Tiers
| Tier | Active Jobs | Daily Jobs | Queues | Workers | Webhooks | Schedules | Workflows |
|---|---|---|---|---|---|---|---|
| Free | 10 | 1,000 | 5 | 3 | 2 | 5 | 2 |
| Starter | 100 | 100,000 | 25 | 25 | 10 | 25 | 10 |
| Enterprise | Unlimited | Unlimited | Unlimited | Unlimited | Unlimited | Unlimited | Unlimited |
Limit Enforcement
Limits are automatically enforced on:
- β
HTTP API:
POST /jobs,POST /jobs/bulk - β
gRPC API:
Enqueueoperation - β Workflows: Counts all jobs in the workflow
- β Schedules: When triggered (manual or automatic)
- β DLQ Retry: When retrying jobs from dead letter queue
- β Workers: Registration and concurrent operations
- β Queues: Creation and configuration
- β Webhooks: Creation and updates
Limit Exceeded Response
When a limit is exceeded, the API returns 403 Forbidden:
{
"error": "limit_exceeded",
"message": "active jobs limit reached (10/10). Upgrade to starter for higher limits.",
"resource": "active_jobs",
"current": 10,
"limit": 10,
"plan": "free",
"upgrade_to": "starter"
}
For gRPC, the status code is RESOURCE_EXHAUSTED with a descriptive message.
Performance Characteristics
HTTP API (with Redis caching enabled):
- First request (cache miss): ~100ms (includes bcrypt)
- Subsequent requests (cache hit): ~50ms (Redis lookup + DB operation)
- ~28x faster with cache compared to bcrypt-only
gRPC API:
- Batch operations: ~50ms per batch
- Streaming: Real-time job delivery with minimal latency
- Recommended for high-throughput workers
Custom Limits
Enterprise customers can request custom limits via custom_limits in the database:
UPDATE organizations
SET custom_limits = '{"max_active_jobs": 10000, "max_jobs_per_day": 1000000}'::jsonb
WHERE id = 'org-id';
π Production Deployment
Docker Compose (Recommended for Single Server)
# Download production compose file
curl -O https://raw.githubusercontent.com/spooled-cloud/spooled-backend/main/docker-compose.prod.yml
# Configure environment
cat > .env << EOF
POSTGRES_PASSWORD=$(openssl rand -base64 16)
JWT_SECRET=$(openssl rand -base64 32)
RUST_ENV=production
JSON_LOGS=true
EOF
# Deploy
docker compose -f docker-compose.prod.yml up -d
# Enable monitoring (optional)
docker compose -f docker-compose.prod.yml --profile monitoring up -d
Kubernetes
# Create namespace and secrets
kubectl create namespace spooled
kubectl create secret generic spooled-secrets \
--namespace spooled \
--from-literal=database-url='postgres://user:pass@postgres:5432/spooled' \
--from-literal=jwt-secret="$(openssl rand -base64 32)"
# Deploy with Kustomize
kubectl apply -k k8s/overlays/production
# Or with Helm (coming soon)
# helm install spooled ./charts/spooled -n spooled
ARM64 / Raspberry Pi / AWS Graviton
Images are automatically built for both amd64 and arm64:
# Explicit platform selection
docker pull --platform linux/arm64 ghcr.io/spooled-cloud/spooled-backend:latest
π Monitoring
Prometheus Metrics
curl -H "Authorization: Bearer $METRICS_TOKEN" http://localhost:9090/metrics
# Key metrics:
# spooled_jobs_pending - Jobs waiting
# spooled_jobs_processing - Jobs in progress
# spooled_job_duration_seconds - Processing time histogram
# spooled_workers_healthy - Healthy worker count
Grafana Dashboard
Access at http://localhost:3000 (admin/admin) when using --profile monitoring.
Pre-configured dashboards:
- Spooled Overview: Job throughput, queue depth, latency
- Worker Status: Health, capacity, distribution
Distributed Tracing (Jaeger)
# Build with OpenTelemetry support
cargo build --features otel
# Run with tracing
OTEL_EXPORTER_OTLP_ENDPOINT=http://jaeger:4317 ./target/release/spooled-backend
π Security
- Authentication: API keys (bcrypt hashed) or JWT tokens
- Multi-tenancy: PostgreSQL Row-Level Security (RLS)
- Rate Limiting: Per-key limits with Redis (fails closed when configured)
- Webhooks: HMAC-SHA256 signature verification
- Input Validation: All inputs sanitized and size-limited
- SSRF Protection: Webhook URLs validated in production
π Documentation
Getting Started
- Quick Start Guide β Get running in 5 minutes
- Getting Started (Laravel users) β Familiar concepts for Laravel developers
- Real-world examples β 5 beginner-friendly examples you can copy/paste
Core Concepts
- Jobs & Queues β Job lifecycle, creation, and processing
- Workers β Building production workers
- Retries & DLQ β Retry configuration and dead letter queue
- Webhooks β Incoming and outgoing webhooks
Reference
- API Usage Guide β Complete REST API reference
- gRPC Server Guide β High-performance gRPC API
- SDKs β Node.js, Python, Go, PHP SDKs
- OpenAPI Spec β OpenAPI 3.1 specification
Operations
- Architecture β System design and data flow
- Deployment Guide β Docker, Kubernetes, production checklist
- Operations Guide β Monitoring, maintenance, troubleshooting
ποΈ Architecture
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β SPOOLED BACKEND β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β REST API (Axum) β gRPC (Tonic) β WebSocket/SSE β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β Queue Manager (FOR UPDATE SKIP LOCKED) β
β Worker Coordination & Heartbeat β
β Scheduler (Cron, Dependencies, Retries) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β PostgreSQL 16+ β Redis 7+ β Prometheus β
β (+ PgBouncer) β (Pub/Sub) β (Metrics) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
π€ Contributing
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing) - Commit changes (
git commit -m 'Add amazing feature') - Push to branch (
git push origin feature/amazing) - Open a Pull Request
π License
Apache License 2.0 - see LICENSE for details.
Built with β€οΈ in Rust