Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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 requireAuthorization: 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 adesignstablewords→ M2M via junction table oncewordstable 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 |
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.
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 |
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.
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-setspublished_timestampto now (if not provided) - Setting
status: "blocked"auto-setsblocked_timestampto now (if not provided)
# 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.
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.
curl -X POST "$BASE/api/query" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"sql": "SELECT count(*) FROM pictures"}'
Quick Start (Terminal)
# 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 (
snakkimoproject,productionenvironment) - Gitea repo:
https://git.hyggecraftery.com/admin/snakkimo-API - Docker: Node 20 Alpine, port 3000
- Health check: Docker-native
HEALTHCHECKonGET /health - Migrations: Run automatically on every server start (idempotent)