Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/soker90/finper/llms.txt

Use this file to discover all available pages before exploring further.

Finper is a self-hosted personal finance management application organized as a pnpm monorepo. A React 19 single-page application communicates with an Express 5 REST API over HTTP. The API owns all business logic and persists data to a local SQLite file via Drizzle ORM. Shared TypeScript types and the database schema each live in their own workspace package so both the frontend and backend stay in sync without duplication.

Monorepo structure

finper/
├── packages/
│   ├── api/      @soker90/finper-api      — Express REST API on :3008
│   ├── client/   @soker90/finper-client   — React 19 + Vite SPA on :5173
│   ├── db/       @soker90/finper-db       — SQLite schema and Drizzle ORM
│   └── types/    @soker90/finper-types    — Shared TypeScript interfaces
├── docs/                                   — Technical documentation
├── Makefile                                — Entry point for all commands
├── docker-compose.yml                      — SQLite + API container setup
└── pnpm-workspace.yaml

Package dependency diagram

client  ──HTTP──▶  api  ──require──▶  db & types (dist/)


                                        SQLite
api depends on db and types via workspace:* and consumes their compiled build output. The API will not start if those packages have not been built first. client consumes the shared interfaces from @soker90/finper-types directly at build time.

Request lifecycle

1

Browser axios call

The React client calls axios.get('accounts') with baseURL set to VITE_API_HOST. The axios interceptor (configured in authService.setSession) automatically attaches the stored JWT as the Authorization: Bearer <token> request header before the call leaves the browser.
2

Express :3008 receives the request

The Express server on port 3008 accepts the incoming HTTP request.
3

preMiddlewareConfig

The request passes through preMiddlewareConfig: helmet, express.json, express.urlencoded, compression, and cors middleware all run in sequence.
4

Route matching

Express matches the path (e.g. /api/accounts) and hands the request to the corresponding router file — for example accounts.routes.ts.
5

authMiddleware — passport JWT

authMiddleware calls passport.authenticate('jwt', cb). On success it sets req.user = username (a plain string) and writes a refreshed token to the Token response header.
6

Controller Promise chain

The controller executes a Bluebird Promise chain: .tap(log) → .then(extractUser) → .then(validateXxxParams) → .tap(validateXxxExist) → .then(service.method) → .then(res.send) | .catch(next)
7

Service layer

The service applies business logic and throws Boom.<x>(...).output for any domain errors.
8

Drizzle ORM → SQLite

The service calls @soker90/finper-db which executes the Drizzle query against the SQLite file.
9

handleError middleware

Any error passed to next(error) is caught by the handleError middleware. Boom-shaped errors become res.status(payload.statusCode).json(payload); unknown errors become HTTP 500.

Build order

OrderCommandWhy
1pnpm installResolves the workspace graph and writes the lockfile.
2make build-types and make build-dbProduces the compiled dist/ artifacts that the API imports at runtime.
3make start-apiStarts Express on :3008. Requires the builds from step 2.
4make start-clientStarts Vite on :5173. Independent of steps 2 and 3 at the build level.

Key architectural decisions

Finper is a self-hosted app designed to run on a single machine without any external infrastructure. SQLite requires no separate server process, stores all data in a single file, and is trivially portable. Drizzle ORM provides a type-safe query builder on top of the better-sqlite3 synchronous driver, and migrations are applied automatically when the API starts via migrate() in server.ts.
Splitting @soker90/finper-db (schema + connection) and @soker90/finper-types (TypeScript interfaces) into their own packages lets both the API and the React client share the same type definitions without coupling the frontend to Drizzle ORM or better-sqlite3. The cost is that both packages must be built before the API can start, and any schema change requires a rebuild of db before restarting the API.
server.ts replaces global.Promise with Bluebird at startup. This gives every controller access to .tap(), which is used to fire logging side-effects and existence validators inside the Promise chain without breaking the data flow. The trade-off is an extra dependency and slightly different rejection semantics compared to native Promises.
Services are instantiated as module-level singletons and passed to controller factory functions (e.g. createUsersController(usersService)). There is no DI container. This keeps the codebase simple and makes unit testing straightforward: tests import the factory and pass a mock service directly.
The client uses SWR for all data-fetching operations. SWR provides automatic caching, background revalidation, and deduplication. For mutations (POST / PUT / DELETE), the client calls plain axios functions and then calls mutate() to trigger SWR revalidation. This pattern avoids duplicated loading states while keeping write paths simple and explicit.

Build docs developers (and LLMs) love