Skip to content
WebberZonePublic

About

Ledger - a self-hosted, single-user expense and receipt tracker built on Laravel

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

Ledger

A single-user cost/expense tracker you can self-host. Snap a receipt on the phone, have it logged in two taps; year-end is a CSV export mapped to your accountant's nominal codes. Built for Beyond Develop Ltd, open-sourced under the MIT licence for anyone who wants the same.

Laravel 13 · PHP 8.3 · SQLite · Breeze (Blade) · Tailwind · Alpine.

What it does

Everything the old "Accounts Tracker" spreadsheet did, plus receipt capture:

  • Three ledgers sharing one shape (single-table inheritance on transactions): Expenses, Income and Cash flows (share capital / director loans). CRUD, filters, GBP totals and CSV export per ledger. Amounts are net + VAT; for non-GBP entries give either an FX rate or the actual GBP charged from the statement — the other is derived.
  • Capture (/capture): tap one takes the photo — a draft expense is created and the image goes through a queued OCR job (Google Vision, or local tesseract as the free fallback); tap two confirms the pre-filled form. Installs to the phone as a PWA (start_url is the capture screen).
  • Reports (/reports): the Summary sheet — per-year Income / Expenses / Profit / Cash flows, expense matrices by report group (accountant view), by category and by payment method, with a personal-cards (to reimburse) subtotal.
  • Balances (/balances): snapshot balances per account+currency, pivoted accounts × dates — the Balances sheet.
  • Ledger (/ledger): any payment method + currency as a statement with a running balance in the original currency — the PayPal USD/EUR sheets, generalised.
  • Categories are two-level: entry-level name (e.g. "Website Expenses") plus a report_group rollup ("Advertising and PR") and an account_code for the accountant's chart.
  • Receipts: many per expense, stored on the private local disk (storage/app/private), served only through an authed route.
  • Money is integer pence everywhere. Floats never touch storage.
  • No soft deletes — records are kept forever; deleting a transaction is a deliberate, permanent act (the six-year record trail relies on simply never deleting).

Importing the old tracker

php artisan ledger:import-tracker <dir> loads CSVs exported from the spreadsheet (income, expenses, cash flows, balances) and prints per-ledger totals to verify against the workbook. The export script lives in the session scratchpad (export_csv.py); it reads a copy of the xlsx and never touches the original. Already run against the tracker as of July 2026 — totals matched to the penny.

Exporting the tracker

