Skip to content

Reflections

Status: Live

Saving a Reflection, reading the one on a piece, and the Profile list all ship in CSL-253. Editing, deleting and per-piece history are not part of this release.

A Reflection is one reader's private written answer to a content piece's reflection prompt. The piece asks a question; the reader writes back; the app keeps it for them.

Three things define it, and all three are deliberate:

It is private. A Reflection belongs to the reader who wrote it and to nobody else. Other readers never see it. Editors never meet it while browsing content in the CMS. It is not moderated, not published, and not part of the content graph.

It outlasts what it answers. Unpublishing a piece, or deleting it outright, does not take a reader's writing with it. The Reflection keeps its own record of the question and of the piece it was about, so it stays readable in Profile either way. Editors can retire content without destroying anything a reader wrote.

There is one per piece. A reader has at most one Reflection on any given Passage, Writing or Article. The first save creates it; a retry, a double tap or a resumed app all resolve to that same record rather than adding another or overwriting it. Once saved, it stays as written — this release has no edit and no delete.

It belongs to the piece, not the route. A writing read on its own and the same writing read as a journey chapter are one piece, so they share one Reflection. A reader who has already written one meets their own words again rather than an empty box. A chapter's passage is read only in its chapter (Journey passages), so its Reflection is met there.

Where it appears

  • Reading a piece — the prompt with a box beneath it, or, if they have already written here, what they wrote.
  • Profile → Reflected — everything they have written, newest first, one card per piece, opening the full read-only view.

What makes a piece reflectable

The reflection_prompt field. A piece with a prompt can be reflected on; a piece without one cannot, because a Reflection keeps the question it answered and there would be none to keep. Editors decide this per piece simply by filling the field in or leaving it empty.

Not in this release

Multiple entries per piece, editing, deleting, per-piece history, counts, previews, grouping, search, sharing and export are all deferred to a later journaling version (CSL-192). Community reflections — an earlier idea where reflections were moderated and shown to other readers as ambient presence — are not planned for V1.

Endpoint map

What Endpoint
Save the reader's Reflection on a piece POST /api/account/reflections/{type}/{id}/
The reader's Reflection on a piece GET /api/account/reflections/{type}/{id}/
Everything the reader has written GET /api/account/reflections/

All three need a bearer token, and none is ever cached — they live under /api/account/ for exactly that reason. The reading endpoints (GET /api/passages/{id}/ and friends) carry no reflection state at all; ask for a reader's own writing separately. (A journey passage's detail does depend on the reader, but only on their access to its journey, not on any reflection: see Passages.)

Addressing a piece

{type} is passage, writing or article — the same three values a journey chapter's content block already uses, and the same word that names the piece's detail endpoint. {id} is its row id.

There is no chapter type. A journey chapter is a piece plus editorial framing, so reflecting inside a journey targets the piece underneath it: take chapter.content.type and chapter.content.id straight from the journey detail payload. That is what makes a reader's writing follow the piece rather than the route they took to it.

Save: POST /api/account/reflections/{type}/{id}/

Send the reader's writing and nothing else:

{"body": "I used to hear this and think it was a paradox…"}

Do not send the question — the server reads it off the piece. Do not send the target — it is in the URL.

201 the first time. 200 on every repeat, returning the record already stored, unchanged. That second sentence is the important one: a reader gets one Reflection per piece and this release has no edit, so a retried request, a double tap or a resumed app can neither duplicate the record nor overwrite it with different text. A save that arrives with new writing for a piece that already has a Reflection is answered with the original.

The practical consequence for the client: read before you compose. Never show the composer for a piece that already has a Reflection — show what the reader wrote. Otherwise a reader types a second answer, gets a 200, and sees their first one come back.

Status When
201 Saved. This is the reader's first Reflection on this piece.
200 One already existed. The response is that record, untouched.
400 The body is empty, whitespace only, or longer than 10,000 characters.
409 The piece has no reflection_prompt, so it cannot be reflected on.
404 No such piece, {type} is not one of the three, or the piece is an unpublished draft the reader has not already written on.
401 No valid bearer token. A Reflection always belongs to a named reader.

The 409 should never reach a reader. The reading payloads carry reflection_prompt, so hide the reflect affordance when it is empty; this status is the backstop, not a normal path.

Read one: GET /api/account/reflections/{type}/{id}/

What the reading screen asks before deciding between the composer and the read-only view.

{"reflection": null}

200 with null when the reader has written nothing here — not a 404, so "nothing saved" stays distinguishable from "wrong id".

404 means the piece is not one this reader can address: it does not exist, it has been deleted, or it is an unpublished draft they have never written on. A draft they have written on stays readable, so a piece unpublished after the fact never strands anyone's Reflection.

The list: GET /api/account/reflections/

The Profile "Reflected" list: everything the reader has written, across every kind of content, newest first. Limit/offset paginated (API Conventions).

Items carry whole bodies, not previews. Opening a card needs no second request — push the full read-only view with the item already in hand. How much of a body a card shows is the client's decision; the API does not truncate.

Time headings like "This week" and "June" are the client's to derive from reflected_on. The API ships no grouping.

The payload

Every Reflection is the same object wherever it appears — the save response, the per-piece read, and each item in the list:

{
  "id": 91,
  "question": "What was that ache, and why did you want it back?",
  "body": "I used to hear this and think it was a paradox…",
  "reflected_on": "2026-07-21T09:12:44Z",
  "content": {
    "type": "passage",
    "id": 42,
    "slug": "the-more-we-get-what-we-now-call-ourselves",
    "kind": "passage",
    "title": "The more we get what we now call ourselves",
    "is_available": true
  }
}

question is the prompt as it stood when the reader answered it, not the piece's current one. An editor rewriting a prompt next month does not restate anyone's existing answer as a reply to a new question.

content is the source pill. Two fields name the piece and they are not the same: type is for routing (which detail endpoint to call), while kind is the label to show — passage, letter, essay, chapter or article. A letter is type: writing, kind: letter.

is_available is false once the piece is unpublished — or deleted outright. One flag covers both, because a client does nothing different between them: the piece is not reachable, so a link into it will not resolve.

A Reflection is never lost with its piece. The question, the body, the title and the piece's id all survive, whichever way the piece went away. Details are read live from the piece while it exists — so a corrected title reaches writing from last month — and fall back to what was recorded at save time once it does not.

Where you read it back differs between the two cases:

The piece was… GET /api/account/reflections/{type}/{id}/ GET /api/account/reflections/
unpublished the Reflection, is_available: false the Reflection, is_available: false
deleted 404 — the route is addressed by the piece the Reflection, is_available: false

So for a deleted piece the Profile list is where a reader finds their writing, which is where they would look anyway. slug also comes back empty for a deleted piece, since there is no row left to read one from — route on id.