Quick start
Try it
Section titled “Try it”You need Docker with Compose. Nothing else: no Node, no database, no account.
git clone https://gitlab.com/jojithedev/wayscribe.git && cd wayscribedocker compose -f infrastructure/compose.yaml \ -f infrastructure/compose.demo.yaml up --buildThat builds the images and boots the API, the interface, PostgreSQL, a queue, and four demo services that bring their own broken integration to investigate. Measured on 2026-09-14 from a fresh clone on a laptop with no Docker layer cache, the build took 38 seconds and the boot 12; the first run also downloads the base images.
Then start a journey:
curl -X POST http://localhost:3100/triggerFour demo services move a Salesforce account through a webhook, a
transformation, PostgreSQL, a queue, a worker, and a third-party API. The
transformation contains a real defect, the queue really retries, and the target
really rejects the result with a 422. About ten seconds after the trigger the
journey has reached its dead-letter state: the demo queue redelivers after 3
seconds and dead-letters on the third receive
(elasticmq.conf).
Open http://localhost:3000 and sign in with the admin token, which is
replace-for-local-development-0000 until you set your own (below). Search
0018Z00002ABC. That is the journey above. (pnpm demo:trigger does the same
as the curl and prints the direct link, if you have Node 24 and pnpm.)
The API binds 127.0.0.1:8080 and the interface 127.0.0.1:3000. If either
port is taken on your machine, move it:
API_PORT=8081 WEB_PORT=3001 docker compose -f infrastructure/compose.yaml \ -f infrastructure/compose.demo.yaml up --buildSet your own ADMIN_TOKEN and ENCRYPTION_KEY before this holds anything real:
cp .env.example .env && printf 'ENCRYPTION_KEY=%s\nADMIN_TOKEN=%s\n' "$(openssl rand -hex 32)" "$(openssl rand -hex 32)" >> .envRecreate the stack with the same up command for them to take effect. What the
demo already recorded stays under the default key, which the new one cannot
read; start from an empty database with down -v first if that matters. The
API logs a warning at every boot while the published development defaults are
still in use.
Supported versions
Section titled “Supported versions”Each row says what is supported and what CI actually runs, which are not always
the same thing. tests/supported-versions.test.ts fails when this table
disagrees with .gitlab-ci.yml or with the engines fields.
| Component | Supported | What CI tests |
|---|---|---|
| Node.js for the SDK and the CLI | 22.12 or later | 22.12.0 and 24: the SDK’s build and unit tests, the CLI’s unit tests, and import and require() of the packed SDK tarball in fresh projects (sdk-node) |
| Node.js for running from a clone | 24 | 24: every other job runs on node:24 or node:24-alpine |
| PostgreSQL | 15 or later | 15, 17 and 18: the whole integration suite, migrations included (database). 16 is not run; it lies between two releases that are |
| Docker Compose | 2.24 or later | not pinned: the manual demo job and the release gate use Alpine’s current docker-cli-compose. 2.24 is the oldest Compose that reads env_file with required: false, which the Compose files use |
| Container images | linux/amd64, linux/arm64 | the runner’s own architecture only: container-scan builds and scans both images on default-branch pipelines. Releases build both platforms; the arm64 images are not run in CI |
doctor fails on PostgreSQL older than 15 and warns on a release newer than
the newest one CI tests.
Instrument your own service
Section titled “Instrument your own service”For a common stack, start from a recipe.
The SDK is not published to npm yet. Until it is, pack it from a clone of this repository, commit the tarball to your application, and depend on it by path:
pnpm installpnpm --silent --filter @wayscribe/node run pack:release /path/to/your-app/vendor/cd /path/to/your-appnpm install ./vendor/wayscribe-node-0.1.0.tgz # records "file:vendor/…tgz"A tarball is a built copy that travels with your application. A path into the
clone instead links to a directory whose build output is not in git, which
breaks on the next git clean, branch switch, or machine without the clone,
and nothing rebuilds it for a job that has no build step of its own. After
pulling a change that adds or updates the tarball, run npm ci before the job
runs again; a deploy that skipped it hung. The
SDK README has the
details. Once the package is published, all of this becomes
npm install @wayscribe/node.
import { createRecorder } from "@wayscribe/node";
const recorder = createRecorder({ endpoint: "http://localhost:8080", apiKey: process.env.WAYSCRIBE_API_KEY, serviceName: "billing-api", environment: "development", // Prints `delivered_first` once events are stored, or why they are not. // Turn it off once the service is known to send. logDiagnostics: true});
// A journey is one record's history. The entity is what you will search for.const journey = recorder.startJourney({ entity: { type: "customer", id: account.Id }});
// Each wrapper runs your code, returns its value unchanged, and records what// went in and what came out. The difference between those two is the point.const customer = await journey.transform("map-account", account, () => toCustomer(account));
const id = await journey.persist("save-customer", customer, () => db.customers.insert(customer));
// Aliases are the other identifiers this record answers to. Now a colleague who// only has the internal ID can still find this journey.journey.identify({ internalCustomerId: String(id) });Then search for account.Id. Or the internal ID. Or any other identifier you
attached.
examples/instrument-a-service is a standalone
project that does this end to end, including the part where a value goes
missing; the instrumentation in it is about thirty lines.
The SDK cannot break your application
Section titled “The SDK cannot break your application”An observability library that takes down the service it observes is worse than no library. This one is built so that cannot happen:
- No runtime dependencies. It brings nothing with it.
- Every entry point is wrapped. A recorder failure increments a counter and returns; it never reaches your code.
- Wrappers rethrow your exact error object, so
instanceofchecks and custom properties on your errors keep working. - The event queue is bounded, and drops oldest under backpressure rather than growing without limit.
- The transport retries behind a circuit breaker and gives up rather than piling up.
shutdown()races the final flush against a timeout and never hangs.- Nothing is written to your console unless you ask for it, apart from six
warnings printed once per process: a required setting that is missing or
empty, an optional setting the recorder could not use, a setting under its
old name, a
journeyIdSecretthat cannot be used, a field whose name looks like a secret that was sent in plain text, and a journey label or displayable alias that looks like personal data (SDK README).