Skip to content

About

Interactive pixel art sprite generator demonstrating Spooled Cloud's distributed job queue. Watch workflows, workers, real-time events, and automatic retries in action. Live demo: example.spooled.cloud

Topics

Resources

Contributing

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

Spooled Logo

SpriteForge

Interactive pixel art generator powered by Spooled Cloud

CI Docker Live Demo License

Live Demo • Quick Start • How It Works • Deployment • API


What is SpriteForge?

SpriteForge is a real-time pixel art sprite generator that demonstrates Spooled Cloud workflows, workers, schedules, retries, and realtime events.

Releases: latest published release

Try it live: example.spooled.cloud

Features Demonstrated

Feature How It's Used
Workflows (DAG) Each sprite is a workflow with parallel frame jobs + dependent assemble job
Workers 3 worker types process frames, assemble sprites, and generate public sprites
Realtime + Reconciliation Spooled WebSocket events are forwarded to the browser over SSE; scoped polling recovers missed updates
Automatic Retries "Chaos mode" simulates transient failures on frame jobs so Spooled retries are visible
Schedules "Sprite of the Minute" generated via cron schedule

Quick Start

Prerequisites

Plan requirements

SpriteForge needs a Starter plan or higher. It will not do its job on a Free key. Limits checked against spooled-backend/src/config/plans.rs (Free / Starter):

Resource SpriteForge uses Free Starter
Workers 3 (frames, assemble, public) 1 5
Queues 3 (spriteforge-frames, -assemble, -public) 2 10
Workflows (counted while pending or running) 1 per Forge click 0 (disabled) 5 at the same time
Active jobs (pending + processing + scheduled) up to 13 per Forge (12 frames + assemble), plus 1 per minute from the public schedule 10 500
Schedules 1 (public sprite) 1 10
Jobs per day the public schedule alone is 1,440/day 1,000 10,000

What happens on Free: the web UI still comes up, but only the first worker registers (the others get 429 QUOTA_EXCEEDED, logged and shown under workers in /health), so /health answers 503 (ok: false, missingWorkers) and POST /api/forge answers 503 workers_unavailable. When the workers do run but the plan has no workflows (Free), POST /api/forge answers 403 feature_disabled ("workflows is not available on the Free plan"), which the app relays as such instead of UNKNOWN_ERROR. On Starter the workflows limit counts workflows that are pending or running, so a sixth simultaneous Forge gets 429 QUOTA_EXCEEDED until one finishes; finished workflows no longer count. Set ENABLE_PUBLIC_SCHEDULE=false if you want to keep the daily job budget for interactive use.

A worker that cannot register for a transient reason (API 5xx, timeout, network) is retried in the background with exponential backoff (2 s up to 60 s), each try on a fresh SpooledWorker; plan-limit and other 4xx refusals are retried every five minutes, so an upgrade or a recovered API is picked up without a restart. /health and Forge turn healthy again as soon as the frames and assemble workers run.

Workers register with stable ids (<WORKER_ID_PREFIX>-frames|assemble|public), so a restart reuses its rows instead of holding the plan worker cap for about two minutes. Worker ids are a global primary key across all Spooled organizations, so the default prefix is spriteforge-<first 8 hex of sha256(SPOOLED_API_KEY)>, not a shared name; if the API still answers 409 (the id belongs to another organization) the worker registers without an id and the server generates one (logged once; each restart then adds a row until the old one expires). Run one replica per prefix (the Kubernetes manifests use one replica and the Recreate strategy for that reason).

Local Development

# Clone the repository
git clone https://github.com/spooled-cloud/spooled-example-spriteforge.git
cd spooled-example-spriteforge

# Copy environment template
cp .env.example .env

# Edit .env and add your API key
# SPOOLED_API_KEY=sp_live_your_key_here

# Install dependencies
npm ci

# Load .env into this shell (Node does not auto-load it), then start
set -a
. ./.env
set +a
npm run dev

Open http://localhost:3000

Docker

# Using Docker Compose (recommended)
cp .env.example .env
# Edit .env with your API key

docker compose up --build

Or run directly:

docker run -p 3000:3000 \
  -e SPOOLED_API_KEY=sp_live_your_key \
  ghcr.io/spooled-cloud/spooled-example-spriteforge:<release-tag>

How It Works

┌─────────────────────────────────────────────────────────────────────────┐
│                              SpriteForge                                │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│   Browser                     Server                    Spooled Cloud   │
│   ───────                     ──────                    ─────────────   │
│                                                                         │
│   ┌──────────┐  click    ┌──────────────┐  create    ┌──────────────┐   │
│   │   UI     │ ────────► │   Node.js    │  ────────► │   Workflow   │   │
│   └──────────┘           │   Server     │            │   + Jobs     │   │
│        ▲                 └──────────────┘            └──────────────┘   │
│        │                        │                           │           │
│        │ SSE              ┌─────┴─────┐              Workers│           │
│        │                  │           │                     ▼           │
│   ┌────┴─────┐      ┌─────┴───┐ ┌─────┴───┐         ┌──────────────┐    │
│   │  Events  │ ◄─── │ frame   │ │ assemble│ ◄────── │   Process    │    │
│   │  Stream  │      │ worker  │ │ worker  │         │   & Return   │    │
│   └──────────┘      └─────────┘ └─────────┘         └──────────────┘    │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

