Full Stack with @nolag/core
The nolag-core repository brings up the whole system on your machine with one script: Postgres, an example host that mounts the @nolag/core library, kraken pointed at it, and a small admin UI. It then imports a demo project and writes out the credentials it minted.
The quickstart is a demonstration, not a deployment. The example host authenticates nobody: anyone who can reach its port can read, create and delete every project and mint credentials for any of them. That is why every published port binds to 127.0.0.1. Do not put it on a network. See what the quickstart is not.
Prerequisites
- Docker with Docker Compose, Git and
curl - Node.js 18 or later, to connect a client and run the test suite
Run it
git clone --branch v0.5.0 https://github.com/NoLagApp/nolag-core.git
cd nolag-core
KRAKEN_CONTEXT=https://github.com/NoLagApp/kraken.git#v0.9.0 ./quickstart/quickstart.sh
KRAKEN_CONTEXT tells Docker where to build kraken from. The compose file in nolag-core v0.5.0 points at a kraken branch that no longer exists, so set it to the kraken v0.9.0 release as above, or to the path of a local kraken checkout.
The script:
- writes a
.envfile on its first run, with a generatedSIGNING_KEY_ENCRYPTION_KEY, Postgres password and kraken internal secret, and the published ports; - runs
docker compose up -d --build --wait, which builds core, the UI and kraken from source and waits until every service reports healthy (the first build takes a few minutes); - imports
quickstart/demo-project.jsonthrough the host'sPOST /v1/projects/import; - writes the minted credentials to
quickstart/credentials.json, readable only by you.
When it finishes:
| Service | Address |
|---|---|
| Admin UI | http://localhost:3401 |
| Example host (core's HTTP API) | http://localhost:3400, with OpenAPI docs at /swagger |
| Broker | ws://localhost:8410/ws |
| Postgres | localhost:5442, user nolag, database nolag_core |
The ports come from .env (UI_PORT, CORE_PORT, KRAKEN_PORT, POSTGRES_PORT), so change them there if something else holds one. Port 18843 is published for kraken's MQTT listener too, but that listener does not accept connections in kraken v0.9.0.
Check both halves:
curl localhost:3400/health
# {"status":"ok","database":"up"}
curl localhost:8410/health
# {"status":"ok"}
The demo project
quickstart/demo-project.json defines:
| App | Access | Topics | Rooms |
|---|---|---|---|
chat | open: every active actor in the project reaches it without a grant | messages, typing | general, random, and vip, which a type grant makes private to service actors |
ops | restricted: needs an explicit grant on the actor | alerts | control |
| Actor | Type | Notes |
|---|---|---|
alice, bob | user | No grants of their own: they reach the open chat app only |
opsbot | service | Granted pubSub on the ops app; reaches the vip room through the type grant |
acme-device | device | Bound to access scope acme |
globex-device | device | Bound to access scope globex |
It also creates one signing key, browser, for client tokens.
quickstart/credentials.json holds what the import minted:
{
"projectId": "01a11ee7-a399-74bc-b934-161d1cc618a4",
"actors": [
{ "ref": "alice", "keyId": "at_live_9c6b0cdc3c4f", "accessToken": "at_live_9c6b0cdc3c4f.<secret>" }
],
"signingKeys": [
{ "ref": "browser", "keyId": "sk_live_d4dcb310e8b8", "signingKey": "sk_live_d4dcb310e8b8.<secret>" }
]
}
(Shortened: there is one entry per actor.) The secrets appear only here. Core stores hashes of actor secrets and an encrypted copy of signing key secrets, so they cannot be shown again. Running the script a second time imports a second demo project with new credentials, rather than failing.
App and room slugs are used exactly as written in the document, so topics are addressed as chat/general/messages. An actor bound to an access scope addresses chat/acme/general/messages; it may also leave the scope out and kraken inserts it. See Access Scopes.
Connect a client
From a new directory next to your nolag-core checkout:
mkdir core-client && cd core-client
npm init -y
npm install @nolag/js-sdk
Save this as connect.mjs and run it with node connect.mjs:
import { readFileSync } from "node:fs";
import { NoLag } from "@nolag/js-sdk";
const creds = JSON.parse(readFileSync("../nolag-core/quickstart/credentials.json", "utf8"));
const token = (ref) => creds.actors.find((a) => a.ref === ref).accessToken;
const url = "ws://localhost:8410/ws";
const bob = NoLag(token("bob"), { url });
bob.on("chat/general/messages", (data) => console.log("bob got:", data));
bob.on("connect", () => {
bob.subscribe("chat/general/messages", (err) => {
console.log(err ? `subscribe failed: ${err.message}` : "bob is subscribed");
});
});
await bob.connect();
const alice = NoLag(token("alice"), { url });
await alice.connect();
alice.emit("chat/general/messages", { text: "hello from alice" }, (err) => {
console.log(err ? `publish failed: ${err.message}` : "alice published");
});
bob is subscribed
alice published
bob got: { text: 'hello from alice' }
The other rules in the demo project hold too. We checked each of these with the quickstart's own tokens:
| Actor | Subscribes to | Result |
|---|---|---|
alice | ops/control/alerts | Refused, unknown_topic (42940): ops is restricted and alice has no grant |
alice | chat/vip/messages | Refused, unknown_topic: the room is private to service actors |
opsbot | ops/control/alerts | Accepted |
opsbot | chat/vip/messages | Accepted, through the type grant |
acme-device | chat/acme/general/messages | Accepted |
acme-device | chat/general/messages | Accepted: kraken inserts the actor's scope |
To connect a browser with a client token instead, sign a JWT with the browser signing key from credentials.json, as shown in Client Tokens.
Verify the stack
The repository carries an acceptance suite that drives the running stack with a real @nolag/js-sdk client: authentication, a pub/sub round trip, restricted apps, private rooms, and isolation between the two tenants. In the nolag-core directory:
npm ci
npm run test:stack
Against the stack above, all 19 tests passed. The suite starts by checking that kraken refuses a token core never issued, so a broker accidentally left on its static token file fails the run instead of passing it.
Reconnects
We restarted kraken with docker compose restart kraken while clients were connected. They reconnected by themselves, but kraken did not restore their subscriptions. The quickstart runs kraken with CONTROL_BACKEND=noop, so kraken never tells core what a connection subscribed to, and core has nothing to hand back on reconnect. A client that subscribes in its connect handler, as connect.mjs does, carried on receiving messages.
Stop and clean up
docker compose down # stop, and keep the database
docker compose down -v # stop, and delete the database volume
What the quickstart is not
Every item here is a deliberate omission, listed so nobody discovers it the hard way:
- No authentication on the example host, and no TLS anywhere.
- Secrets in a plaintext
.env, generated by the script and readable only by you. That is not secret management. - Postgres in a container with a local volume, and no backups, replication or tuning.
- One kraken node. No clustering or failover; a kraken restart drops every connection.
- Message recording off (
RECORD_MESSAGES=false). kraken has no history API either way. - An admin UI with no accounts, because the host behind it has none. It lists, reads, creates (from a project document) and deletes projects. Set
CORS_ORIGINSif you serve it from another origin;*is ignored.
Running core in your own host
The library is the deliverable; the example host is about two hundred lines that show the wiring. A host of your own does three things:
- Owns the database connection. Pass core's entities and migrations to your TypeORM DataSource as the exported arrays, not a glob: a glob relative to your own source never reaches into
node_modules, and core's tables would silently not exist. - Mounts core with
CoreModule.forRoot({ signingKeyEncryptionKey, defaultLimits }). Core reads no environment variables and opens no connections of its own. - Exposes the facades behind your own authentication. The broker-facing routes call
AuthzFacadeand return its results through the exportedtoBroker*adapters, which define the wire format kraken reads. Configuration routes callProjectConfigFacade,ActorTokenFacade,SigningKeyFacadeand the rest.
import { Module } from "@nestjs/common";
import { TypeOrmModule } from "@nestjs/typeorm";
import { CoreModule, allCoreMigrations, coreEntities } from "@nolag/core";
@Module({
imports: [
TypeOrmModule.forRoot({
type: "postgres",
url: process.env.DATABASE_URL,
entities: [...coreEntities],
migrations: [...allCoreMigrations],
migrationsRun: true, // apply core's migrations at startup
}),
CoreModule.forRoot({ signingKeyEncryptionKey: process.env.SIGNING_KEY_ENCRYPTION_KEY }),
],
})
export class HostModule {}
Then point kraken at your broker-facing routes with AUTH_BACKEND=http and AUTH_HTTP_URL, and set BACKEND_SECRET to the bearer secret your routes require. The routes the example host serves, and the document it imports, are described in Project Document. The contract kraken expects is in HTTP Auth Contract.