diff --git a/quotesdb/.nbd/tickets/03bb91.md b/quotesdb/.nbd/tickets/03bb91.md
index 84639ba..8773597 100644
--- a/quotesdb/.nbd/tickets/03bb91.md
+++ b/quotesdb/.nbd/tickets/03bb91.md
@@ -5,3 +5,41 @@ status = "todo"
ticket_type = "task"
dependencies = ["f3dc74", "1f5bb5", "6ed325"]
+++
+
+
+The `quotesdb` API is built with Axum + Tokio, targeting Cloudflare Workers via `workers-rs`. It serves JSON at `/api/*` endpoints and persists data to Cloudflare D1 (production) or a local SQLite file via Turso (development). Source lives in `src/bin/api/`.
+
+Shared types and utilities are in `src/lib.rs` — code placed there must compile for both the host target and `wasm32-unknown-unknown`.
+
+Auth codes are 4-word passphrases (e.g. `ocean-table-purple-storm`) assigned to quotes on creation. They are stored plaintext and used to authorise updates and deletes. The generator must compile and run in both the native host environment and the `wasm32-unknown-unknown` target (workers-rs).
+
+
+
+Implement a `generate_auth_code() -> String` function in `src/lib.rs` that produces a random 4-word passphrase. Place it in shared lib code so both the API (generation) and UI (display) can reference the type. The chosen word list crate must support `no_std` or at minimum compile for `wasm32-unknown-unknown`.
+
+
+
+- Resolve TRIAGE ticket 6ed325 (passphrase crate selection) before choosing the dependency.
+- Must compile for both host (`cargo check`) and `wasm32` (`trunk build`).
+- Do not use `std::fs` or thread-based RNG in shared code — use a WASM-compatible RNG (e.g. `getrandom` with the `js` feature).
+
+
+
+Use `superpowers:test-driven-development` — write a unit test that generates 100 codes and verifies each matches `word-word-word-word` format.
+Use `superpowers:verification-before-completion` before closing.
+
+
+
+Run in order from the `quotesdb/` directory:
+
+```sh
+cargo fmt
+cargo check
+cargo clippy
+cargo test
+```
+
+
+
+`feat(quotesdb): implement WASM-compatible 4-word passphrase auth_code generator`
+
diff --git a/quotesdb/.nbd/tickets/04f865.md b/quotesdb/.nbd/tickets/04f865.md
index 68d7635..f1db407 100644
--- a/quotesdb/.nbd/tickets/04f865.md
+++ b/quotesdb/.nbd/tickets/04f865.md
@@ -5,3 +5,40 @@ status = "todo"
ticket_type = "task"
dependencies = ["c3503b", "93515e", "dc3d2b"]
+++
+
+
+The `quotesdb` UI is a Yew (Rust → Wasm) single-page app compiled by Trunk and hosted on Cloudflare Pages. It communicates with the backend API via `fetch` calls. Source lives in `src/bin/ui/`. Run with `trunk serve` for local development.
+
+The five frontend routes are:
+- `/` — Home (random quote)
+- `/browse` — Paginated quote list
+- `/quotes/:id` — Single quote view/edit/delete
+- `/author/:name` — All quotes by an author
+- `/submit` — New quote form
+
+
+
+Implement `src/bin/ui/main.rs` — the Yew app shell and router:
+1. Set up `BrowserRouter` (from yew-router)
+2. Define a `Route` enum for all five routes
+3. Render each route to its respective page component (stubs are fine initially)
+4. Mount the app to the `#app` div in `index.html`
+
+
+
+- Resolve TRIAGE ticket 166996 (Yew/yew-router version) before starting.
+- The `Route` enum must be exhaustive — all five routes listed above.
+- Page components can be stubs (`html! { "Home"
}`) in this ticket; full implementation is in separate tickets.
+
+
+
+From the `quotesdb/` directory:
+
+```sh
+trunk build
+```
+
+
+
+`feat(quotesdb): implement Yew app shell and BrowserRouter with all 5 routes`
+
diff --git a/quotesdb/.nbd/tickets/05f8ae.md b/quotesdb/.nbd/tickets/05f8ae.md
index d7b10ee..c5b2f75 100644
--- a/quotesdb/.nbd/tickets/05f8ae.md
+++ b/quotesdb/.nbd/tickets/05f8ae.md
@@ -5,3 +5,51 @@ status = "todo"
ticket_type = "task"
dependencies = ["f3dc74", "a5049d", "d792e2", "03bb91", "175382", "6f2e18"]
+++
+
+
+The `quotesdb` API is built with Axum + Tokio, targeting Cloudflare Workers via `workers-rs`. It serves JSON at `/api/*` endpoints and persists data to Cloudflare D1 (production) or a local SQLite file via Turso (development). Source lives in `src/bin/api/`.
+
+Shared types and utilities are in `src/lib.rs` — code placed there must compile for both the host target and `wasm32-unknown-unknown`.
+
+`PUT /api/quotes` creates a new quote. The request body is JSON; `auth_code` is optional — if omitted, one is generated. The response is 201 with the full quote object and the `auth_code` (always returned so the user can save it).
+
+Request body: `{ text, author, source?, tags?, date?, auth_code? }`
+Response 201: `{ quote: {...}, auth_code: "word-word-word-word" }`
+
+
+
+Implement the `PUT /api/quotes` handler:
+1. Deserialise and validate the request body (text and author are required)
+2. Generate a NanoID for the quote ID
+3. Generate an auth_code if not provided in the request
+4. INSERT the quote into the `quotes` table
+5. INSERT any tags into `quote_tags`
+6. Return 201 with the created quote and auth_code
+
+
+
+- Return 422 if `text` or `author` is missing or empty.
+- NanoID generation must be WASM-compatible (see TRIAGE ticket 6f2e18).
+- Use the shared `generate_auth_code()` function from `src/lib.rs`.
+- Tag insertion must use the shared `replace_tags_for_quote()` logic (ticket 175382).
+
+
+
+Use `superpowers:test-driven-development` — write tests for: auto-generated auth_code, custom auth_code, missing required fields 422.
+Use `superpowers:verification-before-completion` before closing.
+
+
+
+Run in order from the `quotesdb/` directory:
+
+```sh
+cargo fmt
+cargo check
+cargo clippy
+cargo test
+```
+
+
+
+`feat(quotesdb): implement PUT /api/quotes — create quote with NanoID and auth_code`
+
diff --git a/quotesdb/.nbd/tickets/07cafb.md b/quotesdb/.nbd/tickets/07cafb.md
index 60f5047..f7e7c81 100644
--- a/quotesdb/.nbd/tickets/07cafb.md
+++ b/quotesdb/.nbd/tickets/07cafb.md
@@ -5,3 +5,27 @@ status = "todo"
ticket_type = "task"
dependencies = ["25c413"]
+++
+
+
+This is a triage decision ticket. It must be resolved before dependent implementation tickets can proceed.
+
+
+
+D1 binding chicken-and-egg: the D1 database ID is not known until after `tofu apply`, but the Worker resource needs the D1 ID at plan time. How do we break this circular dependency?
+
+
+
+1. **Two-phase apply** — apply D1 resource first, capture the output ID, then apply the Worker with the ID. Requires splitting `tofu apply` into two steps.
+2. **`data` source lookup** — use a `cloudflare_d1_database` data source to look up an already-existing D1 database by name. Requires D1 to be created manually first or in a prior apply.
+3. **OpenTofu `depends_on`** — express the dependency explicitly and let OpenTofu plan the two resources in the correct order. May work if the Cloudflare provider handles the reference correctly.
+
+
+
+1. Research the options above and choose the best approach for this project.
+2. Update the `infra/worker.tf` and `infra/d1.tf` resources with the chosen approach. Update ticket a23489 and d0da0b with any constraints.
+3. Mark this ticket done with a note on the chosen approach in the body or a comment.
+
+
+
+`chore(quotesdb): resolve triage — d1-binding-chickenandegg-d1-id-not-known-until-after-apply-b`
+
diff --git a/quotesdb/.nbd/tickets/07feaa.md b/quotesdb/.nbd/tickets/07feaa.md
index 8a16138..a007a16 100644
--- a/quotesdb/.nbd/tickets/07feaa.md
+++ b/quotesdb/.nbd/tickets/07feaa.md
@@ -5,3 +5,27 @@ status = "todo"
ticket_type = "task"
dependencies = ["25c413"]
+++
+
+
+This is a triage decision ticket. It must be resolved before dependent implementation tickets can proceed.
+
+
+
+OpenTofu state backend: should the `.tfstate` file be stored locally (gitignored), in Terraform Cloud (free tier), or in Cloudflare R2 (S3-compatible backend)?
+
+
+
+1. **Local file** — simplest, but state is lost if the machine changes and cannot be shared. Suitable for solo development.
+2. **Terraform Cloud** — free tier available, remote state with locking. Requires a Terraform Cloud account.
+3. **Cloudflare R2** — S3-compatible, keeps state within Cloudflare ecosystem. Requires an R2 bucket and API credentials.
+
+
+
+1. Research the options above and choose the best approach for this project.
+2. Set the chosen backend in `infra/terraform.tf`. Update `infra/.gitignore` if using local state. Document the decision in `infra/README.md`.
+3. Mark this ticket done with a note on the chosen approach in the body or a comment.
+
+
+
+`chore(quotesdb): resolve triage — opentofu-state-backend-local-file-gitignored-vs-terraform-cl`
+
diff --git a/quotesdb/.nbd/tickets/08af7a.md b/quotesdb/.nbd/tickets/08af7a.md
index 49746c9..3b39c45 100644
--- a/quotesdb/.nbd/tickets/08af7a.md
+++ b/quotesdb/.nbd/tickets/08af7a.md
@@ -5,3 +5,38 @@ status = "todo"
ticket_type = "task"
dependencies = ["f3dc74", "a6bce1"]
+++
+
+
+The `quotesdb` API is built with Axum + Tokio, targeting Cloudflare Workers via `workers-rs`. It serves JSON at `/api/*` endpoints and persists data to Cloudflare D1 (production) or a local SQLite file via Turso (development). Source lives in `src/bin/api/`.
+
+Shared types and utilities are in `src/lib.rs` — code placed there must compile for both the host target and `wasm32-unknown-unknown`.
+
+
+
+Write the three documentation files for the API domain:
+1. `README.md` — what the API does, how to run it (`cargo run`), how to test it, license, Claude Code disclaimer
+2. `docs/PLANNING.md` — development phases and work log for the API sub-domain
+3. `docs/ARCHITECTURE.md` — API component overview: router, handlers, database layer, auth, OpenAPI spec
+
+
+
+- README must include the dual Apache-2.0 + MIT license notice.
+- README must include a disclaimer that the software was written with Claude Code (model: claude-sonnet-4-6).
+- ARCHITECTURE.md must describe how the API binary wires together (router → handlers → db layer).
+- PLANNING.md must reflect the actual work done (link to ticket IDs where appropriate).
+
+
+
+Run in order from the `quotesdb/` directory:
+
+```sh
+cargo fmt
+cargo check
+cargo clippy
+cargo test
+```
+
+
+
+`docs(quotesdb): write api README, PLANNING.md, and ARCHITECTURE.md`
+
diff --git a/quotesdb/.nbd/tickets/0bc655.md b/quotesdb/.nbd/tickets/0bc655.md
index c0a60c3..fb14d5c 100644
--- a/quotesdb/.nbd/tickets/0bc655.md
+++ b/quotesdb/.nbd/tickets/0bc655.md
@@ -5,3 +5,27 @@ status = "todo"
ticket_type = "task"
dependencies = ["c3503b"]
+++
+
+
+This is a triage decision ticket. It must be resolved before dependent implementation tickets can proceed.
+
+
+
+Auth code storage strategy for the UI: should the auth code be stored in localStorage (persisted across sessions) or kept only in component state (lost on page reload)?
+
+
+
+1. **Component state only** — auth code is lost on page reload. User must re-enter it each time. Simpler and more secure.
+2. **localStorage per quote ID** — store `auth_code_{id}` in localStorage so the user doesn't need to re-enter it for quotes they created. Risk: plaintext in localStorage.
+3. **Session storage** — same as localStorage but cleared when the tab closes. Middle ground.
+
+
+
+1. Research the options above and choose the best approach for this project.
+2. Update the `AuthModal` component (ticket f850c6) with the chosen strategy. If localStorage is chosen, implement a clear-on-delete path.
+3. Mark this ticket done with a note on the chosen approach in the body or a comment.
+
+
+
+`chore(quotesdb): resolve triage — auth-code-storage-strategy-localstorage-persistence-vs-compo`
+
diff --git a/quotesdb/.nbd/tickets/0d84fa.md b/quotesdb/.nbd/tickets/0d84fa.md
index e6d1b90..95eef9f 100644
--- a/quotesdb/.nbd/tickets/0d84fa.md
+++ b/quotesdb/.nbd/tickets/0d84fa.md
@@ -5,3 +5,27 @@ status = "todo"
ticket_type = "task"
dependencies = ["ce1e4f"]
+++
+
+
+This is a triage decision ticket. It must be resolved before dependent implementation tickets can proceed.
+
+
+
+HTTP client for integration tests: should we use reqwest (async, tokio), hyper (low-level), or ureq (synchronous/blocking)?
+
+
+
+1. **reqwest** — most ergonomic, async, works well with tokio::test. Adds a heavier dependency but is widely used.
+2. **hyper** — low-level, minimal dependencies. More verbose.
+3. **ureq** — synchronous, no async runtime needed. Simple but requires spawning a background thread to run the server.
+
+
+
+1. Research the options above and choose the best approach for this project.
+2. Add the chosen crate to `[dev-dependencies]` in `Cargo.toml`. Update ticket 5f5ba0.
+3. Mark this ticket done with a note on the chosen approach in the body or a comment.
+
+
+
+`chore(quotesdb): resolve triage — http-client-selection-for-integration-tests-reqwest-vs-hyper`
+
diff --git a/quotesdb/.nbd/tickets/0d987f.md b/quotesdb/.nbd/tickets/0d987f.md
index 361b75e..06ebf19 100644
--- a/quotesdb/.nbd/tickets/0d987f.md
+++ b/quotesdb/.nbd/tickets/0d987f.md
@@ -5,3 +5,36 @@ status = "todo"
ticket_type = "task"
dependencies = ["c3503b", "93515e"]
+++
+
+
+The `quotesdb` UI is a Yew (Rust → Wasm) single-page app compiled by Trunk and hosted on Cloudflare Pages. It communicates with the backend API via `fetch` calls. Source lives in `src/bin/ui/`. Run with `trunk serve` for local development.
+
+
+
+Implement a shared `QuoteCard` Yew component (`src/bin/ui/components/quote_card.rs`) that displays:
+- Quote text (styled as a blockquote)
+- Author name (linked to `/author/:name`)
+- Optional source and date
+- Tags as clickable chips (linking to `/browse?tag=X`)
+
+This component is reused on the Home, Browse, Author, and Quote Detail pages.
+
+
+
+- Resolve TRIAGE ticket 5e3e37 (CSS/styling approach) before adding class names.
+- Accept a `Quote` struct as a prop (from shared types in `src/lib.rs`).
+- Author link must navigate to `/author/:name` using yew-router's `Link` component.
+- Tags are optional — render nothing if the quote has no tags.
+
+
+
+From the `quotesdb/` directory:
+
+```sh
+trunk build
+```
+
+
+
+`feat(quotesdb): implement shared QuoteCard component`
+
diff --git a/quotesdb/.nbd/tickets/166996.md b/quotesdb/.nbd/tickets/166996.md
index 6baf316..4e3714b 100644
--- a/quotesdb/.nbd/tickets/166996.md
+++ b/quotesdb/.nbd/tickets/166996.md
@@ -5,3 +5,27 @@ status = "todo"
ticket_type = "task"
dependencies = ["c3503b"]
+++
+
+
+This is a triage decision ticket. It must be resolved before dependent implementation tickets can proceed.
+
+
+
+Yew version selection: which version of Yew and yew-router should be used, and are they compatible with each other and the Nix dev shell?
+
+
+
+1. **Yew 0.21 + yew-router 0.18** — latest stable as of early 2026. Check crates.io for current versions.
+2. **Yew 0.20** — previous stable, more documentation available.
+3. **Check nixpkgs** — the Nix dev shell may pin a specific version via rust-overlay.
+
+
+
+1. Research the options above and choose the best approach for this project.
+2. Pin the chosen versions in `Cargo.toml`. Update ticket 93515e. Document the version in `docs/ARCHITECTURE.md`.
+3. Mark this ticket done with a note on the chosen approach in the body or a comment.
+
+
+
+`chore(quotesdb): resolve triage — yew-version-selection-and-yewrouter-compatibility-021`
+
diff --git a/quotesdb/.nbd/tickets/175382.md b/quotesdb/.nbd/tickets/175382.md
index 59a50ad..a5508f6 100644
--- a/quotesdb/.nbd/tickets/175382.md
+++ b/quotesdb/.nbd/tickets/175382.md
@@ -5,3 +5,45 @@ status = "todo"
ticket_type = "task"
dependencies = ["f3dc74", "a5049d"]
+++
+
+
+The `quotesdb` API is built with Axum + Tokio, targeting Cloudflare Workers via `workers-rs`. It serves JSON at `/api/*` endpoints and persists data to Cloudflare D1 (production) or a local SQLite file via Turso (development). Source lives in `src/bin/api/`.
+
+Shared types and utilities are in `src/lib.rs` — code placed there must compile for both the host target and `wasm32-unknown-unknown`.
+
+Each quote can have multiple tags stored in the `quote_tags` join table. Tags are not normalised — they are stored as plain strings per quote. On create/update, all tags for the quote are replaced atomically.
+
+
+
+Implement tag fetch and upsert logic used by the API handlers:
+1. `fetch_tags_for_quote(pool, quote_id) -> Vec` — SELECT from quote_tags
+2. `replace_tags_for_quote(pool, quote_id, tags: &[String])` — DELETE existing, INSERT new tags in a transaction
+
+This logic should live in a `db` or `tags` module and be called from the create and update handlers.
+
+
+
+- Tag replacement must be atomic (use a transaction).
+- Empty `tags` array means "remove all tags" — this is valid.
+- Cascade delete on `quote_tags` handles tag cleanup when a quote is deleted — no separate delete-tags step needed.
+
+
+
+Use `superpowers:test-driven-development` — write unit tests that verify tag insertion, replacement, and empty-tag cases.
+Use `superpowers:verification-before-completion` before closing.
+
+
+
+Run in order from the `quotesdb/` directory:
+
+```sh
+cargo fmt
+cargo check
+cargo clippy
+cargo test
+```
+
+
+
+`feat(quotesdb): implement tag join logic — fetch and replace tags per quote`
+
diff --git a/quotesdb/.nbd/tickets/182210.md b/quotesdb/.nbd/tickets/182210.md
index d041e61..907287d 100644
--- a/quotesdb/.nbd/tickets/182210.md
+++ b/quotesdb/.nbd/tickets/182210.md
@@ -5,3 +5,27 @@ status = "todo"
ticket_type = "task"
dependencies = ["f3dc74"]
+++
+
+
+This is a triage decision ticket. It must be resolved before dependent implementation tickets can proceed.
+
+
+
+Cloudflare Workers WASM size limit: the free tier has a 1MB Worker script size limit. A Rust binary compiled for workers-rs may exceed this. Does this project require a paid Workers plan?
+
+
+
+1. **Paid Workers plan** — removes the 1MB limit ($5/month). Simplest solution.
+2. **Optimise binary size** — use `opt-level = "z"`, `lto = true`, `strip = true`, `wasm-opt`, and minimise dependencies. May bring the binary under 1MB.
+3. **Split the Worker** — serve static assets from Pages and keep the Worker API-only (fewer dependencies).
+
+
+
+1. Research the options above and choose the best approach for this project.
+2. Check the compiled `api` binary size with `trunk build --release` and `ls -lh`. Update the infra plan accordingly.
+3. Mark this ticket done with a note on the chosen approach in the body or a comment.
+
+
+
+`chore(quotesdb): resolve triage — cloudflare-workers-wasm-size-limit-free-tier-1mb-limit-may-r`
+
diff --git a/quotesdb/.nbd/tickets/1a274d.md b/quotesdb/.nbd/tickets/1a274d.md
index be930dd..1942b1f 100644
--- a/quotesdb/.nbd/tickets/1a274d.md
+++ b/quotesdb/.nbd/tickets/1a274d.md
@@ -5,3 +5,35 @@ status = "todo"
ticket_type = "task"
dependencies = ["c3503b", "04f865", "1e6a09", "0d987f", "fc2f51"]
+++
+
+
+The `quotesdb` UI is a Yew (Rust → Wasm) single-page app compiled by Trunk and hosted on Cloudflare Pages. It communicates with the backend API via `fetch` calls. Source lives in `src/bin/ui/`. Run with `trunk serve` for local development.
+
+The Home page is the landing page of the app. It displays a random quote fetched from `GET /api/quotes/random` and a "Browse all" link to `/browse`.
+
+
+
+Implement the Home page component (`src/bin/ui/pages/home.rs`):
+1. On mount, fetch a random quote from the API via the API client module (ticket 1e6a09)
+2. While loading, show a loading indicator
+3. On success, render the `QuoteCard` component (ticket 0d987f)
+4. On error, render the `ErrorDisplay` component (ticket fc2f51)
+5. Render a "Browse all quotes →" link to `/browse`
+
+
+
+- Use `use_effect_with` (Yew 0.21+) or the equivalent hook to trigger the fetch on mount.
+- The random quote endpoint returns 404 if the database is empty — display a friendly "no quotes yet" message in this case.
+
+
+
+From the `quotesdb/` directory:
+
+```sh
+trunk build
+```
+
+
+
+`feat(quotesdb): implement Home page — random quote display`
+
diff --git a/quotesdb/.nbd/tickets/1ba523.md b/quotesdb/.nbd/tickets/1ba523.md
index 5de02a1..be433a8 100644
--- a/quotesdb/.nbd/tickets/1ba523.md
+++ b/quotesdb/.nbd/tickets/1ba523.md
@@ -5,3 +5,35 @@ status = "todo"
ticket_type = "task"
dependencies = ["c3503b", "04f865", "1e6a09", "fc2f51"]
+++
+
+
+The `quotesdb` UI is a Yew (Rust → Wasm) single-page app compiled by Trunk and hosted on Cloudflare Pages. It communicates with the backend API via `fetch` calls. Source lives in `src/bin/ui/`. Run with `trunk serve` for local development.
+
+The Submit page (`/submit`) provides a form for creating a new quote. On success, it displays the returned auth code prominently so the user can save it.
+
+
+
+Implement the Submit page component (`src/bin/ui/pages/submit.rs`):
+1. Render a form with fields: text (textarea), author, source (optional), date (optional), tags (comma-separated input), auth code (optional)
+2. On submit, call `PUT /api/quotes` via the API client
+3. On 201 success: show a success message and display the returned auth code in a copyable box
+4. On error: render `ErrorDisplay` with the error message
+
+
+
+- The auth code returned must be displayed clearly — it cannot be recovered after the user leaves this page.
+- Validate client-side: text and author are required (non-empty) before submitting.
+- Parse the tags input by splitting on commas and trimming whitespace.
+
+
+
+From the `quotesdb/` directory:
+
+```sh
+trunk build
+```
+
+
+
+`feat(quotesdb): implement Submit page — new quote form with auth code display`
+
diff --git a/quotesdb/.nbd/tickets/1e6a09.md b/quotesdb/.nbd/tickets/1e6a09.md
index 2128c41..f8070a0 100644
--- a/quotesdb/.nbd/tickets/1e6a09.md
+++ b/quotesdb/.nbd/tickets/1e6a09.md
@@ -5,3 +5,40 @@ status = "todo"
ticket_type = "task"
dependencies = ["c3503b", "93515e"]
+++
+
+
+The `quotesdb` UI is a Yew (Rust → Wasm) single-page app compiled by Trunk and hosted on Cloudflare Pages. It communicates with the backend API via `fetch` calls. Source lives in `src/bin/ui/`. Run with `trunk serve` for local development.
+
+The API client module provides typed fetch wrappers around all quotesdb-api endpoints. The UI calls these functions from page components rather than making raw fetch calls directly.
+
+
+
+Implement `src/bin/ui/api.rs` (or `src/bin/ui/client.rs`) with async functions for each endpoint:
+- `list_quotes(page, author, tag) -> Result`
+- `get_quote(id) -> Result`
+- `get_random_quote() -> Result`
+- `create_quote(body) -> Result`
+- `update_quote(id, auth_code, body) -> Result`
+- `delete_quote(id, auth_code) -> Result<()>`
+
+Each function sets the appropriate headers (including `X-Auth-Code` where needed) and deserialises the response.
+
+
+
+- Use `gloo::net::http` or `web_sys::fetch` for HTTP requests (not reqwest — not available in WASM).
+- Resolve TRIAGE ticket a9534d (CORS and Trunk proxy config) — during `trunk serve`, the API URL may differ.
+- All functions must be `async` and return `Result` with a meaningful error type.
+- The base URL should be configurable (env var at compile time or from `window.location.origin`).
+
+
+
+From the `quotesdb/` directory:
+
+```sh
+trunk build
+```
+
+
+
+`feat(quotesdb): implement typed API client module for all quotesdb-api endpoints`
+
diff --git a/quotesdb/.nbd/tickets/1f5bb5.md b/quotesdb/.nbd/tickets/1f5bb5.md
index 16d2fe3..9775fcd 100644
--- a/quotesdb/.nbd/tickets/1f5bb5.md
+++ b/quotesdb/.nbd/tickets/1f5bb5.md
@@ -5,3 +5,39 @@ status = "todo"
ticket_type = "task"
dependencies = ["f3dc74", "a91260"]
+++
+
+
+The `quotesdb` API is built with Axum + Tokio, targeting Cloudflare Workers via `workers-rs`. It serves JSON at `/api/*` endpoints and persists data to Cloudflare D1 (production) or a local SQLite file via Turso (development). Source lives in `src/bin/api/`.
+
+Shared types and utilities are in `src/lib.rs` — code placed there must compile for both the host target and `wasm32-unknown-unknown`.
+
+
+
+Create `Cargo.toml` for the `quotesdb` crate with all API-side dependencies. Include `[[bin]]` entries for both `api` and `ui` binaries, platform-specific dependency sections (`cfg(target_arch = "wasm32")`), dev-dependencies for tests, and the release profile with size optimizations.
+
+
+
+- `workers-rs` and Axum are API-only — gate them under `[target.'cfg(not(target_arch = "wasm32"))'.dependencies]`
+- Yew, wasm-bindgen, and web-sys are UI-only — gate under `[target.'cfg(target_arch = "wasm32")'.dependencies]`
+- The `[profile.release]` block must set `opt-level = "z"`, `lto = true`, `strip = true`, `codegen-units = 1`
+- Resolve TRIAGE tickets 6ed325 (passphrase crate) and 6f2e18 (NanoID crate) before finalising those dependency choices
+
+
+
+Use `superpowers:verification-before-completion` after adding dependencies — run `cargo check` to confirm the dependency tree resolves.
+
+
+
+Run in order from the `quotesdb/` directory:
+
+```sh
+cargo fmt
+cargo check
+cargo clippy
+cargo test
+```
+
+
+
+`chore(quotesdb): set up Cargo.toml with api and ui dependencies`
+
diff --git a/quotesdb/.nbd/tickets/25c413.md b/quotesdb/.nbd/tickets/25c413.md
index 96e51fb..239fc76 100644
--- a/quotesdb/.nbd/tickets/25c413.md
+++ b/quotesdb/.nbd/tickets/25c413.md
@@ -5,3 +5,25 @@ status = "todo"
ticket_type = "project"
dependencies = []
+++
+
+
+This is the sub-project tracking ticket for `quotesdb/infra`. All infrastructure tasks depend on this ticket. The infra domain covers: OpenTofu project setup, Cloudflare Worker, D1 database, Pages project, custom domain, and documentation.
+
+
+
+All `quotesdb/infra` tasks are planned, implemented, validated, and closed. `tofu plan` reports no unexpected changes and `tofu apply` provisions the full Cloudflare stack.
+
+
+
+Use `superpowers:dispatching-parallel-agents` when assigning multiple infra tasks to agents in parallel.
+Use `superpowers:verification-before-completion` before marking this ticket done.
+
+
+
+Run from the `infra/` directory:
+
+```sh
+tofu validate
+tofu plan
+```
+
diff --git a/quotesdb/.nbd/tickets/28e7d9.md b/quotesdb/.nbd/tickets/28e7d9.md
index e82dea8..1c52efb 100644
--- a/quotesdb/.nbd/tickets/28e7d9.md
+++ b/quotesdb/.nbd/tickets/28e7d9.md
@@ -5,3 +5,41 @@ status = "todo"
ticket_type = "task"
dependencies = ["f3dc74", "1f5bb5", "2ec8b1"]
+++
+
+
+The `quotesdb` API is built with Axum + Tokio, targeting Cloudflare Workers via `workers-rs`. It serves JSON at `/api/*` endpoints and persists data to Cloudflare D1 (production) or a local SQLite file via Turso (development). Source lives in `src/bin/api/`.
+
+Shared types and utilities are in `src/lib.rs` — code placed there must compile for both the host target and `wasm32-unknown-unknown`.
+
+The `GET /api/` endpoint serves the OpenAPI 3.1.0 specification as JSON. This endpoint requires no authentication and is the entry point for API documentation and client generation.
+
+
+
+Implement the `GET /api/` handler that returns the OpenAPI spec as `application/json`. The spec can be embedded at compile time using `include_str!("../../../api/openapi.yaml")` (or equivalent path) and parsed/re-serialised as JSON, or generated programmatically.
+
+
+
+- Resolve TRIAGE ticket 2ec8b1 (OpenAPI spec serving strategy) before choosing compile-time embed vs runtime load.
+- The response `Content-Type` must be `application/json`.
+- The spec at `api/openapi.yaml` is the source of truth — validate it with `redocly lint api/openapi.yaml` after any changes.
+
+
+
+Use `superpowers:test-driven-development` — write a test that hits `GET /api/` and asserts the response is valid JSON with an `openapi` key.
+Use `superpowers:verification-before-completion` before closing.
+
+
+
+Run in order from the `quotesdb/` directory:
+
+```sh
+cargo fmt
+cargo check
+cargo clippy
+cargo test
+```
+
+
+
+`feat(quotesdb): implement GET /api/ to serve OpenAPI spec as JSON`
+
diff --git a/quotesdb/.nbd/tickets/2ab7a8.md b/quotesdb/.nbd/tickets/2ab7a8.md
index 1e438b2..79aac35 100644
--- a/quotesdb/.nbd/tickets/2ab7a8.md
+++ b/quotesdb/.nbd/tickets/2ab7a8.md
@@ -5,3 +5,27 @@ status = "todo"
ticket_type = "task"
dependencies = ["ce1e4f"]
+++
+
+
+This is a triage decision ticket. It must be resolved before dependent implementation tickets can proceed.
+
+
+
+Test harness: how do we import and start the quotesdb-api binary in integration tests when it uses workers-rs, which targets the Cloudflare Workers runtime rather than a native Rust binary?
+
+
+
+1. **Native feature flag** — add a `#[cfg(not(target_env = "worker"))]` branch in `main.rs` that exposes a plain Axum server. Integration tests use this branch (compiled for host target).
+2. **Separate test binary** — create a `src/bin/api_test.rs` that is a native Axum server without workers-rs, used only in tests.
+3. **Wrangler dev** — run `wrangler dev` in the background and point tests at it. Complex setup, slower CI.
+
+
+
+1. Research the options above and choose the best approach for this project.
+2. Update ticket 9b581f (test harness) and ticket 6e829e (api main.rs) with the chosen approach.
+3. Mark this ticket done with a note on the chosen approach in the body or a comment.
+
+
+
+`chore(quotesdb): resolve triage — test-harness-how-to-import-and-start-quotesdbapi-in-tests-wo`
+
diff --git a/quotesdb/.nbd/tickets/2c5a57.md b/quotesdb/.nbd/tickets/2c5a57.md
index 697b2bd..d113892 100644
--- a/quotesdb/.nbd/tickets/2c5a57.md
+++ b/quotesdb/.nbd/tickets/2c5a57.md
@@ -5,3 +5,34 @@ status = "todo"
ticket_type = "task"
dependencies = ["c3503b", "93515e"]
+++
+
+
+The `quotesdb` UI is a Yew (Rust → Wasm) single-page app compiled by Trunk and hosted on Cloudflare Pages. It communicates with the backend API via `fetch` calls. Source lives in `src/bin/ui/`. Run with `trunk serve` for local development.
+
+
+
+Implement a shared `Pagination` Yew component (`src/bin/ui/components/pagination.rs`) that renders:
+- A "Previous" button (disabled on page 1)
+- Current page indicator (e.g. "Page 2 of 5")
+- A "Next" button (disabled on the last page)
+
+The component accepts `page`, `total_pages`, and an `on_page_change: Callback` prop.
+
+
+
+- Resolve TRIAGE ticket 5e3e37 (CSS/styling approach) before adding class names.
+- Do not navigate programmatically — call `on_page_change` and let the parent update the URL or state.
+- Render nothing (or a disabled shell) if `total_pages <= 1`.
+
+
+
+From the `quotesdb/` directory:
+
+```sh
+trunk build
+```
+
+
+
+`feat(quotesdb): implement shared Pagination component`
+
diff --git a/quotesdb/.nbd/tickets/2ce22e.md b/quotesdb/.nbd/tickets/2ce22e.md
index a9ebec7..8d1db33 100644
--- a/quotesdb/.nbd/tickets/2ce22e.md
+++ b/quotesdb/.nbd/tickets/2ce22e.md
@@ -5,3 +5,42 @@ status = "todo"
ticket_type = "task"
dependencies = ["f3dc74", "a5049d", "d792e2", "175382"]
+++
+
+
+The `quotesdb` API is built with Axum + Tokio, targeting Cloudflare Workers via `workers-rs`. It serves JSON at `/api/*` endpoints and persists data to Cloudflare D1 (production) or a local SQLite file via Turso (development). Source lives in `src/bin/api/`.
+
+Shared types and utilities are in `src/lib.rs` — code placed there must compile for both the host target and `wasm32-unknown-unknown`.
+
+`GET /api/quotes/random` returns a single random quote from the database. This endpoint **must be registered before** `GET /api/quotes/:id` in the Axum router, or it will never be reached (Axum matches in registration order and ":id" would match the literal string "random").
+
+
+
+Implement the `GET /api/quotes/random` handler that selects a random row from the `quotes` table and returns it with its tags. Return 404 if the database is empty.
+
+
+
+- **Router ordering is critical** — document the ordering requirement in a comment in `main.rs`.
+- Use `ORDER BY RANDOM() LIMIT 1` for SQLite random selection.
+- Include the quote's tags in the response.
+- Return `404 Not Found` with `{"error": "no quotes available"}` if the table is empty.
+
+
+
+Use `superpowers:test-driven-development` — write tests for: random quote returned (non-empty DB), 404 when DB is empty.
+Use `superpowers:verification-before-completion` before closing.
+
+
+
+Run in order from the `quotesdb/` directory:
+
+```sh
+cargo fmt
+cargo check
+cargo clippy
+cargo test
+```
+
+
+
+`feat(quotesdb): implement GET /api/quotes/random`
+
diff --git a/quotesdb/.nbd/tickets/2d1371.md b/quotesdb/.nbd/tickets/2d1371.md
index 3bc0777..d796935 100644
--- a/quotesdb/.nbd/tickets/2d1371.md
+++ b/quotesdb/.nbd/tickets/2d1371.md
@@ -5,3 +5,34 @@ status = "todo"
ticket_type = "task"
dependencies = ["25c413", "07feaa"]
+++
+
+
+Infrastructure is managed with OpenTofu using the Cloudflare provider. Configuration lives in `infra/`. Resources include a Cloudflare Worker (API), Cloudflare D1 database (bound to the worker), and a Cloudflare Pages project (UI frontend).
+
+
+
+Bootstrap the OpenTofu project in `infra/`:
+1. Create `infra/providers.tf` — declare the Cloudflare provider with the required version
+2. Create `infra/terraform.tf` — configure the OpenTofu backend (resolve TRIAGE ticket 07feaa for state backend choice)
+3. Create `infra/.gitignore` — ignore `*.tfstate`, `*.tfstate.backup`, `.terraform/`
+4. Run `tofu init` to initialise the provider
+
+
+
+- Resolve TRIAGE ticket 07feaa (state backend: local file vs Terraform Cloud vs R2) before creating `terraform.tf`.
+- The Cloudflare provider requires an API token — document the expected environment variable (`CLOUDFLARE_API_TOKEN`) in a comment in `providers.tf`, do not hardcode it.
+- Every `resource` and `data` block must have a comment explaining its purpose (per CLAUDE.md).
+
+
+
+Run from the `infra/` directory:
+
+```sh
+tofu validate
+tofu plan
+```
+
+
+
+`chore(quotesdb): bootstrap OpenTofu infra project with Cloudflare provider`
+
diff --git a/quotesdb/.nbd/tickets/2ec8b1.md b/quotesdb/.nbd/tickets/2ec8b1.md
index fb3159e..954640c 100644
--- a/quotesdb/.nbd/tickets/2ec8b1.md
+++ b/quotesdb/.nbd/tickets/2ec8b1.md
@@ -5,3 +5,27 @@ status = "todo"
ticket_type = "task"
dependencies = ["f3dc74"]
+++
+
+
+This is a triage decision ticket. It must be resolved before dependent implementation tickets can proceed.
+
+
+
+OpenAPI spec serving strategy: should the spec be embedded at compile time (include_str! macro) or loaded at runtime from a file or generated programmatically?
+
+
+
+1. **Compile-time embed** — `include_str!("../../api/openapi.yaml")` bakes the YAML into the binary. Simple, no runtime file I/O needed for Workers.
+2. **Runtime load** — read the file at startup. Does not work in Cloudflare Workers (no filesystem).
+3. **Programmatic generation** — use a crate like `utoipa` to generate the spec from handler annotations. Most maintainable but adds complexity.
+
+
+
+1. Research the options above and choose the best approach for this project.
+2. Update ticket 28e7d9 (GET /api/ handler) with the chosen approach. If using utoipa, update `Cargo.toml`.
+3. Mark this ticket done with a note on the chosen approach in the body or a comment.
+
+
+
+`chore(quotesdb): resolve triage — openapi-spec-serving-strategy-embed-yaml-at-compile-time-vs-`
+
diff --git a/quotesdb/.nbd/tickets/33ed29.md b/quotesdb/.nbd/tickets/33ed29.md
index 6750e8e..c67fea0 100644
--- a/quotesdb/.nbd/tickets/33ed29.md
+++ b/quotesdb/.nbd/tickets/33ed29.md
@@ -5,3 +5,27 @@ status = "todo"
ticket_type = "task"
dependencies = ["f3dc74"]
+++
+
+
+This is a triage decision ticket. It must be resolved before dependent implementation tickets can proceed.
+
+
+
+Local dev config: should the API use Turso (file-backed SQLite via libsql) or a D1 binding (via wrangler dev) for local development? How is the selection made at runtime?
+
+
+
+1. **Turso/libsql** — lightweight local SQLite file, no Cloudflare account needed. Connection string via env var. SQLx-compatible.
+2. **Wrangler D1 local** — `wrangler dev` spins up a local D1 emulator. Closer to production but requires wrangler and a Cloudflare account even locally.
+3. **Plain SQLite via sqlx** — use sqlx's SQLite driver with a local file. No Turso dependency needed for dev.
+
+
+
+1. Research the options above and choose the best approach for this project.
+2. Update ticket a5049d (database connection module) and ticket af56a7 (local dev docs) with the chosen strategy.
+3. Mark this ticket done with a note on the chosen approach in the body or a comment.
+
+
+
+`chore(quotesdb): resolve triage — local-dev-config-turso-file-sqlite-vs-d1-binding-selection-s`
+
diff --git a/quotesdb/.nbd/tickets/372790.md b/quotesdb/.nbd/tickets/372790.md
index 5c4bf59..6bbeaed 100644
--- a/quotesdb/.nbd/tickets/372790.md
+++ b/quotesdb/.nbd/tickets/372790.md
@@ -5,3 +5,24 @@ status = "todo"
ticket_type = "task"
dependencies = ["c3503b", "1a274d", "1ba523", "5f1112", "b3ef98", "5cdbd9"]
+++
+
+
+The `quotesdb` UI is a Yew (Rust → Wasm) single-page app compiled by Trunk and hosted on Cloudflare Pages. It communicates with the backend API via `fetch` calls. Source lives in `src/bin/ui/`. Run with `trunk serve` for local development.
+
+
+
+Write the three documentation files for the UI domain:
+1. `README.md` — what the UI is, how to run it (`trunk serve`), how to build (`trunk build`), license, Claude Code disclaimer
+2. `docs/PLANNING.md` — development phases and work log for the UI sub-domain
+3. `docs/ARCHITECTURE.md` — UI component tree overview, routing, API client, WASM compilation notes
+
+
+
+- README must include the dual Apache-2.0 + MIT license notice.
+- README must include a disclaimer that the software was written with Claude Code (model: claude-sonnet-4-6).
+- ARCHITECTURE.md must describe the component hierarchy and how the Yew router maps to page components.
+
+
+
+`docs(quotesdb): write ui README, PLANNING.md, and ARCHITECTURE.md`
+
diff --git a/quotesdb/.nbd/tickets/4a4c26.md b/quotesdb/.nbd/tickets/4a4c26.md
index b8c63f4..488f0ae 100644
--- a/quotesdb/.nbd/tickets/4a4c26.md
+++ b/quotesdb/.nbd/tickets/4a4c26.md
@@ -5,3 +5,41 @@ status = "todo"
ticket_type = "task"
dependencies = ["ce1e4f", "9b581f", "05f8ae"]
+++
+
+
+Integration tests live in `tests/` and exercise the API binary against a temporary SQLite database. They run with `cargo test` and must not require a running Cloudflare environment. The test harness spawns the API server on a random port and returns the base URL.
+
+
+
+Write the `PUT /api/quotes` test suite in `tests/test_create_quote.rs` (or similar). Test cases:
+1. Create with auto-generated auth_code — verify 201, quote object returned, auth_code present in response
+2. Create with custom auth_code in body — verify the provided code is stored and returned
+3. Missing `text` field — verify 422 Unprocessable Entity
+4. Missing `author` field — verify 422 Unprocessable Entity
+5. Create with tags — verify tags appear in the returned quote
+
+
+
+- Use the shared test harness from ticket 9b581f.
+- Auth code in the response must match the pattern `word-word-word-word`.
+- Verify the created quote is retrievable via `GET /api/quotes/:id` after creation.
+
+
+
+Use `superpowers:verification-before-completion` before closing.
+
+
+
+Run in order from the `quotesdb/` directory:
+
+```sh
+cargo fmt
+cargo check
+cargo clippy
+cargo test
+```
+
+
+
+`test(quotesdb): add PUT /api/quotes test suite — create quote`
+
diff --git a/quotesdb/.nbd/tickets/580e66.md b/quotesdb/.nbd/tickets/580e66.md
index b59db3c..736fa9b 100644
--- a/quotesdb/.nbd/tickets/580e66.md
+++ b/quotesdb/.nbd/tickets/580e66.md
@@ -5,3 +5,27 @@ status = "todo"
ticket_type = "task"
dependencies = ["f3dc74"]
+++
+
+
+This is a triage decision ticket. It must be resolved before dependent implementation tickets can proceed.
+
+
+
+Database migration strategy for Cloudflare Workers: how should the `quotes` and `quote_tags` tables be created? Workers do not have a persistent startup phase like a long-running server.
+
+
+
+1. **Startup migration** — run `CREATE TABLE IF NOT EXISTS` in the Worker fetch handler before processing the first request. Simple but adds latency to the first request.
+2. **`wrangler d1 execute`** — apply the schema separately using the wrangler CLI. No runtime overhead but requires a separate CI step.
+3. **SQLx migrate! macro** — embed migrations in the binary and run them at startup. Depends on SQLx compatibility with workers-rs (see TRIAGE e8a330).
+
+
+
+1. Research the options above and choose the best approach for this project.
+2. Update ticket a5049d (database connection + migrations) with the chosen strategy.
+3. Mark this ticket done with a note on the chosen approach in the body or a comment.
+
+
+
+`chore(quotesdb): resolve triage — database-migration-strategy-for-cloudflare-workers-startup-v`
+
diff --git a/quotesdb/.nbd/tickets/5c0c64.md b/quotesdb/.nbd/tickets/5c0c64.md
index cf39be9..58457cc 100644
--- a/quotesdb/.nbd/tickets/5c0c64.md
+++ b/quotesdb/.nbd/tickets/5c0c64.md
@@ -5,3 +5,27 @@ status = "todo"
ticket_type = "task"
dependencies = ["25c413"]
+++
+
+
+This is a triage decision ticket. It must be resolved before dependent implementation tickets can proceed.
+
+
+
+D1 migrations in OpenTofu: how do we apply the SQL schema to a newly created D1 database? Options are a null_resource local-exec in OpenTofu, a separate wrangler d1 execute step, or a manual migration step.
+
+
+
+1. **null_resource local-exec** — run `wrangler d1 execute` as a provisioner in OpenTofu. Ties infra and schema together in one `tofu apply`.
+2. **Separate wrangler step** — document as a manual step after `tofu apply`. Simpler OpenTofu config, slightly more manual.
+3. **API startup migration** — the API runs `CREATE TABLE IF NOT EXISTS` on startup. Works but risks schema drift in production.
+
+
+
+1. Research the options above and choose the best approach for this project.
+2. Update ticket d0da0b (D1 resource), ticket a5049d (migrations module), and ticket 75489a (migration workflow docs) with the chosen strategy.
+3. Mark this ticket done with a note on the chosen approach in the body or a comment.
+
+
+
+`chore(quotesdb): resolve triage — d1-migrations-in-opentofu-nullresource-localexec-vs-separate`
+
diff --git a/quotesdb/.nbd/tickets/5cdbd9.md b/quotesdb/.nbd/tickets/5cdbd9.md
index 1403054..3c2cbe9 100644
--- a/quotesdb/.nbd/tickets/5cdbd9.md
+++ b/quotesdb/.nbd/tickets/5cdbd9.md
@@ -5,3 +5,37 @@ status = "todo"
ticket_type = "task"
dependencies = ["c3503b", "04f865", "1e6a09", "0d987f", "2c5a57", "d3d502", "fc2f51"]
+++
+
+
+The `quotesdb` UI is a Yew (Rust → Wasm) single-page app compiled by Trunk and hosted on Cloudflare Pages. It communicates with the backend API via `fetch` calls. Source lives in `src/bin/ui/`. Run with `trunk serve` for local development.
+
+The Browse page displays a paginated list of all quotes with optional author and tag filters.
+
+
+
+Implement the Browse page component (`src/bin/ui/pages/browse.rs`):
+1. Read `?page`, `?author`, `?tag` from the URL query string
+2. Fetch quotes from `GET /api/quotes` with the query parameters
+3. Render each quote with the `QuoteCard` component
+4. Render the `Pagination` component with prev/next navigation (update URL query params on page change)
+5. Render the `TagFilter` component and an author text input for filtering
+6. Render `ErrorDisplay` on error
+
+
+
+- URL query parameters are the source of truth for current page and filters — use yew-router location hooks to read/write them.
+- Changing a filter should reset to page 1.
+- The author filter is a free-text input (case-insensitive match on the API side).
+
+
+
+From the `quotesdb/` directory:
+
+```sh
+trunk build
+```
+
+
+
+`feat(quotesdb): implement Browse page — paginated list with filters`
+
diff --git a/quotesdb/.nbd/tickets/5d9f5a.md b/quotesdb/.nbd/tickets/5d9f5a.md
index c387150..960e067 100644
--- a/quotesdb/.nbd/tickets/5d9f5a.md
+++ b/quotesdb/.nbd/tickets/5d9f5a.md
@@ -5,3 +5,48 @@ status = "todo"
ticket_type = "task"
dependencies = ["f3dc74", "a5049d", "d792e2", "175382"]
+++
+
+
+The `quotesdb` API is built with Axum + Tokio, targeting Cloudflare Workers via `workers-rs`. It serves JSON at `/api/*` endpoints and persists data to Cloudflare D1 (production) or a local SQLite file via Turso (development). Source lives in `src/bin/api/`.
+
+Shared types and utilities are in `src/lib.rs` — code placed there must compile for both the host target and `wasm32-unknown-unknown`.
+
+`POST /api/quotes/:id` performs a partial update of a quote. The caller must provide the correct auth code via the `X-Auth-Code` request header. Only fields present in the request body are updated; absent fields are left unchanged. Optional fields (`source`, `date`) can be explicitly set to `null` to clear them.
+
+
+
+Implement the `POST /api/quotes/:id` handler:
+1. Extract `:id` from the path
+2. Verify the `X-Auth-Code` header matches the stored `auth_code` — return 403 on mismatch
+3. Apply a partial UPDATE to the `quotes` row (only update supplied fields)
+4. Update `updated_at` timestamp
+5. If `tags` is present in the body, replace all tags for the quote
+6. Return 200 with the updated quote
+
+
+
+- Return 404 if the quote ID does not exist.
+- Return 403 (not 401) on auth code mismatch; do not reveal whether the ID exists to unauthenticated callers.
+- Setting a field to `null` in the request body should clear it (for `source` and `date`).
+- `updated_at` must be set to `CURRENT_TIMESTAMP` on every update.
+
+
+
+Use `superpowers:test-driven-development` — write tests for: valid auth 200, wrong auth 403, not found 404, partial update, null-to-clear.
+Use `superpowers:verification-before-completion` before closing.
+
+
+
+Run in order from the `quotesdb/` directory:
+
+```sh
+cargo fmt
+cargo check
+cargo clippy
+cargo test
+```
+
+
+
+`feat(quotesdb): implement POST /api/quotes/:id — partial update with auth verification`
+
diff --git a/quotesdb/.nbd/tickets/5dbb7d.md b/quotesdb/.nbd/tickets/5dbb7d.md
index f0c8905..1d41f95 100644
--- a/quotesdb/.nbd/tickets/5dbb7d.md
+++ b/quotesdb/.nbd/tickets/5dbb7d.md
@@ -5,3 +5,41 @@ status = "todo"
ticket_type = "task"
dependencies = ["f3dc74", "a5049d", "d792e2", "175382"]
+++
+
+
+The `quotesdb` API is built with Axum + Tokio, targeting Cloudflare Workers via `workers-rs`. It serves JSON at `/api/*` endpoints and persists data to Cloudflare D1 (production) or a local SQLite file via Turso (development). Source lives in `src/bin/api/`.
+
+Shared types and utilities are in `src/lib.rs` — code placed there must compile for both the host target and `wasm32-unknown-unknown`.
+
+`GET /api/quotes/:id` returns a single quote by its NanoID. Returns 404 if no quote with that ID exists.
+
+
+
+Implement the `GET /api/quotes/:id` handler that looks up a quote by NanoID, fetches its tags, and returns the full quote JSON. Return 404 if the ID is not found.
+
+
+
+- Extract the `:id` path parameter using Axum's `Path` extractor.
+- Include the quote's tags in the response.
+- Return `404 Not Found` with `{"error": "not found"}` if the ID doesn't match any row.
+
+
+
+Use `superpowers:test-driven-development` — write tests for: 200 with quote object, 404 not found.
+Use `superpowers:verification-before-completion` before closing.
+
+
+
+Run in order from the `quotesdb/` directory:
+
+```sh
+cargo fmt
+cargo check
+cargo clippy
+cargo test
+```
+
+
+
+`feat(quotesdb): implement GET /api/quotes/:id`
+
diff --git a/quotesdb/.nbd/tickets/5e3e37.md b/quotesdb/.nbd/tickets/5e3e37.md
index 4978ed3..7f53397 100644
--- a/quotesdb/.nbd/tickets/5e3e37.md
+++ b/quotesdb/.nbd/tickets/5e3e37.md
@@ -5,3 +5,27 @@ status = "todo"
ticket_type = "task"
dependencies = ["c3503b"]
+++
+
+
+This is a triage decision ticket. It must be resolved before dependent implementation tickets can proceed.
+
+
+
+CSS/styling approach for the Yew Wasm UI: plain CSS (separate .css file), CDN Tailwind (loaded in index.html), or a Wasm-compatible Rust styling crate?
+
+
+
+1. **Plain CSS** — write a `style.css` file, include it in `index.html`. No build complexity. Simple and portable.
+2. **CDN Tailwind** — add Tailwind CDN `