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.
194 lines
4.7 KiB
Markdown
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
|