One API call to upload. One Rend playback URL. First bytes warmed on Rend's bare-metal edge. And we're open source.
Note
Rend is being built in public. Features become official as they are linked from Rend.so.
Rend is the video platform for developers. POST a video, get back a playback URL. Upload, encoding, storage, delivery, signed playback, analytics, player and SDKs, one coherent surface instead of five services taped together.
Our thesis is simple: latency is round trips, not server time. So Rend deletes round trips, places bytes physically near viewers before they ask, and owns the playback path from cache to viewer.
Rend Cloud serves video through bare-metal playback edge nodes backed by durable storage. Rend controls the playback URL and pre-places the opening seconds of each video on edge-local RAM and NVMe/SSD.
Cloud shape:
| Concern | Rend Cloud v1 |
|---|---|
| API and state | Rust control plane with Postgres metadata |
| Uploads | One-call upload path |
| Origin | S3-compatible object storage, Tigris by default |
| Encoding | ffmpeg workers generate opener, thumbnail, and HLS playback |
| Edge | Bare-metal rend-edge nodes in US East and London with local RAM/NVMe/SSD cache |
| Routing | Rend playback URLs routed by GeoDNS, latency DNS, or regional routing |
| Authorization | Signed playback URLs or tokens validated locally at the edge |
| Analytics | Playback request analytics for request counts, bytes, region, status, and cache state |
| Resilience | Origin or CDN backup path without exposing provider URLs |
Video on demand, built around fast startup:
- Upload API: POST a video, receive a playback URL. One call deep.
- Fast opener path: generate a playable opener early in the upload pipeline
- Rend edge playback: warm openers and first segments to US East and London
- Origin-backed cache: stream cache misses from durable object storage
- HLS playback: opener first, adaptive renditions after that
- Drop-in player with page-load prefetch
- Signed playback: tokens validated locally at the edge
- Playback request analytics: request counts, bytes, status, region, cache state
- SDKs and an MCP server, generated from one OpenAPI spec
- Measured speed: baseline upload-to-playable and first-frame metrics
Rend Cloud v1 is video on demand. Pricing uses two minute-based meters: delivery and storage. Encoding is included. 4K starts in supported regions or approved accounts while delivery economics are measured.
apps/site/— the landing page at Rend.so, Next.js and Tailwind v4services/rend-api/— Rust control-plane API skeletonservices/rend-edge/— Rust playback edge skeletoncrates/— shared Rust cratesmigrations/— Postgres migrations for Rend-owned metadataclickhouse/— ClickHouse schema for raw playback request telemetrycompose.yml— local Postgres, ClickHouse, MinIO, and Redispackages/— shared packages for future apps and services
This repo uses Bun workspaces and Turborepo.
bun install
bun devUse bun run build for production builds and bun typecheck for TypeScript.
bun build is Bun's native bundler command, so it does not run the package
script.
Root .env* files are loaded into app scripts through scripts/with-root-env.mjs.
Copy .env.example to .env.local for local secrets. App-specific .env* files
inside apps/* can override shared root values when a service needs its own
configuration.
This starts the V1 foundation: Postgres, ClickHouse, MinIO, Redis, migrations, health-checking API/edge skeletons, the raw source upload storage path, local async media artifact generation for uploaded sources, and minimal playback request telemetry.
Local media processing requires ffmpeg and ffprobe on PATH, or explicit
binary paths in REND_FFMPEG_PATH and REND_FFPROBE_PATH. Uploads enqueue
Postgres-backed media jobs by default; run the media worker locally with
cargo run -p rend-api -- worker media or bun run backend:media-worker.
Set REND_API_INLINE_MEDIA_PROCESSING=true only when you explicitly want the
old synchronous dev behavior. Worker ffmpeg runs are bounded by
REND_MEDIA_PROCESS_TIMEOUT_SECS.
Playback URLs are signed with REND_PLAYBACK_SIGNING_KEY_ID,
REND_PLAYBACK_SIGNING_SECRET, and REND_PLAYBACK_TOKEN_TTL_SECS; API and edge
processes must use the same key id and secret. The local player bootstrap
returns up to REND_PLAYBACK_BOOTSTRAP_PREFETCH_SEGMENTS first HLS segment
hints, defaulting to 2.
Edges can register with the API via REND_CONTROL_PLANE_URL; API and worker
warm/purge calls fan out to healthy rows in rend.edge_nodes. The
REND_EDGE_WARM_URL and REND_EDGE_PURGE_URL settings are local fallback
targets when no healthy registry edge is active.
Raw playback request telemetry is stored in ClickHouse, not
rend.asset_events. Postgres remains the source of truth for assets, artifacts,
jobs, lifecycle events, deletion state, and other control-plane state. Edge
telemetry is sent asynchronously through a bounded queue and local JSONL spool;
telemetry failures must not fail or materially delay playback. Analytics queries
dedupe by event_id because ClickHouse does not enforce uniqueness.
cp .env.example .env.local
bun run backend:up
cargo check --workspaceRun the services in separate terminals:
bun run backend:api
bun run backend:media-worker
bun run backend:edgeVerify health and readiness:
curl http://127.0.0.1:4000/healthz
curl http://127.0.0.1:4000/readyz
curl http://127.0.0.1:4100/healthz
curl http://127.0.0.1:4100/readyz
curl -H 'x-rend-internal-token: dev-internal-token' http://127.0.0.1:4100/metricsSmoke-test local media artifact generation:
bun run backend:smoke:async-media
bun run backend:smoke:media
bun run backend:smoke:signed-playback
bun run backend:smoke:playback-bootstrap
bun run backend:smoke:playback-telemetry
bun run backend:smoke:edge-coalescing
bun run backend:smoke:asset-events
bun run backend:smoke:lifecycle-sse
bun run backend:smoke:delete-purgeThe smoke flow starts local dependencies, checks ffmpeg -version and
ffprobe -version, generates a fixture video with ffmpeg, starts rend-api if
needed, uploads the fixture with Authorization: Bearer <REND_DEV_API_KEY>, and
verifies:
- the API upload response is honest with
source_state = uploaded,playable_state = not_playable, and no signed playback URL - a queued
rend.media_jobsrow is claimed by the local media worker - Postgres has source, opener, thumbnail, manifest, and segment artifact rows
- the media job ends in
succeededafter artifact generation - generated MinIO objects exist with nonzero byte sizes
The async media smoke also proves queued work survives a worker restart before
processing starts. The playback bootstrap smoke starts rend-edge if needed,
checks that GET /v1/assets/<asset_id>/playback returns 404 until the worker
marks the asset playable, verifies the signed primary, opener, manifest, and
first segment hint URLs through rend-edge, and confirms the local player
harness is served.
The asset events smoke starts rend-api and rend-edge if needed, uploads a
fixture, checks GET /v1/assets/<asset_id>, checks
GET /v1/assets/<asset_id>/events, verifies ordered lifecycle events and
after_sequence polling, and confirms unauthenticated and unknown-asset
requests are rejected.
The lifecycle SSE smoke opens authenticated GET /v1/events, uploads a
fixture, verifies durable lifecycle frames through media processing and edge
warming, and reconnects with Last-Event-ID to prove replay resumes after the
sequence cursor.
The delete/purge smoke uploads and processes a fixture, fetches the signed edge manifest to populate the local cache, deletes the asset, verifies repeat DELETE idempotency, confirms new playback bootstrap returns 404, checks durable deletion and purge lifecycle events, verifies the cached manifest file was removed, and proves the already-issued signed edge URL can still work while the token and origin object remain valid.
The edge coalescing smoke uploads and processes a fixture, fetches a signed
opener URL from playback bootstrap, purges that opener from the edge cache,
launches concurrent cold requests for the same URL, and verifies one MISS, at
least one COALESCED, identical nonempty bodies, and a later HIT.
The playback telemetry smoke starts Postgres, ClickHouse, Redis, MinIO, the API,
the edge, and the media worker; uploads a fixture; waits for HLS playback;
fetches the signed manifest twice; waits for the edge queue/flusher; and verifies
GET /v1/assets/<asset_id>/analytics/playback reports deduped request, byte,
HIT, MISS, and 200 status aggregates. It does not assert watch time,
startup success, viewer identity, or billing-grade accuracy.
Run the local playback benchmark separately from smoke tests:
bun run backend:benchmark:localThe benchmark starts or reuses the same local compose dependencies, API, edge,
and media worker, generates small and medium fixture videos, uploads each
fixture, waits for HLS playback, and records baseline timings for the Playback
Edge V1 path. It prints a human-readable table and writes machine-readable JSON
to .rend/benchmarks/playback-edge-local-<timestamp>.json by default.
The JSON includes git SHA when available, dirty state, host, timestamp, fixture size and duration, cache-state handling, service reuse/startup state, selected non-secret environment settings, and secret presence booleans. It records the current baseline only; there are no performance thresholds and it is not part of the smoke suite.
Useful benchmark overrides:
REND_BENCHMARK_FIXTURES=small bun run backend:benchmark:local
REND_BENCHMARK_OUTPUT=.rend/benchmarks/my-run.json bun run backend:benchmark:local
REND_BENCHMARK_COALESCING_CONCURRENCY=32 bun run backend:benchmark:localEdge cache behavior:
rend-edgevalidates signed playback tokens locally before cache lookup, coalescing, origin fetch, or cache file I/O. The playback hot path does not call Postgres or the control plane.X-Rend-Cache: HITmeans the response was served from an existing local cache file.X-Rend-Cache: MISSmeans this request led the cold fill, fetched from the S3-compatible origin, and wrote the local cache through a temp file followed by rename.X-Rend-Cache: COALESCEDmeans this request waited for an in-flight fill for the same validated cache key and then served the filled local cache file.- Cold fills are coalesced per playback artifact. Different artifacts fill independently and do not wait behind a single global origin lock.
REND_EDGE_MAX_IN_FLIGHT_FILLSbounds distinct concurrent cold fills. The default is64, the hard max is1024, and new distinct fills above the limit fail fast with HTTP 503 while same-artifact waiters may still join the existing fill.- Current cold fills still buffer the origin object before writing the cache and serving the response. True stream-while-writing cache fill remains a follow-up.
Playback telemetry behavior:
rend-edgeemits one request event for each public playback artifact request after the auth/cache/origin outcome is known.- Events include
event_id,observed_at,asset_id,artifact_path,edge_id,region,cache_status,status_code,bytes_served,content_type,duration_ms, and optionalerror_code. - Events never include signed URL query strings, tokens, authorization headers, cookies, full request headers, full URLs, or client IPs.
GET /v1/assets/<asset_id>/analytics/playbackreturns bounded-window,event_id-deduped aggregates only: request count, bytes served, cache status counts, status code counts, first seen, and last seen.
Manual upload, bootstrap, and local playback:
fixture=$(scripts/generate-fixture-video.sh)
curl -i -X POST http://127.0.0.1:4000/v1/videos \
-H 'authorization: Bearer dev-api-key' \
-H 'content-type: video/mp4' \
--data-binary @"$fixture"
response=$(
curl -s -X POST http://127.0.0.1:4000/v1/videos \
-H 'authorization: Bearer dev-api-key' \
-H 'content-type: video/mp4' \
--data-binary @"$fixture"
)
echo "$response"
asset_id=$(printf '%s' "$response" | jq -r .asset_id)
object_key=$(printf '%s' "$response" | jq -r .source_object_key)
curl -s http://127.0.0.1:4000/v1/assets/$asset_id \
-H 'authorization: Bearer dev-api-key' | jq
# Repeat until playable_state is hls_ready.
curl -s http://127.0.0.1:4000/v1/assets/$asset_id \
-H 'authorization: Bearer dev-api-key' | jq
curl -s http://127.0.0.1:4000/v1/assets/$asset_id/events \
-H 'authorization: Bearer dev-api-key' | jq
curl -s http://127.0.0.1:4000/v1/assets/$asset_id/playback \
-H 'authorization: Bearer dev-api-key' | jq
# Delete is authenticated and idempotent. It marks rend.assets.deleted_at and
# asks healthy registered edges, or the fallback purge URL, to purge cached
# playback bytes for the asset.
curl -s -X DELETE http://127.0.0.1:4000/v1/assets/$asset_id \
-H 'authorization: Bearer dev-api-key' | jq
# New bootstrap/token issuance is blocked after deletion.
curl -i http://127.0.0.1:4000/v1/assets/$asset_id/playback \
-H 'authorization: Bearer dev-api-key'
after_sequence=$(
curl -s http://127.0.0.1:4000/v1/assets/$asset_id/events \
-H 'authorization: Bearer dev-api-key' | jq -r '.next_after_sequence // 0'
)
curl -s "http://127.0.0.1:4000/v1/assets/$asset_id/events?after_sequence=$after_sequence&limit=25" \
-H 'authorization: Bearer dev-api-key' | jq
# In another terminal, observe the durable lifecycle stream. Each SSE frame uses
# rend.asset_events.sequence as its id and rend.asset_events.event_type as its
# event name.
curl -N http://127.0.0.1:4000/v1/events \
-H 'authorization: Bearer dev-api-key' \
-H 'accept: text/event-stream'
# Optional asset filter. Unknown well-formed asset ids simply stream no events.
curl -N "http://127.0.0.1:4000/v1/events?asset_id=$asset_id" \
-H 'authorization: Bearer dev-api-key' \
-H 'accept: text/event-stream'
# Replay after a durable sequence cursor. Last-Event-ID takes precedence over
# after_sequence when both are present.
curl -N "http://127.0.0.1:4000/v1/events?after_sequence=$after_sequence" \
-H 'authorization: Bearer dev-api-key' \
-H 'accept: text/event-stream'
curl -N "http://127.0.0.1:4000/v1/events?after_sequence=0" \
-H 'authorization: Bearer dev-api-key' \
-H "Last-Event-ID: $after_sequence" \
-H 'accept: text/event-stream'
# Edge purge is an internal operation protected by x-rend-internal-token. With
# artifact_paths omitted or empty, rend-edge removes supported local playback
# cache files under videos/<asset_id>/.
curl -s -X POST http://127.0.0.1:4100/internal/purge \
-H 'x-rend-internal-token: dev-internal-token' \
-H 'content-type: application/json' \
--data "{\"asset_id\":\"$asset_id\"}" | jq
# To purge a bounded explicit list instead:
curl -s -X POST http://127.0.0.1:4100/internal/purge \
-H 'x-rend-internal-token: dev-internal-token' \
-H 'content-type: application/json' \
--data "{\"asset_id\":\"$asset_id\",\"artifact_paths\":[\"opener.mp4\",\"hls/master.m3u8\"]}" | jq
# Deletion semantics are intentionally narrow: deletion blocks new bootstrap
# responses and future token issuance, and purge removes local edge-cache bytes.
# It does not revoke already-issued signed playback URLs. Those URLs may remain
# valid until token expiry if the origin objects still exist and no real edge
# revocation layer has been implemented.
open "http://127.0.0.1:4000/player?asset_id=$asset_id"
docker compose exec postgres psql -U rend -d rend -c "
select a.id, a.source_state, a.playable_state, ar.kind, ar.id as artifact_id,
ar.object_key, ar.content_type, ar.byte_size
from rend.assets a
join rend.artifacts ar on ar.asset_id = a.id
where a.id = '$asset_id'::uuid
order by ar.kind, ar.object_key;
"
docker compose run --rm --entrypoint /bin/sh minio-init -c "
mc alias set local http://minio:9000 rend_minio rend_minio_password >/dev/null &&
mc stat local/rend-local/$object_key &&
mc stat local/rend-local/videos/$asset_id/opener.mp4 &&
mc stat local/rend-local/videos/$asset_id/thumbnail.jpg &&
mc stat local/rend-local/videos/$asset_id/hls/master.m3u8
"Useful maintenance commands:
docker compose ps
bun run backend:downThe server will be AGPL-3.0. The player and SDKs will be MIT.
Built by Cap Software, the company behind Cap, the open source screen recorder.
Rend.so · Rend.sh · Rend.video