Data format
Comments live under COMMENTS_PATH (default comments/).
comments/
index.json
hello-world.json
posts/another-post.jsonOne aggregate file per post, not one file per comment: the proxy read would otherwise cost one request per comment.
Post file
json
{
"version": 1,
"post": "hello-world",
"updatedAt": "2026-10-30T05:00:00.000Z",
"comments": [
{
"id": "8c8f3690-7bb7-4add-901d-c829b337417e",
"parentId": null,
"name": "Alice",
"content": "Nice.",
"createdAt": "2026-10-30T04:12:00.000Z",
"emailEncrypted": "<base64>",
"emailKeyFingerprint": "a1b2c3d4"
}
]
}| Field | In API | Notes |
|---|---|---|
id | yes | UUID v4, created at submit time |
parentId | yes | Must exist in the same file; two levels max in V1 |
name | yes | Plain text |
content | yes | Plain text, escaped on render |
createdAt | yes | Submit time, not approval time |
emailEncrypted | no | AES-256-GCM, key derived from COMMENTS_SECRET |
emailKeyFingerprint | no | Identifies old ciphertext after rotation |
When no email is provided, all three email fields are omitted.
Index file
json
{
"version": 1,
"updatedAt": "2026-10-30T05:00:00.000Z",
"posts": { "hello-world": { "count": 12, "updatedAt": "2026-10-30T05:00:00.000Z" } }
}The index is a convenience counter. Reads never depend on it, and it may briefly lag.
Slug rules
- Lowercase letters, digits,
-,_,/ - Starts with a letter or digit
- At most 179 characters
- No
.., no absolute path - A trailing
.jsonis stripped
Concurrency
Writes use the file sha as an optimistic lock. A conflicting approval re-reads and retries a bounded number of times. The index write is independent and never fails the approval.
History is permanent
Deleting a comment removes the current version only. Git history keeps it.