spritzer
A standalone, stateful local emulator of the Fly.io Sprites API. Like LocalStack, but for Fly Sprites.
spritzer is the local target for chant's Fly lexicon — its Sprite activities (create, exec, checkpoint, restore) run against spritzer for offline, accountless checkpoint-as-compensation. See the chant docs and the Fly deploy tutorial it powers.

Purpose
Testing a Sprites client, such as a workflow that checkpoints a sandbox before a
risky step and restores it on failure, requires testing against state: a sprite
whose filesystem a command mutates, a checkpoint that captures that filesystem,
and a restore that rewinds to it. A schema mock holds no state, so it cannot
model these behaviors. spritzer keeps sprites, their filesystems, and their
checkpoints in memory and runs a small exec interpreter, so a client's
checkpoint-as-compensation logic can be exercised end to end offline.
spritzer is wire-compatible with the in-process Sprites fake in the chant
lexicon (sprites-fake.ts): the endpoint shapes and the exec interpreter match
it, so the same integration suite passes against the spritzer container image.
Features
- Stateful in-memory store of sprites keyed by name, each with a filesystem
(path → contents) and an ordered list of checkpoints.
exec is a control WebSocket at GET /v1/sprites/{id}/exec speaking the real
Sprites SDK's framed protocol: each binary message is [streamID][payload]
(StreamStdin=0, StreamStdout=1, StreamStderr=2, StreamExit=3, StreamStdinEOF=4).
Behind the frames a small scripted interpreter (echo > path, echo, cat,
rm, true/false, ./risky.sh, and an echo-back default) writes,
overwrites, or fails a filesystem key so the result is observable.
- Checkpoint / restore: create is
POST /v1/sprites/{id}/checkpoint (singular),
streaming NDJSON progress and assigning a server version id (v1, v2, …) with
an optional caller comment; the list is a bare array with create_time and
is_auto; restore takes a checkpoint id in the path, streams NDJSON, replaces
the filesystem with that copy, and returns the sprite to running. This is the
checkpoint-as-compensation primitive.
- A destroyed or missing sprite returns
404 on any subsequent operation.
- A
/_spritzer/health endpoint reporting version and implemented paths.
- Single static binary and distroless container image; no runtime dependencies.
Quick start
Run the container:
docker run --rm -p 4290:4290 ghcr.io/intentius/spritzer:latest
Or install with Go:
go install github.com/intentius/spritzer/cmd/spritzer@latest
spritzer # listens on :4290 by default
Point a Sprites client at it with the same environment variable the real client
uses:
export SPRITES_BASE_URL=http://localhost:4290
The listen address is configurable with -addr or SPRITZER_ADDR (default
:4290).
Usage example
BASE=http://localhost:4290
# Create a sprite (its name is its id).
curl -s -X POST "$BASE/v1/sprites" -d '{"name":"demo"}'
# => {"id":"demo","url":"http://localhost:4290/s/demo"}
# Checkpoint the current state. The server assigns the version id and streams
# NDJSON progress; the id is on the terminal complete event.
curl -s -X POST "$BASE/v1/sprites/demo/checkpoint" -d '{"comment":"pre-run"}'
# => {"type":"info","data":"Creating checkpoint..."}
# {"type":"info","data":" ID: v1"}
# {"type":"complete","data":"Checkpoint v1 created successfully"}
# List the checkpoints (creation order) as a bare array.
curl -s "$BASE/v1/sprites/demo/checkpoints"
# => [{"id":"v1","comment":"pre-run","create_time":"2026-07-11T...Z","is_auto":false}]
# Restore rewinds the filesystem to the checkpoint, addressed by id in the path,
# streaming NDJSON progress.
curl -s -X POST "$BASE/v1/sprites/demo/checkpoints/v1/restore"
# => {"type":"info","data":"Restoring checkpoint v1..."}
# {"type":"complete","data":"Checkpoint v1 restored successfully"}
exec is a control WebSocket at ws://<host>/v1/sprites/{id}/exec. Pass the
command as cmd query params (?cmd=echo&cmd=hi, or a single ?cmd=echo hi).
Every message is a binary frame [streamID][payload]: the server writes stdout
as [1]<bytes>, stderr as [2]<bytes>, then [3]<exitCodeByte>. So
echo hi yields [1]"hi\n" then [3]\x00 (exit 0), and ./risky.sh yields
[2]"risky.sh: failed\n" then [3]\x01 (exit 1).
Comparison
| Capability |
spritzer |
Schema mock |
Real Sprites |
| Sprite filesystem that exec mutates |
Yes |
No |
Yes |
| Checkpoint / restore (rewind on failure) |
Yes |
No |
Yes |
Destroyed-sprite 404 semantics |
Yes |
No |
Yes |
| Runs fully offline |
Yes |
Yes |
No |
| Cost |
Free |
Free |
Billed |
| Real sandboxes, images, code execution |
No |
No |
Yes |
API coverage
Implemented: create, the exec control WebSocket, checkpoint (NDJSON), list
checkpoints (bare array), get one checkpoint, restore-by-id (NDJSON), destroy,
and an inspection GET, plus a /_spritzer/health report. The full table is in
the API coverage docs.
Development
The primary task runner is just; a Makefile
mirrors the same targets.
just build # compile
just test # go test ./...
just race # go test -race ./...
just lint # golangci-lint
just cover # coverage profile
just docs-serve # preview the doc site
Contributing
Contributions are welcome. See CONTRIBUTING.md and the
Code of Conduct.
License
Licensed under the MIT License. Copyright (c) 2026 Intentius.