---
title: TypeScript SDK
path: sdk/typescript
status: published
---

# TypeScript SDK

The TypeScript SDK (`@scailabs/scailog`) is the Node.js emit side of ScaiLog:
`createLogger` plus `pi()` give you GDPR-native structured logging that ships
entries over a Unix socket to the local `scailog-agent`. It is wire-compatible
with the Python, Go, and .NET SDKs — a TS-emitted entry is encrypted at ingest
and decrypts correctly through DSAR.

## Install

```bash
npm install @scailabs/scailog
```

The package is distributed as Node ESM with bundled type definitions, and has no
runtime dependencies.

## Emit a log entry

Create a logger with your tenant and service, then call a level method with a
`call_site_id`, a constant message, and an options object. Wrap any
personal-information value in `pi()`:

```ts
import { createLogger, pi } from '@scailabs/scailog';

const log = createLogger({ tenant: 'acme', service: 'checkout' });

const eventId = log.info('SCAI-CHECKOUT-PAID-001', 'order paid', {
  subject: 'usr_8842', // required whenever a pi() field is present
  fields: {
    method: 'card',
    email: pi('jan@example.nl', 'direct_identifier'),
    ip: pi('203.0.113.7', 'online_identifier'),
  },
});
```

The call returns the entry's ULID `eventId`. Plain fields (`method`) are stored
in cleartext and must stay PI-free; `pi()` fields are encrypted at ingest and
never enter the plaintext full-text index. Keep variable data in `fields`, not in
the message.

### Log levels

Six methods with the same signature
`level(site, message, opts?)`, where `opts` is
`{ subject?, fields?, traceId?, requestId? }`:

```ts
log.trace('SCAI-CACHE-MISS-001', 'cache miss');
log.debug('SCAI-CACHE-EVICT-001', 'evicted key');
log.info('SCAI-USER-LOGIN-001', 'user logged in', { subject: 'usr_8842' });
log.warn('SCAI-RATE-NEAR-001', 'approaching rate limit');
log.error('SCAI-DB-TXN-FAIL-001', 'transaction rolled back');
log.fatal('SCAI-BOOT-CONFIG-001', 'missing required config');
```

## Contract rules

The SDK enforces the shared ScaiLog contract before an entry leaves the process,
throwing an `Error` on a violation:

- **`call_site_id`** (first arg) must match `^[A-Z0-9]+(-[A-Z0-9]+){2,}$`. The
  `AUTO-` / `SCAILOG-` prefixes are reserved.
- **`pi(value, type)`** marks a field as personal information; markers attach to
  fields only, never to the message.
- A `pi()` field **requires** `subject` — enforced client-side (`SUBJECT_REQUIRED`)
  and again at the server.
- Reserved field names (`event_id`, `tenant`, `msg`, …) are rejected.

## Context propagation

`withContext()` — backed by Node's `AsyncLocalStorage` — propagates `subject`,
`traceId`, and `requestId` across `await` boundaries, so request handlers don't
thread them through every call. The logger uses the ambient context as defaults;
explicit options on a call override it, and nested scopes inherit and can
override:

```ts
import { withContext } from '@scailabs/scailog';

await withContext({ requestId: req.id, subject: user.id }, async () => {
  // picks up requestId + subject from the ambient context
  log.info('SCAI-API-HANDLED-001', 'handled');
});
```

## Configuration

`createLogger({ tenant, service, environment?, socketPath? })` constructs a
logger explicitly. `environment` defaults to `prod`, and `socketPath` defaults to
`$SCAILOG_SOCKET` or `/run/scailog/agent.sock`.

`defaultLogger()` builds one from the environment instead, reading
`SCAILOG_TENANT` (default `default`), `SCAILOG_SERVICE` (default `unknown`),
`SCAILOG_ENV` (default `prod`), and `SCAILOG_SOCKET`.

## Flush before exit

Entries are queued and sent by a background sender as NDJSON over the socket. If
the queue is full or the socket is unavailable, the entry is written to stderr in
the same NDJSON shape (with PI values redacted) rather than being silently
dropped. Before a short-lived process exits, drain the queue:

```ts
await log.flush(); // best-effort drain
log.close();
```
