Developing Kern

Notes for building Kern from source and working on the code. If you just want to use the app, see help instead.

Kern is a Tauri 2 app: a React and TypeScript frontend, and a Rust backend that owns all data (SQLite) and every operating-system integration.

Running and building

Requirements. Node 20+, stable Rust (MSVC toolchain on Windows), WebView2 (preinstalled on Windows 11). On Linux, install the Tauri system dependencies: WebKitGTK 4.1, libayatana-appindicator, librsvg and patchelf.

npm install
npm run app:dev      # development (Vite + Tauri, hot reload)
npm run app:build    # release installers in src-tauri/target/release/bundle/
npm test             # frontend unit tests (Vitest)
cd src-tauri && cargo test   # backend unit and integration tests

Set KERN_DATA_DIR before launching to run a development or test instance against a separate data folder, so your real data is never touched. Such an instance runs alongside the normal one and uses its own credential-store namespace.

Linux release bundles (.deb, .rpm, AppImage) are built in an Ubuntu 22.04 container so they run on older distributions too. From any OS with Docker:

docker build -t kern-linux scripts/linux
docker run --rm -v "$PWD:/repo:ro" -v "$PWD/release/linux:/out" \
  -v kern-cargo:/root/.cargo/registry -v kern-target:/build/src-tauri/target kern-linux

Architecture

src/                     React + TypeScript (strict), Vite
  app/                   bootstrap, routes, global effects and shortcuts
  components/ui|layout   design-system controls and the shell
  features/<feature>/    dashboard, tasks, boards, projects, whiteboard, notes,
                         inbox, daily-briefing, focus, search, capture, settings,
                         trash, onboarding, storage, links, ai, github,
                         command-centre, milestones, sessions, workspace,
                         catchup, health, terminal, dev
  lib/                   typed IPC client, query layer, dates, shortcuts, fuzzy matching
  stores/                Zustand stores for transient UI state, settings, router, undo, toasts
  styles/                design tokens (CSS variables), base, controls, layout
src-tauri/src/
  db/                    connection setup, versioned migrations
  domain/                one module per area: pure service functions on &Connection
                         plus thin #[tauri::command] wrappers
  scheduler.rs           one low-frequency thread: briefings, reminders, backups, housekeeping
  window.rs              window state persistence, tray, close/minimise behaviour

Four rules shape most of the code:

  • The frontend never issues SQL. It calls typed commands. Errors come back as { code, message } and are shown as actionable messages with Retry.
  • Persisted data uses TanStack Query, with keys grouped by domain for invalidation. Mutations are optimistic, roll back on failure, and offer Undo where reversible.
  • Heavy modules are lazy chunks — the whiteboard renderer, rich-text editor, settings, calendar, storage chart, API pages and Code tab.
  • Network and filesystem work never touches the UI thread: storage scans, favicon and link fetching, AI requests, git and GitHub all run in async commands on background threads.

Data

  • SQLite (WAL, foreign keys, FTS5 search kept in sync by triggers) at %APPDATA%\app.kern.desktop\kern.db, or ~/.local/share/app.kern.desktop/kern.db on Linux. Managed attachments live alongside it in attachments/ and are referenced by relative paths, so backups restore on other machines.
  • Dates are local calendar dates (YYYY-MM-DD) and times are HH:MM, so due dates never shift with timezone or DST changes. Instants are UTC epoch milliseconds.
  • Migrations are versioned with PRAGMA user_version. Each runs in its own transaction, so an interrupted migration rolls back, and a full copy of the database is written to backups\ before an existing database is migrated.
  • Deleting moves items to a recoverable Trash, purged after a configurable period.

Backups and export

One versioned zip format — manifest.json + kern.db + attachments/ — serves automatic, manual and pre-restore backups.

