# Idea API

A backend for AI agents to organize creative ideas, store generated assets, and run image-generation workflows. Built on Cloudflare Workers (Hono + D1 + R2) with a Replicate proxy baked in. **No Replicate token needed from the caller** — auth is handled server-side.

Base URL: https://idea.backpine.com

All request and response bodies are JSON unless noted. Timestamps in responses are epoch milliseconds. All IDs are UUIDv4 strings assigned by the server.

---

## Mental model

- **Idea** — the top-level "thing you're thinking about". Has a title, description, optional kind, and free-form metadata. Everything else hangs off an idea.
- **Concept** — an optional named group inside an idea (e.g. "hero-images", "drafts", "pages"). Files and generations can attach directly to an idea without a concept, or be grouped via a concept.
- **File** — a single asset stored in R2 (an image, an HTML page, anything). Has filename, content_type, size_bytes, optional description and metadata. Always belongs to an idea; optionally linked to a concept and/or generation.
- **Generation** — one Replicate prediction. Stores the prompt, model, input/output JSON, status, and timestamps. On success, each output URL is streamed into R2 and registered as a File row, automatically attached to the same idea (and concept, if provided).

## Stable file URLs

Every file is accessible at GET /files/{file_id}/content — stable, served with the correct content-type. When you generate HTML that embeds images you generated, embed them as:

    <img src="/files/{image_file_id}/content">

The HTML itself becomes viewable at /files/{html_file_id}/content once uploaded.

## Metadata everywhere

Every resource (idea, concept, file, generation) has an optional metadata field — arbitrary JSON. Use it for anything that doesn't fit the typed columns: prompts, model used, alt text, dimensions, source URLs, tags, etc.

## Typical workflow

1. POST /ideas → get an idea_id.
2. (optional) POST /ideas/{idea_id}/concepts to create named groups.
3. POST /generations to run Replicate models. Each output is auto-saved as a File under the idea.
4. POST /ideas/{idea_id}/files to upload arbitrary content (e.g. an HTML page you wrote that references your generated images via /files/{id}/content).
5. View assets via /files/{id}/content URLs.

---

## Endpoints

### Ideas

**POST /ideas**

Body:

    {
      "title":       "string (required)",
      "description": "string?",
      "kind":        "string? (free-form, e.g. \"landing-page\", \"product\", \"image_set\")",
      "metadata":    object?
    }

Returns 201 with the created idea.

**GET /ideas** — list. Returns 200 with { ideas: [...] }.

**GET /ideas/{id}** — get one. 404 if missing.

**PATCH /ideas/{id}**

Body is any subset of { title, description, kind, metadata }. Omitted fields are left unchanged. Send a field with value null to clear it (except title which is non-null).

**DELETE /ideas/{id}**

Cascade-deletes everything under the idea: its concepts, files (including the R2 objects), and generations. Returns 204.

---

### Concepts

**POST /ideas/{idea_id}/concepts**

Body:

    {
      "name":        "string (required)",
      "kind":        "string?",
      "description": "string?",
      "metadata":    object?
    }

Returns 201 with the created concept.

**GET /ideas/{idea_id}/concepts** — list under an idea.

**GET /concepts/{id}** — get one.

**PATCH /concepts/{id}** — body is any subset of { name, kind, description, metadata }.

**DELETE /concepts/{id}**

Returns 204. Files and generations previously attached to this concept have their concept_id set to NULL — they survive, just unlinked.

---

### Files

**POST /ideas/{idea_id}/files**

Content-Type: multipart/form-data with these fields:

- file — required, the binary payload
- concept_id — optional UUID; must belong to the same idea
- filename — optional override (defaults to the multipart filename)
- description — optional
- metadata — optional, a JSON-encoded string

Returns 201 with the file row.

**GET /ideas/{idea_id}/files**

List files under an idea. Optional filter: ?concept_id={uuid}.

**GET /files/{id}** — get one (metadata).

**GET /files/{id}/content** — stream the bytes from R2 with the file's stored content-type. This is the URL to embed in HTML, share with users, etc.

**PATCH /files/{id}** — body is any subset of { filename, concept_id, description, metadata }. concept_id must belong to the same idea (or be null).

**DELETE /files/{id}** — deletes both the R2 object and the row. Returns 204.

---

### Generations (Replicate proxy)

**POST /generations**

Body:

    {
      "idea_id":     "uuid (required)",
      "concept_id":  "uuid? (same-idea check enforced)",
      "model":       "string (required, e.g. \"black-forest-labs/flux-schnell\")",
      "version":     "string? (required for community models, omit for official models)",
      "input":       object (required, passed straight to Replicate; typically includes \"prompt\"),
      "description": "string?",
      "metadata":    object?,
      "prompt":      "string? (auto-extracted from input.prompt if omitted)",
      "idea_kind":   "string? (sets idea.kind if not already set)",
      "wait":        "number? (seconds, 1–60, default 60)",
      "auto_save":   "boolean? (default true — download outputs to R2 on success)"
    }

Behavior:

- Sends Prefer: wait={wait} to Replicate. Replicate keeps the request open up to 60s.
- For models with a version: uses POST /v1/predictions with { version, input }.
- For official models (no version): uses POST /v1/models/{owner}/{name}/predictions with { input }.
- On status == succeeded with auto_save: downloads every URL in output, streams each into R2, creates one File row per URL linked via generation_id (and concept_id if provided).
- On non-terminal after wait: persists replicate_id + status="processing" and returns immediately. Caller should poll GET /generations/{id}.

Returns 201 with:

    { "generation": { ...full row... }, "files": [ ...materialized file rows... ] }

**GET /generations/{id}**

If the row's status is non-terminal, this endpoint polls Replicate, updates the row, and (on first transition to succeeded) materializes the outputs into R2. Returns 200 with { generation, files }.

**GET /ideas/{idea_id}/generations** — list under an idea.

**POST /generations/{id}/cancel** — cancels the Replicate prediction. Returns 200 with { generation }.

---

### Health / docs

**GET /** — returns this document.

---

## Status values

Generation status is one of: starting, processing, succeeded, failed, canceled. Terminal statuses are succeeded, failed, canceled.

## Error responses

All errors come back as JSON { "error": "..." } with status:

- 400 — bad input (missing required field, invalid JSON metadata, mismatched FK).
- 404 — resource not found.
- 502 — upstream Replicate error (the error message is propagated).
- 500 — unexpected internal error.

## Important behaviors agents should know

- Replicate output URLs (replicate.delivery/...) are deleted by Replicate after ~1 hour. This proxy always downloads to R2 on success (unless auto_save=false), so you can ignore those URLs after the POST /generations call returns.
- generation.output preserves the raw Replicate response (the original URLs included). For the persisted copies, use the files array under that generation, or GET /generations/{id} which returns linked files.
- An idea's full lineage (its concepts, files, generations, R2 objects) is wiped by DELETE /ideas/{id}.
- A concept-delete only unlinks (sets concept_id = NULL on related files and generations) — it never deletes those rows.
- File access via GET /files/{id}/content is anonymous. There is no auth on this API; treat the link as effectively public.