Workflow Structure

When you click "Forge Sprite":

  1. Workflow Created: A DAG workflow with N+1 jobs
  2. Frame Jobs (parallel): Each generates one animation frame
  3. Assemble Job (depends on all frames): Combines frames into sprite
  4. Events Stream: Job status updates flow to your browser in real-time
START ──► [frame-0] [frame-1] [frame-2] ... [frame-N] ──► [assemble] ──► DONE
            │         │         │              │               │
            └─────────┴─────────┴──────────────┘               │
                   all must complete before                    │
                                                               ▼
                                                       sprite rendered

Environment Variables

SPOOLED_API_KEY is the only variable required by the application. The production Compose stack additionally requires CLOUDFLARE_TUNNEL_TOKEN.

See DEPLOY.md#environment-variables for the canonical variable table and .env.example for a copyable template.


Deployment

📘 Full guide: See DEPLOY.md for comprehensive deployment instructions.

Docker Image

The CI workflow publishes multi-architecture (linux/amd64, linux/arm64) images to GHCR. Pushes to main publish latest and a short commit SHA tag; release tags publish the matching version and refresh latest:

ghcr.io/spooled-cloud/spooled-example-spriteforge:latest
ghcr.io/spooled-cloud/spooled-example-spriteforge:<short-commit-sha>
ghcr.io/spooled-cloud/spooled-example-spriteforge:<release-tag>

Docker Compose (Production)

The easiest production deployment with Cloudflare Tunnel:

cp .env.example .env
# Edit .env: add SPOOLED_API_KEY and CLOUDFLARE_TUNNEL_TOKEN

docker compose -f docker-compose.prod.yml up -d

This starts:

  • SpriteForge container (port 3000 internal)
  • Cloudflared tunnel (secure external access)

Kubernetes

# Create secret
kubectl apply -f k8s/base/namespace.yaml
kubectl create secret generic spooled-example-spriteforge-secrets \
  --from-literal=SPOOLED_API_KEY=sp_live_your_key \
  --namespace spooled-example-spriteforge

# Deploy
kubectl apply -k k8s/overlays/production

Included resources:

  • Deployment with liveness/readiness probes
  • Service (ClusterIP)
  • Ingress (nginx + cert-manager TLS)
  • HorizontalPodAutoscaler pinned to one replica (documents the CPU target but does not scale above one)
  • PodDisruptionBudget
  • ServiceAccount + ConfigMap

Platform Deployment Starting Points

These are high-level manual starting points, not tested one-click deployments. This repository does not include Railway, Render, or DigitalOcean manifests; confirm each platform's Docker build, start command, health check, port, persistence, and secret settings before calling a deployment production-ready.

Platform Starting point
Railway Connect the repository, select the Docker build, configure health/port settings, and add SPOOLED_API_KEY
Fly.io fly launch → fly secrets set SPOOLED_API_KEY=... → fly deploy
Render Create a Docker Web Service, configure health/port settings, and add SPOOLED_API_KEY
DigitalOcean Create an App from GitHub, confirm Docker/health/port settings, and add SPOOLED_API_KEY

API

Health Check

GET /health

Returns 200 OK when the Spooled API checks are passing and the frames and assemble workers are running; otherwise 503 with ok: false and the missing workers under missingWorkers (the web UI itself still serves). HEAD answers like GET without a body, here and on the static files, so monitors that probe with HEAD work.

Create Sprite (Forge)

POST /api/forge
Content-Type: application/json

{
  "sessionId": "uuid",
  "seed": "my-sprite",
  "paletteName": "neon",
  "animation": "walk",
  "frameCount": 8,
  "width": 24,
  "height": 24,
  "failChance": 0.1
}

Event Stream (SSE)

GET /api/events?sessionId=<uuid>

Streams realtime events:

  • hello - connection established; includes palettes, queues, session info, and the latest public sprite
  • ping - 15-second keepalive
  • spooled - job lifecycle or global Spooled events
  • public.sprite - new public sprite generated
  • server.realtime - server-to-Spooled WebSocket state

Reconcile Jobs

POST /api/jobs/batch
Content-Type: application/json

{
  "sessionId": "uuid",
  "jobIds": ["job-id"],
  "includeResult": true
}

Returns status for at most 50 job IDs already associated with that in-memory session. The browser polls this endpoint while a forge is active to recover updates or results missed by realtime delivery.


Project Structure

