foghorn

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

  1. Sign up / log in to the foghorn dashboard.
  2. Click "New project", give it a name.
  3. 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 .env file that ships to the browser (e.g. never prefix it NEXT_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