Streaming Infrastructure · Sole Engineer · 2026 · Beta

StreamKeep

Kick deletes every broadcast 30 days after it airs — not even the streamer can get it back. StreamKeep archives the nights that matter and serves them from object storage.

Oracle Cloud, Docker Compose, GitLab CI, Cloudflare R2, Cloudflare CDN, PostgreSQL, Redis, Go, Next.js, TypeScript, HLS (hls.js), ffmpeg, yt-dlp, OAuth 2.0 (PKCE), Pusher, QPay

streamkeep.live
StreamKeep — the library: Mongolian Kick streamers in one place, the archive counters and shorts

StreamKeep is a livestream archive for the Mongolian community on Kick. Kick removes a broadcast 30 days after it airs, and the streamer who made it has no way to recover the file. StreamKeep lets a creator sign in with their own Kick account, select the broadcasts worth preserving, and keep the original video at source quality together with a synchronised chat replay. Playback is public and requires no account.

It is a personal project, designed and built end to end, and running in production under my own operation. The engineering goals were reliability, predictable cost, and an operational surface small enough for one engineer to own. What follows is the architecture, the decisions behind it, and the trade-offs I accepted.

1. Project overview

Livestreaming platforms treat past broadcasts as cache, not as archive. Kick applies a fixed 30-day retention window: after it closes the video object is deleted and cannot be restored on request. For a creator this means the work disappears on a schedule they do not control, and any moment they did not clip at the time is unrecoverable.

StreamKeep addresses this narrowly and deliberately. It does not mirror an entire platform. It gives a creator a way to mark specific broadcasts for preservation before the retention window closes, stores the original rendition rather than a re-encode, and preserves the chat transcript alongside the video so the recording keeps its context.

  • Scope — a personal project, built end to end: product, backend, infrastructure, deployment and on-call.
  • Current state — open beta. 77 broadcasts preserved across 68 channels, 537 hours of video retained.
  • Design priorities — reliability first, then cost predictability, then capacity. Capacity last, because the workload does not yet justify paying for elasticity.

2. Problem and motivation

Before building anything I measured the scale of the problem. A recorder samples Kick’s Mongolian live directory once a minute and records viewership, airtime and chat volume per channel. It still runs: the figures below are its totals since tracking began in September 2026, and every broadcast behind them is deleted 30 days after it airs.

Storage efficiency

Broadcasts are multi-hour source-quality video. Retaining them on compute-attached disk would mean growing the instance for storage rather than for load, and paying for provisioned capacity whether or not it is used. Storage had to be billed by consumption and decoupled from the application host.

Delivery must not touch the application

If video is streamed through the API, every concurrent viewer consumes an application connection and host bandwidth. A single popular archive would then degrade sign-in, browsing and job submission for everyone. Read traffic had to be served by infrastructure that scales independently of the API.

Long-running work cannot run in a request

Fetching and storing a multi-hour broadcast takes far longer than any acceptable HTTP timeout, and it is subject to upstream failures outside my control. This work had to be asynchronous, durable across restarts, and safely retryable.

Cost has to stay predictable

The project is funded by its users on hours-based plans. A cost model that grows with viewership rather than with stored hours would break that relationship, so the unit customers pay for and the unit I am billed for had to match.

That measurement framed four engineering constraints, and those constraints drove every decision that follows.

505
Mongolian channels tracked
1.8M
hours watched
4.7M
chat lines captured

Counters read from streamkeep.live on 26 October 2026. The recorder samples Kick’s Mongolian directory every minute; hours watched and viewer peaks count only sampled time.

3. High-level architecture

The system separates three paths that have very different characteristics: a synchronous request path, an asynchronous archive path, and a delivery path that bypasses the application entirely. The diagram below traces all three and can be panned and stepped through.

Component responsibilities

  • Next.js frontend — the three public surfaces: stream library, live multiview and channel statistics. Bilingual, with the language cookie read server-side so the first paint already matches the reader.
  • Go API — authentication through Kick OAuth 2.0 with PKCE, authorisation, archive submission, and the live recorder that polls the public directory every 60 seconds. It serves metadata only; no video passes through it.
  • PostgreSQL — the system of record: archives and their state, chat transcripts, channel statistics, and the migration ledger. Job status and failure reasons live here, which makes a broken archive a row I can query rather than a log line I have to find.
  • Redis — the job broker and coordination layer, running Asynq. It holds queued and in-flight tasks, live per-archive progress snapshots, and rate-limiting counters.
  • Archive worker — a separate process that consumes the queue and performs the long-running work: fetching the source rendition with yt-dlp and ffmpeg, writing it to object storage, and capturing the chat transcript.
  • Cloudflare R2 — object storage for the original video files. Chosen for consumption-based pricing and, critically, zero egress charges.
  • Cloudflare CDN — delivery on a dedicated domain, in front of R2. Viewers stream HLS from here; the API is never in the path of a byte of video.

Interactive — pan, trace a path, switch theme, or open the guided views

Open full size

4. Technology stack

