System Architecture
Life Copilot is structured as two distinct layers that communicate through a strictly-typed IPC boundary provided by Tauri. This separation keeps the Rust core fast, testable, and platform-independent while allowing the frontend to evolve independently.
Layer Overview
┌─────────────────────────────────────────────────────────────┐
│ Frontend (Qwik / WebView) │
│ │
│ ┌──────────┐ ┌──────────┐ ┌───────────┐ ┌───────────┐ │
│ │ UI Shell │ │ Plugin A │ │ Plugin B │ │ Plugin C │ │
│ └────┬─────┘ └────┬─────┘ └─────┬─────┘ └─────┬─────┘ │
│ └─────────────┴──────────────┴───────────────┘ │
│ Event Bus (JS) │
└───────────────────────────┬─────────────────────────────────┘
│ Tauri IPC (invoke / emit)
│ Capability-gated
┌───────────────────────────▼─────────────────────────────────┐
│ Rust Core (src-tauri) │
│ │
│ ┌───────────────┐ ┌──────────────┐ ┌────────────────┐ │
│ │ Command Layer │ │ Domain Services│ │ Event Emitter │ │
│ │ (#[tauri::cmd])│ │ (tasks, habits) │ │ (app → window)│ │
│ └───────┬───────┘ └───────┬──────┘ └───────┬────────┘ │
│ └───────────────────┴──────────────────┘ │
│ Domain Core │
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ SQLite (rusqlite + migrations) │ │
│ └──────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
Frontend Layer
The frontend runs inside Tauri's embedded WebView. It is a standard Vite/Qwik single-page application — no Node.js runtime, no server during normal operation.
Responsibilities
- Render UI — All visual output. No business logic lives here.
- Host the JS Event Bus — A lightweight pub/sub bus that plugins use to communicate.
- Own the Plugin Registry — Plugins are loaded at startup and registered with the bus.
- Call Tauri Commands — Use
@tauri-apps/api/core'sinvoke()to call Rust commands.
Key Constraint
The frontend must never bypass the IPC boundary to access the database directly. All data reads/writes go through Rust commands. This ensures:
- Data integrity rules live in one place (Rust).
- The capability model can audit every data access.
- Plugin code is sandboxed to the JS Event Bus and cannot call arbitrary Rust commands.
Rust Core Layer
The Rust core runs as a native process and hosts the Tauri runtime. It is the source of truth for all application state.
Command Layer
Tauri commands are the public API surface between the frontend and the Rust core. Every command must:
- Be declared in
src-tauri/capabilities/default.jsonto be callable from the frontend. - Accept and return strongly typed structs that derive
serde::Serializeandserde::Deserialize. - Have a corresponding TypeScript type generated via
ts-rsand committed toapp/src/bindings/. - Return
Result<T, String>— errors are surfaced as rejected JS Promises.
// src-tauri/src/commands/tasks.rs
#[tauri::command]
pub async fn create_task(
db: tauri::State<'_, DbPool>,
payload: CreateTaskPayload,
) -> Result<Task, String> {
services::tasks::create(&db, payload)
.await
.map_err(|e| e.to_string())
}// app/src/bindings/tasks.ts (auto-generated by ts-rs)
import { invoke } from "@tauri-apps/api/core";
import type { CreateTaskPayload, Task } from "./types";
export const createTask = (payload: CreateTaskPayload): Promise<Task> =>
invoke("create_task", { payload });Domain Services
Business logic (validation, state transitions, recurring schedule computation) lives in domain service modules, not in command handlers. Commands are thin HTTP-controller-style adapters.
Event Emitter
The Rust core can push events to the frontend without being asked (e.g., a background reminder fires). These are emitted using app_handle.emit("reminder:due", payload) and received in the frontend by the Event Bus.
Local-First Data Model
All data is stored in an SQLite database on the user's device via rusqlite. There is no mandatory cloud dependency.
Guiding Principles
- Offline by default — The app must be fully functional with no network.
- Sync is optional — A future sync plugin may add cloud backup, but it is not a core concern.
- Append-only history — Completed and deleted items are soft-deleted and retained for history/undo.
- Single writer — Only the Rust core writes to the database. The frontend is read-only via Tauri commands.
See the Database Specification for the full schema.
Plugin System Overview
The plugin system lets first- and third-party code extend Life Copilot's behavior without modifying the core. All bundled features (Todos, Routines, Focus Timer) are implemented as first-party plugins to validate the API.
Plugin Boundaries
| Layer | What plugins can do |
|---|---|
| Frontend JS | Subscribe to and emit events on the JS Event Bus, register UI widgets in predefined slots, call whitelisted Tauri commands. |
| Rust (future) | First-party plugins may register additional Tauri commands via Tauri's native plugin system. Third-party Rust plugins are out of scope until the API is stable. |
See the Plugin API Specification for the full contract.
Technology Decisions
| Decision | Choice | Rationale |
|---|---|---|
| App runtime | Tauri 2.0 | Native performance, small bundle, mobile support, Rust safety. |
| Frontend framework | Qwik | Resumability removes hydration cost in a WebView context. |
| Database | SQLite via rusqlite | Embedded, zero-config, battle-tested, excellent mobile support. |
| IPC type safety | ts-rs code generation | Single source of truth for shared types across Rust and TypeScript. |
| Frontend state | Qwik signals + Context | Co-located with components; no separate state store needed for V1. |