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┘| part | what it is |
|---|---|
secret | ck_live_ plus randomness. Verifies Basic auth and signs HS256 tokens. Server-side only |
keyId | the 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.k1Authorization: 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.