LayerTechnologyWhy
FrontendNext.js, TypeScriptServer rendering for first-paint correctness on a bilingual UI
BackendGoOne static binary per service; low memory footprint on a small VM
DatabasePostgreSQL 16Transactional job state, relational statistics, versioned migrations
Queue & cacheRedis 7, AsynqDurable background jobs, retry semantics, live progress snapshots
Object storageCloudflare R2Consumption-based pricing with no egress fees
DeliveryCloudflare CDN, HLS, hls.jsVideo served at the edge, independent of the application
Media pipelineyt-dlp, ffmpegSource-quality fetch with no transcode step
RuntimeDocker Compose, Linux, Oracle CloudOne host, one declarative file, no control plane to operate
CI/CDGitLab CI, self-hosted runnerBuild, migrate and health-gate a release from a single pipeline

Every choice here optimises for a single operator. Go and Compose keep the number of moving parts low; R2 and the CDN move the expensive, high-volume work onto managed infrastructure that needs no attention from me.

5. Production deployment

Production is five containers on one Linux VM, declared in a single Compose file and rolled out by the pipeline. Each service has a health check and a restart policy; container logs are capped so a runaway process cannot fill the disk.

  • Frontend — the Next.js application.
  • Go API — REST surface plus the live directory recorder.
  • Archive worker — the Asynq consumer that performs archive jobs.
  • PostgreSQL — persistent state, on a named volume.
  • Redis — the job broker and progress cache.

Why this shape

An orchestrated cluster was the obvious alternative and I decided against it. At this workload it would add a control plane to patch, upgrade and debug, and it would buy elasticity the traffic does not need. The relevant question was not which architecture is most scalable, but which one a single engineer can operate correctly at 3am.

  • Operational simplicity — one host to patch, one file to read, no scheduler to reason about during an incident.
  • Cost — a small VM plus consumption-priced storage, with no per-cluster or per-node overhead.
  • Maintainability — the whole runtime is described in one Compose file that is versioned with the application.
  • Sufficient capacity — the API is metadata-only and video is served by the CDN, so the host is not on the critical path for the traffic that actually scales.

The trade-off I accepted

One host and one database mean a host failure is downtime, not a failover, and a schema migration briefly pauses the worker. Both are acceptable at this stage and both are stated plainly rather than engineered around prematurely. The way out, once the workload needs it, is a second node under an orchestrator, which turns a host failure into a failover.

6. Video storage and delivery

This is the decision with the largest effect on both reliability and cost, and it is worth stating explicitly because it is easy to get wrong.

The pattern being avoided

The naive design routes playback through the application: viewer to API server to storage. It is simple to implement and it fails badly. Every concurrent viewer holds an application connection for the duration of a multi-hour video, host bandwidth becomes the ceiling on audience size, and one popular archive degrades sign-in and job submission for every other user. The blast radius of a traffic spike is the entire product.

The pattern used

Playback goes viewer to CDN to object storage, on a dedicated domain. The API issues metadata and never touches a byte of video. HLS segments are requested directly from the edge by the player.

  • Application load is bounded — API concurrency tracks metadata requests, not viewer-hours.
  • Bandwidth cost is bounded — R2 charges no egress, so a broadcast watched a thousand times costs the same to serve as one watched once.
  • Delivery scales independently — audience growth is absorbed by the CDN, with no change to the host.
  • Failure is isolated — a delivery problem does not take down archiving, and an application deploy does not interrupt playback.

One necessary exception

For live multiview, the master playlist is proxied, because that single file is the one whose CORS policy is restricted to the platform’s own origins. Variant playlists and video segments still stream directly from the upstream CDN. The proxy is scoped to the smallest object that requires it rather than to the whole stream.

7. Challenges and lessons learned

Large media is a storage problem, not an application problem

The instinct is to treat video as data the application owns. Treating it as an object the application only references, and keeping it entirely off the request path, removed the majority of the scaling and cost questions before they became problems.

Reliable workers need durable state, not just a queue

A queue alone does not survive contact with retries. Keeping the authoritative status and attempt count in the database, scoping task identity by attempt, and guarding each sub-job with its own claim is what makes a re-queued job safe instead of merely possible.

Most incidents come from the deploy

Adding a verified backup, a strict rollout order and three independent health gates cost an afternoon and removed the failure mode I was most likely to cause myself. The gates that matter are the ones that check the real network path, not just the process.

Simplicity is a capacity decision, not a shortcut

Choosing Compose over an orchestrator was a judgement about what one engineer can operate, not an admission of missing skill. The corresponding obligation is to know exactly which signal would invalidate that choice, and to have the migration path ready before it does.

Design the cost model with the architecture

Selecting storage with no egress charge was an architectural decision as much as a financial one. It is what allows viewing to be free and unauthenticated without the economics inverting as the audience grows.

8. Final reflection

This project represents my engineering approach: build simple systems, automate reliability, optimise cost, and design infrastructure that can evolve with future scale.

StreamKeep is deliberately not the most sophisticated architecture I could have built. It is the one I could build correctly, deploy safely, operate alone, and explain honestly, including its limits. Every decision above has a stated reason and a stated cost, and each is reversible along a path I have already thought through.

Screenshots