This library is currently in development.
agit
agit
SELF-HOST REFERENCE · V2

Backend API guide

REST API reference for self-hosting fivepixels. v2 splits feedback (comments) and replies into separate resources — lists stay light, threads load on click.

Getting started

This is a self-host reference (v2).
The library cares about handler return shapes, not a specific REST stack.
You can rename paths (/comments, etc.) — just keep the JSON shapes.
With fivepixels hosting (coming soon), you do not need to build this API.

Back to Storage & Collaboration guide

At a glance

Wire handlers on <FivePixels />. Each handler calls your API below.

Terminology

REST nameLibrary typeIn plain terms
CommentReportFeedbackOne feedback item
CaseReportCaseAn issue/item inside feedback (the body)
ReplyReportReplyA thread reply
MemberReportAuthorReviewer/assignee dropdown options

Handler ↔ API

PriorityHandlerSuggested RESTPurposeGo to
RequiredonListGET /comments?pathname=...Feedback summaries for current page
RequiredonCreatePOST /commentsCreate feedback
RequiredonUpdatePATCH /comments/{id}Edit cases, status, integrations
v2 recommendedonListRepliesGET /comments/{id}/repliesThread — on marker click
v2 recommendedonCreateReplyPOST /comments/{id}/repliesAdd one reply
OptionalonDeleteDELETE /comments/{id}Delete feedback
OptionalonListAllGET /comments?cursor=&limit=100All pages, summarized
Optionalteam.reviewersGET /membersReviewer dropdown list

Pass onList·onCreate·onUpdate together, or omit all three (localStorage is used).
Without v2 handlers (onListReplies·onCreateReply), the library falls back to v1 (PATCH entire replies array).

How v2 works

  • No realtime connection — API calls only on user action or navigation
  • Filtering is client-side — no search/status filter API needed
  • Body is cases[] — no top-level message field
  • Lists stay lightreply_count + latest_reply instead of replies[]
  • Threads on click — marker/list click → onListReplies
  • One reply at a timePOST /replies (not PATCH whole array)
  • Refresh after changes — re-fetch list or thread after create/update/delete

v1 vs v2

v1 (legacy)v2 (recommended)
ListFull replies[] includedSummary only (reply_count, latest_reply)
ThreadAlready in listGET /replies on click
Add replyPATCH + replace replies arrayPOST /replies one item
DBSingle table + replies JSONBcomments + replies tables

Project scope

<FivePixels project={{ id, env, version }} /> tells the library which app and environment. Store data scoped by projectId and environment.

Frontend propStorage fieldDescription
project.id(managed in API)App/project ID. Default my-app
project.envenvironmentstage, production, etc.
project.versionapp_versionDeployed version
http
GET /api/projects/{projectId}/comments?pathname=/pricing&environment=stage

Create payloads may include environment and app_version. Store them as sent.

Data model

Feedback body lives in Case array (`cases[]`). Multiple issues in one feedback = multiple Cases. Replies are a separate resource.

Case — one piece of feedback body

json
{
  "id": "case-001",
  "text": "가격 카드가 모바일에서 잘림",
  "status": "open",
  "assignee_name": null,
  "created_at": "2026-06-07T09:00:00.000Z",
  "updated_at": "2026-06-07T09:00:00.000Z"
}
  • text — the actual feedback (not whitespace-only)
  • statusopen or resolved
  • assignee_name — assignee name (null if none)
  • When all Cases are resolved, the frontend syncs feedback status to resolved

Comment — full feedback

