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. -![Licence: AGPL-3.0](https://img.shields.io/badge/licence-AGPL--3.0-1f3d30) +[![CI](https://github.com/WarlaxZ/quidly/actions/workflows/ci.yml/badge.svg)](https://github.com/WarlaxZ/quidly/actions/workflows/ci.yml) +[![Latest release](https://img.shields.io/github/v/release/WarlaxZ/quidly?color=1f3d30&label=release)](https://github.com/WarlaxZ/quidly/releases) +[![Container](https://img.shields.io/badge/ghcr.io-warlaxz%2Fquidly-2496ed?logo=docker&logoColor=white)](https://github.com/WarlaxZ/quidly/pkgs/container/quidly) +[![Licence: AGPL-3.0](https://img.shields.io/badge/licence-AGPL--3.0-1f3d30)](LICENSE) ![Next.js 16](https://img.shields.io/badge/Next.js-16-000) ![SQLite](https://img.shields.io/badge/db-SQLite-044a64) -![Tests](https://img.shields.io/badge/tests-261%20passing-3a7d54) -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 @@ + + + + + + + + + +
+
+
+ +
+
+
+
£
+
Quidly
+
+
github.com/WarlaxZ/quidly
+
+ +
+
Self-Assessment, sorted.
+
+
Free, self-hosted bookkeeping & tax for UK landlords.
+
+ +
+
SA105 & PDF export
+
Section 24 relief
+
Multi-property
+
Limited companies
+
Docker
+
+
+
+ + diff --git a/docs/social-preview.png b/docs/social-preview.png new file mode 100644 index 0000000..1440d50 Binary files /dev/null and b/docs/social-preview.png differ diff --git a/package.json b/package.json index f68364e..808f4b0 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "start": "next start", "test": "vitest run", "test:watch": "vitest", + "typecheck": "tsc --noEmit", "db:migrate": "prisma migrate dev", "db:seed": "prisma db seed", "db:studio": "prisma studio", diff --git a/scripts/migrate-akaunting/apply.test.ts b/scripts/migrate-akaunting/apply.test.ts index 3a0cc1a..008afe2 100644 --- a/scripts/migrate-akaunting/apply.test.ts +++ b/scripts/migrate-akaunting/apply.test.ts @@ -2,8 +2,8 @@ import { describe, expect, it, beforeAll, afterAll } from "vitest"; import { PrismaBetterSqlite3 } from "@prisma/adapter-better-sqlite3"; import { PrismaClient } from "@prisma/client"; import Database from "better-sqlite3"; -import { readFileSync, readdirSync, rmSync } from "node:fs"; -import { join } from "node:path"; +import { mkdirSync, readFileSync, readdirSync, rmSync } from "node:fs"; +import { dirname, join } from "node:path"; import { applyPlan } from "./apply"; import type { SourceSnapshot, Mapping } from "./types"; @@ -11,6 +11,9 @@ const TMP = join(process.cwd(), "akaunting-migration", "test-apply.db"); let prisma: PrismaClient; beforeAll(async () => { + // akaunting-migration/ is git-ignored, so it doesn't exist in a fresh clone + // or in CI — better-sqlite3 won't create the parent directory itself. + mkdirSync(dirname(TMP), { recursive: true }); rmSync(TMP, { force: true }); // Build schema by executing every migration SQL in order. const raw = new Database(TMP);