You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
vibed/quotesdb/docs/plans/2026-02-27-quotesdb-design.md

194 lines
4.7 KiB
Markdown

# QuotesDB — Finalized Design
**Date:** 2026-02-27
---
## Database Schema
```sql
CREATE TABLE quotes (
id TEXT PRIMARY KEY, -- NanoID (~21 chars)
text TEXT NOT NULL,
author TEXT NOT NULL,
source TEXT, -- optional: book, speech, etc.
date TEXT, -- optional: ISO date YYYY-MM-DD
auth_code TEXT NOT NULL, -- 4-word passphrase e.g. ocean-table-purple-storm
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE quote_tags (
quote_id TEXT NOT NULL REFERENCES quotes(id) ON DELETE CASCADE,
tag TEXT NOT NULL,
PRIMARY KEY (quote_id, tag)
);
```
---
## API Endpoints
| Method | Path | Description | Auth |
|--------|------|-------------|------|
| GET | `/api/` | OpenAPI spec (JSON) | None |
| GET | `/api/quotes` | List quotes, 10/page. Query: `?page=N&author=X&tag=Y` | None |
| GET | `/api/quotes/random` | Random quote | None |
| GET | `/api/quotes/:id` | Get quote by NanoID | None |
| PUT | `/api/quotes` | Create a quote | None (auth_code optional in body) |
| POST | `/api/quotes/:id` | Update a quote | `X-Auth-Code` header |
| DELETE | `/api/quotes/:id` | Delete a quote | `X-Auth-Code` header |
> **Router order:** `GET /api/quotes/random` must be registered **before** `GET /api/quotes/:id`.
---
## Request/Response Shapes
### PUT /api/quotes — Create
Request body (`auth_code` optional — generated if omitted):
```json
{
"text": "...",
"author": "...",
"source": "Stanford Commencement 2005",
"tags": ["motivation"],
"date": "2005-06-12",
"auth_code": "ocean-table-purple-storm"
}
```
Response `201 Created`:
```json
{
"quote": {
"id": "V1StGXR8_Z5jdHi6B-myT",
"text": "...",
"author": "...",
"source": "Stanford Commencement 2005",
"tags": ["motivation"],
"date": "2005-06-12",
"created_at": "2026-02-27T00:00:00Z",
"updated_at": "2026-02-27T00:00:00Z"
},
"auth_code": "ocean-table-purple-storm"
}
```
### GET /api/quotes — List
Response `200 OK`:
```json
{
"quotes": [...],
"page": 1,
"total_pages": 4,
"total_count": 38
}
```
### GET /api/quotes/:id — Get by ID
Response `200 OK`: the quote object (without `auth_code`).
Response `404 Not Found`:
```json
{ "error": "not found" }
```
### GET /api/quotes/random — Random Quote
Response `200 OK`: the quote object.
Response `404 Not Found` (empty database):
```json
{ "error": "no quotes found" }
```
### POST /api/quotes/:id — Update
Request: same shape as create (fields to update).
Auth: `X-Auth-Code` header required.
Response `200 OK`: updated quote object.
Response `403 Forbidden`: `{ "error": "forbidden" }`
Response `404 Not Found`: `{ "error": "not found" }`
### DELETE /api/quotes/:id — Delete
Auth: `X-Auth-Code` header required.
Response `204 No Content`.
Response `403 Forbidden`: `{ "error": "forbidden" }`
Response `404 Not Found`: `{ "error": "not found" }`
### Error Responses
All error responses use:
```json
{ "error": "message" }
```
With appropriate HTTP status codes.
---
## Auth
- No user accounts. Each quote has an `auth_code` (4-word passphrase).
- Auth codes are stored **plaintext** in the `quotes` table.
- Provided via `X-Auth-Code` header for update and delete operations.
- On mismatch: `403 Forbidden`.
- Auth code is always returned in the create response body.
- If not provided on create, the server generates a random 4-word passphrase.
### Passphrase Generation
Use a curated wordlist of common English words. Generate 4 random words joined by hyphens, e.g. `ocean-table-purple-storm`.
---
## Frontend Routes (Yew)
| Route | Page |
|-------|------|
| `/` | Home — random quote + "Browse all" link |
| `/browse` | Paginated list with author/tag filter controls |
| `/quotes/:id` | Single quote — view, edit (auth prompt), delete (auth prompt) |
| `/author/:name` | All quotes by an author |
| `/submit` | New quote submission form |
---
## Tech Stack
| Layer | Technology |
|-------|-----------|
| Language | Rust |
| Backend framework | Axum + Tokio |
| Backend target | Cloudflare Workers (workers-rs) |
| Database (prod) | Cloudflare D1 (SQLite-compatible) |
| Database (local) | Turso file-backed SQLite |
| Query layer | SQLx |
| Frontend framework | Yew |
| Frontend compile target | wasm32-unknown-unknown |
| Frontend build tool | Trunk |
| Frontend hosting | Cloudflare Pages |
| Infrastructure | OpenTofu + Cloudflare provider |
---
## Infrastructure (Cloudflare)
- **Worker:** `quotesdb-api` — handles all API requests
- **D1 Database:** bound to the Worker as `DB`
- **Pages:** `quotesdb-ui` — serves the Wasm frontend
- **Custom domain:** `quotes.elijah.run` — pointing to the Pages project