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 isapps/renderer/src/db/driver.ts, which adapts the plugin to core’sSqlDriver; the repositories are bound once against it inapps/renderer/src/db/repositories.ts. The browser build opens the database throughapps/renderer/src/db/web/instead (SQLite in WASM), behind the same connection shape (Repository Seam). @datler/coreimports no React, no Tauri and no UI. Its shipping code importsdrizzle-orm(with itssqlite-coreandsqlite-proxyentry points),zod, and@jeliroco-multimedia/blunt-mechanisms/randomfor the demo profiles’ seeded randomness, which are its three declared dependencies. Test-only imports (vitest,node:sqlite,node:fs,node:path) stay in*.test.tsfiles andpackages/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) andgraph.tsare not re-exported: callers get repositories, not tables.@datler/synctakes 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-tokensis 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.
packageManagerin the rootpackage.jsonpinspnpm@12.4.1, and pnpm switches to the version it names.pnpm-workspace.yamllists the members and leaves outapps/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;minimumReleaseAgeExcludelifts the hold for@jeliroco-multimedia/*, the first-party libraries, so their releases install the day they ship. Without that exclude,pnpm updateanswers “Already up to date” while a release inside the hold exists.allowBuildsnames 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, symlinkednode_modulesmakes each package declare what it imports. tsconfig.base.json:strictwithnoUncheckedIndexedAccess,bundlerresolution,noEmit. Each workspace’stsconfig.jsonextends it.pnpm run typecheckruns every workspace’stypecheckscript, then the scripts’ own (tsconfig.scripts.json).biome.json: one formatter and linter for the tree. Every*.cssfile is excluded, since Biome’s CSS parser cannot read Tailwind v4’s at-rules, and so areapps/docs,.claude/workflows, generated JSON and drizzle’s metadata.noArrayIndexKeyis off: positional draft rows, matrix order and option codes key by index on purpose, and Biome reports the rule on thekey=line, where a multiline JSX element leaves no line for a suppression comment. Every other exception is abiome-ignoreat its site, with its reason. Biome prints 20 diagnostics by default;pnpm run lint --max-diagnostics=noneprints 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,nodefor**/*.test.tsanddomfor**/*.test.tsxunder jsdom. Tests sit beside their source in every workspace.pnpm run test:rustrunscargo testfor 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).
Related
Architecture Overview · Repository Seam · Design Tokens · DB Dev Commands · Public Site