originv0.2.0

Architecture in the code

How the binding rules play out: Tauri as host, the composition root, modules, events, the frontend boundary and the platform contracts.

The rules are in ARCHITECTURE.md. This page explains how they play out in the code.

Why Tauri is only the host

A service that holds an AppHandle can only run inside a Tauri process. That makes it untestable without a desktop session and couples it to a Tauri major version. So domain code depends on traits instead:

NotificationService  (crates/origin-platform)

TauriNotificationService  (adapters/origin-notifications-tauri)

tauri-plugin-notification

A Tauri 2 → 3 migration touches host/ and adapters/*-tauri, nothing else.

The composition root

Every product has exactly one function that assembles it. There is no service locator, no global state, and no runtime lookup by name — a missing dependency is a build error:

ApplicationBuilder::new()
    .storage(defaults::storage(app)?)
    .secret_store(defaults::secret_store(config))
    .notifications(defaults::notifications(app))
    .opener(defaults::opener(app))
    .module(PulseModule)
    .build()

Storage, credentials and notifications have no implicit default. Silently defaulting storage would ship an application that loses data; silently defaulting credentials would keep tokens in process memory. ApplicationBuilder::in_memory() provides all three at once, and its name says exactly what you get.

Modules

A module is a compile-time feature area. It registers services and subscribes to events:

impl ApplicationModule for PulseModule {
    fn id(&self) -> &'static str { "pulse" }

    fn register(&self, registry: &mut ModuleRegistry) -> Result<()> {
        registry.provide(Arc::new(PulseService::new(registry.platform().clone())));
        Ok(())
    }
}

There is no dynamic plugin loading. Everything in the binary got there by being linked in, which keeps the dependency graph honest and the binary auditable.

Events versus direct calls

  • Need a result now? Call the service. snapshot(), refresh(), store_secret().
  • Might several independent components react? Publish a typed event.

The bus is keyed by type, so subscribe::<PlatformEvent>() is checked by the compiler. Products publish their own enums on the same bus. Adding a variant breaks exhaustive subscribers — on purpose.

The frontend boundary

@casoon/origin-client is the only package that imports @tauri-apps/api. Everything else calls typed functions:

import { settings } from "@casoon/origin-client";
await settings.set("demo.critical_above", 40);

cargo xtask validate fails the build if a component imports Tauri APIs directly.

Storage and freshness

Storage is dumb persistence: it stores and returns records as given, expired or not. Cache decides what “stale” means, using the injected Clock. Every backend therefore agrees on expiry, and TTL behaviour is testable without sleeping.

External services stay Source of Truth. Deleting the local database costs a resync and nothing else.

Platform contracts

Host- and OS-specific capabilities are defined as traits in origin-platform, keeping domain logic and modules free from desktop framework details:

  • WorkspaceFs: Read-only access to directory listings and files, confined to a user-confirmed WorkspaceRoot with symlink traversal protection.
  • WorkspaceWatcher: Subscribes to filesystem changes within registered repository roots.
  • ProcessRunner: Starts external processes strictly constrained by an auditable ProcessAllowlist.
  • GlobalShortcutService: Registers system-wide key combinations (e.g. for quick capture).
  • ConfirmationService: Prompts the user before sensitive actions (e.g. mutating MCP tool calls, loopback token grants). Defaults to DenyingConfirmationService.
  • TrayService: Manages system tray items, status badges, and context menu events.

Each contract provides a corresponding in-memory implementation in origin-platform (MemoryWorkspaceFs, MemoryWorkspaceWatcher, MemoryProcessRunner, NoopGlobalShortcutService), ensuring full testability in headless environments.

Edit this page on GitHub · Docs for v0.2.0