- Ruby 55.9%
- HTML 40.9%
- JavaScript 2.4%
- Dockerfile 0.5%
- Shell 0.2%
- Other 0.1%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| .claude | ||
| app | ||
| bin | ||
| config | ||
| db | ||
| documentation | ||
| lib/tasks | ||
| log | ||
| public | ||
| script | ||
| storage | ||
| test | ||
| tmp | ||
| vendor | ||
| .annotaterb.yml | ||
| .dockerignore | ||
| .gitattributes | ||
| .gitignore | ||
| .herb.yml | ||
| .standard.yml | ||
| .woodpecker.yml | ||
| CLAUDE.md | ||
| config.ru | ||
| Dockerfile | ||
| Gemfile | ||
| Gemfile.lock | ||
| mise.toml | ||
| package-lock.json | ||
| package.json | ||
| Procfile.dev | ||
| Rakefile | ||
| README.md | ||
| why_logo_200.svg | ||
why-accounting
The internal invoicing and bookkeeping application of whyservices OÜ, built with Rails 8.1, Hotwire and Tailwind 4.
It keeps clients, projects, invoices and their line items; renders invoices as PDF in
English or German; mails invoices and payment reminders with the PDF attached; records
vendors, expense accounts and bills with their receipts, named
YYYY-MM-DD_vendor_invoicenumber.ext; and imports LHV bank statements (CSV), keeps every
upload, and registers its bookings one by one as bills or invoice payments. Bookings seen
in an earlier upload are skipped.
There is no login. Run it only where you control who can reach it (see
documentation/issues/no-authentication-2026-10-07.md).
Setup
Requirements: mise and network access to the PostgreSQL host
db.
mise trust # once per checkout
mise install # Ruby 4 and Node, as pinned in mise.toml
The development server needs the development database password. Put it in
mise.local.toml (git-ignored) next to mise.toml:
[env]
WHY_ACCOUNTING_DB_PASS_DEV = "development database password"
Tests, linters and CI do not need it. Then install dependencies and prepare the database:
bundle install
npm install
npx playwright install chromium # once per machine, for system tests
bin/rails db:prepare
bin/setup does the installation and database steps in one go and starts the server.
JavaScript packages are managed with npm only (package-lock.json).
Running
bin/dev
bin/dev starts Procfile.dev through overmind: the Rails server on
http://localhost:3002, esbuild and the Tailwind CLI in watch mode, and a Tailscale
funnel to port 3002. Stop the tunnel process if you do not want the app reachable from
outside.
In development no mail leaves the machine: every mail opens at
http://localhost:3002/letter_opener (letter_opener_web). Only production delivers
through mailbox.org SMTP; tests use the :test delivery method.
There is no job worker. Active Job runs :inline, so everything, including Active
Storage's analysis and purging of receipts, happens within the request.
Tests
bin/rails test # models, controllers, mailers, PDF, i18n, docs
bin/rails test:system # Playwright browser test
PLAYWRIGHT_HEADED=1 bin/rails test:system
The test database is why_accounting_test on host db. To use a separate database (a
second checkout, or a parallel run), set WHY_ACCOUNTING_TEST_DB:
RAILS_ENV=test WHY_ACCOUNTING_TEST_DB=why_accounting_test_2 bin/rails db:test:prepare test
Linters
bundle exec standardrb # Ruby
bundle exec herb lint # ERB (.herb.yml and package.json pin 0.11.0)
bin/brakeman # security
bundle exec i18n-tasks missing # translations; also unused, normalize
bundle exec bundle-audit check --update # vulnerable gems
Continuous integration
Woodpecker runs .woodpecker.yml on git.whyservices.net:
| Step | When | What |
|---|---|---|
lint |
every push, pull request and tag | standardrb, herb, brakeman, bundle-audit |
test |
every push, pull request and tag | bin/rails test and bin/rails test:system against a Postgres service named db |
build |
tag | builds Dockerfile, pushes git.whyservices.net/whyservices/accounting:<tag> |
deploy |
tag, after a successful build | caprover deploy of that image to the app why-accounting |
Steps run in order, so a lint or test failure stops the release.
Deployment
Production is the CapRover app why-accounting on
https://captain.d.p.0000-shed.hel1.he.wlhs.eu. To release, tag a commit on main with
the next number (tags are plain integers) and push the tag:
git tag 39 main
git push origin 39
The image (ruby:4-alpine, puma on port 3000) runs bin/rails db:prepare on start,
so pending migrations are applied before the server starts. Production uses one
PostgreSQL database, set by DB_HOST, DB_NAME, DB_USER and DB_PASS, and stores
receipts on Backblaze B2.
The CapRover app settings, environment variables and Woodpecker secrets it needs are
listed in documentation/user_manual/deployment.md.
Documentation
| Directory | Contents |
|---|---|
documentation/plans/ |
Approved plans, archived as written |
documentation/history/ |
Numbered technical records of each milestone: what changed, why and how |
documentation/features/ |
One page per feature, for users and developers new to the code |
documentation/user_manual/ |
How to use the application, development-setup.md for developers and deployment.md for operations |
documentation/development_manual/ |
Implementation notes per subsystem: contracts and traps for whoever changes the code |
documentation/issues/ |
Only the no-authentication issue, not yet moved to the Basecamp board |
documentation/references/ |
External documents the project relies on |
Issues are tracked on the Basecamp card table https://app.basecamp.com/5939009/buckets/43205343/card_tables/10389155796 (Triage column), not in the repository.
Receipts are uploaded from the browser straight to Backblaze B2, in development too; the
bucket needs a CORS rule, described in documentation/user_manual/deployment.md.
CLAUDE.md and .claude/project_rules.md guide AI coding agents; they import the
shared rules through the .rails_rules symlink, which is per machine and git-ignored.