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 otel)FOR UPDATE SKIP LOCKED):8080) + real gRPC (GRPC_PORT, default :50051) with streaming support# 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
| 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 |
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):
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)custom_limits (DB, per-org override)SPOOLED_PLAN_LIMITS_JSON: JSON object mapping tier → overrides (same keys as organizations.custom_limits)SPOOLED_PLAN_FREE_LIMITS_JSON, SPOOLED_PLAN_STARTER_LIMITS_JSON, SPOOLED_PLAN_PRO_LIMITS_JSON, SPOOLED_PLAN_ENTERPRISE_LIMITS_JSONExample:
{
"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), null means unlimited.
You can override individual fields per tier with env vars:
unlimited / none / null / -1):SPOOLED_PLAN_<TIER>_MAX_JOBS_PER_DAYSPOOLED_PLAN_<TIER>_MAX_ACTIVE_JOBSSPOOLED_PLAN_<TIER>_MAX_QUEUESSPOOLED_PLAN_<TIER>_MAX_WORKERSSPOOLED_PLAN_<TIER>_MAX_API_KEYSSPOOLED_PLAN_<TIER>_MAX_SCHEDULESSPOOLED_PLAN_<TIER>_MAX_WORKFLOWSSPOOLED_PLAN_<TIER>_MAX_WEBHOOKSSPOOLED_PLAN_<TIER>_MAX_PAYLOAD_SIZE_BYTESSPOOLED_PLAN_<TIER>_RATE_LIMIT_RPSSPOOLED_PLAN_<TIER>_RATE_LIMIT_BURSTSPOOLED_PLAN_<TIER>_JOB_RETENTION_DAYSSPOOLED_PLAN_<TIER>_HISTORY_RETENTION_DAYSWhere <TIER> is one of: FREE, STARTER, PRO, ENTERPRISE.
# 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
| 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 |
| 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 |
| 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 |
| 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) |
| 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) |
| 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 |
| 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 |
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/v1/billing/status |
Get billing status |
POST |
/api/v1/billing/portal |
Create Stripe customer portal session |
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 |
# 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
}'
# 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"}
}'
# 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": {}}
}
]
}'
# 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"
}'
Spooled provides a real gRPC API using HTTP/2 + Protobuf for high-performance worker communication.
grpc.spooled.cloud:443localhost:50051 (or whatever GRPC_PORT is set to)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
The service definitions are in proto/spooled.proto:
```protobuf 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 streamin
$ claude mcp add spooled-backend \
-- python -m otcore.mcp_server <graph>