json
{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "pathname": "/pricing",
  "report_id": "price-card",
  "report_type": "item",
  "target_selector": "[data-report-id=\"price-card\"]",
  "cases": [
    {
      "id": "case-001",
      "text": "가격 카드가 모바일에서 잘림",
      "status": "open",
      "assignee_name": null,
      "created_at": "2026-06-07T09:00:00.000Z",
      "updated_at": "2026-06-07T09:00:00.000Z"
    }
  ],
  "status": "open",
  "field_values": { "isBug": true },
  "position": {
    "target": { "x": 0.25, "y": 0.75 },
    "viewport": { "x": 0.42, "y": 0.18, "width": 1440, "height": 900 },
    "scrollY": 120,
    "anchor": null
  },
  "created_at": "2026-06-07T09:00:00.000Z",
  "environment": "stage",
  "app_version": "1.2.0",
  "author_id": "user-1",
  "author_name": "김아영",
  "integrations": {
    "github": {
      "issue_number": 42,
      "issue_url": "https://github.com/org/repo/issues/42",
      "issued_at": "2026-06-07T10:00:00.000Z"
    }
  }
}

field_values holds custom field values from <FivePixels fields={[...]} />. Separate from cases[].text.

CommentSummary — list API response

Returned by onList / onListAll. No `replies[]`. Only reply_count and latest_reply.

json
{
  "id": "cmt-001",
  "pathname": "/pricing",
  "report_id": "price-card",
  "report_type": "item",
  "cases": [
    {
      "id": "case-001",
      "text": "가격 카드가 모바일에서 잘림",
      "status": "open",
      "assignee_name": null,
      "created_at": "2026-06-07T09:00:00.000Z",
      "updated_at": "2026-06-07T09:00:00.000Z"
    }
  ],
  "status": "open",
  "field_values": { "isBug": true },
  "position": { "...": "..." },
  "created_at": "2026-06-07T09:00:00.000Z",
  "reply_count": 3,
  "latest_reply": {
    "id": "rpl-003",
    "message": "iOS Safari에서 재현됨",
    "created_at": "2026-06-07T11:30:00.000Z",
    "status": "found_error",
    "case_ids": ["case-001"],
    "author_type": "manager",
    "author_name": "PM"
  }
}

Zero replies → reply_count: 0, latest_reply: null.

Comment key fields

FieldRequiredDescription
pathnamePage path (e.g. /pricing)
report_idDOM data-report-id
report_typegroup or item
casesCase array (1 or more)
statusopen | git_issued | resolved | archived
field_valuesCustom field values
positionMarker position (see below)
reply_countlist onlyNumber of replies
latest_replylist onlyLatest reply summary (includes case_ids)
target_selectorCSS selector when no report_id
integrationsGitHub Issue metadata

position — marker placement

Keys are camelCase (scrollY, reportId, etc.). One JSON column is fine.

json
{
  "target": { "x": 0.25, "y": 0.75 },
  "viewport": { "x": 0.42, "y": 0.18, "width": 1440, "height": 900 },
  "scrollY": 120,
  "anchor": null
}

anchor example (inside modal, etc.)

json
{
  "reportId": "modal-demo",
  "reportType": "group",
  "x": 0.5,
  "y": 0.5
}

Reply

json
{
  "id": "rpl-001",
  "comment_id": "cmt-001",
  "message": "재현 확인했습니다",
  "created_at": "2026-06-07T10:00:00.000Z",
  "status": "suggested",
  "case_ids": ["case-001"],
  "parent_reply_id": null,
  "author_type": "manager",
  "author_name": "리뷰어"
}
statusMeaning
suggestedSuggestion or answer
additional_questionFollow-up question
found_errorError found
recheck_requestedRecheck requested
resolvedResolved
author_typeMeaning
userFeedback author
managerReviewer/PM
systemSystem message (GitHub Issue, etc.)

API endpoints

Paths are examples. Adapt to your conventions, but keep request/response JSON shapes.

GET — current page list (onList)

DB: comments SELECT + replies aggregation (reply_count, latest_reply)

Called on page load, pathname change, and after create/update/delete.

http
GET /api/projects/{projectId}/comments?pathname=/pricing&environment=stage
json
[
  {
    "id": "cmt-001",
    "pathname": "/pricing",
    "report_id": "price-card",
    "cases": [{ "...": "..." }],
    "reply_count": 3,
    "latest_reply": { "...": "..." },
    "...": "..."
  }
]
  • Return items with exact pathname match
  • Sort by created_at descending (recommended)
  • Include reply_count and latest_reply — no replies[]

