Getting started with foghorn
A step-by-step walkthrough for integrating foghorn — as a human reading this,
or as an AI agent building the integration on someone's behalf. For the
exhaustive API/error/limits reference, see reference.md
(or the rendered version at /docs/reference) — this guide is the "how do I
actually wire this up" version.
The four things to know first
- Project — your tenant. Everything (keys, channels, usage) is scoped to one.
- API key pair — every project gets a public key (
pk_live_...) and a secret key (sk_live_...), issued together, shown once. - Public key = client-side. Safe to ship to a browser. Can only connect and subscribe — never emit.
- Secret key = server-side only. Never send it to a browser. It's the only thing that can emit a message into a channel.
If you remember nothing else: the secret key never touches client code. Every example below reflects that split.
Step 1 — Create a project and get your keys
- Sign up / log in to the foghorn dashboard.
- Click "New project", give it a name.
- You'll see both keys exactly once — copy them now:
pk_live_...→ goes in your client-side app config.sk_live_...→ goes in your server's environment variables/secrets manager. Never in a.envfile that ships to the browser (e.g. never prefix itNEXT_PUBLIC_).
The WebSocket URL and emit URL are the same for every project on this deployment — only your keys differ. They're already filled in in the examples below; see Base URLs for the concrete values.
Step 2 — Client-side: connect and subscribe
This runs in the browser (or anywhere with a WebSocket global — a mobile
webview, Node with the ws polyfill, etc.). Only the public key is
involved here.
Install the SDK
npm install @foghorn/sdk
Connect and subscribe to a channel
import { FoghornClient } from "@foghorn/sdk";
const client = new FoghornClient({
url: "wss://y8ymlv721c.execute-api.us-east-1.amazonaws.com/prod",
publicKey: "pk_live_xxxxxxxx",
});
// Fires once the socket is open — including after an automatic reconnect,
// so re-subscribing here means you never lose your subscriptions.
client.onOpen(() => {
client.subscribe("room-1");
});
// Fires for every message emitted to a channel you're subscribed to.
client.onMessage((msg) => {
console.log(`[${msg.channel}]`, msg.payload);
});
client.connect();
That's the whole client integration. Reconnection with backoff and re-subscribing after a reconnect happen automatically — you don't write that logic yourself.
What you get in onMessage
Whatever was sent to the emit endpoint, verbatim:
client.onMessage((msg) => {
// msg.channel -> "room-1"
// msg.payload -> whatever JSON your backend sent (see Step 3)
});
Cleaning up
client.unsubscribe("room-1"); // stop receiving messages on this channel
client.disconnect(); // close the socket, no auto-reconnect after this
Listening for problems
client.onClose(({ code, reason }) => {
// the socket dropped - the SDK will auto-reconnect unless you called disconnect()
});
client.onErrorMessage((err) => {
// the server rejected something you sent (bad channel name, etc.)
console.warn(err.error);
});
Full method-by-method reference: SDK reference.
Step 3 — Server-side: emit a message
This runs on your backend only — wherever your server-side code lives.
The secret key is required here, and this is a plain HTTP POST, so it
works from any language, not just JavaScript.
The request
POST /emit
Authorization: Bearer sk_live_xxxxxxxx
Content-Type: application/json
{ "channel": "room-1", "payload": { "text": "hello from the backend" } }
Node / TypeScript example
async function notifyRoom(channel: string, payload: unknown) {
const res = await fetch(`${process.env.FOGHORN_EMIT_URL}/emit`, {
method: "POST",
headers: {
"content-type": "application/json",
authorization: `Bearer ${process.env.FOGHORN_SECRET_KEY}`,
},
body: JSON.stringify({ channel, payload }),
});
if (!res.ok) {
const body = await res.json();
throw new Error(`emit failed: ${body.error}`);
}
return res.json(); // { ok, channel, delivered, subscriberCount }
}
await notifyRoom("room-1", { text: "hello from the backend" });
From any other language
It's a plain HTTP POST with a JSON body and a bearer token — call it however
your language calls HTTP. Example with curl, useful for testing:
curl -X POST "$FOGHORN_EMIT_URL/emit" \
-H "authorization: Bearer $FOGHORN_SECRET_KEY" \
-H "content-type: application/json" \
-d '{"channel":"room-1","payload":{"text":"hello from curl"}}'
Reading the response
{ "ok": true, "channel": "room-1", "delivered": 3, "subscriberCount": 3 }
subscriberCount is how many connections were subscribed at emit time;
delivered is how many actually received it (can be lower if a connection
had already dropped — foghorn cleans those up automatically, no action
needed on your end).
Full request/response/error details: HTTP API reference.
Putting both sides together
A typical flow: your backend does something (a job finishes, a comment gets posted, a price changes), calls the emit endpoint, and every browser tab subscribed to that channel updates instantly.
┌─────────────┐ POST /emit (secret key) ┌──────────────┐
│ Your │ ─────────────────────────────────────▶ │ foghorn │
│ backend │ │ connection │
└─────────────┘ │ plane │
└──────┬───────┘
│ pushes to every
│ subscribed connection
▼
┌──────────────┐
│ Browser │
│ (public key, │
│ @foghorn/sdk)│
└──────────────┘
Nothing in the middle needs your attention — no polling, no manual fan-out logic. Subscribe once on the client, emit once on the server.
Next steps
- Full API/protocol reference — every endpoint, every error, exact limits
- Recipes — reconnection handling, rotating a compromised key, checking subscriber counts
- Changelog — what's shipped and when