Datler tracks user-defined trackables and the timestamped entries logged against them. It is local-first: each device keeps the whole of a profile in its own SQLite file and reads only that file, so nothing on screen waits on the network (Profiles, Synced Databases).

The layers

flowchart TB
  ui["Renderer: apps/renderer"] --> core["@datler/core: domain, repositories, migrator, sync engine"]
  core --> drv["SqlDriver"]
  drv --> plug["plugin-sql, in the Tauri shell"]
  drv --> wasm["SQLite in WASM, in the browser build"]
  drv --> nodesql["node:sqlite, in the tooling and tests"]
  plug --> file[("One SQLite file per profile")]
  wasm --> file
  core <-->|"changes"| relay["Sync Worker and relay: apps/sync"]
  • The renderer (apps/renderer) is the whole UI: Vite, React 19, TanStack Router and TanStack Query, built from the shared component library (Jeliroco Components). It reads and writes through the repositories of @datler/core and nothing else.
  • @datler/core (packages/core) is the domain: the schema, zod payload checks, the repositories over an injected SqlDriver (Repository Seam), the migration runner (Migration Runner), the evaluator that turns entries into readings, and the sync engine. It runs the same in the WebView, in node for the db:* tooling, and in the tests.
  • The Tauri shell (apps/desktop) hosts the WebView on Windows and Android. It registers the SQL plugin, the local scan plugin the phone uses to read labels, and a handful of file commands; it owns no schema and no domain logic. Its surface is listed in Distribution.
  • The browser build (--mode web) is the same renderer with SQLite compiled to WASM, running in a worker, in place of the Tauri plugin.
  • The sync service (apps/sync) is a Cloudflare Worker that holds accounts and relays each synced profile’s changes through a Durable Object; triggers in each device’s database capture the changes, below the repositories (Synced Databases).

The workspaces, the public site and the toolchain are in Monorepo Layout.

Three rules keep the layers apart

  1. The renderer reaches the database only through @datler/core. The one file that imports @tauri-apps/plugin-sql is apps/renderer/src/db/driver.ts, which adapts it to SqlDriver; the browser build’s equivalent is apps/renderer/src/db/web/. Everything else calls repositories, so the backend under them can change without touching the UI.
  2. @datler/core imports no React and no Tauri. It is headless, which is what lets the tooling and the tests run the real domain in node against node:sqlite.
  3. Migrations run in JS at boot, through core’s runner, not in Rust. The app and the tooling apply the same drizzle-generated SQL and track it in one __drizzle_migrations table (Migration Runner).

Tauri rather than Electron

The separation follows Vodler, a sibling Jeliroco app: a headless domain, a typed boundary, and a UI that never sees SQL. Vodler gets it from an Electron shell with tRPC over IPC between a Node main process and the renderer. Datler has no Node main process: the domain runs in the WebView, and the boundary is the repository seam rather than a transport. Tauri rather than Electron, for small binaries and because Tauri v2 builds the same app for Android, which Electron cannot. The shell therefore stays as thin as the plugins and file access require.

Monorepo Layout · Repository Seam · Migration Runner · Synced Databases · Distribution · Database Schema