API keys

The secret.keyId format, what each half is for, and why the account is not in the key.

The format

A key is {secret}.{keyId} — the secret first, the key id as the suffix:

ck_live_7BZ882hK4pQm1x.k1
└──────── secret ────────┘ └id┘
partwhat it is
secretck_live_ plus randomness. Verifies Basic auth and signs HS256 tokens. Server-side only
keyIdthe lookup handle, globally unique across accounts. Also the JWT kid

Parsing splits on the last dot, so a secret containing dots of its own survives. In Ruby that is rpartition("."); elsewhere it is lastIndexOf. Splitting on the first dot will appear to work and then break on the one key whose secret has a dot in it.

The account is not in the key

There is no account id in the wire format. The node looks the key up by keyId and reads the account off the stored record.

This is why the key id has to be globally unique rather than unique per account: it is the only thing the lookup has to go on. It also means a key cannot be pointed at a different account by editing it — the scope comes from the record, which only the console writes.

Where keys come from

Keys are created in the console at app.blackevin.com and pushed to the realtime node, which stores them along with the account they belong to. There is no environment variable that bootstraps a key, and no way to mint one against a running node without the console.

Revoking a key in the console removes the record. The next request carrying it gets a 401 — including on a connection that was already open, at its next authenticated operation.

Using one

Server-side, hand it to the SDK and forget about the encoding:

const realtime = new Blackevin.Realtime({ key: process.env.BLACKEVIN_KEY });
const rest = new Blackevin.Rest({ key: process.env.BLACKEVIN_KEY });

Over the wire it appears two ways:

ws://host/?key=ck_live_7BZ882hK4pQm1x.k1
Authorization: Basic Y2tfbGl2ZV83Qlo4ODJoSzRwUW0xeC5rMQ==

The Basic value is the key verbatim, base64'd. Not user:password. Ably writes base64("keyName:keySecret") with a colon; a Blackevin key already carries both halves in secret.keyId, so there is nothing to join. The colon form is rejected as an invalid credential — a 401, not a silent success.

KEY='ck_live_7BZ882hK4pQm1x.k1'
AUTH="Basic $(printf '%s' "$KEY" | base64)"

printf, not echo — the node trims the decoded value, so echo's trailing newline survives here, but it will not survive the next service you point this at.

Never in a browser

A key is your account: publish anywhere, read every history, until you notice and rotate. Anything running where a user can read it gets a token instead, minted by your server and scoped to what that one user may touch.

Capabilities on a key

A key carries its own capability map, and a token minted from it can only ever be a subset. So the useful pattern is one key per trust level:

  • a broad key your backend holds
  • a narrow key whose tokens are all a browser will ever get

A token asking for more than its key allows is refused at issue time, not at connect time — which is the error you want, since it names the key rather than the session.

See capabilities.

On this page