Skip to content

Latest commit

 

History

History
174 lines (126 loc) · 7.27 KB

quickstart.md

File metadata and controls

174 lines (126 loc) · 7.27 KB

Quickstart

Create a document, write content into it, and read it back. Three calls, from a server. For the credential these calls need, see Authentication.

This page uses the service-role key, because writing content requires it. Run these from a backend, never from a browser.

Before you start

You need two values.

  • <BASE_URL> — where the REST API answers. Locally that is http://localhost:4000. On a server, use your PUBLIC_RESTAPI_URL.
  • <SUPABASE_SERVICE_ROLE_KEY> — the service-role key from your Supabase project settings.

1. Create a document

curl -X POST '<BASE_URL>/api/documents' \
  -H 'Authorization: Bearer <SUPABASE_SERVICE_ROLE_KEY>' \
  -H 'Content-Type: application/json' \
  -d '{
    "title": "Quarterly report",
    "slug": "quarterly-report"
  }'

title and slug are required. The slug is normalised, and the server generates a 19-character documentId.

{
  "success": true,
  "data": {
    "documentId": "kR4pZ2mQ7tY1nB8xW3v",
    "slug": "quarterly-report"
  }
}

Keep that documentId. Every content call addresses a document by id, never by slug. A taken slug returns 409 rather than being renamed quietly.

You can also seed content in this one call, by adding a content field holding a Tiptap document. That field is accepted only under the service-role key, and it lands as version 1.

2. Write content

curl -X PATCH '<BASE_URL>/api/documents/<DOCUMENT_ID>/content?mode=replace' \
  -H 'Authorization: Bearer <SUPABASE_SERVICE_ROLE_KEY>' \
  -H 'Content-Type: application/json' \
  -d '{
    "content": {
      "type": "doc",
      "content": [
        {
          "type": "heading",
          "attrs": { "level": 1 },
          "content": [{ "type": "text", "text": "Quarterly report" }]
        },
        {
          "type": "paragraph",
          "content": [{ "type": "text", "text": "Revenue grew 12%." }]
        }
      ]
    }
  }'

Replace <DOCUMENT_ID> with the id from step 1.

Two modes exist. replace swaps the whole body, and it is the default. append adds your top-level nodes after the existing ones.

A 200 means the content reached the live document, and its save was handed off. People with the document open see the change at once, without reconnecting.

{
  "success": true,
  "data": {
    "documentId": "kR4pZ2mQ7tY1nB8xW3v",
    "mode": "replace",
    "version": 1
  }
}

version is the saved version that holds your write. It is missing when the 200 came before the save, as step 3 explains.

3. Read it back

curl '<BASE_URL>/api/documents/<DOCUMENT_ID>/content?format=json' \
  -H 'Authorization: Bearer <SUPABASE_SERVICE_ROLE_KEY>'
{
  "success": true,
  "data": {
    "documentId": "kR4pZ2mQ7tY1nB8xW3v",
    "version": 1,
    "format": "json",
    "content": {
      "type": "doc",
      "content": [
        {
          "type": "heading",
          "attrs": { "level": 1, "toc-id": "a1B2c3D4e5F6g7H8" },
          "content": [{ "type": "text", "text": "Quarterly report" }]
        },
        {
          "type": "paragraph",
          "content": [{ "type": "text", "text": "Revenue grew 12%." }]
        }
      ]
    }
  }
}

A document created without content has no saved version, so your first write is version 1. Each later saved version adds one.

Notice the toc-id the server added. That is covered below.

Pass format=text instead to get plain text, one line per block.

To read the document as Markdown, call the export route with format=md. It returns the raw .md file, not the JSON envelope. It takes the service-role key or a user token, and it always needs one, even on a public document.

curl '<BASE_URL>/api/documents/<DOCUMENT_ID>/export?format=md' \
  -H 'Authorization: Bearer <SUPABASE_SERVICE_ROLE_KEY>'

This read returns the stored copy, not the live one. It can trail an actively edited document by up to 60 seconds. Your own write can trail too. On a document nobody has open, the PATCH 200 carries version once the write is saved. On an open document that 200 has no version.

A document that exists with no content yet returns version: 0 and an empty document. That is not an error.

Content rules that cause a 422

A 422 here usually means valid JSON that breaks a document rule, not a malformed body.

The first node must be a level-1 heading. A document without one is refused. Otherwise the editor would invent a title on first open and save a heading you never wrote.

An image is inline. Put it inside a paragraph or a heading. An image at the top level is refused.

The root array is flat. A heading holds inline text only. The blocks that follow a heading are its siblings, not its children.

Three caps also apply: 5 MiB per payload, 50,000 nodes, and a nesting depth of 100.

About toc-id

Headings and tables carry a toc-id. The server assigns one wherever it is missing. That id is the address a heading's chat thread, its fold state, and its ?id= deep link all hang off.

Round-trip the ids. When you GET a document, edit it, and replace it, send the existing toc-id values back. Drop them and every heading gets a new address, which detaches its chat thread.

On append, a duplicate id is permanent. The server de-duplicates within one payload only, and never reads the live document's ids. So if you send an id a heading already owns, both headings end up sharing one chat thread and one anchor. Next step: omit toc-id on append and let the server mint one, or GET first and avoid the ids already in use.

Writing a lot of content

Every PATCH stores a full version of the document. So 500 small writes leave roughly 500 stored versions.

The rate limit is per address, and your server sets its own value. Read X-RateLimit-Limit on a response that passes through a limiter, which means one that came through your proxy. See Rate limiting.

Next step: prefer one replace, or a few large append calls, over many small ones. If you are importing a long document, build the whole Tiptap document first and send it once.

Retrying safely

replace is safe to retry when every heading in it carries its toc-id. Sending it twice then leaves the same document. Without the ids, each retry mints new ones, so a heading's chat thread detaches again. Each retry also stores one more version.

append is not. A timeout may mean the write landed anyway, so a blind retry can append twice. Next step: wait about a minute, then GET the document and check before retrying an append. 503 SAVE_NOT_CONFIRMED means the write was applied. 503 DOCUMENT_BUSY applied nothing; retry after a few seconds. Send writes to one document in sequence.

A repeated 500 on the same document means server-side persistence is stuck for it. Next step: stop retrying and tell an operator, because each attempt keeps changing what people see without saving it.

Where to go next