diff --git a/AI_POLICY.md b/AI_POLICY.md new file mode 100644 index 000000000..9af991610 --- /dev/null +++ b/AI_POLICY.md @@ -0,0 +1,73 @@ +# AI Usage Policy + +ProjectX is maintained by real people. Every pull request, issue, and discussion is read and reviewed by humans who give their time to this project. Submitting unreviewed, untested AI output shifts the burden of that review onto maintainers - that is the problem this policy addresses. It is not a problem with AI tools themselves. + +## The Rules + +- **All AI use must be disclosed.** If any part of your contribution was written, generated, or significantly shaped by an AI tool, say so in the PR description. There are no exceptions and no minimum threshold. See [Disclosure Format](#disclosure-format) below. + +- **You must understand every line you submit.** If you cannot explain what a piece of code does, why it is structured the way it is, and how it fits into the surrounding system - do not submit it. "The AI wrote it" is not an answer during review. + +- **AI-generated tests must validate real behavior.** Tests that mock away the logic under test, assert on implementation details instead of outcomes, or trivially pass regardless of what the code does will be rejected. Read every assertion before submitting. + +- **No AI-generated media.** Images, icons, audio, video, and other non-text assets must be human-created or properly licensed. Text and code are the only acceptable forms of AI-generated content, subject to the rules above. + +- **Clean up before submitting.** Remove AI boilerplate, placeholder comments, dead code, and empty catch blocks. Submit only what you would be comfortable having reviewed line by line. + +## Disclosure Format + +Include this in your PR description whenever AI tools were used: + +``` +AI tools used: [tool name, e.g. Claude, Copilot, Cursor] +Extent: [what it helped with, e.g. "generated the DTO scaffolding and test boilerplate; all service logic and ownership checks written manually"] +``` + +Be honest. A brief, accurate note is better than a vague one that obscures how much the AI contributed. + +## Consequences + +PRs that contain undisclosed AI use or that are clearly unreviewed AI output will be closed immediately with a short explanation. There is no detailed feedback given for this - that burden belongs to the contributor, not the reviewer. + +You are always welcome to resubmit once the work is genuinely reviewed, tested, and properly disclosed. The door is not closed permanently. + +## Watch Out for These + +AI tools make specific, recurring mistakes on this codebase. Be especially careful here. + +**Drizzle ORM hallucinations.** LLMs are trained heavily on Prisma and will generate Prisma-style calls - `.findMany()`, `.findUnique()`, `.create()` - that do not exist in Drizzle. Drizzle uses a query builder. Verify every database call against the [Drizzle documentation](https://orm.drizzle.team/docs/overview) and against how the rest of the codebase does it. + +**NestJS decorator and module mistakes.** AI frequently invents decorator options, misuses pipes, omits required module imports, or generates module wiring that looks plausible but is wrong. Check every decorator and every module registration. + +**Ownership and multi-tenancy logic.** ProjectX is a multi-user app. Every query that touches user-owned data must filter by `userId`. AI-generated service methods routinely omit this. A missed ownership check is a data exposure bug, not a style issue. This applies to every module that manages libraries, books, collections, reading sessions, annotations, and preferences. + +**Authorization patterns.** We have specific conventions for permission gates: `@RequirePermission(...)` on the backend, `usePermissions()` on the frontend, and the `LensService` superuser-bypass pattern. AI does not know these patterns and will generate code that bypasses them or reimplements them incorrectly. Write auth and permission logic yourself. + +**Tests that prove nothing.** AI-generated tests often mock the exact thing being tested, assert on shallow output without triggering the real path, or simply confirm that a function returns without throwing. Before submitting a test, ask: would this test fail if the implementation were broken? If the answer is no, the test is not worth having. + +## How to Use AI Well Here + +AI is a genuinely useful tool when applied with judgment. Here is what works well on this project: + +- Use AI to scaffold boilerplate: DTOs, module wiring, Vue component structure, test file setup. Review and adjust everything it produces. +- Use AI to explain unfamiliar patterns, library APIs, or architectural concepts. Understanding-driven use is the best use. +- Write authentication, authorization, and ownership-critical logic yourself. These are the areas where a mistake has real consequences for real users. +- When AI generates a service method, read every line with the question: does this correctly scope to the current user? Does it throw the right exception for unauthorized access? +- Run the full stack after every AI-assisted change. `pnpm dev` plus manual verification is not optional. +- Use AI to generate a test structure, then replace every assertion with one you wrote and understand yourself. + +## A Note on Maintainers + +These rules apply to outside contributors. Maintainers are exempt. They have demonstrated judgment and accountability over time, and are trusted to apply discretion in how they use AI tools in their own work. + +## AI is Welcome Here + +To be clear: this policy is not an anti-AI stance. Maintainers use AI tools. Many parts of this codebase were written with AI assistance. + +The problem this policy addresses is unqualified, unreviewed output being submitted as a contribution - work that the contributor has not run, does not understand, and cannot stand behind. That is a burden on maintainers regardless of whether it came from a human or a machine. + +Use the tools. Do the work. + +--- + +_This policy was written with reference to the [Ghostty AI Policy](https://github.com/ghostty-org/ghostty/blob/main/AI_POLICY.md), which we found thoughtful and worth building from._ diff --git a/COMMIT_GUIDELINES.md b/COMMIT_GUIDELINES.md new file mode 100644 index 000000000..bb2563f68 --- /dev/null +++ b/COMMIT_GUIDELINES.md @@ -0,0 +1,274 @@ +# Commit Message Guidelines + +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. + +## Format + +``` +(): + +[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 imperative mood. + +The **footer** is optional. Use it to reference issues, close PRs, or document breaking changes. + +--- + +## Header + +``` +(): + │ │ │ + │ │ └─ 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|lens|types|docker|deps|server|client + │ + └─ Type: feat|fix|db|perf|refactor|style|docs|test|build|ci|chore|security|revert +``` + +The `` and `` fields are required. The `()` field is optional. + +### Type + +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 doesn't 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 isn't practical, use the type that best represents the dominant intent. + +### Scope + +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 | +| `lens` | Custom lenses 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 | + +### Summary + +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 + +--- + +## Body + +Write a body when the **why** behind the change isn't obvious from the header alone. + +When to write a body: + +- The motivation isn't clear from the summary +- The change has side effects, tradeoffs, or gotchas worth calling out +- You're 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 + +--- + +## Footer + +Use the footer to link related issues or PRs, or to announce breaking changes. + +### Linking issues and PRs + +``` +Fixes #123 +Closes #456 +Ref #789 +``` + +`Fixes` and `Closes` automatically close the linked issue when the PR is merged. Use `Ref` for a related issue that should stay open. + +### Breaking changes + +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. + +--- + +## Examples + +### `feat` + +``` +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 don't send pagination params +get the same behavior as before. + +Closes #301 +``` + +### `fix` + +``` +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` + +``` +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` + +``` +perf(library): avoid redundant file stat calls during rescan +``` + +### `refactor` + +``` +refactor(metadata): extract provider selection into its own service +``` + +### `style` + +``` +style(reader): tighten chapter list spacing in sidebar +``` + +### `docs` + +``` +docs: add Kobo device setup guide to README +``` + +### `test` + +``` +test(collections): cover concurrent shelf update race condition +``` + +### `build` + +``` +build(docker): switch to slim node base image in final stage +``` + +### `ci` + +``` +ci: add e2e smoke suite to pull request workflow +``` + +### `chore` + +``` +chore(deps): upgrade drizzle-orm to 0.38 +``` + +### `security` + +``` +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: 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. +``` diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 38dd648b8..33d2b684e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,76 +1,66 @@ # Contributing to ProjectX -Thanks for your interest in contributing! Whether you're fixing a bug, suggesting a feature, or improving docs, every contribution is appreciated. +Thanks for your interest in contributing to ProjectX. Whether you are fixing a typo or building a major feature, this guide walks you through the process from start to finish. -## What is ProjectX? +**Companion docs:** -ProjectX is a self-hostable digital library for managing and reading books and comics, with first-class Kobo device support. - -**Tech stack:** - -- **Frontend:** Vue 3, TypeScript, Tailwind CSS -- **Backend:** NestJS, Fastify, Drizzle ORM -- **Database:** PostgreSQL -- **Deployment:** Docker - ---- - -## Project Structure - -``` -projectx/ -├── client/ # Vue 3 frontend -├── server/ # NestJS backend -│ └── src/ -│ ├── modules/ # Feature modules (one per domain) -│ ├── db/ # Drizzle schema, migrations -│ └── common/ # Guards, filters, decorators -└── packages/ - └── types/ # Shared types between client and server -``` - -> **Important:** Types shared between the frontend and backend live in `packages/types/` and are imported via the `@projectx/types` alias. Never duplicate shared types in both workspaces. Always add them here. - ---- - -## Before You Start - -> **Issue first, PR second.** Every pull request must be linked to an approved issue. If you want to work on something, open an issue and wait for a maintainer to explicitly approve it before writing any code. A thumbs up reaction or a comment saying "go ahead" on the issue counts as approval. PRs submitted without a linked, approved issue will be closed. - -This protects your time and ours: it ensures the work is wanted and you're heading in the right direction before investing effort. - -To set your PR up for success, make sure: - -- It is linked to an approved issue -- It includes proof of manual testing (screenshots, screen recording, or test output) -- It contains a single focused change, no bundled unrelated work -- It is reasonably sized. PRs with 1000+ changed lines are hard to review. Split them up. -- No new dependencies are added without prior discussion - -If you're unsure whether something fits the project's direction, open an issue and ask before building it. +- [DEVELOPMENT.md](DEVELOPMENT.md) - Local setup, architecture, commands, and technical reference. +- [TESTING.md](TESTING.md) - Test architecture, E2E suites, coverage thresholds, and harness details. +- [COMMIT_GUIDELINES.md](COMMIT_GUIDELINES.md) - Commit message format, types, scopes, and examples. --- ## Where to Start -Not sure where to begin? Look for issues labeled: +Not sure what to work on? Look for issues tagged with: -- [`good first issue`](../../labels/good%20first%20issue) - small, well-scoped tasks ideal for newcomers -- [`help wanted`](../../labels/help%20wanted) - tasks where a contribution would be especially welcome +- [`good first issue`](../../labels/good%20first%20issue) - Scoped tasks with clear acceptance criteria. Great for your first contribution. +- [`help wanted`](../../labels/help%20wanted) - Larger tasks where maintainer bandwidth is limited. + +If nothing there catches your eye, browse the open issues or propose something new. + +### Documentation Contributions + +Doc improvements are always welcome and have a lighter process: + +- No tests required. +- Use the `docs/` branch prefix (e.g. `docs/fix-setup-instructions`). +- Focus areas: fixing inaccuracies, improving examples, adding missing guides, clarifying confusing sections. + +The rest of this guide still applies (issue first, PR template, etc.), but expect a faster review cycle. --- ## Security Issues -Please do not open public issues for security vulnerabilities. See [SECURITY.md](.github/SECURITY.md) for how to report them privately. +Do **not** open public issues for security vulnerabilities. Use GitHub's [private vulnerability reporting](../../security/advisories/new) instead. See [SECURITY.md](.github/SECURITY.md) for details. --- -## Getting Started +## Contribution Workflow -### Fork and Clone +This is the end-to-end journey from idea to merged PR. Each phase builds on the previous one. -[Fork the repository](../../fork) on GitHub, then clone your fork locally: +### Phase 1: Find or Create an Issue + +> **Issue first, PR second.** Every pull request must link to an approved issue. PRs without one will be closed. + +- Browse [open issues](../../issues) for something you want to tackle. +- If you have a new idea, open one using the [bug report](../../issues/new?template=bug_report.yml) or [feature request](../../issues/new?template=feature_request.yml) template. +- Not sure if your idea fits the roadmap? Ask in a new issue before investing time. + +### Phase 2: Get Maintainer Approval + +Wait for explicit approval before writing code. Approval looks like: + +- A :+1: reaction on your issue. +- A comment such as "go ahead", "approved", or "let's do it". + +This step prevents wasted effort on changes that conflict with the project direction. Small bug fixes with clear reproduction are usually approved quickly. + +### Phase 3: Fork and Clone + +[Fork the repository](../../fork), then clone your fork: ```bash git clone https://github.com//projectx.git @@ -78,9 +68,127 @@ cd projectx git remote add upstream https://github.com/neonsolstice/projectx.git ``` -### Keep Your Fork in Sync +> **Trusted contributors** with direct push access can skip the fork and work on branches in the main repository. The rest of this guide assumes the fork workflow. -Before starting new work, pull the latest changes from upstream: +### Phase 4: Set Up Your Environment + +If this is your first time: + +```bash +pnpm setup +``` + +This handles everything: dependencies, PostgreSQL, migrations, and seed data. See [DEVELOPMENT.md](DEVELOPMENT.md) for prerequisites, manual setup steps, and troubleshooting. + +### Phase 5: Create Your Branch + +Always branch from the latest `main`: + +```bash +git fetch upstream +git checkout -b / upstream/main +``` + +Use these branch prefixes: + +| Prefix | Use for | Example | +| ----------- | -------------------------------------- | -------------------------------- | +| `feat/` | New features | `feat/reading-progress-sync` | +| `fix/` | Bug fixes | `fix/pdf-page-count-off-by-one` | +| `refactor/` | Code restructuring | `refactor/library-scan-pipeline` | +| `docs/` | Documentation | `docs/opds-setup-guide` | +| `chore/` | Maintenance (deps, tooling, CI, build) | `chore/bump-drizzle-v1` | + +### Phase 6: Implement Your Change + +Start the dev server and get to work: + +```bash +pnpm dev +``` + +Refer to [DEVELOPMENT.md](DEVELOPMENT.md) for architecture details, project structure, database workflows, and conventions. A few ground rules: + +- **One logical change per PR.** Do not bundle a bug fix with a refactor or unrelated cleanup. +- **No unapproved dependencies.** Propose new dependencies in the linked issue first (see [Adding Dependencies](#adding-dependencies)). +- **Keep the scope tight.** If you discover something unrelated that needs fixing, open a separate issue for it. + +### Phase 7: Write and Run Tests + +Testing expectations depend on what you changed: + +| Change type | What is expected | +| ----------------------- | ----------------------------------------------- | +| Bug fix | Regression test proving the bug is fixed | +| New backend feature/API | Server unit tests for the new behavior | +| New frontend logic | Client unit tests (composables, utilities) | +| UI-only change | Manual verification evidence (screenshot/video) | +| Refactor | Existing tests stay green, no new tests needed | + +Run the full test suite: + +```bash +pnpm test +``` + +If your change touches an area covered by an existing E2E suite, run it locally before pushing: `pnpm run e2e:run -- `. Use `pnpm run e2e:list` to see available suites. + +For E2E tests, suite IDs, coverage thresholds, and the test harness, see [TESTING.md](TESTING.md). + +### Phase 8: Verify Code Quality + +The project has two automated gates that catch issues before they reach CI: + +- **Pre-commit hook** runs `lint-staged` on your staged files (ESLint + Prettier). +- **Pre-push hook** runs `pnpm verify:fast` (lint + typecheck + tests). Your push is blocked if this fails. + +Before marking your PR ready for review, run the full quality gate yourself: + +```bash +pnpm verify +``` + +See the [Code Quality](DEVELOPMENT.md#code-quality) section in DEVELOPMENT.md for individual commands. + +### Phase 9: Commit Your Work + +Follow the format in [COMMIT_GUIDELINES.md](COMMIT_GUIDELINES.md). The short version: + +``` +(): +``` + +- **Type:** `feat`, `fix`, `db`, `perf`, `refactor`, `style`, `docs`, `test`, `build`, `ci`, `chore`, `security`, `revert` +- **Scope** (optional): `auth`, `books`, `library`, `metadata`, `kobo`, `scanner`, etc. +- **Summary:** Imperative mood, lowercase, no period. Describe what the commit does, not what you did. + +Examples: `feat(kobo): add shelf sync endpoint`, `fix(reader): correct page count for multi-volume PDFs` + +### Phase 10: Open a Pull Request + +Push your branch and open a PR against `main`: + +```bash +git push origin feat/your-feature-name +``` + +When creating the PR: + +- **Fill out the PR template.** It asks for a summary, testing evidence, screenshots (if UI changed), and non-obvious decisions. +- **Link your issue** with `Closes #123` in the description. +- **Open as a draft** if you want early direction feedback before the code is complete. +- **Disclose AI usage** if applicable. See [AI_POLICY.md](AI_POLICY.md) for the format and expectations. + +### Phase 11: Respond to Review + +- Expect feedback within a few days, sometimes sooner. +- Push additional commits to address review comments. Do not force-push during review unless a reviewer asks you to. +- Do not resolve review threads yourself. Let the reviewer confirm the fix. +- If feedback is unclear, ask for clarification in the thread. + +### Phase 12: After Merge + +Your changes ship in a future release when maintainers cut one. Keep your fork in sync for your next contribution: ```bash git fetch upstream @@ -91,167 +199,49 @@ git push origin main --- -## Development Setup +## Quick-Reference Checklist -### Prerequisites - -- Node.js >= 24 -- pnpm >= 9 -- Docker - -### First-time setup - -```bash -docker compose up -d # start PostgreSQL -cp server/.env.example server/.env -pnpm install -cd server && pnpm db:migrate -``` - -### Daily workflow - -```bash -pnpm dev # start server + client -pnpm verify # run all checks before pushing -``` - -Other useful commands: - -```bash -pnpm verify:fast # lint + typecheck + tests (pre-push gate) -pnpm db:reset # wipe and reseed local database -pnpm e2e:list # list available e2e suite ids -``` - ---- - -## Branch Naming - -Create branches from `main` using these prefixes: - -| Prefix | Use for | Example | -| ----------- | ------------------ | -------------------------------- | -| `feat/` | New features | `feat/reading-progress-sync` | -| `fix/` | Bug fixes | `fix/pdf-page-count-off-by-one` | -| `refactor/` | Code restructuring | `refactor/library-scan-pipeline` | -| `docs/` | Documentation | `docs/opds-setup-guide` | - ---- - -## Commit Messages - -We follow [Conventional Commits](https://www.conventionalcommits.org/): - -``` -(): -``` - -**Types:** `feat`, `fix`, `docs`, `refactor`, `test`, `chore` - -Examples: - -``` -feat(library): add bulk metadata refresh for selected books -fix(cbz): handle archives with no image files gracefully -feat(opds): add pagination support to feed endpoints -fix(stats): reading session duration calculation off after timezone change -chore(deps): upgrade drizzle-orm to 0.31 -test(shelf): add coverage for concurrent shelf update race condition -``` - ---- - -## Testing Expectations - -`pnpm verify` must pass, but that is the minimum, not the goal. Here is what is expected per change type: - -| Change type | Expected tests | -| ------------------- | -------------------------------------------------------------------------- | -| Bug fix | A regression test that would have caught the bug | -| New backend feature | Unit tests for the service, covering the main logic paths | -| New API endpoint | At minimum, a test for the happy path and error cases | -| New composable | Unit tests covering the reactive logic | -| UI-only change | Manual testing with screenshot evidence in the PR | -| Refactor | Existing tests must continue to pass. Add none if coverage stays the same. | - -AI-generated tests deserve extra scrutiny. A test that always passes regardless of the implementation is worse than no test at all. - ---- - -## Adding Dependencies - -Do not add new dependencies without prior discussion in the linked issue. Every new dependency is a security surface, a bundle size cost, and a long-term maintenance burden. - -If a dependency is needed, explain in the issue: what it does, why an existing dependency or a small custom implementation won't work, and how actively it is maintained. - ---- - -## Breaking Changes and Self-Hosted Upgrades - -ProjectX users upgrade by pulling a new Docker image. A change that breaks an existing deployment without a documented upgrade path will not be merged until that path is clearly defined. - -If your PR introduces a breaking change (schema migration, removed API field, changed env var, modified config format): - -- Call it out explicitly in the PR description -- Include the upgrade steps a user would need to follow -- For schema changes, make sure a Drizzle migration is included. Never hand-write SQL. - ---- - -## Submitting a Pull Request - -Use a **draft PR** early if you want feedback on direction before the work is done. It signals the PR is not ready for review and avoids back-and-forth on a finished implementation that needs to be rethought. - -The project has two automatic gates you should be aware of: - -- **pre-commit:** runs `lint-staged` on your staged files. ESLint and Prettier are applied automatically. You do not need to format or lint manually before committing. -- **pre-push:** runs `pnpm verify:fast` (lint + typecheck + tests). If this fails, the push is blocked. Fix the errors before pushing. - -`pnpm verify:fast` is a fast check but does not include the `app-smoke` e2e suite. Before opening a PR, run the full gate: - -```bash -pnpm verify -``` - -Before marking your PR ready for review: +Before marking your PR ready for review, confirm: - [ ] Linked to an approved issue -- [ ] `pnpm verify` passes locally (includes `app-smoke`, stricter than pre-push) -- [ ] You ran the full stack and manually verified the change works -- [ ] UI changes include a screenshot or screen recording -- [ ] Tests are included per the expectations above -- [ ] No new dependencies added without prior discussion - [ ] Branch is up to date with `main` -- [ ] The PR contains a single logical change - -When you open your PR, a template will appear. Fill it out completely. - -**Responding to feedback:** Address all review comments before asking for a re-review. Don't just resolve threads without fixing the underlying issue. PRs with no activity for 30 days after a review request will be closed. You are welcome to reopen when ready. +- [ ] One logical change per PR +- [ ] `pnpm verify` passes locally +- [ ] Tests included per the [testing expectations table](#phase-7-write-and-run-tests) +- [ ] Full-stack behavior manually validated +- [ ] UI changes include screenshot or recording +- [ ] PR template fully completed +- [ ] No unintended files (build artifacts, `.env`, personal configs) +- [ ] No unapproved new dependencies --- -## AI-Assisted Contributions +## Policies -Contributions using AI tools are welcome, but the quality bar is the same as human-written code. **If you ship it, you own it.** +### Adding Dependencies -PRs where the contributor clearly never ran the code, didn't test it, or can't explain what it does make review very difficult and will be closed without detailed feedback. +Do not add dependencies without prior discussion in the linked issue. If proposing one, document: -**If you use AI to help write code, you must still:** +- What problem it solves. +- Why existing dependencies or a small custom implementation are insufficient. +- Maintenance status and community activity of the package. -- **Run the code yourself.** Start the full stack and manually verify the change works. Trusting AI output without running it is not acceptable. -- **Review every line.** You must be able to explain any part of your change during review. "The AI suggested it" is not an answer. -- **Keep PRs focused.** One feature or fix per PR. Do not submit a dump of everything the AI generated. -- **Scrutinize AI-generated tests.** They often pass trivially without asserting anything meaningful. Tests that don't validate real behavior will be rejected. -- **Clean up.** Remove dead code, placeholder comments, empty catch blocks, and unnecessary boilerplate. +### Breaking Changes and Self-Hosted Upgrades ---- +ProjectX is self-hosted. Any change that can break existing deployments must include a clear upgrade path before merge. -## What Happens After Merge +If your PR introduces a breaking change (schema, API contract, environment variable, config format): -Merged changes ship with the next release. There is no fixed release cadence. Releases happen when enough meaningful changes have accumulated. If your fix is urgent, mention it in the PR and it can be prioritized. +- Call it out explicitly in the PR description. +- Document the required upgrade steps. +- Include Drizzle migrations for schema changes. Never hand-write migration SQL. + +### AI-Assisted Contributions + +See [AI_POLICY.md](AI_POLICY.md). The short version: disclose all AI usage in your PR description and make sure you understand every line of the diff. --- ## License -By contributing, you agree that your contributions will be licensed under the same license as this project. +By contributing, you agree that your contributions are licensed under the same license as the project (AGPL-3.0). diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 325029a33..c3e24b272 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -1,31 +1,171 @@ # Development Guide -## Prerequisites +Everything you need to work in this codebase: architecture, setup, workflows, conventions, and commands. -- Node.js >= 24 -- pnpm >= 9 -- Docker +For the contribution process (issues, PRs, review), see [CONTRIBUTING.md](CONTRIBUTING.md). For test architecture and E2E suite details, see [TESTING.md](TESTING.md). --- -## First-time Setup +## Architecture Overview + +ProjectX is a pnpm monorepo with three workspaces: a Vue 3 single-page app, a NestJS API server, and a shared types package. + +```mermaid +graph LR + Client["Client
Vue 3 SPA
localhost:5173"] + Server["Server
NestJS + Fastify
localhost:3000/api"] + DB["PostgreSQL
Drizzle ORM"] + WS["WebSocket
Socket.IO"] + Types["Shared Types
@projectx/types"] + + Client -- "HTTP (fetch)" --> Server + Client <-. "real-time events" .-> WS + Server --> DB + Server --> WS + Types -.- Client + Types -.- Server +``` + +| Layer | Tech | Location | +| ------------ | ------------------------------- | ----------------- | +| Frontend | Vue 3, Tailwind CSS v4, Vite | `client/` | +| Backend | NestJS 11, Fastify, Drizzle ORM | `server/` | +| Database | PostgreSQL 16, pgvector | Docker container | +| Shared types | TypeScript | `packages/types/` | +| Real-time | Socket.IO | Server + Client | + +--- + +## Project Structure + +```text +projectx/ +├── client/ # Vue 3 frontend +│ └── src/ +│ ├── features/ # Feature modules (composables + components) +│ ├── components/ui/ # Shared UI components +│ ├── lib/ # API client, utilities +│ ├── router/ # Vue Router config +│ └── assets/ # Tailwind theme, static assets +├── server/ # NestJS backend +│ └── src/ +│ ├── modules/ # Feature modules (controller + service + dto) +│ ├── db/ +│ │ ├── schema/ # Drizzle table definitions (one file per domain) +│ │ └── migrations/ # Generated SQL migrations +│ ├── common/ # Guards, filters, decorators, pipes +│ └── config/ # Typed config (app, db, auth) +│ └── test/ +│ ├── e2e/ # E2E test helpers and harness +│ └── *.e2e-spec.ts # E2E test suites +├── packages/ +│ └── types/ # Shared TypeScript types +│ └── src/ # Exported via @projectx/types +├── scripts/ # Bootstrap, DB reset, E2E orchestration +├── docker/ # Postgres init scripts (extensions) +├── .github/ # CI workflows, PR/issue templates +└── .husky/ # Git hooks (pre-commit, pre-push) +``` + +> **Shared types** live in `packages/types/` and are imported as `@projectx/types`. Never duplicate types between client and server. + +--- + +## Prerequisites + +- **Node.js** >= 24 - [nodejs.org](https://nodejs.org) +- **pnpm** >= 9 - `npm install -g pnpm` (or see [pnpm.io](https://pnpm.io/installation)) +- **Docker** - [docs.docker.com/get-started/get-docker](https://docs.docker.com/get-started/get-docker/) + +--- + +## First-Time Setup + +### Option A: One Command ```bash pnpm setup ``` -This creates `server/.env` from the example, installs all dependencies, starts PostgreSQL, runs migrations, and seeds baseline data. +This runs all the steps below automatically. If it succeeds, skip to [Running the App](#running-the-app). -If you prefer to do it manually: +--- + +### Option B: Step by Step + +#### Step 1 - Clone the repository + +```bash +git clone https://github.com//projectx.git +cd projectx +``` + +If you are an external contributor, fork first and clone your fork. See [CONTRIBUTING.md](CONTRIBUTING.md) for details. + +#### Step 2 - Create your environment file + +```bash +cp server/.env.example server/.env +``` + +Only do this once. If `server/.env` already exists, skip this step. Copying again will overwrite any local changes you have made. + +The defaults in `.env.example` work out of the box for local development. You only need to edit this file if you want to change ports, point to a different database, or configure optional features like email. + +See the [Key Environment Variables](#key-environment-variables) section for what each variable does. + +#### Step 3 - Install dependencies ```bash -pnpm run db:up # start PostgreSQL -cp server/.env.example server/.env # first time only pnpm install +``` + +This installs dependencies for all three workspaces (server, client, packages/types) in one shot. + +#### Step 4 - Start PostgreSQL + +```bash +docker compose -f docker-compose.dev.yml up -d --wait +``` + +This starts a PostgreSQL 16 container (with the `pgvector` extension) on port 5432. The `--wait` flag blocks until the database is healthy. Data is persisted in a named Docker volume across restarts. + +> **Already have PostgreSQL running?** You can point `DATABASE_URL` in `server/.env` at your existing instance, but you will still need the `uuid-ossp`, `pg_trgm`, and `vector` extensions installed. Using Docker is the easiest path. + +#### Step 5 - Apply database migrations + +```bash pnpm run db:migrate +``` + +This creates all tables and indexes. Migrations are generated by Drizzle Kit and live in `server/src/db/migrations/`. + +#### Step 6 - Seed baseline data + +```bash pnpm run db:seed ``` +This populates the database with the data required for the app to function (default roles, permissions, initial settings). You only need to run this once. + +#### Step 7 - Start the app + +```bash +pnpm dev +``` + +If everything worked, you will see the server, client, and types watcher start up. The app is ready when you see output from all three processes. + +--- + +### Verifying Your Setup + +| Check | Expected | +| ---------------------------------------- | ------------------------------------------- | +| Open http://localhost:5173 | ProjectX login page loads | +| Open http://localhost:3000/api/v1/health | Returns `{"status":"ok"}` | +| `docker ps` | Shows `projectx-postgres` container running | + --- ## Running the App @@ -34,226 +174,101 @@ pnpm run db:seed pnpm dev ``` -Starts the server and client concurrently with hot reload. +This starts the server, client, and types watcher concurrently with hot reload. | Service | URL | | ------- | ------------------------- | -| API | http://localhost:3000/api | | Client | http://localhost:5173 | +| API | http://localhost:3000/api | -If PostgreSQL stopped (e.g. after a machine restart): +If PostgreSQL is not running, start it first with `pnpm run db:up`. + +
+Running workspaces separately ```bash -pnpm run db:up # then pnpm dev +# Terminal 1: Server +cd server && pnpm start:dev + +# Terminal 2: Client +cd client && pnpm dev ``` +If you are editing shared types (`packages/types/`), also start the types watcher: + +```bash +# Terminal 3: Types (only needed when editing packages/types/) +pnpm --filter @projectx/types dev +``` + +
+ --- -## Database Workflows +## Key Environment Variables -### Apply pending migrations +These are the variables you are most likely to need during development. See `server/.env.example` for the full list with comments. -After pulling changes that include new migrations: +| Variable | Purpose | Default (dev) | +| --------------- | ---------------------------------- | ------------------------------------------------------ | +| `DATABASE_URL` | PostgreSQL connection string | `postgres://projectx:projectx@localhost:5432/projectx` | +| `PORT` | Server listen port | `3000` | +| `NODE_ENV` | Runtime mode | `development` | +| `JWT_SECRET` | Signing key for auth tokens | `change-me-in-production` | +| `APP_DATA_PATH` | Storage for covers, avatars, cache | `../local/data` | +| `APP_URL` | Base URL for email links | `http://localhost:5173` | + +--- + +## Database + +### Schema Organization + +Database tables are defined as Drizzle `pgTable()` calls in `server/src/db/schema/`, one file per domain (e.g. `books.ts`, `auth.ts`, `kobo.ts`). All files are re-exported from `server/src/db/schema/index.ts`. + +Type inference uses `typeof table.$inferSelect` and `$inferInsert`. Never write manual type aliases for DB rows. + +### Common Commands + +| Command | What it does | +| -------------------------------------- | ----------------------------------------------- | +| `pnpm run db:up` | Start PostgreSQL via Docker | +| `pnpm run db:migrate` | Apply pending migrations | +| `cd server && pnpm db:generate ` | Generate a migration from schema changes | +| `pnpm run db:seed` | Seed baseline data | +| `pnpm run db:reset` | Drop everything, re-migrate, re-seed (dev only) | +| `cd server && pnpm db:studio` | Open Drizzle Studio (visual DB browser) | + +### Making a Schema Change + +1. Edit the relevant file in `server/src/db/schema/`. +2. Generate a migration: `cd server && pnpm db:generate describe-your-change` +3. Review the generated SQL in `server/src/db/migrations/`. +4. Apply it: `pnpm run db:migrate` + +Never hand-write migration SQL. Always let Drizzle Kit generate it from schema diffs. + +### Migration Strategy + +**Before production launch (current phase):** We maintain a single baseline migration (`0000_*.sql`). When adding new tables or columns, wipe the migrations folder and regenerate: ```bash -pnpm run db:migrate +rm -rf server/src/db/migrations +docker compose -f docker-compose.dev.yml down -v && docker compose -f docker-compose.dev.yml up -d --wait +cd server && pnpm db:generate baseline && pnpm run db:migrate ``` -### Add a schema change +**After production launch:** Never wipe migrations. Every schema change gets its own incremental migration. -1. Edit the relevant file in `server/src/db/schema/` -2. Generate a migration: - ```bash - cd server && pnpm db:generate add-book-tags - ``` - This creates a new SQL file in `server/src/db/migrations/`. Review it before applying. -3. Apply it: - ```bash - pnpm run db:migrate - ``` +### Full Reset -> **Pre-launch rule:** Before the app ships, maintain a single baseline migration (`0000_*.sql`) rather than stacking incremental ones. When adding a schema change pre-launch: -> -> ```bash -> rm -rf server/src/db/migrations -> docker compose -f docker-compose.dev.yml down -v -> pnpm run db:up -> cd server && pnpm db:generate baseline -> pnpm run db:migrate -> ``` -> -> After go-live, never wipe migrations. Every change gets its own incremental migration. - -### Full database reset - -Wipes all data, re-runs migrations, re-seeds, and clears generated files (covers, author images): +When local migration state is broken or you need a clean slate: ```bash pnpm run db:reset ``` -Use this when: - -- Migrations are in a broken or inconsistent state -- A migration requires starting from scratch (e.g. column type change with no clean upgrade path) -- You want a known-clean local state - -### Inspect the database - -```bash -cd server && pnpm db:studio -``` - -Opens Drizzle Studio in your browser for browsing and editing tables directly. - ---- - -## Testing - -```bash -pnpm run test # all tests (server + client) -pnpm run test:server # server unit tests only -pnpm run test:client # client unit tests only -pnpm run e2e:run -- all # run all e2e suites sequentially -pnpm run e2e:run -- app-smoke # app smoke suite (default DB) -pnpm run e2e:run -- scanner-scenarios # scanner scenario suite (dedicated projectx_e2e DB) -pnpm run e2e:run -- scanner-file-operations # scanner file operation suite (dedicated projectx_e2e DB) -pnpm run e2e:run -- auth-session-security # auth session security suite (dedicated projectx_e2e DB) -pnpm run e2e:run -- auth-recovery-oidc-logout # auth recovery and OIDC logout suite (dedicated projectx_e2e DB) -pnpm run e2e:run -- book-bucket-ingest-finalize # Book Bucket ingest/finalize suite (dedicated projectx_e2e DB) -pnpm run e2e:run -- metadata-write # metadata write suite (dedicated projectx_e2e DB) -pnpm run e2e:run -- metadata-lock # metadata lock suite (dedicated projectx_e2e DB) -pnpm run e2e:run -- authorization-matrix # authorization matrix suite (dedicated projectx_e2e DB) -pnpm run e2e:run -- opds-auth-catalog # OPDS auth/catalog suite (dedicated projectx_e2e DB) -pnpm run e2e:run -- reader-state-isolation # reader state isolation suite (dedicated projectx_e2e DB) -pnpm run e2e:run -- email-lifecycle # email lifecycle suite (dedicated projectx_e2e DB) -pnpm run e2e:run -- users-admin-lifecycle # users admin lifecycle suite (dedicated projectx_e2e DB) -pnpm run e2e:all # shortcut for running all suites -pnpm run e2e:list # list supported suite ids -``` - -Watch mode while working on a specific area: - -```bash -cd server && pnpm test:watch -``` - -Run a focused scanner scenario: - -```bash -pnpm run e2e:run -- scanner-scenarios --testNamePattern=book-per-folder-disc-folder-flattening -``` - -### E2E runner architecture - -- All suite metadata lives in `scripts/e2e/suite-registry.mjs` (suite id, spec file, JUnit output, DB prep mode). -- Local and CI use the same command: `pnpm run e2e:run -- `. -- Use `all` as a composite suite id to run every configured suite in order, or use `pnpm run e2e:all`. -- Dedicated-db suites auto-start local PostgreSQL when `CI != true`, then reset and migrate the e2e database through `pnpm run e2e:db:prepare`. -- JUnit outputs are written to `test-results/server/`. - -### E2E suite details - -All suites write JUnit XML to `test-results/server/`. Dedicated-db suites also auto-start PostgreSQL locally (when needed) and use the `projectx_e2e` database. - -| Suite id | DB mode | JUnit output | Extra artifacts | -| ----------------------------- | ------------ | ------------------------------------------- | ------------------------------------------------ | -| `app-smoke` | default-db | `app-smoke-e2e-junit.xml` | none | -| `scanner-scenarios` | dedicated-db | `scanner-scenarios-e2e-junit.xml` | `scanner-scenarios-e2e-scenarios.json` | -| `scanner-file-operations` | dedicated-db | `scanner-file-operations-e2e-junit.xml` | `scanner-file-operations-e2e-scenarios.json` | -| `auth-session-security` | dedicated-db | `auth-session-security-e2e-junit.xml` | none | -| `auth-recovery-oidc-logout` | dedicated-db | `auth-recovery-oidc-logout-e2e-junit.xml` | none | -| `book-bucket-ingest-finalize` | dedicated-db | `book-bucket-ingest-finalize-e2e-junit.xml` | `book-bucket-ingest-finalize-e2e-scenarios.json` | -| `metadata-write` | dedicated-db | `metadata-write-e2e-junit.xml` | `metadata-write-e2e-scenarios.json` | -| `metadata-lock` | dedicated-db | `metadata-lock-e2e-junit.xml` | none | -| `authorization-matrix` | dedicated-db | `authorization-matrix-e2e-junit.xml` | none | -| `opds-auth-catalog` | dedicated-db | `opds-auth-catalog-e2e-junit.xml` | `opds-auth-catalog-e2e-scenarios.json` | -| `reader-state-isolation` | dedicated-db | `reader-state-isolation-e2e-junit.xml` | `reader-state-isolation-e2e-scenarios.json` | -| `email-lifecycle` | dedicated-db | `email-lifecycle-e2e-junit.xml` | `email-lifecycle-e2e-scenarios.json` | -| `users-admin-lifecycle` | dedicated-db | `users-admin-lifecycle-e2e-junit.xml` | `users-admin-lifecycle-e2e-scenarios.json` | - -### E2E in CI (how to trigger) - -All suite workflows call the reusable `E2E Runner (reusable)` workflow, which runs: - -```bash -pnpm run e2e:run -- -``` - -| Workflow name | Suite id | Triggered by | -| ----------------------------------- | ----------------------------- | --------------------------------------------------------------------------- | -| `E2E - App Smoke` | `app-smoke` | `workflow_dispatch`, `push`, `pull_request` | -| `E2E - Scanner Scenarios` | `scanner-scenarios` | `workflow_dispatch`, `push`, `pull_request` | -| `E2E - Scanner File Operations` | `scanner-file-operations` | `workflow_dispatch`, nightly schedule (`0 4 * * *`), `push`, `pull_request` | -| `E2E - Auth Session Security` | `auth-session-security` | `workflow_dispatch`, `push`, `pull_request` | -| `E2E - Auth Recovery OIDC Logout` | `auth-recovery-oidc-logout` | `workflow_dispatch`, `push`, `pull_request` | -| `E2E - Book Bucket Ingest Finalize` | `book-bucket-ingest-finalize` | `workflow_dispatch`, `push`, `pull_request` | -| `E2E - Metadata Write` | `metadata-write` | `workflow_dispatch`, `push`, `pull_request` | -| `E2E - Metadata Lock` | `metadata-lock` | `workflow_dispatch`, `push`, `pull_request` | -| `E2E - Authorization Matrix` | `authorization-matrix` | `workflow_dispatch`, `push`, `pull_request` | -| `E2E - OPDS Auth Catalog` | `opds-auth-catalog` | `workflow_dispatch`, `push`, `pull_request` | -| `E2E - Reader State Isolation` | `reader-state-isolation` | `workflow_dispatch`, `push`, `pull_request` | -| `E2E - Email Lifecycle` | `email-lifecycle` | `workflow_dispatch`, `push`, `pull_request` | -| `E2E - Users Admin Lifecycle` | `users-admin-lifecycle` | `workflow_dispatch`, `push`, `pull_request` | - -Manual trigger steps: - -1. Open GitHub Actions. -2. Select any `E2E - ...` workflow for the suite you want to run. -3. Click **Run workflow**. - -Each run uploads `test-results/server/` as an artifact and publishes JUnit annotations from `test-results/server/*-e2e-junit.xml`. - ---- - -## Code Quality - -Run before pushing: - -```bash -pnpm run verify:fast # lint + typecheck + tests (same as pre-push hook) -pnpm run verify # above + e2e app-smoke suite (run before opening a PR) -``` - -Individual checks: - -```bash -pnpm run lint:check # check for lint errors -pnpm run lint:fix # auto-fix lint errors -pnpm run typecheck # server + client baseline typecheck -pnpm run typecheck:full # server + full client typecheck (slower) -``` - -Format: - -```bash -cd server && npx prettier --write . -cd client && npx prettier --write . -``` - ---- - -## Common Scenarios - -### Pulled changes that include a new migration - -```bash -pnpm run db:migrate -``` - -### Pulled changes that changed the DB schema significantly and your local state is stale - -```bash -pnpm run db:reset -``` - -### Something is wrong and you want a completely clean slate - -```bash -pnpm run db:reset -``` - -If that is not enough (e.g. Docker volume is corrupted): +If that is insufficient (e.g. corrupted Docker volumes), stop the container with `-v` to drop volumes, then start fresh: ```bash docker compose -f docker-compose.dev.yml down -v @@ -261,3 +276,228 @@ pnpm run db:up pnpm run db:migrate pnpm run db:seed ``` + +--- + +## Testing + +| Command | What it runs | +| -------------------------------- | -------------------------------- | +| `pnpm run test` | Server + client unit tests | +| `pnpm run test:server` | Server unit tests only | +| `pnpm run test:client` | Client unit tests only | +| `cd server && pnpm test:watch` | Server tests in watch mode | +| `pnpm run e2e:run -- ` | Run a single E2E suite | +| `pnpm run e2e:all` | Run all E2E suites sequentially | +| `pnpm run e2e:list` | List available E2E suite IDs | +| `pnpm run coverage` | Unit tests with coverage reports | + +Server unit tests use Vitest with `@nestjs/testing`. Client tests use Vitest with `@vue/test-utils` in a jsdom environment. Most E2E suites boot the full app against a dedicated test database; a few smoke-style suites run against the dev database. + +For test architecture, suite details, coverage thresholds, the E2E harness, and examples, see [TESTING.md](TESTING.md). + +--- + +## Code Quality + +### Quality Gates + +| Command | What it checks | When to use | +| ------------------------ | -------------------------------------------- | ---------------------------- | +| `pnpm run verify:fast` | Lint + typecheck + tests | Quick check while developing | +| `pnpm run verify` | Lint + typecheck + tests (full) | Before opening a PR | +| `pnpm run verify:strict` | Format check + lint + full typecheck + tests | Strictest gate | + +### Individual Checks + +| Command | What it does | +| ----------------------- | -------------------------------------- | +| `pnpm run lint:check` | ESLint across server + client | +| `pnpm run lint:fix` | ESLint auto-fix across server + client | +| `pnpm run typecheck` | TypeScript type checking | +| `pnpm run format` | Prettier format across server + client | +| `pnpm run format:check` | Prettier check (no write) | + +### Git Hooks + +Two hooks run automatically via [Husky](https://typicode.github.io/husky/): + +- **Pre-commit:** Runs `lint-staged` on staged files. This auto-fixes lint issues and formats code with Prettier. You do not need to format manually before committing. +- **Pre-push:** Runs `pnpm verify:fast`. Your push is blocked if lint, typecheck, or tests fail. + +--- + +## Must-Know Conventions + +These are the rules that most commonly cause PR rejections. Following them from the start will save you review cycles. + +### Backend + +- **One module per feature.** Each domain lives in `server/src/modules//` with its own controller, service, module, and `dto/` subfolder. +- **DTOs for all input.** Request bodies are validated via `class-validator` decorators on DTO classes. The global `ValidationPipe` rejects unknown fields. +- **Throw NestJS exceptions.** Use `NotFoundException`, `BadRequestException`, `ForbiddenException`, etc. Never throw raw `Error`. +- **Multi-user ownership.** User-owned data needs a `userId` foreign key. Services must check ownership and throw `ForbiddenException` for non-owners. + +### Frontend + +- **Composition API only.** Every component uses `