open_stream

Encrypted message sink · Elixir / OTP

The server sees n0i·5e.
You hold the key.

OpenStream relays messages between your applications and keeps a viewable log. Everything is sealed before it arrives — a breached stream is static without your key.

$ curl -X POST http://localhost:4000/api/streams
← creates a stream in ~1 ms
tail — pQ3xWJd9…/events
⬡ AES-256-GCM · key never sent ~0 msg/s

A sink, not a broker

No topics, no consumer groups, no dashboards to babysit. Point two apps at one URL and watch the messages land.

Ciphertext or nothing

Payloads are opaque bytes. Seal them with AES-256-GCM before they leave your process; OpenStream stores what it cannot read.

RAM-speed ring buffer

One BEAM process and one ETS table per stream. No disk on the hot path, reads bypass the process entirely.

Wire it up in three requests

The whole protocol is HTTP. Anything that can POST bytes can publish; anything that can read an event stream can tail.

step 1 Create a stream The id is a 128-bit slug — the address is the secret. The ingest token is shown once and stored only as a hash.
$ curl -X POST http://localhost:4000/api/streams
{
  "stream_id":    "pQ3xWJd9tKvR2mA8LfYw1g",
  "ingest_token": "u4… (keep this)",
  "viewer_url":   "/s/pQ3xWJd9tKvR2mA8LfYw1g"
}
step 2 Seal and publish Encrypt in your app, then POST the envelope. The server never parses it — it stores bytes and assigns a sequence number.
// node — same idea in any language with AES-GCM
const key = /* 32 random bytes, shared out-of-band */;
const iv  = crypto.randomBytes(12);
const c   = crypto.createCipheriv("aes-256-gcm", key, iv);
const ct  = Buffer.concat([c.update(msg), c.final(), c.getAuthTag()]);

await fetch(url + "/messages", {
  method: "POST",
  headers: { authorization: `Bearer ${token}` },
  body: JSON.stringify({ v: 1, alg: "A256GCM",
    iv: iv.toString("base64"), ct: ct.toString("base64") })
});
step 3 Tail the log Server-sent events, replayed from any sequence number. Or open the viewer and put the key after the # — fragments never reach the server.
$ curl -N http://localhost:4000/api/streams/pQ3xWJd9…/events?since=0
id: 1
data: {"seq":1,"ts":1752300000000,"payload":"eyJ2IjoxLCJhbGciOiJBMjU2R0NNIi…"}

# or, in a browser — key stays in the fragment:
http://localhost:4000/s/pQ3xWJd9…#k=<base64url key>

Obscurity is the address. Encryption is the wall.

Unlisted stream ids keep casual eyes out. The key keeps everyone out — including us.

⬡ what the server holds

  • seq — a monotonic counter
  • ts — arrival time, milliseconds
  • payload — your bytes, unread
  • sha256(token) — never the token itself

◆ what only you hold

  • the key — generated in your process
  • plaintext — exists only at the edges
  • the viewer key — lives in the URL fragment
  • the ingest token — shown exactly once

Streams live in memory as a bounded ring buffer and evaporate after a day of silence. There is no database to subpoena, no disk to image — the ciphertext itself is the only record, and it expires.