Unikraft Cloud backend
The Unikraft Cloud backend serves the same API as the Cloudflare one from a single image: a Rust server replaces the Worker and the Durable Objects, and PostgreSQL replaces their SQLite. Clients cannot tell the difference.
One instance, two processes
instance (Unikraft Cloud, base-compat runtime)
├── pulsify-cloud-entrypoint starts both processes, stops the instance if one exits
├── postgres Unix socket /var/run/postgresql only, data in /data/postgres
└── pulsify-cloud-server (Rust) :8080, gateway + one actor per root
volume /data survives restarts and redeployments
PostgreSQL runs beside the server rather than on its own instance:
- Nothing to secure between them. PostgreSQL listens on a Unix socket only: no port, no password, no TLS.
- They sleep and wake together. With stateful scale-to-zero, one snapshot holds the server and the database, so a resumed instance answers its first frame without a database reconnection.
- One thing to deploy. One image, one volume, one instance per deployment.
The trade-off is vertical scaling: one instance serves all its roots. Each root is still an isolated actor that processes its events in order, exactly like a Durable Object, so splitting roots across instances later changes no semantics.
1. Build the image
The server depends on the protocol crate of this repository. The script stages a self-contained build context (Kraftfile, Dockerfile, server and protocol sources) and runs unikraft build on it.
# From a clone of the Pulsify repository
unikraft login
bun run cloud:unikraft -- --output <my-org>/pulsify-cloud:latest
2. Create a volume and run
Keep the database on a volume mounted at /data. The idle policy suspends the instance between frames, even with sockets open; stateful=true resumes it from memory. Client heartbeats wake it briefly: pings go out when a silent socket is needed, plus one per silent socket every 2 s while someone uses the page. Tabs left open send nothing, so the instance sleeps. A short heartbeat.intervalMs or a long heartbeat.activeMs keeps it awake longer.
unikraft volume create --set metro=fra --set name=pulsify-data --set size=1Gi
unikraft run --metro fra \
-m 1G \
-p 443:8080/tls+http \
--scale-to-zero policy=idle,cooldown-time=5000,stateful=true \
-e PULSIFY_TOKEN_SECRET="$(openssl rand -base64 48)" \
-e PULSIFY_API_KEY="$(openssl rand -base64 48)" \
--volume pulsify-data:/data \
--image <my-org>/pulsify-cloud:latest
# fqdn: young-thunder-fbafrsxj.fra.unikraft.app → PULSIFY_URL=https://…
Store both secrets: your backend needs PULSIFY_TOKEN_SECRET to sign tokens and PULSIFY_API_KEY to grant rights. Then follow the tutorial with the instance URL.
Run it locally
The same image runs in Docker:
bun run cloud:unikraft --docker pulsify-cloud
docker run --rm -p 8080:8080 \
-e PULSIFY_TOKEN_SECRET="a local secret of at least thirty-two characters" \
-e PULSIFY_API_KEY="a local api key of at least thirty-two characters" \
-v pulsify-data:/data \
pulsify-cloud
curl http://localhost:8080/v1/health # {"ok":true}
Configuration
| Variable | Default | Meaning |
|---|---|---|
PULSIFY_TOKEN_SECRET | required | HS256 secret shared with your backend, 32+ characters. |
PULSIFY_API_KEY | required | Management key, 32+ characters. |
PORT | 8080 | Port of the Rust server. |
PGDATA | /data/postgres | Database directory; initialized on first boot. |
DATABASE_URL | the local Unix socket | Point the server to another PostgreSQL instead. |
PULSIFY_POOL_SIZE | 16 | PostgreSQL connections of the server. |
PULSIFY_SHARED_BUFFERS | 64MB | PostgreSQL shared_buffers; raise it with memory. |
How it maps to Cloudflare
| Cloudflare | Unikraft Cloud |
|---|---|
| Worker: routing, tokens, API key | Rust server, same routes and errors |
| Durable Object per root | Actor per root in the server, alive while a socket or request uses it |
| SQLite per root | One PostgreSQL database; grants are keyed by root, rows by document ID |
| Subscription table for hibernation | Subscriptions in memory: the process keeps its sockets |
| Hibernation | Stateful scale-to-zero |
The schema is the self-hosted PostgreSQL patch schema plus one grant table. It is exported for tooling and embedded in the server:
import { UNIKRAFT_CLOUD_POSTGRES_SCHEMA } from "pulsify-cloud-server/unikraft-cloud";
// pulsify_op_rows, pulsify_doc_clocks, pulsify_append(): the self-hosted patch schema
// pulsify_cloud_grants(root, subject, prefix, role): the grant tables of every root
Operations
- Backups: the whole state is the volume. Snapshot it, or point
DATABASE_URLat a managed PostgreSQL if you prefer its backups. - Usage:
GET /v1/roots/{root}/usagewith the API key returns documents, rows, bytes and open sockets of a root. - Stopping: the entrypoint stops the server, then PostgreSQL, on
SIGTERM. If either process dies, the instance exits so the platform restarts both.
bun run test:cloud --backend unikraft # cargo build + local PostgreSQL
bun run test:cloud --backend unikraft --image pulsify-cloud # the Docker image