diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md
new file mode 100644
index 0000000..11a1798
--- /dev/null
+++ b/.github/CODE_OF_CONDUCT.md
@@ -0,0 +1,42 @@
+# Code of Conduct
+
+## The short version
+
+Be decent. Assume the person on the other end is acting in good faith and is
+busy. Quidly is a small project maintained in someone's spare time.
+
+## Expected behaviour
+
+- Keep criticism about the code, not the person.
+- Accept that "no" is a legitimate answer to a feature request, and that
+ maintainers get to scope their own project.
+- Remember that people file issues about their own tax affairs. Treat financial
+ details someone shares in an issue as something they trusted you with, and
+ don't repost it elsewhere.
+
+## Unacceptable behaviour
+
+- Harassment, insults, or personal attacks.
+- Discriminatory language or imagery, or unwelcome sexual attention.
+- Publishing others' private information without permission.
+- Sustained disruption of discussions, or demanding free labour.
+
+## Scope
+
+This applies to all project spaces — issues, pull requests, discussions,
+commit messages — and to public spaces when someone is representing the project.
+
+## Enforcement
+
+Report problems to the maintainer via
+[a private security advisory](https://github.com/WarlaxZ/quidly/security/advisories/new)
+if the report itself is sensitive, or by opening an issue if it isn't.
+
+The maintainer may warn, edit or remove contributions, or block accounts.
+Enforcement decisions rest with the maintainer, who will act in proportion to
+what happened.
+
+## Attribution
+
+Adapted in spirit from the [Contributor Covenant](https://www.contributor-covenant.org),
+shortened to match the size of the project.
diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md
new file mode 100644
index 0000000..9fc808f
--- /dev/null
+++ b/.github/CONTRIBUTING.md
@@ -0,0 +1,73 @@
+# Contributing to Quidly
+
+Thanks for taking an interest. Quidly is a self-hosted bookkeeping app for UK
+landlords, and it deals with people's tax returns — so correctness matters more
+here than velocity.
+
+## Ground rules
+
+- **Money is integer pence, end-to-end.** There is no floating-point arithmetic
+ anywhere in the money path. If a change introduces a `number` that represents
+ a fractional currency amount, it will be rejected. Store pence, format at the
+ edge.
+- **Tax logic needs a test and a source.** Any change to rates, bands, or
+ thresholds must cite the HMRC page it came from in the PR description, and
+ come with a test that pins the expected figure.
+- **This is not the Next.js you may know.** Quidly is on Next.js 16. Read the
+ relevant guide under `node_modules/next/dist/docs/` before writing app code —
+ APIs and conventions differ from older versions.
+- **Prisma v7 conventions.** The datasource URL lives in `prisma.config.ts`.
+ Migrations are hand-authored SQL applied with `prisma migrate deploy` — not
+ `migrate dev`.
+
+## Getting set up
+
+```bash
+npm install
+npm run set-password # paste the DEV (backslash-escaped) hash into .env
+# .env also needs SESSION_SECRET=... (openssl rand -base64 32) and DATABASE_URL="file:./dev.db"
+npx prisma migrate deploy
+npx prisma db seed
+npm run dev # http://localhost:3000
+```
+
+## Before you open a PR
+
+```bash
+npm test # vitest
+npm run typecheck # tsc --noEmit
+npm run build # next build
+```
+
+All three run in CI on every PR. Green locally means green there.
+
+## Scope
+
+Quidly is deliberately **single-user per install** and **UK-first**. Two kinds
+of proposal are usually out of scope:
+
+- Multi-tenancy, user management, or role systems.
+- Tax regimes for other countries.
+
+If you want either, a fork is a completely reasonable answer — the AGPL exists
+for exactly that.
+
+Good places to start, in rough order of how much they help:
+
+1. **Tax-year rates.** Each April, rates and thresholds change. Updating a rate
+ year is well-scoped and high-value.
+2. **Bank CSV formats.** More UK bank export formats parsed out of the box.
+3. **Bug reports with a reproduction.** Genuinely valuable even without a fix.
+
+## Reporting bugs
+
+Use the issue templates — they ask for the version, whether you're on Docker or
+a dev install, and the tax year, which is almost always what a diagnosis turns
+on. For anything security-related, see [SECURITY.md](SECURITY.md) instead; do
+not open a public issue.
+
+## Licence
+
+Quidly is AGPL-3.0. By contributing you agree your contribution is licensed
+under the same terms. Note the AGPL's network clause: if you run a modified
+Quidly as a service that others use, you must offer them your changes.
diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
new file mode 100644
index 0000000..75e2448
--- /dev/null
+++ b/.github/FUNDING.yml
@@ -0,0 +1,8 @@
+# Uncomment a line once the corresponding account is set up. GitHub shows a
+# "Sponsor" button on the repo for whichever of these are populated, and an
+# unconfigured platform is left out rather than shown as broken.
+#
+# github: [WarlaxZ] # requires GitHub Sponsors to be enabled first
+# ko_fi: yourhandle
+# buy_me_a_coffee: yourhandle
+# custom: ["https://example.com/sponsor-quidly"]
diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml
new file mode 100644
index 0000000..13538bd
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.yml
@@ -0,0 +1,118 @@
+name: Bug report
+description: Something isn't working the way it should
+labels: ["bug"]
+body:
+ - type: markdown
+ attributes:
+ value: |
+ Thanks for reporting this. Please don't paste real financial data —
+ redact figures and addresses, or make up equivalent numbers.
+
+ If this is a security issue, **stop** and use
+ [private vulnerability reporting](https://github.com/WarlaxZ/quidly/security/advisories/new)
+ instead.
+
+ - type: textarea
+ id: what-happened
+ attributes:
+ label: What happened
+ description: What did you see, and what did you expect instead?
+ validations:
+ required: true
+
+ - type: textarea
+ id: steps
+ attributes:
+ label: Steps to reproduce
+ placeholder: |
+ 1. Go to Transactions
+ 2. Add an expense of £120 against property X
+ 3. Open the SA105 page
+ 4. Box 20 shows £0 instead of £120
+ validations:
+ required: true
+
+ - type: dropdown
+ id: install-method
+ attributes:
+ label: How are you running Quidly?
+ options:
+ - Docker (docker compose)
+ - Docker (ghcr.io image)
+ - Local dev (npm run dev)
+ - Local production (npm run build && npm start)
+ - Other
+ validations:
+ required: true
+
+ - type: input
+ id: version
+ attributes:
+ label: Version or commit
+ description: The release tag, image tag, or `git rev-parse --short HEAD`.
+ placeholder: v0.1.0
+ validations:
+ required: true
+
+ - type: dropdown
+ id: area
+ attributes:
+ label: Which part of the app?
+ options:
+ - Bookkeeping (transactions, recurring, vendors)
+ - Bank CSV import
+ - Receipt scanning (AI extraction)
+ - SA105 / personal tax
+ - Limited company (corporation tax, dividends, director's loan)
+ - What-if planner / salary-vs-dividend optimiser
+ - Deductions assistant (mileage, use of home)
+ - Akaunting migration
+ - Auth / login / sessions
+ - Setup, Docker, or configuration
+ - UI / theming
+ - Something else
+ validations:
+ required: true
+
+ - type: dropdown
+ id: tax-year
+ attributes:
+ label: Tax year involved
+ description: Leave as N/A if this isn't a tax calculation issue.
+ options:
+ - N/A
+ - 2025-26
+ - 2026-27
+ - 2027-28
+ - Earlier year
+ validations:
+ required: true
+
+ - type: textarea
+ id: logs
+ attributes:
+ label: Logs
+ description: >
+ Relevant output from `docker compose logs quidly` or the dev server, plus
+ any browser console errors. Redact secrets.
+ render: shell
+
+ - type: textarea
+ id: environment
+ attributes:
+ label: Environment
+ placeholder: |
+ Host OS: Debian 12
+ Browser: Firefox 141
+ Reverse proxy: Caddy
+ COOKIE_SECURE: unset
+
+ - type: checkboxes
+ id: checks
+ attributes:
+ label: Before submitting
+ options:
+ - label: I searched existing issues and this isn't a duplicate
+ required: true
+ - label: I've removed any real financial or personal data from this report
+ required: true
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
new file mode 100644
index 0000000..d983ac3
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1,15 @@
+blank_issues_enabled: false
+contact_links:
+ - name: Question or setup help
+ url: https://github.com/WarlaxZ/quidly/discussions
+ about: >
+ Ask in Discussions rather than opening an issue — setup, Docker, and
+ "how do I record X" questions are answered there.
+ - name: Report a security vulnerability
+ url: https://github.com/WarlaxZ/quidly/security/advisories/new
+ about: Please report privately, not in a public issue.
+ - name: Official HMRC guidance
+ url: https://www.gov.uk/renting-out-a-property
+ about: >
+ Quidly produces estimates, not tax advice. For what you actually owe,
+ check HMRC or ask an accountant.
diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml
new file mode 100644
index 0000000..fe73e54
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.yml
@@ -0,0 +1,65 @@
+name: Feature request
+description: Suggest something Quidly should do
+labels: ["enhancement"]
+body:
+ - type: markdown
+ attributes:
+ value: |
+ Before writing this up, note two things that are **deliberately out of
+ scope**: multi-user / multi-tenant support (each install is single-user),
+ and tax regimes for countries other than the UK. Requests for either will
+ be closed — though forking is a perfectly good answer, and the AGPL is
+ there for that.
+
+ - type: textarea
+ id: problem
+ attributes:
+ label: What problem are you hitting?
+ description: >
+ Describe the situation rather than the solution. "I have three properties
+ with different mortgage rates and can't see which is loss-making" is more
+ useful than "add a per-property P&L chart".
+ validations:
+ required: true
+
+ - type: textarea
+ id: proposal
+ attributes:
+ label: What would you like Quidly to do?
+ validations:
+ required: true
+
+ - type: dropdown
+ id: area
+ attributes:
+ label: Which part of the app?
+ options:
+ - Bookkeeping (transactions, recurring, vendors)
+ - Bank CSV import
+ - Receipt scanning (AI extraction)
+ - SA105 / personal tax
+ - Limited company
+ - What-if planner / optimiser
+ - Deductions assistant
+ - Migration from other software
+ - Setup, Docker, or configuration
+ - UI / theming
+ - Something else
+ validations:
+ required: true
+
+ - type: textarea
+ id: workaround
+ attributes:
+ label: How are you working around it today?
+ description: Spreadsheet, another tool, manual calculation, not at all?
+
+ - type: checkboxes
+ id: checks
+ attributes:
+ label: Before submitting
+ options:
+ - label: I searched existing issues and discussions for this idea
+ required: true
+ - label: This isn't multi-user support or a non-UK tax regime
+ required: true
diff --git a/.github/ISSUE_TEMPLATE/tax_accuracy.yml b/.github/ISSUE_TEMPLATE/tax_accuracy.yml
new file mode 100644
index 0000000..87d48df
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/tax_accuracy.yml
@@ -0,0 +1,105 @@
+name: Tax calculation or rate issue
+description: A figure Quidly produces looks wrong, or a rate needs updating
+labels: ["bug", "tax-accuracy"]
+body:
+ - type: markdown
+ attributes:
+ value: |
+ Tax accuracy reports are the most useful issues this project gets.
+
+ Quidly produces **estimates, not advice** — but an estimate should still
+ be right. Please use made-up round numbers that still show the problem
+ rather than your real figures.
+
+ - type: dropdown
+ id: calculation
+ attributes:
+ label: Which calculation?
+ options:
+ - SA105 box value
+ - £1,000 property allowance
+ - Section 24 finance-cost reducer
+ - Income tax bands (rUK)
+ - Income tax bands (Scottish)
+ - National Insurance
+ - Corporation tax / marginal relief
+ - Dividend tax
+ - Director's loan (s455 or benefit-in-kind)
+ - Mileage allowance
+ - Use of home
+ - Salary-vs-dividend optimiser
+ - What-if planner
+ - Something else
+ validations:
+ required: true
+
+ - type: dropdown
+ id: tax-year
+ attributes:
+ label: Tax year
+ options:
+ - 2025-26
+ - 2026-27
+ - 2027-28
+ - Earlier year
+ validations:
+ required: true
+
+ - type: textarea
+ id: inputs
+ attributes:
+ label: Inputs
+ description: The figures you entered, using illustrative round numbers.
+ placeholder: |
+ Rental income: £12,000
+ Mortgage interest: £4,000
+ Allowable expenses: £2,000
+ Other (employment) income: £30,000
+ Property allowance: not claimed
+ validations:
+ required: true
+
+ - type: input
+ id: expected
+ attributes:
+ label: Expected figure
+ placeholder: "£1,600 finance-cost reducer"
+ validations:
+ required: true
+
+ - type: input
+ id: actual
+ attributes:
+ label: Figure Quidly produced
+ placeholder: "£800"
+ validations:
+ required: true
+
+ - type: textarea
+ id: source
+ attributes:
+ label: Source
+ description: >
+ Link the HMRC page, helpsheet, or SA105 note supporting the expected
+ figure. This is what makes the fix verifiable — please don't skip it.
+ placeholder: https://www.gov.uk/guidance/income-tax-when-you-rent-out-a-property-working-out-your-rental-income
+ validations:
+ required: true
+
+ - type: input
+ id: version
+ attributes:
+ label: Version or commit
+ placeholder: v0.1.0
+ validations:
+ required: true
+
+ - type: checkboxes
+ id: checks
+ attributes:
+ label: Before submitting
+ options:
+ - label: The figures above are illustrative, not my real tax data
+ required: true
+ - label: I understand Quidly gives estimates and is not affiliated with HMRC
+ required: true
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 0000000..68f8ba8
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,24 @@
+## What this changes
+
+
+
+## Why
+
+
+
+## Checklist
+
+- [ ] `npm test` passes
+- [ ] `npm run typecheck` passes
+- [ ] `npm run build` passes
+- [ ] Any money value added or changed is **integer pence**, not a float
+- [ ] Tax rates / bands / thresholds cite an HMRC source above, and have a test
+ pinning the expected figure
+- [ ] New Prisma migrations are hand-authored SQL, applied with `migrate deploy`
+- [ ] Docs updated (`README.md`, `.env.example`, or `docs/`) if behaviour or
+ configuration changed
+
+## Screenshots
+
+
diff --git a/.github/SECURITY.md b/.github/SECURITY.md
new file mode 100644
index 0000000..5b4a0a9
--- /dev/null
+++ b/.github/SECURITY.md
@@ -0,0 +1,57 @@
+# Security Policy
+
+Quidly holds financial records and is designed to be exposed to a network, so
+security reports are welcome and taken seriously.
+
+## Reporting a vulnerability
+
+**Please do not open a public issue.**
+
+Report privately via GitHub's
+[private vulnerability reporting](https://github.com/WarlaxZ/quidly/security/advisories/new)
+— that opens a draft advisory only you and the maintainer can see.
+
+Please include:
+
+- What the issue is and roughly how bad you think it is.
+- Steps to reproduce, or a proof of concept.
+- The Quidly version or commit, and whether you're running Docker or a dev install.
+- Whether you're behind a reverse proxy, and whether `COOKIE_SECURE` is set.
+
+You can expect an acknowledgement within a few days. Because Quidly is
+maintained by one person in their own time, a fix may take longer than that —
+you'll get an honest estimate rather than silence.
+
+## Supported versions
+
+Quidly is pre-1.0 and ships from `main`. Only the latest release receives
+security fixes; there are no maintained backport branches.
+
+## Scope
+
+In scope:
+
+- Authentication and session handling (iron-session, argon2id password hashing).
+- Authorisation gaps that expose data across properties or companies.
+- SQL injection, XSS, CSRF, SSRF, path traversal.
+- Anything that leaks financial data, uploaded receipts, or the session secret.
+- Container issues, e.g. unnecessary privilege in the runtime image.
+
+Out of scope — these are documented design decisions, not bugs:
+
+- **Single-user by design.** Each install has one account. "A logged-in user can
+ see all the data in the install" is the intended behaviour.
+- **`COOKIE_SECURE=false` weakens cookies.** That is the documented trade-off
+ for plain HTTP on a trusted LAN. Deployments behind HTTPS should leave it unset.
+- Running Quidly on the public internet without a TLS-terminating proxy.
+- Missing rate limiting on login for a single-user LAN app — though a concrete
+ attack here is still worth reporting.
+
+## Hardening notes for operators
+
+- Serve behind HTTPS (see the Caddy example in the README) and leave
+ `COOKIE_SECURE` unset.
+- `SESSION_SECRET` should be 32+ random characters, e.g. `openssl rand -base64 32`.
+- Back up the `quidly-data` volume — it holds the SQLite database and uploads.
+- If you set `ANTHROPIC_API_KEY` for receipt scanning, receipt images are sent
+ to Anthropic's API. Leave it unset if that is not acceptable to you.
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..04a59c8
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,54 @@
+name: CI
+
+on:
+ push:
+ branches: [main]
+ pull_request:
+ workflow_dispatch:
+
+# A newer push to the same branch supersedes an in-flight run.
+concurrency:
+ group: ci-${{ github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ test:
+ name: Test, typecheck & build
+ runs-on: ubuntu-latest
+
+ env:
+ # Throwaway SQLite file and secret — CI never touches real data.
+ DATABASE_URL: file:./ci.db
+ SESSION_SECRET: ci-session-secret-at-least-32-characters-long
+
+ steps:
+ - uses: actions/checkout@v7
+
+ # Node 22 matches the Dockerfile's runtime, so CI fails on the same
+ # things production does.
+ - uses: actions/setup-node@v7
+ with:
+ node-version: 22
+ cache: npm
+
+ - name: Install dependencies
+ run: npm ci
+
+ # Prisma v7: the datasource URL comes from prisma.config.ts, and
+ # migrations are hand-authored SQL applied with `migrate deploy`.
+ - name: Generate Prisma client
+ run: npx prisma generate
+
+ - name: Apply migrations
+ run: npx prisma migrate deploy
+
+ - name: Test
+ run: npm test
+
+ - name: Build
+ run: npm run build
+
+ # After `next build`, so the generated route types under .next/types are
+ # on disk and get checked too.
+ - name: Typecheck
+ run: npm run typecheck
diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml
new file mode 100644
index 0000000..9b2f93a
--- /dev/null
+++ b/.github/workflows/docker-publish.yml
@@ -0,0 +1,87 @@
+name: Publish container
+
+on:
+ push:
+ branches: [main]
+ tags: ["v*"]
+ workflow_dispatch:
+
+env:
+ REGISTRY: ghcr.io
+
+concurrency:
+ group: publish-${{ github.ref }}
+ cancel-in-progress: false
+
+jobs:
+ publish:
+ name: Build & push to GHCR
+ runs-on: ubuntu-latest
+
+ permissions:
+ contents: read
+ packages: write
+ id-token: write
+ attestations: write
+
+ steps:
+ - uses: actions/checkout@v7
+
+ # GHCR rejects uppercase image names, and this repo's owner is "WarlaxZ".
+ # Derive it rather than hardcoding so forks publish to their own namespace.
+ - name: Lowercase the image name
+ id: img
+ run: echo "name=${GITHUB_REPOSITORY,,}" >> "$GITHUB_OUTPUT"
+
+ # arm64 is emulated. better-sqlite3 normally resolves a prebuilt binary
+ # so this stays tolerable, but the arm leg is still the slow one. If it
+ # becomes a problem, split into a matrix across ubuntu-latest and
+ # ubuntu-24.04-arm and merge the digests with `docker buildx imagetools`.
+ - uses: docker/setup-qemu-action@v4
+ with:
+ platforms: arm64
+
+ - uses: docker/setup-buildx-action@v4
+
+ - uses: docker/login-action@v4
+ with:
+ registry: ${{ env.REGISTRY }}
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Derive tags and labels
+ id: meta
+ uses: docker/metadata-action@v6
+ with:
+ images: ${{ env.REGISTRY }}/${{ steps.img.outputs.name }}
+ tags: |
+ type=semver,pattern={{version}}
+ type=semver,pattern={{major}}.{{minor}}
+ type=raw,value=latest,enable={{is_default_branch}}
+ type=sha,format=short
+ labels: |
+ org.opencontainers.image.title=Quidly
+ org.opencontainers.image.description=Free, self-hosted UK-landlord bookkeeping and tax — Self-Assessment, sorted.
+ org.opencontainers.image.licenses=AGPL-3.0-or-later
+
+ - name: Build and push
+ id: build
+ uses: docker/build-push-action@v7
+ with:
+ context: .
+ platforms: linux/amd64,linux/arm64
+ push: true
+ tags: ${{ steps.meta.outputs.tags }}
+ labels: ${{ steps.meta.outputs.labels }}
+ cache-from: type=gha
+ cache-to: type=gha,mode=max
+
+ # Signed provenance, so operators can verify the image was built from
+ # this repo rather than pushed by hand:
+ # gh attestation verify oci://ghcr.io/warlaxz/quidly:latest -R WarlaxZ/quidly
+ - name: Attest provenance
+ uses: actions/attest-build-provenance@v4
+ with:
+ subject-name: ${{ env.REGISTRY }}/${{ steps.img.outputs.name }}
+ subject-digest: ${{ steps.build.outputs.digest }}
+ push-to-registry: true
diff --git a/README.md b/README.md
index 9b6a036..a4b47aa 100644
--- a/README.md
+++ b/README.md
@@ -2,12 +2,18 @@
**Self-Assessment, sorted.** — free, self-hosted UK-landlord bookkeeping and tax.
-
+[](https://github.com/WarlaxZ/quidly/actions/workflows/ci.yml)
+[](https://github.com/WarlaxZ/quidly/releases)
+[](https://github.com/WarlaxZ/quidly/pkgs/container/quidly)
+[](LICENSE)


-
-Quidly is a self-hosted bookkeeping app built for UK landlords: track rent and expenses across your properties, then turn them into an SA105 you can file. Run it on your own box, keep your own data.
+Quidly is a self-hosted **bookkeeping and tax app for UK landlords**: track rent
+and expenses across your properties, then turn them into an **SA105** you can
+file with your **Self Assessment** tax return. It handles the **£1,000 property
+allowance**, the **Section 24** finance-cost reducer, **Scottish tax bands**, and
+limited-company landlords too. Run it on your own box, keep your own data.
## Screenshots
@@ -19,9 +25,54 @@ Quidly is a self-hosted bookkeeping app built for UK landlords: track rent and e
+## Quick start
+
+```bash
+mkdir quidly && cd quidly
+curl -O https://raw.githubusercontent.com/WarlaxZ/quidly/main/docker-compose.yml
+curl -o .env https://raw.githubusercontent.com/WarlaxZ/quidly/main/.env.example
+# set SESSION_SECRET in .env, then generate a login:
+docker compose run --rm quidly npm run set-password # paste the Docker-Compose hash into .env
+docker compose up -d # open http://localhost:3000
+```
+
+That pulls the prebuilt multi-arch image (`linux/amd64` and `linux/arm64`, so a
+Raspberry Pi or ARM VPS works). See [Self-hosting](#self-hosting-with-docker)
+for the `$` gotcha, HTTPS, and building from source instead.
+
+## Who it's for
+
+Landlords who file their own **Self Assessment** and would rather not keep the
+books in a spreadsheet — whether that's one buy-to-let, an HMO, or a portfolio
+across several properties, held personally or through a limited company.
+
+If you're currently on **Akaunting**, there's a
+[migration path](#migrating-from-akaunting) that brings your transactions,
+vendors and categories across.
+
## What it is
-Quidly is free and self-hosted, with a UK-first design. **Money is stored as integer pence end-to-end** — there is no floating-point arithmetic in the money path, so nothing drifts or rounds unexpectedly. Each install is **single-user**. Quidly is **not affiliated with HMRC**; it produces estimates, not tax advice.
+Quidly is free and self-hosted, with a UK-first design. **Money is stored as
+integer pence end-to-end** — there is no floating-point arithmetic in the money
+path, so nothing drifts or rounds unexpectedly. Each install is **single-user**.
+Quidly is **not affiliated with HMRC**; it produces estimates, not tax advice.
+
+## How it compares
+
+| | Quidly | Spreadsheet | Landlord SaaS | General accounting software |
+| --- | :---: | :---: | :---: | :---: |
+| Cost | Free | Free | Monthly subscription | Monthly subscription |
+| Your data stays on your hardware | ✅ | ✅ | ❌ | ❌ |
+| Knows what an SA105 box is | ✅ | ❌ | Varies | ❌ |
+| Section 24 finance-cost reducer | ✅ | Manual | Varies | ❌ |
+| Scottish tax bands | ✅ | Manual | Varies | Varies |
+| Limited-company landlord support | ✅ | Manual | Varies | ✅ |
+| Won't silently mis-round your figures | ✅ | ❌ | — | — |
+| Multi-user / multi-tenant | ❌ | — | ✅ | ✅ |
+| Files directly with HMRC | ❌ | ❌ | Varies | Varies |
+
+Deliberate trade-offs, not oversights: Quidly is single-user per install, UK-only,
+and produces figures and a PDF rather than submitting to HMRC for you.
## Features
@@ -29,23 +80,46 @@ Quidly is free and self-hosted, with a UK-first design. **Money is stored as int
- **Receipt scanning** — optional AI extraction using your own Anthropic key.
- **SA105 & personal tax** — £1,000 property allowance, Section 24 finance-cost reducer, Scottish bands, and SA105 PDF export.
- **Limited companies** — corporation tax (including marginal relief), dividends, director's loan s455 + benefit-in-kind.
+- **Deductions assistant** — mileage log and use-of-home claims.
- **Plan ahead** — a what-if planner plus a salary-vs-dividend optimiser.
- **Light & dark themes.**
-## Self-host with Docker (recommended)
+## Self-hosting with Docker
+
+The [Quick start](#quick-start) above pulls
+[`ghcr.io/warlaxz/quidly`](https://github.com/WarlaxZ/quidly/pkgs/container/quidly).
+To build from this checkout instead:
```bash
git clone https://github.com/WarlaxZ/quidly.git && cd quidly
cp .env.example .env # then set SESSION_SECRET
-docker compose run --rm quidly npm run set-password # paste the Docker-Compose hash into .env
-docker compose up -d # open http://localhost:3000
+docker compose -f docker-compose.yml -f docker-compose.build.yml run --rm quidly npm run set-password
+docker compose -f docker-compose.yml -f docker-compose.build.yml up -d --build
```
-**Password hash & the `$` gotcha.** Docker Compose interpolates bare `$` in `env_file` values as variable references and strips them. Put the argon2 hash in `.env` with **each `$` doubled** (`$$`). `set-password` prints exactly that "Docker-Compose" form for you — it prints three forms in total: the dev backslash-escaped form, the Docker-Compose doubled form, and the raw unescaped hash. Use the Docker-Compose one here.
+**Password hash & the `$` gotcha.** Docker Compose interpolates bare `$` in
+`env_file` values as variable references and strips them. Put the argon2 hash in
+`.env` with **each `$` doubled** (`$$`). `set-password` prints exactly that
+"Docker-Compose" form for you — it prints three forms in total: the dev
+backslash-escaped form, the Docker-Compose doubled form, and the raw unescaped
+hash. Use the Docker-Compose one here.
-**Data persists** in the `quidly-data` Docker volume — the SQLite database and uploads both live under `/data`, so they survive restarts and upgrades.
+**Data persists** in the `quidly-data` Docker volume — the SQLite database and
+uploads both live under `/data`, so they survive restarts and upgrades. Back that
+volume up.
-**`COOKIE_SECURE`.** Leave it unset (the secure default) when serving behind HTTPS. Set `COOKIE_SECURE=false` **only** if you serve plain HTTP on a trusted LAN, otherwise login cookies won't be sent.
+**Upgrading.** `docker compose pull && docker compose up -d`. Migrations run
+automatically on start.
+
+**`COOKIE_SECURE`.** Leave it unset (the secure default) when serving behind
+HTTPS. Set `COOKIE_SECURE=false` **only** if you serve plain HTTP on a trusted
+LAN, otherwise login cookies won't be sent.
+
+**Verifying the image.** Published images carry signed build provenance:
+
+```bash
+gh attestation verify oci://ghcr.io/warlaxz/quidly:latest -R WarlaxZ/quidly
+```
## HTTPS reverse proxy (Caddy)
@@ -55,7 +129,9 @@ quidly.example.com {
}
```
-Caddy auto-provisions and renews TLS certificates for you. Behind HTTPS, leave `COOKIE_SECURE` unset. Traefik and nginx work equally well — just point the proxy at `localhost:3000`.
+Caddy auto-provisions and renews TLS certificates for you. Behind HTTPS, leave
+`COOKIE_SECURE` unset. Traefik and nginx work equally well — just point the proxy
+at `localhost:3000`.
## Local development
@@ -66,16 +142,25 @@ npm run set-password # paste the DEV (backslash-escaped) hash into .e
npx prisma migrate deploy # apply migrations (Prisma v7: hand-authored SQL)
npx prisma db seed # seed HMRC categories
npm run dev # http://localhost:3000
-npm test # 198 tests
+
+npm test # vitest
+npm run typecheck # tsc --noEmit
+npm run build # next build
```
-Prisma v7 note: the datasource URL is configured in `prisma.config.ts` (read from `DATABASE_URL`), and migrations are hand-authored SQL applied with `migrate deploy` — **not** `migrate dev`.
+Prisma v7 note: the datasource URL is configured in `prisma.config.ts` (read from
+`DATABASE_URL`), and migrations are hand-authored SQL applied with
+`migrate deploy` — **not** `migrate dev`.
+
+See [CONTRIBUTING.md](.github/CONTRIBUTING.md) for conventions — in particular the
+integer-pence rule and the requirement that tax-rate changes cite an HMRC source.
## Migrating from Akaunting
Import your existing Akaunting data (transactions, vendors, categories) into Quidly.
-**Prerequisites:** Docker (for the analyse step) and a MySQL/MariaDB dump of your Akaunting database (`.sql`).
+**Prerequisites:** Docker (for the analyse step) and a MySQL/MariaDB dump of your
+Akaunting database (`.sql`).
```bash
# 1. Analyse the dump — loads it into a throwaway MariaDB and writes a review pack
@@ -92,7 +177,12 @@ npm run migrate:akaunting:apply
npm run migrate:akaunting:apply -- --attachments-dir /path/to/akaunting/storage
```
-Each Akaunting *company* becomes a Quidly *property*, categories are mapped onto SA105 boxes (you review the suggestions), and recurring items carry across. Amounts are treated as GBP; non-GBP transactions are listed and skipped. The import is idempotent — re-running is safe and never duplicates (imported rows are tagged with an `externalRef`). Files under `akaunting-migration/` are git-ignored as they may contain financial data.
+Each Akaunting *company* becomes a Quidly *property*, categories are mapped onto
+SA105 boxes (you review the suggestions), and recurring items carry across.
+Amounts are treated as GBP; non-GBP transactions are listed and skipped. The
+import is idempotent — re-running is safe and never duplicates (imported rows are
+tagged with an `externalRef`). Files under `akaunting-migration/` are git-ignored
+as they may contain financial data.
**📖 Full walkthrough: [docs/migrating-from-akaunting.md](docs/migrating-from-akaunting.md)** — how to export your Akaunting dump, review the category mapping, handle recurring items and attachments, and troubleshoot.
@@ -108,17 +198,40 @@ Each Akaunting *company* becomes a Quidly *property*, categories are mapped onto
| `DATABASE_URL` | Set by Docker | SQLite connection string. Dev default `file:./dev.db`; Docker sets `file:/data/quidly.db`. |
| `UPLOAD_DIR` | Set by Docker | Upload directory. Dev default `./uploads`; Docker sets `/data/uploads`. |
| `COOKIE_SECURE` | Optional | Set to `false` only for plain-HTTP on a trusted LAN. Leave unset behind HTTPS. |
-| `ANTHROPIC_API_KEY` | Optional | Enables AI receipt scanning, using your own Anthropic key. |
+| `ANTHROPIC_API_KEY` | Optional | Enables AI receipt scanning, using your own Anthropic key. Receipt images are sent to Anthropic; leave unset to keep everything local. |
| `EXTRACTION_MODEL` | Optional | Vision model for receipt extraction. Defaults to a cheap vision-capable Claude model. |
## Tech stack
-Next.js 16 (App Router) · Prisma v7 + SQLite (better-sqlite3 adapter) · Tailwind v4 · iron-session + argon2id · Vitest · Docker.
+Next.js 16 (App Router) · Prisma v7 + SQLite (better-sqlite3 adapter) · Tailwind
+v4 · iron-session + argon2id · Vitest · Docker.
## Tax accuracy & caveats
-Quidly produces **estimates, not advice** — verify HMRC rates each April against the official guidance. It is currently configured for the **2025-26** tax year; other years fall back to 2025-26 with an in-app notice. Each install is single-tenant.
+Quidly produces **estimates, not advice** — verify HMRC rates each April against
+the official guidance. Rates are configured for the **2025-26, 2026-27 and
+2027-28** tax years; years outside that range fall back to the latest configured
+year with an in-app notice.
+
+**Quidly does not submit anything to HMRC.** It produces the figures and an SA105
+PDF; you or your accountant do the filing. Making Tax Digital for Income Tax is
+being phased in for landlords — check
+[HMRC's guidance](https://www.gov.uk/guidance/using-making-tax-digital-for-income-tax)
+for whether it applies to you yet, because Quidly cannot make quarterly digital
+submissions on your behalf.
+
+Each install is single-tenant. Found a figure that looks wrong? Please
+[open a tax-accuracy issue](https://github.com/WarlaxZ/quidly/issues/new?template=tax_accuracy.yml)
+with an HMRC link — those are the most useful reports this project gets.
+
+## Contributing & security
+
+- [CONTRIBUTING.md](.github/CONTRIBUTING.md) — conventions, scope, and what makes a good PR.
+- [SECURITY.md](.github/SECURITY.md) — please report vulnerabilities privately, not as public issues.
## Licence
-Licensed under [AGPL-3.0](LICENSE). Quidly is open, but the AGPL means that if you run a modified version as a network service, you must share your changes with its users. Commercial or hosted licensing from the author is possible if the AGPL doesn't fit your use.
+Licensed under [AGPL-3.0](LICENSE). Quidly is open, but the AGPL means that if
+you run a modified version as a network service, you must share your changes with
+its users. Commercial or hosted licensing from the author is possible if the AGPL
+doesn't fit your use.
diff --git a/docker-compose.build.yml b/docker-compose.build.yml
new file mode 100644
index 0000000..8b975c1
--- /dev/null
+++ b/docker-compose.build.yml
@@ -0,0 +1,9 @@
+# Overlay that builds Quidly from this checkout instead of pulling the
+# published image. Useful for development, or if you'd rather not trust a
+# prebuilt binary with your financial data.
+#
+# docker compose -f docker-compose.yml -f docker-compose.build.yml up -d --build
+services:
+ quidly:
+ build: .
+ image: quidly-local
diff --git a/docker-compose.yml b/docker-compose.yml
index 9003557..e9673f1 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1,7 +1,9 @@
services:
quidly:
- build: .
- image: quidly
+ # Prebuilt multi-arch image (linux/amd64 + linux/arm64).
+ # To build from source instead:
+ # docker compose -f docker-compose.yml -f docker-compose.build.yml up -d --build
+ image: ghcr.io/warlaxz/quidly:latest
env_file: .env
environment:
DATABASE_URL: file:/data/quidly.db
diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh
index 1b2be9a..139e74d 100755
--- a/docker-entrypoint.sh
+++ b/docker-entrypoint.sh
@@ -5,6 +5,14 @@ set -e
mkdir -p "${UPLOAD_DIR:-/data/uploads}"
chown -R node:node /data
+# If a command was passed, run that instead of the server — this is what makes
+# docker compose run --rm quidly npm run set-password
+# work. Deliberately skips migrate/seed so one-shot commands don't touch the
+# database.
+if [ "$#" -gt 0 ]; then
+ exec gosu node "$@"
+fi
+
echo "Quidly: applying database migrations..."
gosu node npx prisma migrate deploy
diff --git a/docs/social-preview.html b/docs/social-preview.html
new file mode 100644
index 0000000..ae4dd98
--- /dev/null
+++ b/docs/social-preview.html
@@ -0,0 +1,124 @@
+
+
+