Export everything adds portable copies: data/<table>.json, notes/*.md with front matter, whiteboards/*.json and settings.json. Separate exports produce tasks as CSV, notes as Markdown and projects as JSON.

Restore validates the archive (manifest, integrity check, schema version), shows a preview, writes a safety backup, copies the data in with SQLite's online backup API and swaps attachments atomically.

Kern Cloud internals

The server is a separate project — see self-hosting and the protocol, which is the document to read before changing either side. Development builds point elsewhere with KERN_CLOUD_URL, for example http://localhost:8787/kern/v1.

That environment variable is the only way to move the address, and deliberately so. The function that returns the API base takes no argument and reads no setting. The session token is sent as a bearer to whatever it returns, and the request is made in Rust, where the window's content security policy doesn't apply — so an address the front end could write would be a way to hand the token to another host. Settings are writable from the front end, so a backend-trusted address must not live there. Self-hosting uses the environment variable too.

  • Offline mode is the ordinary kern.db, unchanged and account-free.
  • Spaces. The personal space and each workspace get a local replica with the same schema, so every feature works the same and offline. Switching spaces swaps the connection. Per-computer tables (settings, AI accounts and usage, folder trust, caches, notification log, linked-file index) are parked under a _device_ prefix in replicas, with kern.db attached as local, so unqualified names resolve to the device copy. Migrations unpark, migrate and re-park, so every schema step applies to replicas unchanged.
  • Sync. Triggers on the synced tables record changed rows in an outbox. A push sends field-level diffs against a shadow copy — the server's version as last known — or the full row for new records. Personal columns (pins, last opened, whiteboard viewport, local repository links) never sync. The server merges per field and replies with merged rows, applied as a three-way merge in which fields with unsent local edits keep the local value. Deletes win and leave tombstone shadows, so stale copies can't resurrect a record.
  • Worker. One background thread, woken by the replica's commit hook, pushes and pulls with backoff and emits status and applied-change events; the interface invalidates the matching queries.
  • Session. The bearer token is stored in the OS credential store and used only by the backend. The interface opens the realtime socket with a single-use ticket, hands incoming changes to the apply path, and shares presence and live drags.
  • Whiteboards save diffs against a per-editor baseline and three-way merge collaborators' saves; undo only reverts your own edits. Images placed on a cloud whiteboard are compressed in the interface (WebP, ≤1600 px, stepped down to ~400 KB), uploaded once per space — de-duplicated by hash — and referenced rather than embedded. Other computers show the original when they have it, otherwise download the shared copy into a cache folder.
  • Signing out reports whether the server actually ended the session. The local token is removed either way, but a revoke that never reached the server leaves the session usable by anyone holding a copy, so the interface says so rather than claiming a clean sign-out.
  • Backups, restore and "export everything" cover offline data only. Automatic orphaned-asset cleanup is skipped once cloud replicas exist, since their whiteboards may use attachment files.

Tests: cargo test cloud for the engine and client, npx vitest run collab for whiteboard merging, and an opt-in two-replica test against a running server.

Privacy and permissions

No network access is needed. Every network use is optional and controlled in Settings → Privacy, with a master switch that turns all of it off:

  • website favicons and link titles, fetched directly from the linked site and never through a third-party proxy, cached locally; private and local addresses are refused;
  • AI requests and connection tests, only when you make them — there is no polling;
  • GitHub issue sync and pull requests, only for projects you connect;
  • Kern Cloud, only after turning on online collaboration and signing in.

Settings → Privacy & Security also covers what Kern shows and keeps locally:

SettingWhat it does
Blur contents when Kern isn't focusedBlurs the window's contents with no transition, so nothing is readable mid-fade.
Show details in notificationsOff means reminders, mentions and focus alerts carry no task titles, comment text or names.
Trust expires afterAn expired folder decision reads as restricted immediately, and the downgrade is written back at the next launch. 0 never expires.
Record recently opened itemsOff stops recording and clears the table, so it forgets what was already recorded.
Allow plugin and syntax downloadsGates syntax installation on top of the network switch.
  • Files that commonly hold secrets — .env, *.pem, *.key, id_rsa, anything named credentials or secret — are never offered to AI features, trusted folder or not. That isn't a setting.
  • AI API keys and the GitHub token live in the OS credential store (Windows Credential Manager, macOS Keychain, Secret Service), never in SQLite, logs or backups. Only the backend uses them. GitHub can also use the GitHub CLI's login.
  • Linked project folders are separate from projects: deleting a project never touches its folder. Folders are restricted by default; programs, git commands and file changes need an explicit "trusted" decision, recorded per exact folder.
  • Tauri capabilities are limited to window show/focus, native open and save dialogs, notifications, autostart and the optional global quick-capture shortcut. The CSP allows one remote connection: the Kern Cloud realtime socket. Opening files and URLs, and all file writes, go through Kern's own commands.
  • The asset protocol can only read the app's attachments folder and the favicon cache, and the CSP does not allow remote images.

Passkeys and locked projects

A passkey is a credential in the OS authenticator — Windows Hello, or a security key over USB. WebAuthn does not work inside the webview, so registration and assertion run in Rust and the front end never sees key material. macOS and Linux report that passkeys aren't available; implementing one of them means writing three functions and nothing above them changes.

The things that aren't obvious from the code:

  • It is a gate, not encryption. The database holds the same rows either way, so the lock guards the running app, not the data at rest. Say so when describing the feature; don't let it read as "encrypted projects".
  • Signatures aren't re-verified. Kern is its own relying party on the same machine as the authenticator, so the gate checks that the OS returned an assertion for a credential registered here with its user-verified flag set. A verifying key would live inside the process being protected.
  • Locks only bite while this computer has a passkey, and a reset clears every passkey and lock without asking for one. Both exist so a synced lock or a reset Hello container can't strand a project forever; neither gives up anything that reading the database wouldn't.
  • Credentials are per computer while the lock itself syncs, so the lock follows the project and each computer registers its own passkey.

Where the gate sits. Every way into a project's content is covered, in one of three ways; adding a new read path means picking one of them.

KindHow
SQL predicateSpliced into the query — task lists, notes, whiteboards, activity, upcoming milestones, mentions, counts and the bulk exports
Hard refusalA readable check at the top — single-item getters, boards, comments, attachments, environment variables, health, catch-up, repositories and the two file roots
Post-filterRows dropped from a finished list — search, recents, trash, task relations

Two resolvers do most of the work: the folder root and the repository root are where every file, terminal, git, dependency, environment and index path resolves a directory, so gating those covers the dev tools without touching them. A locked project keeps its name and shows blanked counts, which is what lets the sidebar and the unlock screen work.

Deliberately not gated, all three for a reason: writes (nothing can reach them behind the lock screen, and they reveal nothing); whole-database backups and export-everything (they copy the file, which is data at rest and the documented way back from a lost authenticator, and the scheduler depends on them); and the trash purge path (it must not skip a locked project). The targeted exports do respect the gate, and the JSON one refuses by name rather than quietly leaving content out.

Key behaviours

  • Daily rundown. A deterministic, offline summary with a transparent score — priority, days overdue, due today, pinned, in progress, carried over, project deadline, minus blocked. Each suggestion shows its breakdown, and can be dismissed for the day or pinned. The first rundown of each day is stored as a snapshot; the evening review stores what was done and what moved.
  • Recurring tasks. Completing an occurrence keeps it as history and spawns the next, with its checklist reset. Missed occurrences are skipped, not back-filled. "From completion" schedules relative to the completion day.
  • Focus timer. Timestamp-based, so it stays accurate when minimised. The completion notification is fired from a native thread.
  • Task completion. A left-to-right strikethrough, CSS only with no layout measurement; writes are ordered and coalesced per task so rapid clicks always converge on the last one. Instant with reduced motion.
  • AI routing. Manual, automatic failover, priority or cost-conscious. Failover only replays requests that produced no output, honours retry-after, never rotates between keys in the same organisation after a pool-wide limit, and is bounded.
  • GitHub issues. Two-way, three-way-merged per field, with the newer edit winning when both sides changed. Deleting a task unlinks it; it never closes the issue.
  • Spelling. On Windows, WebView2 draws the underline but offers no corrections, so Kern does both itself through the OS spell-checking API — the computer's own dictionaries, nothing downloaded, nothing sent. Elsewhere the native menu handles it and Kern stays out of the way. Results are cached per block of text.
  • Flowcharts. The shape tool draws the standard vocabulary beside the general shapes, and connectors can route automatically, re-pointing at whichever sides their two objects now face as you drag. Mermaid flowchart and graph text is read and written, and the reader never throws — it returns a sentence for anything it can't parse.

Kern Lite

The same window can run as full Kern or as Kern Lite, a small task tracker in its own lazily loaded chunk. Both use the same backend and data, so there is nothing to sync between them.

The window layer tracks the mode, gives each its own minimum size and remembered geometry, and handles --lite and --full on the command line — also forwarded by the single-instance plugin, and used by the Windows installer's "Kern Lite" Start menu shortcut. A start-mode setting (ask, Kern, Lite) picks the mode at launch.

Switching cloud space or account resets the interface: the query cache is cleared and the shell remounts, so nothing from the previous data stays on screen.

Projects and dev tools

A project needs only a name. Everything else — folders, repositories, commands, services — is optional and only appears once it's connected, so a renovation or a school assignment never shows git tabs or empty developer panels.

  • Command Centre. Each project's overview is an editable widget grid starting from a preset that fits the project. Development widgets load lazily and only if added.
  • Resume Workspace. Kern remembers each project's tab, board, whiteboard, open task, open file, selected repository and terminal layout, and restores it, skipping anything that no longer exists. Resuming never runs commands, restarts services or starts assistants.
  • Milestones work in every project and can optionally be releases, with a version, repository, linked pull requests and issues and a changelog draft. Publishing a release is always done by the user on GitHub; Kern only opens a prefilled draft.
  • Work sessions are timestamp-based, so sleep, minimising and restarts never drift. A long silence before a heartbeat is offered as a gap to keep or exclude. No keystrokes, screenshots or other apps are tracked.
  • Repositories. A project can link zero, one or many. Unlinking never touches disk or GitHub. Git commands are repository-scoped and still need a trusted folder.
  • Worktrees are always read from git worktree list, including ones Kern didn't create. A dirty worktree needs explicit confirmation, a locked one is never removed, and branches are only deleted with git branch -d. Kern never runs clean, reset, force-push or history rewrites.
  • Start Work on a task begins a session and, with a trusted repository, can create or reuse a branch or worktree with a git-validated name. Completing a task never commits, pushes, merges or deletes.
  • Environment detection reads configuration files only and checks PATH; nothing is executed. Saved commands are a program plus an argument list — no shell string — run only when clicked, in a trusted folder.
  • Terminal. Real pseudoterminals rendered with xterm.js, loaded on first use. Processes run with the user's permissions (trust is not a sandbox), are stopped as a tree on close, and quitting with running commands asks first. Output is never persisted.
  • Environment profiles. Plain values in SQLite, secrets in the OS credential store with only a reference in SQLite, injected into the launched process only. Importing a .env defaults likely credentials to secrets; exporting secrets needs explicit consent.
  • Local services are only the endpoints the user adds, checked only while the project is open. "Running" means it answered; "healthy" only when a configured HTTP check passed.
  • Dependencies come from manifests and lockfiles; a declared range is never shown as a resolved version. Advisory lookups are explicit and send only package names and resolved versions.
  • Health is a state — on track, needs attention, blocked, no recent data — with clickable reasons and configurable rules. No scores.
  • Catch-up builds a deterministic local digest with numbered sources and works with no AI. The optional AI summary shows what's included and its size first, honours content permissions, redacts key-like strings, and drops any claim that doesn't cite a known source.
  • Universal search adds milestones, saved commands, workspaces, synced issues, cached pull requests and commits, and linked-folder files and code from an incremental index that skips dependencies, build output, ignored, binary and secret-looking files.

Releases and updates

Kern updates itself. When a newer release exists, the sidebar's Update available button swaps the window for a minimal update screen, downloads the signed installer — up to three attempts, returning to the running version if all fail — and installs it. Copies installed with the .exe update silently; .msi copies are per-machine, so their installer runs passively and Windows asks for approval. On the first start of a new version, a What's new dialog shows that release's notes.

The updater reads the release manifest from the newest non-pre-release GitHub release and only installs files signed with the updater key. The public key is in the Tauri configuration; the private key lives outside the repository. Back it up: if it's lost, installed copies can't verify any future update and everyone has to reinstall by hand once.

To release a new version:

  1. Bump the version in package.json, package-lock.json, Cargo.toml, Cargo.lock and tauri.conf.json; commit, tag v<version> and push both.
  2. Write the release notes in Markdown — a ## What's new section, and an optional ## Install section that the in-app dialog leaves out.
  3. npm run release -- --notes notes.md --publish

The script builds the installers with updater signatures, writes the release manifest, and creates the GitHub release with the installers and manifest attached. Without --publish it stops after building.

Security releases

--security makes a release a required update: any copy older than a release carrying that marker updates without asking, even when newer ordinary releases have come out since. If it doesn't finish, Kern stays usable — it is local-first, so nobody is locked out of their data — shows a Security update required notice, and tries again every ten minutes and on each start. Copies with network access turned off in Settings → Privacy don't check, so they aren't forced either. Use it only for real security fixes.

Working on the server

The server, Kern Platform and kernctl are in the Kern-Cloud-Self-Host repository.

cd server   && npm install && npm run dev        # the server, on :8787
cd platform && npm install && npm run app:dev    # Kern Platform
cd server   && npm run typecheck && npm test && npm run test:e2e
cd platform && npm run typecheck && npm test
sh -n cli/kernctl && sh -n cli/install.sh

Point a development build of Kern at it with KERN_CLOUD_URL=http://localhost:8787/kern/v1, which overrides the choice in Settings and locks it.

Read the protocol before changing either side — particularly adding to either side, which says when a change needs a protocol bump, how a new synced table must be added to both halves, and why a management capability has to land in both clients.

Contributing

Help of any size is welcome. Bugs and ideas go to issues; keep pull requests focused and tested, and open an issue first for anything large.

Before a pull request: npm test, cargo test, and a typecheck. If you touch sync, the protocol or anything in the privacy and permissions section above, say so in the description — those are the parts where a quiet change is expensive.

Contributions are covered by the project's contributor licence agreement, in CLA.md in the repository. The Kern app and the server are separate projects under their own licences; the server is Apache-2.0.