List aggregation SQL (PostgreSQL)

sql
SELECT
  c.*,
  COUNT(r.id) AS reply_count,
  (
    SELECT row_to_json(lr)
    FROM (
      SELECT id, message, created_at, status, case_ids, author_type, author_name
      FROM replies
      WHERE comment_id = c.id
      ORDER BY created_at DESC
      LIMIT 1
    ) lr
  ) AS latest_reply
FROM comments c
LEFT JOIN replies r ON r.comment_id = c.id
WHERE c.project_id = $1 AND c.environment = $2 AND c.pathname = $3
GROUP BY c.id
ORDER BY c.created_at DESC;

GET — all pages (onListAll, optional)

DB: comments + replies aggregation (same as onList), no pathname filter + cursor pagination

http
GET /api/projects/{projectId}/comments?cursor=abc&limit=100&environment=stage
json
{
  "items": [
    { "...CommentSummary" }
  ],
  "nextCursor": "def"
}

Omit or null nextCursor on the last page. Skip this API if onListAll is not wired.

GET — thread (onListReplies)

DB: replies WHERE comment_id = :id, ORDER BY created_at ASC

Called when opening a marker or list thread. Return 404 if comment not found.

http
GET /api/projects/{projectId}/comments/cmt-001/replies
json
[
  {
    "id": "rpl-001",
    "comment_id": "cmt-001",
    "message": "재현 확인했습니다",
    "created_at": "2026-06-07T10:00:00.000Z",
    "status": "suggested",
    "case_ids": ["case-001"],
    "parent_reply_id": null,
    "author_type": "manager",
    "author_name": "리뷰어"
  }
]
  • That comment's replies only, created_at ascending
  • Each Reply includes case_ids

POST — create feedback (onCreate)

DB: comments INSERT (includes cases JSONB; server generates id, created_at)

http
POST /api/projects/{projectId}/comments
Content-Type: application/json
json
{
  "pathname": "/pricing",
  "report_id": "price-card",
  "report_type": "item",
  "target_selector": "[data-report-id=\"price-card\"]",
  "cases": [
    {
      "id": "case-001",
      "text": "가격 카드가 모바일에서 잘림",
      "status": "open",
      "assignee_name": null,
      "created_at": "2026-06-07T09:00:00.000Z",
      "updated_at": "2026-06-07T09:00:00.000Z"
    }
  ],
  "status": "open",
  "field_values": { "isBug": true },
  "position": {
    "target": { "x": 0.5, "y": 0.5 },
    "viewport": { "x": 0.42, "y": 0.18, "width": 1440, "height": 900 },
    "scrollY": 120,
    "anchor": null
  },
  "environment": "stage",
  "app_version": "1.2.0",
  "author_id": "user-1",
  "author_name": "김아영"
}
json
{
  "id": "cmt-001",
  "pathname": "/pricing",
  "cases": [{ "...": "..." }],
  "status": "open",
  "reply_count": 0,
  "latest_reply": null,
  "...": "..."
}
  • Server generates comment id; client sends cases[].id
  • At least one case; each text must not be whitespace-only
  • Response 201: reply_count: 0, latest_reply: null

POST — create reply (onCreateReply)

DB: replies INSERT (includes case_ids JSONB)

http
POST /api/projects/{projectId}/comments/cmt-001/replies
Content-Type: application/json
json
{
  "message": "iOS Safari에서 재현됨",
  "status": "found_error",
  "case_ids": ["case-001"],
  "parent_reply_id": null,
  "author_type": "manager",
  "author_name": "PM"
}
  • INSERT into replies table
  • case_ids must reference that comment's cases[].id
  • GitHub Issue notifications use author_type: system on the same API

PATCH — update feedback (onUpdate)

DB: comments UPDATE (partial merge: cases, status, field_values, integrations, etc.)

Send only changed fields (partial update). v2 does not accept a replies field.