spooled-example-spriteforge/
├── .github/
│   └── workflows/
│       └── ci.yml              # CI/CD pipeline
├── k8s/
│   ├── base/                   # Base Kubernetes manifests
│   └── overlays/
│       ├── development/        # Dev overrides
│       └── production/         # Prod overrides
├── public/
│   ├── index.html              # Main HTML
│   ├── styles.css              # Styles
│   └── app.js                  # Frontend logic
├── server/
│   ├── server.mjs              # HTTP server + workers
│   └── spriteforge.mjs         # Pixel art generation
├── .env.example                # Environment template
├── Dockerfile                  # Multi-stage Docker build
├── docker-compose.yml          # Local Docker development
├── package.json
└── README.md

Development

Scripts

npm run dev      # Start the server (same command as npm start; no file watcher)
npm start        # Start production server

Testing Locally with Spooled

You can run against:

  1. Spooled Cloud (recommended): Sign up at spooled.cloud
  2. Self-hosted Spooled: See spooled-backend

For self-hosted:

SPOOLED_BASE_URL=http://localhost:8080
SPOOLED_WS_URL=ws://localhost:8080

Architecture Notes

Why One Replica?

The Kubernetes manifests pin both the Deployment and HPA to one replica. SSE clients, job-to-session routing, workflow mappings, rate-limit buckets, and the latest public sprite are process-local.

A multi-replica deployment therefore needs more than worker scaling: use sticky sessions for browser requests and move routing/state to shared infrastructure (for example Redis), or split the stateless HTTP frontend from singleton event-routing/schedule responsibilities.

Job Cleanup (Handling Many Users)

When 100+ users test the demo, jobs and workflows accumulate. SpriteForge handles this automatically:

1. Job Expiration and Retention

  • Interactive workflow jobs receive an expiresAt timestamp 24 hours in the future by default (JOB_RETENTION_HOURS).
  • The Spooled backend cleanup task runs every five minutes. Explicit expiration removes pending, scheduled, failed, or dead-letter jobs after expiresAt; completed/cancelled jobs use organization retention instead.

2. Workflow Cleanup

  • Completed, failed, or cancelled workflows use organization job-retention limits.
  • Current built-in defaults are Free 3 days, Starter 14 days, Pro 30 days, and Enterprise 90 days; organization overrides may change them.

3. In-Memory Cleanup

  • Session mappings (for routing events) are cleaned every 10 minutes
  • Mappings older than 1 hour are automatically removed

4. Queue Reuse

  • Queues (spriteforge-frames, spriteforge-assemble, spriteforge-public) are reused by all users
  • No queue accumulation occurs

Tip: For high-traffic public demos, set JOB_RETENTION_HOURS=1 in your .env file.

Monitor via health endpoint:

curl http://localhost:3000/health
# Returns stats: { activeSessions, trackedJobs, trackedWorkflows, totalWorkflows, totalJobs }

SDK and Security Notes

SpriteForge declares @spooled/sdk as ^1.4.1 in package.json; the committed package-lock.json records the exact SDK version reproduced by npm ci. Review and record both for each release, but do not force the SDK version to equal the SpriteForge application version. It uses SpooledClient, SpooledWorker, and WebSocket SpooledRealtime; workflows are created with client.workflows.create(), dependency results are read with client.workflows.jobs.getDependencies() and client.jobs.get(), and the public schedule uses client.schedules.

When deploying publicly:

  • Use a dedicated Spooled organization for the demo.
  • Set plan limits and retain the app’s built-in per-IP request limits.
  • The live event stream (GET /api/events) is capped to MAX_SSE_CLIENTS (default 500) per process and MAX_SSE_PER_IP (default 30, because many visitors share one NAT address and each open tab holds a stream) per client IP; over the cap the server answers 503 sse_capacity / 429 sse_per_ip_limit with Retry-After. Per-IP limits trust cf-connecting-ip / x-real-ip / x-forwarded-for by default; set TRUST_PROXY=false (also 0, no, off) if the port is reachable without Cloudflare or an ingress in front. sessionId must be 8-64 URL-safe characters on /api/forge and /api/jobs/batch too (400 invalid_session_id otherwise).
  • Keep SPOOLED_API_KEY server-side; the browser only calls SpriteForge endpoints.
  • Every response carries X-Frame-Options: DENY, X-Content-Type-Options: nosniff, Referrer-Policy: strict-origin-when-cross-origin, a Permissions-Policy and a Content-Security-Policy (this origin plus the Google Fonts stylesheet and the Cloudflare Web Analytics beacon a zone may inject; no inline script or style; no framing). CONTENT_SECURITY_POLICY replaces the policy (off sends none); see DEPLOY.md. HSTS is left to your proxy.

Contributing

Contributions are welcome! Please read our Contributing Guide first.

# Fork the repo, then:
git checkout -b feature/my-feature
# Make changes
git commit -m "feat: add my feature"
git push origin feature/my-feature
# Open a PR

License

Apache 2.0 © Spooled Cloud


Built with ⚡ by Spooled Cloud

About

Interactive pixel art sprite generator demonstrating Spooled Cloud's distributed job queue. Watch workflows, workers, real-time events, and automatic retries in action. Live demo: example.spooled.cloud

Topics

Resources

Contributing

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages