From b5f574510740f535ca3a974c317043be74620d38 Mon Sep 17 00:00:00 2001 From: admin Date: Wed, 20 May 2026 13:47:31 +0200 Subject: [PATCH] docs: add README with full API and database documentation Co-Authored-By: Claude Sonnet 4.6 --- README.md | 264 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 264 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..bf7b341 --- /dev/null +++ b/README.md @@ -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 ` +- **Health:** `GET /health` — public, no token needed + +--- + +## Authentication + +Pass the token in every request: + +``` +Authorization: Bearer +``` + +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//.`. +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//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/" \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"status": "published"}' + +# Block with reason +curl -X PATCH "$BASE/api/pictures/" \ + -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/" \ + -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)