# Development A practical reference for building, running, and contributing to Koinkat. For a high-level map of the codebase, read [`architecture.md`](./architecture.md). For the history of the public-distribution restructure, read [`restructure-audit.md`](./restructure-audit.md). ## Prerequisites - **Node.js** >= 20 (Vite 6 + React 19 baseline). - **Rust** stable + the platform Tauri toolchain prerequisites (see ). - **npm** for package management. `package-lock.json` is committed. No global Tauri CLI is needed - `@tauri-apps/cli` is a dev dependency and is invoked through `npm run tauri:*` scripts. ## Three build modes Koinkat ships with three Vite build modes. Each one flips a set of compile-time flags in `vite.config.ts` that control what code reaches the bundle. | Mode | Command | Mocks | Debug routes | Sandbox UI | Tauri identifier | |---|---|---|---|---|---| | `development` | `npm run dev` / `npm run tauri:dev` | On by default | Visible | Visible | `com.koinkat.app` | | `demo` | `npm run build:demo` / `npm run tauri:build:demo` | On by default | Hidden | Visible | `com.koinkat.app.demo` | | `production` | `npm run build` / `npm run tauri:build` | **Build fails if present** | Removed | Hidden | `com.koinkat.app` | ### What each flag controls Defined in `vite.config.ts` via Vite's `define`, declared in `src/vite-env.d.ts`. At build time Vite replaces each occurrence with the literal `true` or `false`; Rollup then tree-shakes any branch (and its imports) where the literal is `false`. | Flag | dev | demo | prod | Purpose | |---|---|---|---|---| | `__KOINKAT_ALLOW_MOCKS__` | `true` | `true` | `false` | Whether `src/mocks/` may be imported at all | | `__KOINKAT_EB_MOCK_DEFAULT__` | `true` unless `KOINKAT_EB_REAL=1` | `true` | `false` | Whether the dispatcher in `src/services/enable-banking-service.ts` actually routes to the mock | | `__KOINKAT_ALLOW_DEBUG_ROUTES__` | `true` | `false` | `false` | Whether `/rules` is registered in the router | | `__KOINKAT_ALLOW_SANDBOX_UI__` | `true` | `true` | `false` | Whether the Sandbox card is rendered in the workspace-creation hub | The sandbox **runtime** stays intact across all modes - existing users with sandbox workspaces keep working after a production build. Only the "create a new sandbox workspace" UI surface is gated. ### Three-layer defense that mocks never reach production 1. **Compile-time flag replacement** - Vite replaces `__KOINKAT_ALLOW_MOCKS__` with the literal `false` in production, and Rollup tree-shakes every guarded branch + its imports. 2. **Mock-default flag** - `__KOINKAT_EB_MOCK_DEFAULT__` is also `false` in production, so even if the guard flag misfired, `IS_MOCK` would still be `false`. 3. **Post-bundle scan** - `forbidMocksInProductionBundle` in `vite.config.ts` fails the build if any chunk's `moduleIds` contain `/src/mocks/` or `\src\mocks\`. ## Commands | Script | Purpose | |---|---| | `npm run dev` | Vite dev server, port 1420. Typically run via `npm run tauri:dev`. | | `npm run tauri:dev` | Full desktop dev - Vite + Tauri window with hot reload. | | `npm run build` | Production bundle (`tsc && vite build --mode production`). | | `npm run build:demo` | Demo bundle (`tsc && vite build --mode demo`) with mocks on for tutorials / marketing. | | `npm run tauri:build` | Production Tauri installer. | | `npm run tauri:build:demo` | Demo Tauri installer (`com.koinkat.app.demo`), installable side-by-side with production. | | `npm run preview` | `vite preview` - serve `dist/` for manual QA. | | `npm run typecheck` | `tsc --noEmit`. | | `npm run test` | Vitest run once. Today only covers `src/domain/money.ts`. | | `npm run test:watch` | Vitest in watch mode. | | `npm run test:ui` | `@vitest/ui` dashboard. | No linter is configured. `npm run typecheck` is the only static gate; run it after editing any typed file. ### Testing Vitest 4.1.5 is installed. The only test file today is `src/domain/money.test.ts` (42 tests covering `dec`, `qCent`, `qRate`, `convertAmount`, `tryConvert`, `requirePositiveAmount`, `requireNonNegativeAmount`, plus a rounding-mode regression that pins half-up rounding). Tests sit beside the file they cover; vitest's default include glob picks them up automatically. Expanding coverage beyond money math is on the list - see `restructure-audit.md` "Still open". ## Testing the real Enable Banking client in dev Dev mode defaults to mocks-on so contributors don't need credentials. To exercise the real client, set `KOINKAT_EB_REAL=1` for the session: ```powershell $env:KOINKAT_EB_REAL = '1'; npm run tauri:dev # PowerShell ``` ```bash KOINKAT_EB_REAL=1 npm run tauri:dev # bash / zsh ``` The variable only affects `development` mode: demo always mocks, and production never mocks regardless of it. Then, in the Connection page (account hub): - Pick **Sandbox** with the credentials of an Enable Banking application registered in the SANDBOX environment (Control Panel > Applications; sandbox apps activate automatically and have their own application ID and key pair). The sandbox uses the same `api.enablebanking.com` host; its `GET /aspsps` returns Enable Banking's test banks, including the fully simulated Mock ASPSP. Remember to whitelist your redirect URL on the sandbox application - the whitelist is per application. - Or pick **Linked** with production credentials to test against real banks. ## Environment variables There are no committed `.env*` files. The `.gitignore` blocks `.env` and `.env.*` entirely - build-mode behavior is driven by the Vite `mode` at build time, with one opt-in exception: `KOINKAT_EB_REAL=1` flips a development session to the real Enable Banking client (see above). If you need a local variable (e.g. `TAURI_DEV_HOST` for LAN mobile testing), create `.env.local`; it stays gitignored. ## OAuth callback The Enable Banking authorization flow requires a publicly-reachable redirect URL. Koinkat uses a static GitHub Pages page checked into this repo at `docs/callback/index.html`. It receives `?code=…` from Enable Banking and presents the code for the user to paste back into the desktop app. The default redirect is hardcoded in `src/lib/constants.ts` (`OAUTH_CALLBACK_URL = 'koinkat://auth-callback'`) for the deep-link path; the database column `api_configs.redirect_url` defaults to `https://marcosburlino.github.io/koinkat-callback/` for the browser-mediated path. If you fork the project, swap both URLs to your own callback host. The Tauri deep-link plugin is registered for the `koinkat://` scheme in `src-tauri/tauri.conf.json`. ## Database & migrations The SQLite database file is opened by `@tauri-apps/plugin-sql` at the path `sqlite:koinkat.db` (handled in `src/db/database.ts`). It lives in the platform's per-app data directory (e.g. `%APPDATA%\com.koinkat.app` on Windows). Schema files: - `src/db/schema-v2.sql` - current schema (`users`, `koinkat_accounts`, `accounts`, `transactions`, `categories`, `bank_connections`, `linked_accounts`, `api_configs`, `exchange_rates`, plus the budget / split tables). - `src/db/schema.sql` - legacy v1 schema, kept for reference. - `src/db/migration-v2.sql` … `migration-v8.sql` - incremental DDL applied in order on every app boot. Migrations are managed through Tauri's SQL plugin migration runner. **Always create a new `migration-vN.sql` rather than editing an existing one** - installed apps replay missing migrations on next launch, but never re-run ones that already succeeded. ## Money & exchange rates - All monetary values are stored and computed as `big.js` decimals via the helpers in `src/domain/money.ts` (`dec`, `qCent`, `qRate`, `convertAmount`, `tryConvert`). Never use `number` arithmetic for amounts - it's why `dec()` rejects `number` at runtime. - Cross-currency totals use USD as a pivot via the fawazahmed0 currency-api JSON files. Primary host is `cdn.jsdelivr.net`, fallback is `*.currency-api.pages.dev`. Both are whitelisted in the Tauri CSP (`src-tauri/tauri.conf.json`). - Daily rates are cached in the `exchange_rates` table and refreshed by `ensureTodayRates()` on Shell bootstrap and before every bank sync. ## Style & conventions - TypeScript strict mode (`tsconfig.json`). No `any` outside narrowly scoped DTOs. - React 19 functional components only. Stores use Zustand (`src/stores/`). - Routing through `react-router-dom` v7. The Shell renders ``; every routed page sits inside it. Non-routed views (`UserRegister`, `UserLogin`, `Connection`) are rendered conditionally by the Shell based on bootstrap state. - Styling: Tailwind v4 via `@tailwindcss/vite` and `src/index.css`. Theme tokens come from CSS variables (`--bg`, `--text-muted`, …), set via the `data-theme` attribute on ``. - Snake-case database rows, camelCase domain types. Mappers live alongside the types in `src/types/models.ts` (`toAccount`, `toTransaction`, …). ## Git hygiene - `junk/` is the on-disk quarantine for old prototypes, personal notes, and the `.pem` keys that previously sat at the repo root. Gitignored. - `.agents/`, `.claude/`, and `AGENTS.md` are gitignored. Never commit internal planning docs. - `*.pem`, `*.key`, `*.p8`, `*.p12`, and the well-known credential filenames (`credentials.json`, `secrets.json`, …) are gitignored. If you ever need to share a key, do it out-of-band. ## Adding a new dev-only surface Pick the right flag for the visibility you want: ```tsx // New debug route (visible in development only): {__KOINKAT_ALLOW_DEBUG_ROUTES__ && ( } /> )} // New UI element that should appear in dev + demo but not production: {__KOINKAT_ALLOW_SANDBOX_UI__ && } ``` For a brand-new visibility class (e.g. an "admin" tier), add a fourth boolean to the `define` block in `vite.config.ts`, declare it in `src/vite-env.d.ts`, and gate uses with `if (__KOINKAT_ALLOW_…__) { … }`. ## Pre-release blockers See [`restructure-audit.md`](./restructure-audit.md) "Still open" section for the items that must be resolved before a public 1.0 release. As of 2026-05-16: - **Resolved** (since the 2026-04-28 follow-up): bundle metadata icons (32 / 128 / 128@2x / .icns / .ico), category, short / long description populated in `tauri.conf.json`; `copyright` / `publisher` / `homepage` filled. CSP includes the jsDelivr CDN that `fx-fetch.ts` needs as its primary FX host. Top-level React error boundary in place; its GitHub-issues link points at the real repo. Money-math test suite added. Minimal root `README.md` added. - **Still open**: no code-signing identity / notarization profile. No LICENSE file (deferred until publication strategy is decided). Test suite now covers money math, sync fallbacks, categorization, recurring matching, and keychain storage. No linter. CI + release workflows added 2026-07-02 (see below). ## CI and releases (added 2026-07-02) - **CI** (`.github/workflows/ci.yml`) runs on every push/PR: a version consistency check (package.json / tauri.conf.json / Cargo.toml must match), `npm run typecheck`, `npm run test`, the production bundle (which includes the mock-leak scanner), and `cargo check` on Ubuntu, Windows, and macOS. The three-OS Rust matrix exists because the shell has platform-conditional code (keychain backends, single-instance, deep-link registration). - **Releases** (`.github/workflows/release.yml`): push a `vX.Y.Z` tag and the workflow builds installers for all three platforms via `tauri-apps/tauri-action` and attaches them to a DRAFT GitHub release for manual review. Builds are unsigned until signing secrets are configured (see the comments in the workflow). - **Dependencies**: Dependabot watches npm, cargo, and the workflow actions weekly (`.github/dependabot.yml`). - Release steps for maintainers live in `CONTRIBUTING.md`.