3.4 KiB
App Spese
Full-stack personal finance app built with Next.js App Router, TypeScript, Tailwind CSS, and PostgreSQL.
The browser only talks to Next.js Route Handlers. PostgreSQL credentials and session tokens stay on the server.
Docker Compose
Copy the example environment file and choose a strong database password:
cp .env.example .env
docker compose up --build -d
The first startup initializes the PostgreSQL schema from database/init.sql.
Open http://localhost:3000: the app redirects to /setup and asks you to
create the first account. The setup page becomes unavailable as soon as that
account exists.
Local Development
Start PostgreSQL with Docker:
docker compose up -d postgres
The compose file publishes PostgreSQL on localhost:5432 by default so the
local Next.js development server can reach it.
Create .env.local:
DATABASE_URL=postgresql://appspese:change-me@localhost:5432/appspese
Install dependencies and run Next.js:
pnpm install
pnpm dev
Data Model
Tables:
usersandsessions: local credentials and revocable login sessions.subscriptions: recurring payment rules.subscription_payments: paid or unpaid state for generated renewals.bills: expected manual bills with due dates.one_time_payments: immediate manual outgoing payments.payments_ledger: single source of truth for real paid outgoing money.payment_titles: reusable deduplicated payment names.income: incoming money.user_settings: one row of preferences per user.
Subscriptions and bills count as projected obligations until paid. Paid
outgoing money is written to payments_ledger, with unique database constraints
preventing duplicate ledger rows.
Subscriptions keep service activation and billing separate. Recurring payments start from the first payment date, can stop at an optional end date or after an optional number of installments, and may otherwise continue indefinitely. Suspended and terminated subscriptions do not generate new virtual renewals, while already recorded payments remain available as history. Custom renewal intervals use an exact quantity and unit, supporting schedules such as every two weeks, every three months, or every two years.
Database Migrations
database/init.sql contains the complete schema for new installations.
Versioned files in database/migrations update existing volumes. After pulling
an update that includes a new migration, apply it once with:
cat database/migrations/001_subscription_lifecycle.sql | docker compose exec -T postgres psql -v ON_ERROR_STOP=1 -U appspese -d appspese
cat database/migrations/002_subscription_custom_intervals.sql | docker compose exec -T postgres psql -v ON_ERROR_STOP=1 -U appspese -d appspese
Authentication
There is no public registration page. The /setup page and its API endpoint are
available only while the database contains no users. The first request that
creates an account takes a PostgreSQL transaction lock, preventing concurrent
requests from creating multiple initial accounts.
- Passwords are hashed with bcrypt.
- Login sessions use random secrets stored in an HTTP-only cookie.
- Only a SHA-256 hash of each session secret is stored in PostgreSQL.
- API Route Handlers verify the current user server-side and ignore client-provided user identifiers.
Verification
pnpm lint
pnpm build
docker compose config