One pnpm workspace, internal scope @datler/*, with the members apps/* and packages/* except apps/docs.

datler/
├── apps/
│   ├── desktop/        @datler/desktop: the Tauri v2 shell (src-tauri) and the local scan plugin (plugins/scan)
│   ├── renderer/       @datler/renderer: Vite, React 19, TanStack Router and Query; the whole UI
│   ├── storybook/      @datler/storybook: the component kitchen sink (Storybook 10, Vite builder)
│   ├── web/            @datler/web: the landing page at datler.ca and its Worker
│   ├── sync/           @datler/sync: the sync service, a Hono Worker over D1 with a relay Durable Object per profile
│   └── docs/           the Quartz site that publishes this vault's public notes; not a workspace member
├── packages/
│   ├── core/           @datler/core: domain, zod schemas, repositories and SQL adapters, migrator, sync engine
│   └── design-tokens/  @datler/design-tokens: datler-tokens.css, the Tailwind v4 bridge datler-theme.css, the typed theme catalog
├── scripts/
│   ├── db/             the db:* tooling (tsx over node:sqlite and @datler/core) and the bundled dataset generators
│   ├── drive/          the CDP drive harness (pnpm run drive)
│   └── docs/           the glossary generator and its checks
├── docs-vault/         the glossary vault, an input to the build
├── obsidian/Datler/    this vault
├── _generated/         vault-kit's generated views (gitignored)
└── vault.config.json

How the layers fit together, and why, is in Architecture Overview; the public site’s three Workers are in Public Site.

Boundary rules

  • The renderer imports @datler/core, never @tauri-apps/plugin-sql. The one exception is apps/renderer/src/db/driver.ts, which adapts the plugin to core’s SqlDriver; the repositories are bound once against it in apps/renderer/src/db/repositories.ts. The browser build opens the database through apps/renderer/src/db/web/ instead (SQLite in WASM), behind the same connection shape (Repository Seam).
  • @datler/core imports no React, no Tauri and no UI. Its shipping code imports drizzle-orm (with its sqlite-core and sqlite-proxy entry points), zod, and @jeliroco-multimedia/blunt-mechanisms/random for the demo profiles’ seeded randomness, which are its three declared dependencies. Test-only imports (vitest, node:sqlite, node:fs, node:path) stay in *.test.ts files and packages/core/test/, so no Node built-in reaches the app’s bundle.
  • @datler/core’s public surface is its barrel, src/index.ts. db.ts (the Drizzle sqlite-proxy bridge), schema.ts (the Drizzle tables) and graph.ts are not re-exported: callers get repositories, not tables.
  • @datler/sync takes only types from core in shipping code (apps/sync/src/relay-log.ts); its tests drive core’s real sync client against the Worker, so core is a dev dependency there.
  • @datler/design-tokens is the one source of the visual system, read by the renderer, Storybook and the landing page (Design Tokens).

Shared toolchain

One root config per concern, which each workspace extends rather than copies.

  • pnpm. packageManager in the root package.json pins pnpm@12.4.1, and pnpm switches to the version it names. pnpm-workspace.yaml lists the members and leaves out apps/docs, a vendored Quartz checkout that installs itself with npm and its own lockfile. pnpm holds a newly published release for a day before it installs it; minimumReleaseAgeExclude lifts the hold for @jeliroco-multimedia/*, the first-party libraries, so their releases install the day they ship. Without that exclude, pnpm update answers “Already up to date” while a release inside the hold exists. allowBuilds names the two dependencies whose install scripts must run, esbuild and workerd, since pnpm runs no dependency’s lifecycle script unless it is named. pnpm rather than npm: npm, as configured here, holds every release for seven days with no way to exempt a scope, and installs the newest version old enough without an error, so a first-party library that ships several releases a day lands versions behind with nothing to say so; and pnpm’s strict, symlinked node_modules makes each package declare what it imports.
  • tsconfig.base.json: strict with noUncheckedIndexedAccess, bundler resolution, noEmit. Each workspace’s tsconfig.json extends it. pnpm run typecheck runs every workspace’s typecheck script, then the scripts’ own (tsconfig.scripts.json).
  • biome.json: one formatter and linter for the tree. Every *.css file is excluded, since Biome’s CSS parser cannot read Tailwind v4’s at-rules, and so are apps/docs, .claude/workflows, generated JSON and drizzle’s metadata. noArrayIndexKey is off: positional draft rows, matrix order and option codes key by index on purpose, and Biome reports the rule on the key= line, where a multiline JSX element leaves no line for a suppression comment. Every other exception is a biome-ignore at its site, with its reason. Biome prints 20 diagnostics by default; pnpm run lint --max-diagnostics=none prints them all; with a -- before the flag, pnpm passes it through and Biome reads it as a path.
  • vitest.config.ts: one root runner with two projects split by environment, node for **/*.test.ts and dom for **/*.test.tsx under jsdom. Tests sit beside their source in every workspace. pnpm run test:rust runs cargo test for the shell.

Names

Every name in the code says Datler: the environment variables (DATLER_DB_PATH, DATLER_BACKGROUND, DATLER_PROFILE), the storage keys, spelt datler. (datler.activeProfile, datler.sync.session) or datler: (datler:theme, datler:catalog-view), datler-tokens.css and datler-theme.css, and the browser build’s database pool, datler-profiles. The app’s shared pieces are in apps/renderer/src/components/common/. The app was called Everything Tracker first, and what a device saved under that name moves once: storage keys spelt et. or et: at boot, before anything reads them (moveRenamedKeys in packages/core/src/renamed-keys.ts, imported first by main.tsx); the desktop’s profiles.json is read through the same rule (withCurrentKeyNames) and saved under the new names on its next write; and the browser build’s databases move from the et-profiles pool into datler-profiles when the worker starts (moveOldPool in apps/renderer/src/db/web/sqlite.worker.ts).

Architecture Overview · Repository Seam · Design Tokens · DB Dev Commands · Public Site