Reports → Export XLSX writes everything back into an .xlsx laid out like the original spreadsheet. If your own template is at storage/app/templates/tracker-template.xlsx (it's git-ignored, so it stays on your server), the export uses it. Otherwise the export uses the blank template bundled at resources/templates/tracker-template.xlsx, which has the same sheets, tables and Summary formulas with plain styling. To rebuild that template, run python/venv/bin/python python/scripts/build_blank_template.py resources/templates/tracker-template.xlsx.

Upgrading from Docket

This app was called Docket until October 2026; the name now belongs to a separate read-later project. An existing install keeps working without changes, but tidy up when convenient:

  • Git remote: git remote set-url origin https://github.com/WebberZone/TheLedger.git. Do this before the new TheDocket repository exists, or git pull will fetch the wrong project.
  • .env: rename DOCKET_* variables to LEDGER_*. The old names are still read as fallbacks.
  • Commands: docket:install, docket:optimize, docket:ocr, docket:prune-captures and docket:import-tracker remain as aliases of their ledger:* replacements.
  • APP_NAME: spatie/laravel-backup names its bucket folder after APP_NAME. Changing it from Docket starts a fresh folder, so move the existing archives across (or leave APP_NAME alone) to keep retention and backup:monitor working on the full history. It also changes the session cookie name, so you will be logged out once.
  • Run php artisan config:cache after deploying, as usual.

Installation

Requirements: PHP 8.3+ (with pdo_sqlite, mbstring, fileinfo), Composer, Node 20+. Optional: tesseract or Google Vision credentials for receipt OCR, Python 3 for the Accounts Tracker XLSX export.

Both installers do the same work — create .env, generate the app key, create the SQLite database, run the migrations and create the single owner account (there is no registration flow) — and both offer to seed a starter taxonomy of categories, business units and payment methods you can rename in Settings, or leave you to start empty.

Web installer (recommended on a server)

git clone https://github.com/WebberZone/TheLedger.git ledger
cd ledger
composer install --no-dev
npm install && npm run build

Point your web server at public/, then open https://your-domain/install in a browser. The wizard checks the server requirements (PHP version, extensions, writable directories), then asks for a setup code, your site URL, name, email and password, and installs.

The setup code proves you control the server: anyone can find a fresh deployment's /install, so it won't create the account without it. Read it on the server once the setup page has loaded:

cat storage/app/install/setup-code

The installer only exists while Ledger has no user account. Once your login is created it leaves a marker (storage/app/install/installed) and /install permanently redirects to the login screen. If the database can't be read, it answers 503 rather than treating the site as fresh. Ledger has exactly one account: neither installer, nor db:seed, will create a second one, and db:seed refuses to create the first without LEDGER_USER_PASSWORD set (there is no default password).

Console installer

git clone https://github.com/WebberZone/TheLedger.git ledger
cd ledger
composer setup      # deps + frontend build + guided php artisan ledger:install

The console installer is safe to re-run (--force) and never deletes data. For scripted/non-interactive installs pass the answers as flags:

php artisan ledger:install --url=https://ledger.example.com \
    --name="Jane Doe" --email=jane@example.com --password=... \
    --no-interaction   # omit --password to have one generated and shown once

Then, for local use:

php artisan serve        # or use Herd/Valet
php artisan queue:work   # needed for receipt OCR jobs

For receipt OCR set GOOGLE_VISION_CREDENTIALS in .env, or just install tesseract for the free local fallback; with neither, uploads simply skip extraction.

Tests: php artisan test. Dev server (app + queue + logs + Vite): composer dev.

Deployment (any small VPS; mine is OCI Always Free, Ubuntu)

  1. Nginx + PHP 8.3-FPM, standard Laravel vhost with root at public/. Let's Encrypt via certbot --nginx -d your-domain. HTTPS is required for the PWA install + camera capture.

  2. Visit https://your-domain/install (or run php artisan ledger:install) and give it the real production URL and your login details, then in .env set APP_ENV=production, APP_DEBUG=false, GOOGLE_VISION_CREDENTIALS (or install tesseract), BACKUP_* (see below), and a real mailer (MAIL_*) so backup-failure emails actually arrive.

  3. php artisan config:cache && php artisan route:cache && php artisan view:cache.

  4. Queue worker under systemd:

    [Service]
    ExecStart=/usr/bin/php /var/www/ledger/artisan queue:work --tries=3 --max-time=3600
    Restart=always
    User=www-data
  5. One cron entry drives the Laravel scheduler (backups, cleanup, monitor, and the OCI keep-alive burn that stops idle reclaim):

    * * * * * cd /var/www/ledger && php artisan schedule:run >> /dev/null 2>&1
    

Backups (spatie/laravel-backup)

Daily at 02:00 server time: SQLite is dumped to a file, zipped with the receipts directory, verified, and shipped to the offsite disk — an S3-compatible bucket that must live outside OCI (Backblaze B2 or Cloudflare R2; the OCI account itself is the risk being insured against). Retention: 14 daily / 8 weekly / 24 monthly / 7 yearly. backup:monitor emails BACKUP_NOTIFY_EMAIL if the newest backup is older than a day.

Fill in .env:

BACKUP_ACCESS_KEY_ID=...
BACKUP_SECRET_ACCESS_KEY=...
BACKUP_BUCKET=ledger-backups
BACKUP_ENDPOINT=https://<account-id>.r2.cloudflarestorage.com
BACKUP_REGION=auto

Test a restore now, not in year three

The database in the archive is an SQL dump (db-dumps/*.sql, written by sqlite3 .dump), not a database file: it has to be imported into a new database. Do it on a copy of the server if you can:

php artisan backup:run                  # take one
# download the newest zip from the bucket, then:
unzip Ledger/….zip -d /tmp/restore
ls /tmp/restore/db-dumps/               # one .sql file

# Build a new database from the dump and check it.
sqlite3 /tmp/restore/restored.sqlite < /tmp/restore/db-dumps/*.sql
sqlite3 /tmp/restore/restored.sqlite "PRAGMA integrity_check;"      # expect: ok
sqlite3 /tmp/restore/restored.sqlite "SELECT count(*) FROM transactions;"

# Swap it in with nothing writing: stop the worker and the site first.
php artisan down
sudo systemctl stop ledger-worker       # whatever runs queue:work
mkdir -p /tmp/before-restore && cp database/database.sqlite* /tmp/before-restore/
mv /tmp/restore/restored.sqlite database/database.sqlite
rm -f database/database.sqlite-wal database/database.sqlite-shm
rsync -a /tmp/restore/…/storage/app/private/ storage/app/private/
chown -R www-data: database storage/app/private
php artisan migrate --force             # if the backup predates a release
php artisan up
sudo systemctl start ledger-worker

Open the app, confirm a known expense and its receipt image are present. The originals stay in /tmp/before-restore until you're satisfied.

Out of scope, deliberately

Invoicing, bank feeds, double-entry, VAT/MTD, HMRC APIs. Company is not VAT-registered and outside MTD; this is record-keeping that feeds the CT600, nothing more.

About

Ledger - a self-hosted, single-user expense and receipt tracker built on Laravel

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages