journal/docs/frontend-csharp-backend-wiring.md
Jacob Schmidt 54bef33f0b Refactor frontend state to store-first architecture
- add and expand feature stores for entries, fragments, todos, lists, settings

- move CRUD logic into store helpers and simplify component state handling

- update SidePanel + button to create section-specific items

- switch fragment UX to view-first with edit/create modes

- add and update docs for store-based state management

- remove deprecated account route
2026-02-25 20:52:46 -06:00

6.2 KiB

Wiring Frontend to the C# Backend

This document explains how to connect the Journal.App frontend to the C# backend in this repository.

Current Backend Reality

In this repo today, the C# backend projects in Journal.slnx are:

  • Journal.Core
  • Journal.Sidecar
  • Journal.SmokeTests

There is currently no Journal.Api project in the solution file, so the primary integration path is:

  • Frontend (Svelte/Tauri) -> Tauri bridge -> Journal.Sidecar (stdin/stdout JSON protocol)

Command Protocol (C#)

Journal.Core.Entry.HandleCommandAsync accepts a JSON command envelope and returns:

  • success: { "ok": true, "data": ... }
  • failure: { "ok": false, "error": "..." }

Command model (Journal.Core/Models/Command.cs):

{
  "action": "entries.list",
  "correlationId": "optional-string",
  "id": "optional",
  "type": "optional",
  "tag": "optional",
  "payload": {}
}

Useful actions for frontend wiring:

  • entries.list
  • entries.load
  • entries.save
  • search.entries
  • vault.load_all
  • vault.save_current_month
  • db.status
  • db.hydrate_workspace

Use a small frontend client that sends commands through one bridge function. The bridge can be backed by:

  • a Tauri command that talks to a managed sidecar process, or
  • a local HTTP adapter (if you add one).

1. Define shared frontend command/response types

Create Journal.App/src/lib/backend/types.ts:

export type BackendCommand = {
  action: string;
  correlationId?: string;
  id?: string;
  type?: string;
  tag?: string;
  payload?: unknown;
};

export type BackendOk<T> = { ok: true; data: T };
export type BackendErr = { ok: false; error: string };
export type BackendResponse<T> = BackendOk<T> | BackendErr;

2. Create one backend client entrypoint

Create Journal.App/src/lib/backend/client.ts:

import { invoke } from "@tauri-apps/api/core";
import type { BackendCommand, BackendResponse } from "./types";

export async function sendCommand<T>(command: BackendCommand): Promise<T> {
  const response = await invoke<BackendResponse<T>>("sidecar_command", { command });

  if (!response.ok) {
    throw new Error(response.error || "Backend command failed");
  }

  return response.data;
}

This keeps all UI code backend-agnostic.

3. Build domain helpers (entries example)

Create Journal.App/src/lib/backend/entries.ts:

import { sendCommand } from "./client";

export async function listEntries(dataDirectory?: string) {
  return sendCommand<string[]>({
    action: "entries.list",
    payload: { dataDirectory }
  });
}

export async function loadEntry(filePath: string) {
  return sendCommand<{ filePath: string; content: string; section?: string }>({
    action: "entries.load",
    payload: { filePath }
  });
}

export async function saveEntry(args: {
  filePath?: string;
  content: string;
  title?: string;
  section?: string;
  date?: string;
}) {
  return sendCommand<{ filePath: string }>({
    action: "entries.save",
    payload: args
  });
}

4. Use client in UI state

In page/component code:

  • on panel item click: call loadEntry(filePath)
  • on editor save button: call saveEntry({ filePath, content })
  • on app init: call listEntries() to populate list

Tauri Bridge Notes

Your frontend should not spawn/process-manage the sidecar directly. Keep that in the Tauri layer.

Bridge responsibilities:

  • start and keep one sidecar process alive
  • write command JSON lines to sidecar stdin
  • read stdout lines and map responses by correlationId
  • return parsed response to frontend
  • restart sidecar if it crashes

If you have not implemented this yet, create one Tauri command such as:

  • sidecar_command(command)

and route all frontend calls through it.

Vault/Auth Flow

Recommended startup sequence:

  1. Prompt for vault password in UI.
  2. Call vault.load_all (or db.hydrate_workspace) once.
  3. Backend stores session password (DatabaseSessionService) for subsequent commands.
  4. Continue with entries.list, entries.load, etc.

Do not store raw vault password in long-lived frontend state.

Error Handling Pattern

Always normalize backend errors in one place:

  • backend client throws Error(message) when ok: false
  • UI catches and displays your custom modal
  • include correlationId on commands for tracing/logging

Optional HTTP Path (If You Add Journal.Api)

If you later add Journal.Api with POST /api/command, keep the same command envelope and swap transport only:

  • replace invoke("sidecar_command", ...) with fetch("/api/command", ...)
  • keep sendCommand interface unchanged

That lets UI code remain identical.

Minimal Next Steps

  1. Add src/lib/backend/types.ts, client.ts, entries.ts.
  2. Wire EditorPanel save button to entries.save.
  3. Wire SidePanel item load to entries.load.
  4. Add vault unlock modal + vault.load_all on startup.
  5. Keep all backend calls behind sendCommand only.

Frontend Store Architecture (Current)

Current frontend uses feature stores in Journal.App/src/lib/stores/:

  • entries.ts -> entriesStore
  • fragments.ts -> fragmentsStore
  • todos.ts -> todoListsStore, todosStore
  • lists.ts -> listsStore
  • settings.ts -> settingsTags, settingsFragmentTypes

Current pattern is store-first for most feature CRUD and parsing (especially fragments and todos), with UI components invoking store helpers.

State/CRUD Gaps Still Needed

To fully standardize state management:

  1. Move settings add/edit/remove logic into settings.ts helper functions (currently in route component code).
  2. Add full CRUD helpers for entries.ts and lists.ts (update/remove/reorder, not only draft creation).
  3. Make todo list metadata + todo items update atomically through a single store API wrapper.
  4. Move calendar-created entries out of local component state into a dedicated calendar store.
  5. Add persistence/hydration strategy between stores and backend (entries.load/save, vault.load_all, etc.).
  • Keep all feature data mutations in store helper APIs.
  • Keep route/component files focused on view state and command orchestration.
  • Keep backend transport (sendCommand) separate from pure local store mutation helpers, then compose both in thin feature services.