8.3 KiB
AGENTS.md
This document defines the laws, principles, and rule sets that govern this codebase. Any agent or developer making changes has to adhere to these rules.
Before starting off — Read the README.md file to understand the project's goals and objectives.
Agent Rules (Override Everything)
- No testing by yourself — All testing is done by the team.
- No assumptions about code or domain logic — Always confirm and be sure before making changes.
- No running scripts — Do not run build, dev, test, or migrate scripts unless explicitly approved.
- No touching migration files — Do not mess with the
migrationssql dir, as those are generated manually via drizzle orm
More rules are only to be added by the human, in case such a suggestion becomes viable.
1. Project Overview
- Monorepo: Turbo repo
- Package Manager: pnpm
- Language: TypeScript everywhere
- Node: >= 24
Applications
| App | Purpose |
|---|---|
apps/main |
SvelteKit UI — the primary user-facing application |
apps/processor |
Hono web server — intended for asynchronous processing (jobs, workers). Currently minimal; structure is evolving. |
Packages
Packages live under packages/ and are standalone, modular pieces consumed by apps:
| Package | Purpose |
|---|---|
@pkg/logic |
Domain logic (DDD, controllers, repositories) |
@pkg/db |
Drizzle schema, database access |
@pkg/logger |
Logging, getError for error construction |
@pkg/result |
Result type, ERROR_CODES, tryCatch |
@pkg/keystore |
Redis instance (sessions, 2FA, etc.) |
@pkg/settings |
App settings / env config |
Data Stores
- PostgreSQL (Drizzle) — Primary relational data (auth, users, accounts, etc.)
- Redis (Valkey) — Sessions, 2FA verification state (via
@pkg/keystore)
Additional stores (NoSQL DBs, R2, etc.) may be introduced later. Follow existing domain patterns when adding new data access.
2. The Logic Package: DDD + Layered Architecture
The @pkg/logic package contains all domain logic. It follows:
- Domain-Driven Design (DDD) — Bounded contexts as domains
- Layered architecture — Clear separation of concerns
- Result-style error handling — Errors as values; avoid try-catch in domain code
Domain Structure
Each domain is a folder under packages/logic/domains/:
domains/
<domain-name>/
data.ts # Types, schemas (Valibot)
repository.ts # Data access
controller.ts # Use cases / application logic
errors.ts # Domain-specific error constructors (using getError)
The logic package is pure domain logic — no HTTP routes or routers. API exposure is handled by the main app via SvelteKit remote functions. Auth uses config.base.ts with better-auth. Add new domains as needed; mirror existing patterns.
Path Aliases (logic package)
@/*→./*@domains/*→./domains/*@core/*→./core/*
Flow Execution Context
Domain operations receive a FlowExecCtx (fctx) for tracing and audit:
type FlowExecCtx = {
flowId: string;
userId?: string;
sessionId?: string;
};
3. Error Handling: Result Pattern (neverthrow)
Errors are values, not exceptions. The codebase uses Result-style handling.
Current Conventions
- Logic package — Uses
neverthrow(ResultAsync,okAsync,errAsync) for async operations that may fail. @pkg/result— ProvidesResult<T, Err>,ERROR_CODES, andtryCatch(). TheResulttype is legacy; So don't reach for it primarily.- Use
ERROR_CODESfor consistent error codes. getError()— From@pkg/logger. Use at boundaries when converting a thrown error to anErrobject:
return getError({ code: ERROR_CODES.XXX, message: "...", description: "...", detail: "..." }, e).- Domain errors — Each domain has an
errors.tsthat exports error constructors built withgetError. Use these instead of ad-hoc error objects. - Check before use — Always check
result.isOk()/result.isErr()before usingresult.value; never assume success.
Err Shape
type Err = {
flowId?: string;
code: string;
message: string;
description: string;
detail: string;
actionable?: boolean;
error?: any;
// Flexible, but more "defined base fields" in the future
};
4. Frontend (Main App)
The main app is a SvelteKit application with a domain-driven UI structure.
Structure
- Routes: File-based routing under
src/routes/. Layout groups (e.g.(main),auth) wrap related pages. - Domain-driven UI: Feature code lives under
src/lib/domains/<domain>/— each domain has its own folder with view models, components, and remote functions. - View Model (VM) pattern: Domain logic and state for a screen live in
*.vm.svelte.tsclasses. VMs hold reactive state ($state), orchestrate remote function calls, and expose methods. Pages import and use a VM instance.
SvelteKit Remote Functions
The main app uses SvelteKit remote functions as the primary API layer — replacing Hono routers in the logic package. Each domain has a *.remote.ts file that exposes query (reads) and command (writes) functions, called directly from VMs. Auth context and FlowExecCtx are built inside each remote function from event.locals via helpers in $lib/core/server.utils.
Naming convention: *SQ for queries, *SC for commands.
Global Stores
Shared state (user, session, breadcrumbs) lives in $lib/global.stores.ts.
Conventions
- Pages are thin: they mount a VM, render components, and wire up lifecycle.
- VMs own async flows, polling, and error handling for their domain.
- VMs call remote functions directly; remote functions invoke logic controllers.
- UI components under
$lib/components/are shared; domain-specific components live in$lib/domains/<domain>/.
5. Processor App
The processor is a Hono server intended for background work and async jobs. Its structure is still evolving and it is to be updated soon.
When logic is added, processing logic should live under src/domains/<domain>/ and call into @pkg/logic controllers and repositories.
6. Observability
The stack uses OpenTelemetry end-to-end for traces, logs, and metrics, shipped to a SigNoz instance (via OTel Collector).
How it fits together
apps/mainbootstraps the OTel SDK ininstrumentation.server.ts(auto-instrumentation via@opentelemetry/sdk-node). SvelteKit'stracingandinstrumentationexperimental flags wire this into the request lifecycle.@pkg/loggerships Winston logs to OTel viaOpenTelemetryTransportV3— logs are correlated with active traces automatically.@pkg/logic/core/observability.tsprovides two tracing helpers for domain code:traceResultAsync— wraps aResultAsyncoperation in an OTel span. Use this in repositories and controllers.withFlowSpan— lower-level span wrapper for non-Result async code.
- Both helpers accept
fctxand stamp spans withflow.id,flow.user_id, andflow.session_idfor end-to-end trace correlation.
Convention
When adding new domain operations in repositories or controllers, wrap them with traceResultAsync. Keep span names consistent and descriptive (e.g. "user.getUserInfo"). Do not add ad-hoc spans outside these helpers.
7. Validation & Schemas
- Valibot is used for schema validation in the logic package and in remote function input.
- Domain data types are defined in
data.tsper domain. - Use
v.InferOutput<typeof Schema>for TypeScript types. - Remote functions pass Valibot schemas to
query()andcommand()for input validation.
8. Package Naming
- Apps:
@apps/*(e.g.@apps/main,@apps/processor) - Packages:
@pkg/*(e.g.@pkg/logic,@pkg/db,@pkg/logger)