First off, thank you for considering contributing to FinTrack! This guide covers everything you need to get the project running from scratch and understand the development workflow.
Please read our Code of Conduct before contributing. To report a security vulnerability, see SECURITY.md instead of opening a public issue.
- Project Overview
- Getting Started
- Environment Variables
- Database Management
- Running Individual Apps
- Testing
- Extending the Project
- Development Workflow
- Pull Request Process
- Troubleshooting
- Questions?
FinTrack is a monorepo managed by Turborepo.
apps/api: Node.js/Express backend with Prisma.apps/web: Next.js/React frontend with Tailwind CSS.apps/bot: Telegram bot for quick transaction entry.packages/types: Shared TypeScript definitions and Zod schemas.
Prerequisites: Node.js 22 (CI and Docker target; engines allows 18+), PostgreSQL 15, pnpm 10+, Docker & Docker Compose.
Regardless of the installation method, start by setting up your environment variables:
git clone https://github.com/BODMAT/FinTrack.git
cd FinTrack
# Use the dx CLI to create all necessary .env files from examples
bash dx setup
# → Edit each apps/*/.env. In Docker, compose overrides the PostgreSQL/Redis/Mongo hosts.The dx script is a project-agnostic Docker Executioner CLI that wraps docker compose commands. Run bash dx help to see all available commands.
Each service is accessible directly on its own port for easier debugging.
Commands:
bash dx run setup:dx— First-time setup: start all containers + initialize both dev and test databases (migrate + seed).bash dx dev— Start all containers in detached mode (without DB initialization).bash dx ps— List containers with their current status and health (initservice withExitedstatus is normal in dev mode).bash dx logs— Follow logs for all services (orbash dx logs apifor a specific service).bash dx api— Open a shell inside the API container (shortcut forbash dx shell api).bash dx run prisma:api:studio:dx— Start Prisma Studio inside the API container.bash dx run db:api:setup:dx— Initialize dev database inside Docker (migrate + seed).bash dx run db:api:test:setup:dx— Initialize test database (fintrack_test) inside Docker.bash dx run db:api:test:reset:dx— Wipe and re-initialize the test database.bash dx run test:api:dx— Run all API tests inside Docker.bash dx restart api— Restart a service after changing its.envfile.bash dx down— Stop and remove containers.
Access Points:
- Web App: http://localhost:5173/FinTrack
- API: http://localhost:8000/api
- Swagger Docs: http://localhost:8000/api-docs
- Prisma Studio: http://localhost:5555 (only after running
bash dx run prisma:api:studio:dx) - pgAdmin: http://localhost:5050 (Login:
admin@fintrack.dev/admin)- Setup: Right-click Servers → Register → Server.
- Connection: Host:
postgres, Port:5432, Database:fintrack, Username:fintrack, Password:fintrack.
All services are proxied behind Nginx. Only port 8080 is exposed externally — Nginx routes requests to the appropriate service internally.
Commands:
bash dx pbuild— Build all production images (required before first run).bash dx prod— Start the production stack.bash dx plogs— Follow production logs (orbash dx plogs apifor a specific service).bash dx pshell api— Open a shell inside a running production container.bash dx pdown— Stop and remove production containers (requires confirmation).
Access Points:
- Web App: http://localhost:8080/FinTrack
- API: http://localhost:8080/api
- Swagger Docs: http://localhost:8080/api-docs
- Prisma Studio and pgAdmin are disabled in production by default for security reasons.
For those who prefer running dependencies (Node, Postgres etc.) manually.
# Install dependencies
pnpm install
# Create both dev (fintrack) and test (fintrack_test) DBs, run migrations, and seed the dev DB
# (shared packages are built automatically when you run `pnpm run dev`)
pnpm run setup:local
# Start all apps via Turborepo
pnpm run devAccess Points:
- Web App: http://localhost:5173/FinTrack
- API: http://localhost:8000/api
- Swagger Docs: http://localhost:8000/api-docs
- Prisma Studio: http://localhost:5555 (only after running
pnpm run prisma:api:studio) - pgAdmin (locally installed app or via browser):
- Desktop App: Use your natively installed pgAdmin 4 application.
- Setup: Right-click Servers → Register → Server.
- Connection: Host:
localhost, Port:5432, Database/Username/Password: (your local Postgres credentials).
- Web Interface (via Docker): Run only the tool:
bash dx dev pgadmin.- Access: http://localhost:5050 (Login:
admin@fintrack.dev/admin). - Setup: Right-click Servers → Register → Server.
- Connection to local DB: Use Host:
host.docker.internal(Win/Mac) or172.17.0.1(Linux).
- Access: http://localhost:5050 (Login:
- Desktop App: Use your natively installed pgAdmin 4 application.
bash dx setup copies all example files automatically. For manual setup:
| File | Example | Notes |
|---|---|---|
.env (repo root) |
.env.example |
Docker Compose build args — NEXT_PUBLIC_TELEGRAM_BOT_ID |
apps/api/.env |
apps/api/.env.example |
Dev + Docker — secrets; Docker hosts injected by compose |
apps/api/.env.test |
apps/api/.env.test.example |
Tests — fintrack_test; Docker hosts rewritten by scripts/test-env.cjs |
apps/web/.env |
apps/web/.env.example |
Set NEXT_PUBLIC_API_URL, NEXTAUTH_SECRET, Google OAuth |
apps/bot/.env |
apps/bot/.env.example |
Dev + Docker — TELEGRAM_BOT_TOKEN; Docker hosts injected by compose. Prod values live on the VM (see README → Bot VM Deploy Setup) |
Each example file is split into REQUIRED (app won't boot without these) and
OPTIONAL (safe defaults + feature toggles) blocks; apps/api/.env.example adds a
REQUIRED IN PRODUCTION block (localhost defaults that must be overridden before
deploying) — read each file for per-variable details.
After applying migrations, you can populate your database using one of the following methods:
The easiest way to get a fully working environment with migrations applied and rich test data populated.
- Docker:
bash dx run db:api:setup:dx - Local:
pnpm run db:api:setup
Wipes the database schema and re-initializes it with fresh seed data. Useful for development resets.
- Docker:
bash dx run db:api:reset:dx - Local:
pnpm run db:api:reset
Best for a fresh install to get basic test accounts and system defaults.
- Docker:
bash dx run prisma:api:seed:dx - Local:
pnpm run prisma:api:seed
Best for working with realistic data or sharing progress with the team.
1. Create a Dump (Export) To share your data with a colleague:
- Docker:
bash dx run dump:db:dx - Local:
pnpm run dump:db - The dump file will be created in the
dumps/db/directory.
2. Restore (Append Mode)
Adds data from a .sql file in dumps/db/ to your existing records without deleting anything.
- Docker:
bash dx run restore:db:dx - Local:
pnpm run restore:db
3. Restore (Wipe & Sync Mode) Clears your current schema and restores the dump exactly. Best for a full sync.
- Docker:
bash dx run restore:db:reset:dx - Local:
pnpm run restore:db:reset
Note: You can combine them! For example, run Seed to get admin users, then Restore (Append) a dump with specific transactions. If you use Wipe & Sync, it will remove any previously seeded data.
The test database (fintrack_test) is separate from the dev DB and is used exclusively by automated tests. Tests are self-contained — they do not seed data; each test creates and cleans up its own records.
Setup (first time or after a migration):
- Docker:
bash dx run db:api:test:setup:dx - Local:
pnpm run db:api:test:setup
Full reset (wipe schema + re-run all migrations):
- Docker:
bash dx run db:api:test:reset:dx - Local:
pnpm run db:api:test:reset
Run a reset whenever migrations change or tests leave the DB in a broken state.
cd apps/api
cp .env.example .env
# fill in the REQUIRED block (DATABASE_URL, ACCESS_TOKEN_SECRET, GOOGLE_CLIENT_ID, TELEGRAM_BOT_TOKEN ...);
# OPTIONAL vars (GROQ_API_KEY_1, STRIPE_*, ...) gate features and can stay empty
pnpm run prisma:migrate:dev # apply migrations
pnpm run prisma:seed # optional seed data
pnpm run dev # tsc -w + nodemonTests:
pnpm run test:unit # unit tests only (no DB, fast)
pnpm run test:integration # integration tests (requires fintrack_test DB)
pnpm run test:light # unit + integration (used by pre-push hook)
pnpm run test:stress # stress / load tests
pnpm run test:e2e # end-to-end tests
pnpm run test # all test tiers
pnpm run test:watch # watch mode (unit + integration)Tests load .env.test automatically — no manual NODE_ENV export needed. Ensure fintrack_test exists first (pnpm run db:api:test:setup from the repo root).
cd apps/web
cp .env.example .env
# set NEXT_PUBLIC_API_URL, NEXTAUTH_SECRET, GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, NEXT_PUBLIC_TELEGRAM_BOT_ID
pnpm run dev # http://localhost:5173/FinTrackProduction (Docker):
# from repo root
bash dx pbuild web # build web image
bash dx prod web # start web containercd apps/bot
cp .env.example .env
# fill in TELEGRAM_BOT_TOKEN with your Telegram bot token (from @BotFather)
# fill in API_URL to point at the running API, and REDIS_URL
pnpm run dev # tsc -w + nodemon dist/bot.jsProduction (Docker):
# from repo root
bash dx pbuild bot # build bot image
bash dx prod bot # start bot container# build before running any app
pnpm --filter @fintrack/types build| Tier | Script (app-level) | Root shortcut | What it covers | Needs DB |
|---|---|---|---|---|
| unit | test:unit |
— | Pure logic, no I/O | No |
| integration | test:integration |
— | HTTP handlers via Supertest | Yes (fintrack_test) |
| light | test:light |
test:api:light |
unit + integration combined | Yes |
| stress | test:stress |
test:api:stress |
High-volume / concurrency | Yes |
| e2e | test:e2e |
test:api:e2e |
Full user flows | Yes |
| all | test |
test:api |
Every tier | Yes |
Every tier has a :dx variant that targets the Docker environment (e.g. test:light:dx, test:api:light:dx).
| File | Used by | DB host |
|---|---|---|
apps/api/.env.test |
local + Docker test:* scripts |
localhost; :dx scripts rewrite it to postgres via scripts/test-env.cjs |
Local:
# from repo root
pnpm run test:api:light # unit + integration (quick feedback loop)
pnpm run test:api # all tiers
pnpm run test:api:stress # stress suite
pnpm run test:api:e2e # e2e suite
# from apps/api
pnpm run test:unit
pnpm run test:integration
pnpm run test # all tiers
pnpm run test:watch # watch modeDocker:
bash dx run test:api:dx # all API tests inside the container
bash dx run test:api:light:dx # light tests inside the containerOr via root scripts:
bash dx run test:api:light:dx
bash dx run test:api:dxTests require a dedicated fintrack_test database. setup:local / setup:dx create and migrate it automatically on first run.
If you need to (re-)initialize it manually:
# Local
pnpm run db:api:test:setup # create + migrate
pnpm run db:api:test:reset # wipe + create + migrate
# Docker
bash dx run db:api:test:setup:dx
bash dx run db:api:test:reset:dxRun
db:api:test:resetafter any schema migration or if tests leave the database in a broken state.
- Tests are self-contained — each test creates and tears down its own data.
- Integration tests run in band (
--runInBand) to avoid transaction conflicts. - The pre-push hook runs type-check, web tests, and light API tests automatically — only stress and e2e need manual triggering.
- CI runs the full suite (
pnpm run test) against a live PostgreSQL service.
Each feature domain lives under apps/api/src/modules/<name>/ and consists of three files:
apps/api/src/modules/<name>/
service.ts ← Prisma queries and business logic
controller.ts ← HTTP handlers: parse request, validate with Zod, call service, send response
route.ts ← Express Router: wire middleware (auth, rate-limit) + controllers
After creating the three files, register the router in apps/api/src/routes/apiRoutes.ts:
import { myRouter } from "../modules/<name>/route.js";
apiRouter.use("/<name>", myRouter);If the module introduces new request/response shapes, add Zod schemas to packages/types first — both the API and the web app import from there.
If it requires a new database model, add it to apps/api/prisma/schema.prisma and run:
pnpm exec prisma migrate dev --name <name> # inside apps/apiEach protected page follows this structure:
apps/web/src/
api/<name>.ts ← typed API functions (Axios + Zod via handleRequest)
app/(protected)/<name>/
page.tsx ← async Server Component: prefetch + Suspense wrapper
<Name>Client.tsx ← "use client": TanStack Query hooks + UI
_components/ ← page-scoped sub-components
store/<name>.ts ← Zustand store (only if state is truly global)
src/api/<name>.ts — call handleRequest(api.get/post/..., zodSchema) for automatic validation:
import { handleRequest } from "@/utils/api";
import api from "./api";
import { myResponseSchema, type MyResponse } from "@fintrack/types";
export const getMyData = async (): Promise<MyResponse> =>
handleRequest(api.get("/my-endpoint"), myResponseSchema);page.tsx — async server component, wraps client in <Suspense>:
import { Suspense } from "react";
import { MyClient } from "./MyClient";
import { prefetchProtected } from "@/lib/server/prefetchProtected";
export default async function MyPage() {
const initialData = await prefetchProtected();
return (
<Suspense fallback={<div>Loading…</div>}>
<MyClient initialData={initialData} />
</Suspense>
);
}MyClient.tsx — client component with TanStack Query:
"use client";
import { useQuery } from "@tanstack/react-query";
import { getMyData } from "@/api/<name>";
export function MyClient({ initialData }: { initialData: … }) {
const { data } = useQuery({ queryKey: ["my-data"], queryFn: getMyData });
// render UI
}The bot currently lives in a single file apps/bot/src/bot.ts. Add new commands directly there:
bot.command("mycommand", async (ctx) => {
await ctx.reply("Hello from mycommand");
});When the number of commands grows, extract handlers into separate files and import them into bot.ts.
This project uses Conventional Branch:
<type>/<short-description>
Common types: feat, fix, chore, docs, refactor, test.
Examples:
feat/monobank-importfix/refresh-token-expirychore/update-deps
This project uses Conventional Commits:
<type>(<scope>): <short description>
Common types: feat, fix, chore, docs, refactor, test.
Common scopes: api, web, bot, types, ci, docker.
Commit format:
- use a
scopewhen the change clearly belongs to one app or area - use repo-level scopes such as
api,web,bot,types,ci, ordockerfor broad changes - use narrower scopes only when they add real signal, and keep them consistent across the repo
- omit the
scopeonly for very small or repo-wide changes subjectmust stay on one line- use the imperative mood
- keep the subject short and specific
- keep the subject at or below 72 characters
- add a
bodyonly when the reason is not obvious, the change is large, or the commit changes behavior in a way that needs context - mark breaking changes with
!in the subject and aBREAKING CHANGE:note in the body when applicable
Body format:
- separate the body from the subject with one blank line
- explain
why, notwhat - use one or more paragraphs when needed
- wrap lines at about 72 characters
- keep the body concise and avoid filler
- use
BREAKING CHANGE:to describe the migration impact when the change is not backwards compatible
List format in commit bodies:
- use a blank line before every list
- do not indent normal bullet lists
- start every bullet with
- - start every bullet with a lowercase letter
- when you use labels in a list, keep the
label: detailpattern consistent across the whole list - keep the bullet style consistent within the same commit message
Examples:
feat(web): add dark mode toggle
fix(api): revoke all sessions on password change
Previously only the current session was invalidated, leaving other
active sessions valid after a password reset. This left a security
gap for users who changed their password.
feat(api)!: rename transaction status fields
Client integrations that read the transaction status payload must be
updated before deploying this change.
BREAKING CHANGE: the transaction status payload shape changed and
existing clients must be updated.
test(api): add coverage for untested API paths
Add coverage for the main untested API flows:
- auth guards: 401 checks for protected endpoints
- authz: role and verification rules
- user and ai flows: happy paths and negative cases
| Gate | Trigger | What runs |
|---|---|---|
| pre-commit | git commit |
If pnpm is available: lint-staged; otherwise Docker fallback: bash dx run lint-staged |
| pre-push | git push |
If pnpm is available: type-check + web tests + light API tests; otherwise Docker fallback: bash dx run check-types + bash dx run test:web:dx + bash dx run test:api:light:dx |
| CI | PR / push to master or main |
Full integration tests, security audit, Docker build |
Integration tests (
pnpm run test) require a live PostgreSQL instance — run them manually viapnpm run test:apiwhen working on API changes, or let CI handle them.
pnpm run test:api:light— Run unit + integration tests locally (fastest feedback loop).pnpm run test:api— Run all API test tiers locally.bash dx run test:api:light:dx— Run light tests inside Docker (same as pre-push hook).pnpm run tidy— Run ESLint and Prettier fixers.pnpm run build— Build all packages and apps.pnpm run check-types— Type-check all packages without emitting.pnpm run db:api:reset— Wipe and re-seed the dev database.pnpm run db:api:test:reset— Wipe and re-migrate the test database.pnpm run reinstall— Wipenode_modulesand reinstall dependencies (reinstall:dxfor Docker — preserves database volumes).pnpm run reinstall:fresh— Wipenode_modules, lock files, build output and reinstall (reinstall:fresh:dxfor Docker).pnpm run clean:dist— Remove build output and lint/build caches only (clean:dist:dxfor Docker).pnpm run clean:locks— Remove all lock files only (runpnpm installafter to regenerate).
Use different patterns for root and app-level scripts:
-
Root
package.json(monorepo orchestration):
action:scope[:type][:env] -
apps/*/package.json(local app scripts):
action[:type][:env]
Where:
action: operation (test,db,prisma,dev,setup)scope: target app/package (api,web,bot) — only for root scriptstype(optional): subtype (e2e,light,stress,migrate,clean,test)env(optional): execution environment (dxfor Docker)
Examples:
- Root:
test:api,test:api:e2e:dx,db:api:test:reset,prisma:api:seed - App-level (
apps/api):test,test:e2e,test:watch,prisma:migrate:deploy
Move a script from apps/* to root when at least one is true:
- It is run from CI/Husky as a standard repo entrypoint.
- It needs cross-app coordination (for example
api + web, ordb + prisma + seed). - It requires environment switching (
localvsdx) that should be uniform for contributors. - It should be discoverable as a team-wide workflow (
setup,test,db reset,release checks).
Keep a script only in apps/* when:
- It is purely app-internal and called mainly by developers working in that app.
- It is a low-level primitive used by root orchestration (
prisma:migrate:deploy,test:watch,dev).
- Modify
apps/api/prisma/schema.prisma. - Run
pnpm exec prisma migrate dev --name <migration_name>inapps/api. - Update
packages/typesif data structures changed.
Migration names use snake_case and follow the pattern:
<action>_<target>[_<detail>]
| Action | When to use |
|---|---|
create |
new table |
add |
new column, index, or constraint |
remove |
drop column, index, or table |
rename |
rename a column or table |
alter |
change type, nullability, or default |
init |
first / baseline migration |
Examples:
pnpm exec prisma migrate dev --name init_schema
pnpm exec prisma migrate dev --name create_transactions_table
pnpm exec prisma migrate dev --name add_category_to_transactions
pnpm exec prisma migrate dev --name add_unique_email_to_users
pnpm exec prisma migrate dev --name add_index_on_transactions_user_id
pnpm exec prisma migrate dev --name remove_legacy_token_from_sessions
pnpm exec prisma migrate dev --name rename_amount_to_total_in_transactions
pnpm exec prisma migrate dev --name alter_balance_type_to_decimalPrisma prepends a timestamp automatically — the name you provide becomes the human-readable suffix: 20240518143022_add_category_to_transactions.
The standard cycle for any change — bug fix, feature, or chore. Small, low-risk changes can take a shorter path — see Lightweight Changes.
- Open an issue using one of the templates in
.github/ISSUE_TEMPLATE/—fix_request.md,feat_request.md, orchore_request.md. Keep the prefilled title prefix (fix:,feat:,chore:) and fill every section before writing any code. - Create a branch from
masterfollowing the Branch Naming convention. Optionally prefix with the issue number for traceability:feat/42-csv-export,fix/17-refresh-token. - Make your changes — commit incrementally following Commit Conventions.
- Quality checks run automatically — lint + format on
git commit, type-check + web tests + light API tests ongit push. See Quality Gates for the full breakdown. - Open a PR targeting
master— GitHub auto-loads.github/PULL_REQUEST_TEMPLATE.md. Reuse the issue title verbatim, fill every section (Description, Type of change, How Has This Been Tested?, Checklist), and link the issue withCloses #Nso GitHub auto-closes it on merge. - After merge — delete the branch.
The contribution flow scales to the size and risk of the change. Small, low-risk changes do not need a tracking issue, the full PR template, or a blocking approval — they are still opened as a PR and still run through CI; only the tracking issue, the long template, and the required review are dropped.
Lightweight flow (no issue, short PR description) — use it when all of these hold:
- the change is self-contained and reviewable at a glance
- it does not change behavior in a non-obvious way
- it does not touch the database schema / migrations, a public API contract, or security-sensitive code
- it is a fix, docs, config, or chore — not a new feature or module
Examples: a typo or docs edit, a config tweak, a dependency bump, a one-file bug fix.
Merging a lightweight PR — once CI is green, the author may merge their own PR without waiting for an approval. Review is welcome but not blocking: a reviewer can comment before or after merge, and anything they flag is fixed in a follow-up. This keeps trivial work from stalling on a second pair of eyes it does not need. A full-flow PR still needs a reviewer's approval before merge.
Full flow (issue + full PULL_REQUEST_TEMPLATE.md) — use it when any of these hold:
- a new feature or module
- a behavior or contract change, a database migration, or anything security-related
- a breaking change
- anything that benefits from design discussion before coding
Commit count is a rough hint, not the rule — branches are squash-merged, so they collapse to a single commit in
masterregardless of how many you made. Gate on scope and risk, not on commit count. When in doubt, use the full flow, and a reviewer can always ask to promote a lightweight PR to the full process.
Which path applies is enforced automatically by .github/CODEOWNERS: it lists the sensitive paths — database schema and migrations, any .env*.example, auth and middleware code, packages/types, CI workflows, and deploy config. A PR that touches any of them needs an owner's approval before it can merge; a PR that touches none can be self-merged once CI is green. (Branch protection: 0 baseline approvals + Require review from Code Owners.)
The PR template carries this short form in a comment at the top — for a lightweight PR, delete the full template and use that What / Why / Testing form instead.
PRs into master are merged via squash commit — all branch commits collapse into one, keeping the main history clean and linear.
Stacked branches (a branch based off another feature branch, not master) are merged into their parent via a regular merge commit to preserve the intermediate history:
master
└── feat/payment-system ← squash-merged into master when ready
└── feat/payment-webhook ← regular-merged into feat/payment-system
Once the parent branch is squash-merged into master, rebase any surviving child branches onto master before continuing.
Delete branches after merging — once a branch is merged into master, delete it to keep the repository clean. GitHub preserves all deleted branches and their commits, so they can be restored at any time if needed.
Drop everything and rebuild from scratch:
Local:
pnpm run db:api:drop:all # drops fintrack + fintrack_test DBs and the fintrack role
pnpm run setup:local # recreates both DBs, runs migrations, seeds dev DBDocker:
bash dx downv # stop and remove all containers and volumes
bash dx run setup:dx # start containers + initialize both DBsscripts/clean.sh (wired into package.json) wipes any combination of
node_modules / lock files / build output across the whole monorepo. Local
variants prompt for confirmation; :dx variants skip confirmation (-y) and
target the Docker environment — run them via bash dx run <script> or
pnpm run <script>.
Common workflows:
# Reinstall after a dependency or package-manager change
pnpm run reinstall # wipe node_modules + pnpm install (host)
bash dx run reinstall:dx # same, Docker (stops containers, wipes node_modules volumes, restarts)
# Fresh reinstall — wipe lock files and build output too
pnpm run reinstall:fresh # host
bash dx run reinstall:fresh:dx # Docker
# Clear only build output and lint/build caches
pnpm run clean:dist # host
bash dx run clean:dist:dx # Docker runner
# Remove lock files and regenerate
pnpm run clean:locks && pnpm install # host
bash dx run clean:locks:dx && bash dx exec pnpm install # DockerNote (Docker):
node_modulesdirectories are Docker named volumes — they cannot be removed viarm, so there is noclean:modules:dx. Usereinstall:dxinstead: stops containers, removesnode_modulesvolumes via the Docker API, restarts. Database volumes (postgres_data,pgadmin_data) are preserved.
Run bash scripts/clean.sh --help for the full flag reference and list of --dist targets.
Reach out via fintrack.community@gmail.com.