Journal Backend (.NET)
A .NET 10 backend for the Project Journal app. Provides core journal functionality as a class library with a sidecar console app for Tauri integration and an optional HTTP API.
Project Structure
backend/
├── Journal.Core/ Class library — all business logic
│ ├── Models/
│ │ ├── Fragment.cs Domain model (validated, owns Guid ID)
│ │ ├── Command.cs Stdin command shape for sidecar protocol
│ │ ├── ParsedSection.cs Parsed section model for entry parity work
│ │ ├── SectionTitles.cs Canonical section title list (Python parity)
│ │ └── JournalEntry.cs Entry domain (`date/raw_content/sections/fragments` + merge + markdown reconstruction)
│ ├── Dtos/
│ │ └── FragmentDtos.cs Immutable records for API boundary
│ │ ├── FragmentDto Read (what goes out)
│ │ ├── CreateFragmentDto Create (what comes in)
│ │ └── UpdateFragmentDto Update (partial, all fields optional)
│ ├── Repositories/
│ │ ├── IFragmentRepository.cs Interface (data access contract)
│ │ ├── InMemoryFragmentRepository.cs In-memory implementation (tests/dev)
│ │ └── FileFragmentRepository.cs File-backed implementation (default)
│ ├── Services/
│ │ ├── IFragmentService.cs Interface (business logic contract)
│ │ ├── FragmentService.cs Validates, calls repo, maps to DTOs
│ │ ├── IEntrySearchService.cs Entry search contract (content parity)
│ │ ├── EntrySearchService.cs Searches decrypted `.md` entries by raw content query
│ │ ├── IJournalConfigService.cs Config contract for path/vault/AI/speech settings parity
│ │ ├── JournalConfigService.cs Env/default-backed config surface aligned with Python keys
│ │ ├── IAiService.cs AI bridge contract (optional provider)
│ │ ├── DisabledAiService.cs No-op AI provider for deterministic disabled mode
│ │ ├── PythonSidecarAiService.cs Local Python sidecar adapter (stdin/stdout JSON)
│ │ ├── SidecarCli.cs CLI runner (`vault` + `search`) used by Sidecar host
│ │ ├── JournalParser.cs Date + section + checkbox + fragment parser slices (Phase 2)
│ │ ├── IVaultCryptoService.cs Vault crypto contract
│ │ ├── VaultCryptoService.cs AES-256-GCM + PBKDF2 compatibility layer
│ │ ├── IVaultStorageService.cs Vault load/workflow contract
│ │ └── VaultStorageService.cs Monthly naming + load/decrypt/extract workflow
│ ├── Entry.cs Command dispatcher (stdin/stdout)
│ ├── ServiceCollectionExtensions.cs DI registration helper
│ └── Journal.Core.csproj
│
├── Journal.Sidecar/ Console app — Tauri sidecar bridge
│ ├── App.cs Boots DI container, runs Entry.RunAsync()
│ └── Journal.Sidecar.csproj References Journal.Core
│
├── Journal.Api/ Web API — HTTP endpoint wrapper (optional)
│ ├── Program.cs
│ └── Journal.Api.csproj
│
└── README.md
Architecture
Each layer only knows about the one below it:
Sidecar (stdin/stdout) ──┐
├──► Services (business logic) ──► Repositories (data access)
API (HTTP/JSON) ─────────┘
- Models — Domain objects with validation. The source of truth.
- DTOs — Immutable records that cross the API boundary. Internal logic never leaks out.
- Repositories — Where data lives. Current default is file-backed; can evolve to SQLite/EF Core without touching anything above.
- Services — Business rules, validation, orchestration. Doesn't know about HTTP or stdin.
- Entry — Transport adapter. Translates stdin/stdout JSON into service calls.
Dependencies
- Journal.Core —
Microsoft.Extensions.DependencyInjection.Abstractions(interface-only, lightweight) - Journal.Sidecar —
Microsoft.Extensions.DependencyInjection(full container implementation) + referencesJournal.Core - Journal.Api —
Microsoft.AspNetCore.OpenApi+ ASP.NET shared framework
Building
# Build everything (building Sidecar also rebuilds Core if changed)
dotnet build backend\Journal.Sidecar\Journal.Sidecar.csproj
# Build just the library
dotnet build backend\Journal.Core\Journal.Core.csproj
# Format code
dotnet format backend\Journal.Core\Journal.Core.csproj
Publishing
Publish as a single-file self-contained executable (no .NET runtime install needed):
dotnet publish backend\Journal.Sidecar\Journal.Sidecar.csproj -c Release -r win-x64 --self-contained -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true
Output: backend\Journal.Sidecar\bin\Release\net10.0\win-x64\publish\Journal.Sidecar.exe (~70MB, everything bundled)
To exclude debug symbols: add -p:DebugType=none
For a smaller build that requires .NET 10 on the target machine:
dotnet publish backend\Journal.Sidecar\Journal.Sidecar.csproj -c Release -r win-x64 -p:PublishSingleFile=true
Sidecar Protocol
The sidecar communicates over stdin/stdout using JSON lines. One JSON line in, one JSON line out. When run with no command-line args, this protocol mode is used by default.
Sidecar CLI
Journal.Sidecar also supports direct vault and search CLI commands:
# Load vaults into decrypted data workspace
dotnet run --project Journal.Sidecar/Journal.Sidecar.csproj -- vault load
# Save (rebuild) monthly vaults from decrypted markdown files
dotnet run --project Journal.Sidecar/Journal.Sidecar.csproj -- vault save
# Search entries (query + filters)
dotnet run --project Journal.Sidecar/Journal.Sidecar.csproj -- search "common text" --tag stress --type !TRIGGER --start-date 2026-02-01 --end-date 2026-02-28 --section Summary --checked "med taken"
Password prompt behavior:
- If
--passwordis omitted, CLI prompts withVault password:(hidden input in terminal mode). - For automation/non-interactive use, pass
--password <value>.
Optional path overrides:
--vault-dir <path>--data-dir <path>- Env fallback:
JOURNAL_VAULT_DIR,JOURNAL_DATA_DIR,JOURNAL_APP_DIR
Search CLI flags:
- positional
query(optional) --tag/-t(repeatable)--type/-y(repeatable)--start-date/-s(yyyy-MM-dd)--end-date/-e(yyyy-MM-dd)--section/-sec--checked/-chk(repeatable)--unchecked/-uchk(repeatable)--data-dir <path>(optional override)
Config Keys (Parity Surface)
JournalConfigService exposes and normalizes key settings expected from Python config:
- Paths:
JOURNAL_PROJECT_ROOT,JOURNAL_APP_DIR,JOURNAL_DATA_DIR,JOURNAL_VAULT_DIR,JOURNAL_LOG_DIR,JOURNAL_PID_FILE,JOURNAL_SERVER_CONTROL_FILE - Vault format:
JOURNAL_MONTHLY_VAULT_FORMAT(default%Y-%m.vault) - AI endpoints/models:
CLOUDAI_API_KEY,CLOUDAI_API_URL,LLAMA_CPP_URL,LLAMA_CPP_MODEL,LLAMA_CPP_TIMEOUT,EMBEDDING_API_URL,EMBEDDING_MODEL_NAME,MODEL_CONTEXT_TOKENS,CHUNK_TOKEN_BUDGET - AI bridge mode:
JOURNAL_AI_PROVIDER(noneorpython-sidecar),JOURNAL_PYTHON_EXE,JOURNAL_AI_SIDECAR_PATH,JOURNAL_AI_TIMEOUT_MS - Speech/NLP:
MICROPHONE_DEVICE_INDEX,SPEECH_RECOGNITION_ENGINE,WHISPER_MODEL_SIZE,JOURNAL_NLP_BACKEND
Command Format
{
"action": "fragments.create",
"id": null,
"type": null,
"tag": null,
"payload": { "type": "!TRIGGER", "description": "stomach drop" }
}
Fields:
action— The operation to perform (e.g.fragments.list,fragments.create)id— Target entity ID (for get/update/delete)type/tag— Filter parameters (for search)payload— Request body, deserialized into the appropriate DTO per action
Available Actions
| Action | Description | Requires |
|---|---|---|
fragments.list |
List all fragments | — |
fragments.get |
Get fragment by ID | id |
fragments.create |
Create a new fragment | payload (CreateFragmentDto) |
fragments.update |
Update a fragment | id, payload (UpdateFragmentDto) |
fragments.delete |
Delete a fragment | id |
fragments.search |
Search by type/tag | type and/or tag |
entries.list |
List decrypted markdown entries in a data directory | optional payload.dataDirectory |
entries.load |
Load one entry file and return parsed metadata + raw content | payload.filePath |
entries.save |
Save/merge entry content to file (fragment append or full merge path) | payload.content, optional payload.filePath, payload.mode |
db.status |
Return DB key/schema compatibility + SQLCipher runtime snapshot | payload.password, optional payload.dataDirectory |
db.initialize_schema |
Write SQL schema bootstrap (journal_schema.sql) for parity tables |
optional payload.dataDirectory |
db.hydrate_workspace |
Perform C# DB hydration step for decrypted workspace (schema bootstrap + metadata) | payload.password, optional payload.dataDirectory |
config.get |
Return current backend config snapshot | — |
ai.health |
Return AI bridge health/provider status | — |
ai.summarize_entry |
Summarize one entry through AI provider | payload.content, optional payload.fileStem |
ai.summarize_all |
Summarize a set of entries through AI provider | payload.entries[] |
ai.chat |
Send chat prompt through AI provider bridge | payload.prompt |
ai.embed |
Generate embedding vector through AI provider bridge | payload.content |
search.entries |
Search decrypted entry content with optional parity filters | payload.dataDirectory, optional payload.query, payload.section, payload.startDate, payload.endDate, payload.tags[], payload.types[], payload.checked[], payload.unchecked[] |
vault.initialize |
Ensure vault directory exists | payload.password, payload.vaultDirectory |
vault.load_all |
Load/decrypt all monthly vaults into data directory | payload.password, payload.vaultDirectory, payload.dataDirectory |
vault.save_current_month |
Save only current month vault (optimized path) | payload.password, payload.vaultDirectory, payload.dataDirectory, optional payload.nowUtc |
vault.rebuild_all |
Rebuild all monthly vaults from decrypted .md data |
payload.password, payload.vaultDirectory, payload.dataDirectory |
vault.clear_data_directory |
Clear decrypted data directory and recreate it | payload.dataDirectory |
Response Format
Success:
{ "ok": true, "data": { "id": "abc-123", "type": "!TRIGGER", "description": "...", "time": "...", "tags": [] } }
Error:
{ "ok": false, "error": "Description is required" }
Extending with New Modules
The Command class is generic — new modules use the same dot-notation pattern:
vault.unlock → IVaultService (future)
vault.lock
entries.list → IEntryService (future)
entries.create
ai.health → IAiService (implemented bridge)
ai.summarize_* → IAiService (implemented bridge)
ai.chat → IAiService (implemented bridge)
ai.embed → IAiService (implemented bridge)
db.status → IJournalDatabaseService (implemented SQLCipher parity/runtime checks)
search.query → ISearchService (future)
db.status and db.hydrate_workspace now include:
CipherVersion(fromPRAGMA cipher_version)EncryptedAtRest(true when DB header is not plaintext SQLite)
To add a module:
- Create model, DTO, repository, and service in
Journal.Core/ - Register the new service in
ServiceCollectionExtensions.cs - Inject the service into
Entry.csand add cases to the action switch - No changes needed to
Command.csorApp.cs
Dependency Injection
ServiceCollectionExtensions.cs wires everything up. Any host (sidecar, API, tests) calls:
services.AddFragmentServices();
This registers:
IFragmentRepository→FileFragmentRepository(singleton — persisted fragment store)IFragmentService→FragmentService(transient — fresh instance per request)
Fragment Store Location
FileFragmentRepository persists data to:
- default:
.journal-sidecar/fragments.jsonunder current working directory - override:
JOURNAL_FRAGMENT_STORE_PATHenvironment variable
Legacy Vault Compatibility Note
The legacy Python placeholder file _init_vault.vault is treated as obsolete.
During vault load, the C# backend ignores this file for decryption and removes it.
This preserves compatibility while migrating older vault directories forward.