API conventions

URL shape

/api/v1/<resource> for stable endpoints. Versioned via the path. Sub-resources nest: /api/v1/pages/<id>/blocks.

HTTP verbs

  • GET — idempotent read
  • POST — create
  • PATCH — partial update (default for edits)
  • PUT — full replace (rare, use sparingly)
  • DELETE — soft delete (sets deleted_at); pass ?permanent=true for hard delete

Error shape

Every error response:

json
{ "error": "human-readable message", "code": "machine_code", "details": {} }

Status codes used: 400 (bad input), 401 (no auth), 403 (auth but no permission), 404 (not found), 409 (conflict), 410 (gone — expired), 422 (validation), 429 (rate limit), 500 (us).

Authentication

Two paths, in order of precedence:

  1. Authorization: Bearer <jwt> — for signed-in users (preferred)
  2. x-edit-token: <token> — for anonymous edits (per-doc capability)

Server middleware (lib/verify-auth.ts) returns {userId, email}or null. Routes that allow either signed-in or token holders should check both.

Rate limiting

Sliding window, IP-based, 60 req/min default. Override per-route in middleware.ts. Returns 429 with Retry-After header.

Pagination

Cursor-based, not offset. ?cursor=<opaque>&limit=20. Response includes nextCursor (or null if last page).

Things to avoid

  • Don't put service-role key calls behind the public API. Use server-component reads or the cron path.
  • Don't return more than 200 rows in one response. Paginate.
  • Don't emit raw Postgres errors to clients — sanitize via mapPgError.