> Part of the walkerOS documentation. Project overview and full index: <https://www.walkeros.io/llms.txt>

# SQLite

<!-- -->

[Server](#)[ ](https://github.com/elbwalker/walkerOS/tree/main/packages/server/destinations/sqlite)

<!-- -->

[Source code](https://github.com/elbwalker/walkerOS/tree/main/packages/server/destinations/sqlite)[ ](https://www.npmjs.com/package/@walkeros/server-destination-sqlite)

<!-- -->

[Package](https://www.npmjs.com/package/@walkeros/server-destination-sqlite)

<!-- -->

Beta

Persists walkerOS events to SQLite. One destination, two drivers behind a single interface: [`better-sqlite3`](https://github.com/WiseLibs/better-sqlite3) for local files (and `:memory:`), and [`@libsql/client`](https://github.com/tursodatabase/libsql-client-ts) for remote Turso / libSQL / sqld over HTTP or WebSocket. Driver selection is URL-driven; both SDKs are optional peer dependencies so you install only the one you need.

<!-- -->

Where this fits

SQLite is a **server destination** in the walkerOS flow:

Receives events server-side from the collector, serializes them into a canonical row, and inserts them into a SQLite-compatible database. Good fit for single-host deployments (local file), embedded analytics (in-memory), and edge-deployed Turso databases.

## Installation[​](#installation "Direct link to Installation")

```
# Local file / :memory:
npm install @walkeros/server-destination-sqlite better-sqlite3

# Remote Turso / libSQL
npm install @walkeros/server-destination-sqlite @libsql/client
```

* Integrated
* Bundled

```
import { startFlow } from '@walkeros/collector';
import { destinationSQLite } from '@walkeros/server-destination-sqlite';

await startFlow({
  destinations: {
    sqlite: {
      code: destinationSQLite,
      config: {
        settings: {
          sqlite: {
            url: './events.db',
          },
        },
        setup: true,
      },
    },
  },
});
```

Add to your `flow.json` destinations:

```
"destinations": {
  "sqlite": {
    "package": "@walkeros/server-destination-sqlite",
    "config": {
      "settings": {
        "sqlite": {
          "url": "./events.db"
        }
      },
      "setup": true
    }
  }
}
```

[See bundled mode setup](https://www.walkeros.io/docs/getting-started/modes/bundled.md) | [CLI reference](https://www.walkeros.io/docs/apps/cli.md)

## Configuration[​](#configuration "Direct link to Configuration")

This <!-- -->destination<!-- --> uses the standard <!-- -->destination<!-- --> config wrapper (consent, data, env, id, ...). For the shared fields see [destination<!-- --> configuration](https://www.walkeros.io/docs/destinations.md#configuration). Package-specific fields live under `config.settings` and are listed below.

## Settings[​](#settings "Direct link to Settings")

| Property    | Type                 | Description                                                                                                                                                                                         | More |
| ----------- | -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---- |
| `sqlite*`   | `sqlite`             | SQLite / libSQL configuration                                                                                                                                                                       |      |
| `url*`      | `string`             | SQLite connection URL. libsql://, http(s)://, ws(s):// route to libSQL/Turso. Anything else is treated as a local file path via better-sqlite3. Use ':memory:' for an ephemeral in-memory database. |      |
| `authToken` | `string`             | libSQL / Turso auth token. Ignored for better-sqlite3 (local) connections.                                                                                                                          |      |
| `table`     | `string`             | Target table name. Defaults to "events".                                                                                                                                                            |      |
| `schema`    | `'auto' \| 'manual'` | \[DEPRECATED] Use config.setup instead. "auto" maps to "setup: true" (run \`walkeros setup destination.\<id>\`); "manual" maps to "setup: false". Removed in the next major.                        |      |

\* Required fields

## Mapping[​](#mapping "Direct link to Mapping")

Per-event rules under `config.mapping`. For the standard rule fields (consent, condition, data, batch, name, policy) see [mapping](https://www.walkeros.io/docs/mapping.md).

| Property | Type     | Description                                                                            | More |
| -------- | -------- | -------------------------------------------------------------------------------------- | ---- |
| `table`  | `string` | Override target table name for this rule. Takes precedence over settings.sqlite.table. |      |

## Examples

### Custom table

A destination-level table setting inserts events into a custom SQLite table with the same column layout.

Event

```
{
  "name": "form submit",
  "data": {
    "type": "contact"
  },
  "context": {
    "dev": [
      "test",
      1
    ]
  },
  "globals": {},
  "custom": {
    "completely": "random"
  },
  "user": {
    "session": "sess-99",
    "id": ""
  },
  "nested": [
    {
      "entity": "child",
      "data": {
        "is": "subordinated"
      }
    }
  ],
  "consent": {},
  "id": "evt-2",
  "trigger": "test",
  "entity": "form",
  "action": "submit",
  "timestamp": 1700000101,
  "timing": 3.14,
  "source": {
    "count": 1,
    "trace": "0a1b2c3d4e5f60718293a4b5c6d7e8f9",
    "type": "browser",
    "platform": "web",
    "url": "https://example.com/contact"
  }
}
```

Out

```
client.runInsert([
  1700000101,
  "evt-2",
  "form submit",
  "form",
  "submit",
  "sess-99",
  "",
  "https://example.com/contact",
  "",
  "",
  "{\"type\":\"contact\"}",
  "{}",
  "{}"
])
```

### Default insert

A walker event is inserted into the default events table with canonical columns and JSON-encoded sections.

Event

```
{
  "name": "page view",
  "data": {
    "title": "Home"
  },
  "context": {
    "dev": [
      "test",
      1
    ]
  },
  "globals": {
    "env": "prod"
  },
  "custom": {
    "completely": "random"
  },
  "user": {
    "session": "sess-1",
    "id": "user-42"
  },
  "nested": [
    {
      "entity": "child",
      "data": {
        "is": "subordinated"
      }
    }
  ],
  "consent": {
    "analytics": true
  },
  "id": "evt-1",
  "trigger": "load",
  "entity": "page",
  "action": "view",
  "timestamp": 1700000100,
  "timing": 3.14,
  "source": {
    "count": 1,
    "trace": "0a1b2c3d4e5f60718293a4b5c6d7e8f9",
    "type": "browser",
    "platform": "web",
    "url": "https://example.com/",
    "referrer": "https://example.com/prev"
  }
}
```

Out

```
client.runInsert([
  1700000100,
  "evt-1",
  "page view",
  "page",
  "view",
  "sess-1",
  "user-42",
  "https://example.com/",
  "Home",
  "https://example.com/prev",
  "{\"title\":\"Home\"}",
  "{\"env\":\"prod\"}",
  "{\"analytics\":true}"
])
```

### Order insert

An order complete is inserted with numeric data serialized as JSON in the data column.

Event

```
{
  "name": "order complete",
  "data": {
    "id": "ORD-1",
    "total": 99
  },
  "context": {
    "shopping": [
      "complete",
      0
    ]
  },
  "globals": {},
  "custom": {
    "completely": "random"
  },
  "user": {
    "session": "",
    "id": ""
  },
  "nested": [
    {
      "entity": "product",
      "data": {
        "id": "ers",
        "name": "Everyday Ruck Snack",
        "color": "black",
        "size": "l",
        "price": 420
      },
      "context": {
        "shopping": [
          "complete",
          0
        ]
      },
      "nested": []
    },
    {
      "entity": "product",
      "data": {
        "id": "cc",
        "name": "Cool Cap",
        "size": "one size",
        "price": 42
      },
      "context": {
        "shopping": [
          "complete",
          0
        ]
      },
      "nested": []
    },
    {
      "entity": "gift",
      "data": {
        "name": "Surprise"
      },
      "context": {
        "shopping": [
          "complete",
          0
        ]
      },
      "nested": []
    }
  ],
  "consent": {},
  "id": "evt-3",
  "trigger": "load",
  "entity": "order",
  "action": "complete",
  "timestamp": 1700000102,
  "timing": 3.14,
  "source": {
    "count": 1,
    "trace": "0a1b2c3d4e5f60718293a4b5c6d7e8f9",
    "type": "collector",
    "schema": "4"
  }
}
```

Out

```
client.runInsert([
  1700000102,
  "evt-3",
  "order complete",
  "order",
  "complete",
  "",
  "",
  "",
  "",
  "",
  "{\"id\":\"ORD-1\",\"total\":99}",
  "{}",
  "{}"
])
```

### Table override

A mapping rule overrides the target table so specific events are inserted into a dedicated SQLite table.

Event

```
{
  "name": "order complete",
  "data": {
    "id": "ORD-2",
    "total": 42
  },
  "context": {
    "shopping": [
      "complete",
      0
    ]
  },
  "globals": {},
  "custom": {
    "completely": "random"
  },
  "user": {
    "session": "",
    "id": ""
  },
  "nested": [
    {
      "entity": "product",
      "data": {
        "id": "ers",
        "name": "Everyday Ruck Snack",
        "color": "black",
        "size": "l",
        "price": 420
      },
      "context": {
        "shopping": [
          "complete",
          0
        ]
      },
      "nested": []
    },
    {
      "entity": "product",
      "data": {
        "id": "cc",
        "name": "Cool Cap",
        "size": "one size",
        "price": 42
      },
      "context": {
        "shopping": [
          "complete",
          0
        ]
      },
      "nested": []
    },
    {
      "entity": "gift",
      "data": {
        "name": "Surprise"
      },
      "context": {
        "shopping": [
          "complete",
          0
        ]
      },
      "nested": []
    }
  ],
  "consent": {},
  "id": "evt-4",
  "trigger": "load",
  "entity": "order",
  "action": "complete",
  "timestamp": 1700000103,
  "timing": 3.14,
  "source": {
    "count": 1,
    "trace": "0a1b2c3d4e5f60718293a4b5c6d7e8f9",
    "type": "collector",
    "schema": "4"
  }
}
```

Mapping

```
{
  "settings": {
    "table": "orders"
  }
}
```

Out

```
client.runInsert([
  1700000103,
  "evt-4",
  "order complete",
  "order",
  "complete",
  "",
  "",
  "",
  "",
  "",
  "{\"id\":\"ORD-2\",\"total\":42}",
  "{}",
  "{}"
])
```

The destination opens the connection during `init()` and prepares the INSERT statement once. Schema creation lives in the `setup` lifecycle (see below), not in `init()`. On `destroy()` the connection is closed cleanly. User-provided clients (wired via `env.client` or `settings.sqlite._client`) are left untouched.

## Setup[​](#setup "Direct link to Setup")

Create the events table and apply pragmas with one command:

```
walkeros setup destination.sqlite
```

This runs `CREATE TABLE IF NOT EXISTS` with the canonical 15-column walkerOS Event v4 schema and applies four pragmas:

* `journal_mode = WAL` (better concurrent reads)
* `synchronous = NORMAL` (good durability vs. perf balance)
* `foreign_keys = ON`
* `temp_store = MEMORY`

Setup is idempotent. Re-running against a populated database is a safe no-op. Drift between the declared schema and the actual table is logged as `WARN setup.drift {field, declared, actual}`. Setup never auto-mutates an existing table, no `ALTER TABLE`, no destructive recreates.

The default 15-column schema mirrors the canonical walkerOS Event v4 layout. Only `name` is `NOT NULL`. See [Event Model](https://www.walkeros.io/docs/getting-started/event-model.md) for the full field reference.

```
CREATE TABLE IF NOT EXISTS events (

  name      TEXT NOT NULL,

  data      TEXT,

  context   TEXT,

  globals   TEXT,

  custom    TEXT,

  user      TEXT,

  nested    TEXT,

  consent   TEXT,

  id        TEXT,

  trigger   TEXT,

  entity    TEXT,

  action    TEXT,

  timestamp TEXT,

  timing    INTEGER,

  source    TEXT

)
```

Nested objects (`data`, `context`, `globals`, `custom`, `user`, `nested`, `consent`, `source`) are stored as JSON strings.

Override defaults in `config.setup`:

```
"setup": {
  "pragmas": { "journal_mode": "DELETE" },
  "indexes": [{ "name": "idx_events_name", "columns": ["name"] }]
}
```

`setup: true` accepts all defaults. `setup: false` (or omitted) means `walkeros setup destination.sqlite` is a no-op for this destination.

Use `mapping.settings.table` to route specific events to a dedicated table (for example orders to `orders`, identities to `identities`).

### Migration from `schema`[​](#migration-from-schema "Direct link to migration-from-schema")

Deprecated

The package-local `settings.sqlite.schema` setting is deprecated. The framework now owns the setup lifecycle through `config.setup`. The deprecated form still works and emits a one-time WARN through the destination logger.

| Old (`settings.sqlite.schema`) | New (`config.setup`) | Effect                                             |
| ------------------------------ | -------------------- | -------------------------------------------------- |
| `'auto'`                       | `true`               | `walkeros setup destination.sqlite` creates table. |
| `'manual'`                     | `false`              | Setup is a no-op. Bring your own schema + mapping. |
| omitted                        | omitted              | No-op until `setup` is set explicitly.             |

Remove the `schema` field from `settings.sqlite` and add `setup: true` (or `false`) at the `config` level.

## Drivers[​](#drivers "Direct link to Drivers")

### Local, `better-sqlite3`[​](#local-better-sqlite3 "Direct link to local-better-sqlite3")

Sync native driver, fastest option for single-host deployments. The URL is treated as a filesystem path; `:memory:` works too. All four default pragmas are honored.

```
{
  "sqlite": {
    "url": "/var/lib/walkeros/events.db"
  }
}
```

### Remote, `@libsql/client`[​](#remote-libsqlclient "Direct link to remote-libsqlclient")

Async HTTP/WSS driver for [Turso](https://turso.tech/), self-hosted [sqld](https://github.com/tursodatabase/libsql), or any libSQL-compatible endpoint. Auth via `authToken`. The remote server controls journaling, so a client-side `journal_mode` pragma is silently ignored. The other pragmas (`synchronous`, `foreign_keys`, `temp_store`) still apply.

```
{
  "sqlite": {
    "url": "libsql://my-db.turso.io",
    "authToken": "$env.TURSO_TOKEN"
  }
}
```

URL prefixes `libsql://`, `http://`, `https://`, `ws://`, `wss://` route to the libSQL driver. Anything else (bare paths, `:memory:`) routes to `better-sqlite3`.

## Limitations[​](#limitations "Direct link to Limitations")

* v1 issues one `INSERT` per event. A `pushBatch` path is planned for v2.
* Connection death is not auto-retried; a fatal driver error logs and drops events until the flow restarts.
