Every commit tells a story. A well-written commit message makes it easy to understand why a change was made, trace regressions, and produce a meaningful changelog. These guidelines define the format for all commits to this repository.
<type>(<scope>): <summary>
[body]
[footer]
The header is required on every commit and must follow the format described below.
The body is required on all commits except docs. When present, it must be at least 20 characters and written in the imperative mood.
The footer is optional. Use it to reference issues, close PRs, or document breaking changes.
<type>(<scope>): <summary>
│ │ │
│ │ └─ imperative mood, lowercase, no period at the end
│ │
│ └─ Scope (optional): auth|books|library|metadata|kobo|opds|reader|
│ collections|annotations|authors|series|cover|
│ users|stats|notifications|settings|scanner|
│ email|audit|smart-scope|types|docker|deps|server|client
│
└─ Type: feat|fix|db|perf|refactor|style|docs|test|build|ci|chore|security|revert
The <type> and <summary> fields are required. The (<scope>) field is optional.
The type signals the intent of the change at a glance.
| Type | When to use |
|---|---|
feat |
Introduces a new feature or user-visible capability |
fix |
Patches a bug in existing behavior |
db |
Adds or modifies a Drizzle schema, generates a migration, or changes seed data |
perf |
Improves performance without changing observable behavior |
refactor |
Restructures code without fixing bugs or adding features |
style |
Visual/CSS-only changes: Tailwind tweaks, spacing, color token adjustments |
docs |
Documentation changes only |
test |
Adds or corrects tests without touching production code |
build |
Changes to the build system, tooling config, or external dependencies (Dockerfile, tsconfig, pnpm) |
ci |
CI/CD pipeline changes: GitHub Actions workflows and scripts |
chore |
Routine maintenance that does not fit any other category |
security |
Addresses a security vulnerability; preferred over fix so security patches are identifiable in history |
revert |
Reverts a previous commit; the body must reference the reverted SHA and explain why |
If a change genuinely spans multiple types (for example, it adds a feature and fixes a bug), split it into two commits. If that is not practical, use the type that best represents the dominant intent.
Scope is optional but strongly encouraged. It narrows the type to the affected area of the codebase, making the log significantly more readable.
Use one scope from the table below. If a change affects multiple areas, use the most dominant one.
| Scope | What it covers |
|---|---|
auth |
Authentication, authorization, JWT, OIDC, sessions |
library |
Library management, file system scanning |
books |
Book data, CRUD, book-level state |
authors |
Author data and management |
series |
Book series grouping and management |
cover |
Book cover image storage, retrieval, and optimization |
metadata |
Metadata providers, fetch logic, quality scoring, preferences |
kobo |
Kobo device sync and integration |
opds |
OPDS catalog endpoints |
reader |
In-app reader, reading position, reader preferences |
collections |
Collections and shelf management |
annotations |
Highlights, bookmarks, notes |
users |
User management, profiles, roles |
stats |
Reading sessions, statistics, progress tracking |
notifications |
In-app and email notifications |
settings |
Application settings and configuration |
scanner |
File scanner and upload pipeline |
email |
Email providers, templates, and send preferences |
audit |
Audit logging and event tracking |
smart-scope |
Custom Smart Scopes and collection views |
types |
Shared types in packages/types/ |
docker |
Dockerfile and Docker Compose files |
deps |
Dependency upgrades (pair with build or chore) |
server |
Backend-wide changes with no single-module scope |
client |
Frontend-wide changes with no single-module scope |
The summary is a short, imperative description of what the commit does.
Rules:
- Use the imperative mood: "add", "fix", "remove" (not "added" or "adds")
- Start with a lowercase letter
- No period at the end
- Keep it under 72 characters
- Describe what the commit does, not why; that belongs in the body
Write a body when the why behind the change is not obvious from the header alone.
When to write a body:
- The motivation is not clear from the summary
- The change has side effects, tradeoffs, or gotchas worth calling out
- You are reverting a commit; reference the SHA and explain the reason
- The approach taken deserves a brief explanation
Format:
- Separate from the header with a blank line
- Use the imperative mood, consistent with the summary
- Wrap lines at ~100 characters
- Explain why this change exists, not what changed; the diff already shows that
Use the footer to link related issues or PRs, or to announce breaking changes.
Closes #123
Fixes: #456
RESOLVED owner/repo#789
Ref #101
GitHub-supported closing keywords are: close, closes, closed, fix, fixes, fixed, resolve, resolves, resolved (case-insensitive; optional colon is allowed, for example Closes: #123).
Use Ref #<issue> for a related issue that should stay open.
Ref is valid for commit footers, but it does not satisfy the PR-description requirement in CI (PR descriptions must include a GitHub closing keyword).
If the commit introduces a breaking change, open the footer with BREAKING CHANGE: followed by a brief summary, a blank line, and clear migration instructions.
BREAKING CHANGE: rename BOOKS_PATH env var to LIBRARY_PATH
Update your .env or Docker environment to use LIBRARY_PATH instead.
The old name is no longer recognized and the app will not start without it.
Breaking changes must also be explicitly called out in the PR description, including the upgrade steps a user would need to follow.
feat(kobo): sync reading position on device reconnect
feat(opds): add pagination to catalog feed endpoints
The OPDS feed now supports ?page= and ?limit= query params.
Default page size is 20. Clients that do not send pagination params
get the same behavior as before.
Closes #301
fix(reader): epub chapter navigation skips last page
fix(stats): reading session duration wrong after timezone change
Duration was computed using local time offsets rather than UTC,
causing sessions that crossed a DST boundary to report incorrect
elapsed time.
Fixes #412
db(books): add language column to books table
db(users): add user_preferences table for per-user app settings
Generates a migration. Existing users get default values on deploy.
perf(library): avoid redundant file stat calls during rescan
refactor(metadata): extract provider selection into its own service
style(reader): tighten chapter list spacing in sidebar
docs: add Kobo device setup guide to README
test(collections): cover concurrent shelf update race condition
build(docker): switch to slim node base image in final stage
ci: add e2e smoke suite to pull request workflow
chore(deps): upgrade drizzle-orm to 0.38
security(auth): reject tokens signed with legacy algorithm
Previous tokens signed with HS256 were still accepted after the
migration to RS256. This change enforces RS256-only validation.
BREAKING CHANGE: all existing sessions will be invalidated on deploy.
Users will need to log in again.
revert: revert "feat(kobo): sync reading position on device reconnect"
Reverts commit a1b2c3d.
Broke sync for devices running firmware < 4.32. Reverting until
a compatibility shim is in place.