docs: add README with full API and database documentation

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-20 13:47:31 +02:00
parent 0f35459b86
commit b5f5745107

264
README.md Normal file
View File

@@ -0,0 +1,264 @@
# snakkimo-API
REST API server for the snakkimo project. Connects to a PostgreSQL database and stores picture assets on Hetzner Object Storage.
- **Base URL:** `https://hyggecraftery.com/api/snakkimo`
- **Auth:** All `/api/*` routes require `Authorization: Bearer <token>`
- **Health:** `GET /health` — public, no token needed
---
## Authentication
Pass the token in every request:
```
Authorization: Bearer <token>
```
Tokens are configured via the `API_TOKENS` environment variable in Coolify (comma-separated for multiple tokens).
---
## Environment Variables
| Variable | Description |
|---|---|
| `DB_HOST` | PostgreSQL internal hostname (Coolify service UUID) |
| `DB_PORT` | PostgreSQL port (default: `5432`) |
| `DB_NAME` | Database name |
| `DB_USER` | Database user |
| `DB_PASSWORD` | Database password |
| `DB_SSL` | `true` or `false` |
| `PORT` | API server port (default: `3000`) |
| `API_TOKENS` | Comma-separated Bearer tokens |
| `S3_ACCESS_KEY` | Hetzner Object Storage access key |
| `S3_SECRET_KEY` | Hetzner Object Storage secret key |
---
## Database
**Host:** Coolify internal network (same project as the API)
**Database:** `snakkimo`
**Schema:** `public`
### Table: `pictures`
Stores AI-generated pictures with metadata.
| Column | Type | Default | Description |
|---|---|---|---|
| `id` | `UUID` | `gen_random_uuid()` | Primary key, auto-generated |
| `status` | `VARCHAR(20)` | `uploaded` | `uploaded` · `published` · `blocked` |
| `blocked_reason` | `VARCHAR(20)` | `null` | `regenerate` · `not_to_use` |
| `generation_prompt` | `TEXT` | `null` | Prompt used to generate the image |
| `generation_timestamp` | `TIMESTAMPTZ` | `null` | When the generation was started |
| `generation_duration_s` | `NUMERIC(10,3)` | `null` | How long generation took in seconds |
| `published_timestamp` | `TIMESTAMPTZ` | `null` | Auto-set when status → `published` |
| `blocked_timestamp` | `TIMESTAMPTZ` | `null` | Auto-set when status → `blocked` |
| `blurhash` | `TEXT` | `null` | Blurhash string for image placeholder |
| `picture_link` | `TEXT` | `null` | Public Hetzner URL — set automatically on upload |
| `design` | `TEXT` | `null` | Design category (values TBD) |
| `created_at` | `TIMESTAMPTZ` | `NOW()` | Auto-set on insert |
| `updated_at` | `TIMESTAMPTZ` | `NOW()` | Auto-updated on every change (trigger) |
**Status flow:**
```
uploaded → published
uploaded → blocked
published → blocked
```
**Planned relations:**
- `design` → will become a FK to a `designs` table
- `words` → M2M via junction table once `words` table exists
---
## Storage
**Provider:** Hetzner Object Storage (S3-compatible)
**Endpoint:** `https://fsn1.your-objectstorage.com`
**Bucket:** `snakkimo`
**Public base URL:** `https://snakkimo.fsn1.your-objectstorage.com`
Files are stored under `pictures/<picture_id>/<uuid>.<ext>`.
Deleting a picture via the API also deletes the file from the bucket.
---
## API Endpoints
### Pictures
#### `GET /api/pictures`
List all pictures, newest first.
**Query params:**
| Param | Default | Description |
|---|---|---|
| `status` | — | Filter by status (`uploaded`, `published`, `blocked`) |
| `limit` | `50` | Max rows (max 500) |
| `offset` | `0` | Pagination offset |
```bash
curl "$BASE/api/pictures" \
-H "Authorization: Bearer $TOKEN"
# With filter
curl "$BASE/api/pictures?status=published&limit=10" \
-H "Authorization: Bearer $TOKEN"
```
---
#### `GET /api/pictures/:id`
Get a single picture by UUID.
```bash
curl "$BASE/api/pictures/88602a2b-e608-429a-9e69-78a772128cb8" \
-H "Authorization: Bearer $TOKEN"
```
---
#### `POST /api/pictures`
Create a new picture entry. Returns the new row with status `uploaded`.
**Body (JSON):**
| Field | Type | Description |
|---|---|---|
| `generation_prompt` | string | Prompt text |
| `generation_timestamp` | ISO 8601 | When generation started |
| `generation_duration_s` | number | Duration in seconds |
| `blurhash` | string | Blurhash placeholder |
| `design` | string | Design category |
```bash
curl -X POST "$BASE/api/pictures" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"generation_prompt": "A sunset over mountains",
"generation_duration_s": 4.2
}'
```
---
#### `POST /api/pictures/:id/upload`
Upload an image file to Hetzner and store the public URL in `picture_link`.
Replaces any previously uploaded file for this entry.
**Body:** `multipart/form-data`, field name `file`, max 20 MB.
```bash
curl -X POST "$BASE/api/pictures/<ID>/upload" \
-H "Authorization: Bearer $TOKEN" \
-F "file=@/path/to/image.jpg"
```
**Response:** Updated picture row including `picture_link`.
---
#### `PATCH /api/pictures/:id`
Update one or more fields of a picture.
**Writable fields:** `status`, `blocked_reason`, `generation_prompt`, `generation_timestamp`, `generation_duration_s`, `published_timestamp`, `blocked_timestamp`, `blurhash`, `picture_link`, `design`
**Auto-behavior:**
- Setting `status: "published"` auto-sets `published_timestamp` to now (if not provided)
- Setting `status: "blocked"` auto-sets `blocked_timestamp` to now (if not provided)
```bash
# Publish a picture
curl -X PATCH "$BASE/api/pictures/<ID>" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"status": "published"}'
# Block with reason
curl -X PATCH "$BASE/api/pictures/<ID>" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"status": "blocked", "blocked_reason": "not_to_use"}'
```
---
#### `DELETE /api/pictures/:id`
Delete a picture entry from the database **and** remove the file from Hetzner Object Storage.
Returns `204 No Content` on success.
```bash
curl -X DELETE "$BASE/api/pictures/<ID>" \
-H "Authorization: Bearer $TOKEN" \
-w "\nHTTP %{http_code}"
```
---
### Utilities
#### `GET /api/tables`
List all tables in the public schema.
#### `GET /api/tables/:table`
Read rows from any table (`?limit=100&offset=0`).
#### `POST /api/query`
Execute raw SQL. Destructive statements (`DROP`, `TRUNCATE`, `DELETE FROM`) require the header `X-Confirm-Destructive: yes`.
```bash
curl -X POST "$BASE/api/query" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"sql": "SELECT count(*) FROM pictures"}'
```
---
## Quick Start (Terminal)
```bash
# Set once
export TOKEN="dev_ccfd6fd149806a900311e253b0c3b5d1e107a68ab5619a5fde61f107ce9098ce"
export BASE="https://hyggecraftery.com/api/snakkimo"
# 1. Create entry
ID=$(curl -s -X POST "$BASE/api/pictures" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"generation_prompt": "test", "generation_duration_s": 1.5}' \
| python3 -c "import sys,json; print(json.load(sys.stdin)['id'])")
echo "Created: $ID"
# 2. Upload image
curl -X POST "$BASE/api/pictures/$ID/upload" \
-H "Authorization: Bearer $TOKEN" \
-F "file=@/path/to/image.jpg"
# 3. Publish
curl -X PATCH "$BASE/api/pictures/$ID" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"status": "published"}'
# 4. Delete (also removes from Hetzner)
curl -X DELETE "$BASE/api/pictures/$ID" \
-H "Authorization: Bearer $TOKEN"
```
---
## Deployment
- **Platform:** Coolify (`snakkimo` project, `production` environment)
- **Gitea repo:** `https://git.hyggecraftery.com/admin/snakkimo-API`
- **Docker:** Node 20 Alpine, port 3000
- **Health check:** Docker-native `HEALTHCHECK` on `GET /health`
- **Migrations:** Run automatically on every server start (idempotent)