http
PATCH /api/projects/{projectId}/comments/cmt-001
Content-Type: application/json
json
{
  "cases": [
    {
      "id": "case-001",
      "text": "수정된 케이스 내용",
      "status": "resolved",
      "assignee_name": "PM",
      "created_at": "2026-06-07T09:00:00.000Z",
      "updated_at": "2026-06-07T12:00:00.000Z"
    }
  ],
  "status": "resolved"
}

Custom field update example

json
{
  "field_values": { "isBug": true, "isImportant": false }
}

GitHub Issue example

json
{
  "status": "git_issued",
  "integrations": {
    "github": {
      "issue_number": 42,
      "issue_url": "https://github.com/org/repo/issues/42",
      "issued_at": "2026-06-07T10:00:00.000Z"
    }
  }
}
Allowed PATCH fieldsDescription
casesReplace entire case array
statusFeedback status
field_valuesCustom fields
integrationsGitHub etc.
report_id / report_typeChange target element (rare)

Do not PATCH position after create — there is no move-marker UI.
v1 fallback: without onCreateReply, the frontend may PATCH a replies array. v2 APIs can ignore it or return 400.

DELETE (onDelete, optional)

DB: comments DELETE (prefer CASCADE for replies)

http
DELETE /api/projects/{projectId}/comments/cmt-001

Response 204 No Content. Prefer ON DELETE CASCADE for replies.

Team / reviewers API (optional)

Separate from feedback CRUD. fivepixels only accepts team.reviewers as a prop. Without an API, hardcode in frontend or fall back to free-text input.

Role split

DataSourceStored?
team.reviewersGET /members → propNo (dropdown options)
team.userLogin sessionNo (current user)
cases[].assignee_nameReview actionsYes, in comments.cases JSON

reviewers = dropdown options. assignee_name = actual assignment. No FK needed — store as a string snapshot.

Member shape

json
{
  "id": "1",
  "name": "Alex, QA Assistant",
  "publicKey": "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----"
}
  • publicKey — only needed with requireReviewerKey
  • Empty reviewers array → free-text input fallback

GET — reviewer list

DB: project_members (or team DB) SELECT — no FK to feedback tables needed

http
GET /api/projects/{projectId}/members?environment=stage
json
[
  { "id": "1", "name": "Alex, QA Assistant" },
  { "id": "2", "name": "Sophia, QA" }
]

Frontend wiring example

tsx
function AppWithFivePixels() {
  const [reviewers, setReviewers] = useState<ReportAuthor[]>([]);

  useEffect(() => {
    fetch("/api/projects/my-app/members?environment=stage")
      .then((r) => r.json())
      .then(setReviewers);
  }, []);

  return (
    <FivePixels
      project={{ id: "my-app", env: "stage" }}
      team={{
        user: { id: "demo-user", name: "Alex" },
        reviewers,
      }}
      onList={...}
      onCreate={...}
      onUpdate={...}
    />
  );
}

Errors · auth

Error responses

The library shows a generic failure message on HTTP errors. Response body format is up to your API standard.

CodeSituation
400Invalid JSON or missing required fields
404Unknown comment or reply id
409(optional) Concurrent edit conflict
500Server error

Auth signature (optional)

With requireReviewerKey, create/update/reply bodies may include auth.

json
{
  "auth": {
    "author_id": "reviewer-1",
    "algorithm": "ECDSA-P256-SHA256",
    "action": "feedback:create",
    "signed_at": "2026-06-07T09:00:00.000Z",
    "signature": "base64url..."
  }
}
actionWhen
feedback:createFeedback creation
feedback:updateFeedback edit
reply:createReply creation

Skip auth verification if you do not use team keys.

Frontend wiring · call flow

When your backend is ready, wire handlers and team prop like this:

tsx
<FivePixels
  project={{ id: "my-app", env: "stage", version: "1.2.0" }}
  team={{
    user: { id: "demo-user", name: "Alex" },
    reviewers, // GET /members 결과
  }}
  onList={({ pathname }) =>
    fetch(
      `/api/projects/my-app/comments?pathname=${encodeURIComponent(pathname)}&environment=stage`,
    ).then((r) => r.json())
  }
  onListAll={({ cursor, limit }) => {
    const params = new URLSearchParams({ limit: String(limit), environment: "stage" });
    if (cursor) params.set("cursor", cursor);
    return fetch(`/api/projects/my-app/comments?${params}`).then((r) => r.json());
  }}
  onListReplies={(commentId) =>
    fetch(`/api/projects/my-app/comments/${commentId}/replies`).then((r) => r.json())
  }
  onCreate={(payload) =>
    fetch("/api/projects/my-app/comments", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(payload),
    }).then((r) => r.json())
  }
  onCreateReply={(commentId, payload) =>
    fetch(`/api/projects/my-app/comments/${commentId}/replies`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(payload),
    }).then((r) => r.json())
  }
  onUpdate={(id, payload) =>
    fetch(`/api/projects/my-app/comments/${id}`, {
      method: "PATCH",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(payload),
    }).then((r) => r.json())
  }
  onDelete={(id) =>
    fetch(`/api/projects/my-app/comments/${id}`, { method: "DELETE" }).then(() => undefined)
  }
/>

Call flow

text
[앱 시작]
  → GET /members → team.reviewers 주입

[페이지 진입]
  → GET /comments?pathname=...
  → 요약 목록 (cases + reply_count, replies[] 없음)

[마커 +N / hover]
  → onList 응답의 reply_count, latest_reply 사용

[스레드 열기]
  → GET /comments/{id}/replies
  → Reply[] (case_ids 포함)

[피드백 작성]
  → POST /comments (cases[] 포함)
  → GET /comments?pathname=... (재조회)

[답변 작성]
  → POST /comments/{id}/replies (case_ids 포함)
  → GET /comments/{id}/replies 또는 목록 재조회

[케이스 / 상태 수정]
  → PATCH /comments/{id}
  → GET /comments?pathname=... (재조회)

[삭제]
  → DELETE /comments/{id}
  → GET /comments?pathname=... (재조회)

[전체 목록 탭]
  → GET /comments?cursor=&limit=100

DB · checklist

Recommended two-table schema

sql
comments
  id              UUID PK
  project_id      VARCHAR
  environment     VARCHAR
  pathname        VARCHAR
  report_id       VARCHAR
  report_type     VARCHAR
  target_selector VARCHAR NULL
  cases           JSONB          -- Case[] 배열
  status          VARCHAR
  field_values    JSONB
  position        JSONB
  app_version     VARCHAR
  author_id       VARCHAR
  author_name     VARCHAR
  integrations    JSONB
  created_at      TIMESTAMPTZ
  updated_at      TIMESTAMPTZ

replies
  id              UUID PK
  comment_id      UUID FK → comments(id) ON DELETE CASCADE
  message         TEXT
  status          VARCHAR
  case_ids        JSONB          -- string[] (케이스 ID 목록)
  parent_reply_id UUID NULL
  author_type     VARCHAR
  author_name     VARCHAR
  created_at      TIMESTAMPTZ

Suggested indexes: (project_id, environment, pathname, created_at DESC), (comment_id, created_at ASC)

You do not need to build

  • Keyword/status filter APIs
  • Embedded replies array / PATCH overwrite (v2)
  • Realtime push / WebSocket
  • Individual position updates
  • Top-level message field (legacy — use cases[])

Pre-launch checklist

  • GET /comments — pathname filter, CommentSummary[] (no replies[], includes cases[])
  • GET /comments/{id}/replies — Reply[] with case_ids, created_at ASC
  • POST /comments — at least one case, server generates id/created_at
  • POST /comments/{id}/replies — single reply INSERT with case_ids
  • PATCH /comments/{id} — partial merge, no replies field
  • position camelCase (scrollY, reportId), other fields snake_case
  • (optional) DELETE, listAll + cursor, projectId/environment scope
  • (optional, recommended) GET /members → team.reviewers
fivepixels