Quick start
Get Commently running locally in under 5 minutes. You need Node.js 20+ and PostgreSQL 16+.
# Clone and install git clone https://github.com/itayshmool/commently.git cd commently npm install # Create the database createdb commently # Configure cp .env.example .env # Edit .env — set DATABASE_URL and ADMIN_API_KEY # Run migrations and start npm run db:migrate npm run dev # → Commently listening on port 3002
Once running, register your first tenant:
curl -X POST http://localhost:3002/api/tenants \ -H "Authorization: Bearer YOUR_ADMIN_KEY" \ -H "Content-Type: application/json" \ -d '{"name": "my-app"}' # → {"success":true,"data":{"id":"...","name":"my-app","api_key":"sk_commently_..."}} # Save the api_key — it's shown only once.
Authentication
Commently uses two authentication mechanisms:
Tenant API keys (for your app)
Every request from your application must include the X-API-Key header. Keys use the format sk_commently_<32 random chars> and are stored as SHA-256 hashes.
X-API-Key: sk_commently_abc123def456...
Admin key (for tenant management)
Tenant management endpoints require the master admin key set via the ADMIN_API_KEY environment variable, passed in the Authorization header:
Authorization: Bearer your-admin-key
author_id / author_name with each request. Commently only verifies that your app is who it says it is.
Response format
All responses follow a consistent envelope:
{
"success": true,
"data": { ... }
}
{
"success": false,
"error": {
"code": "NOT_FOUND",
"message": "Comment not found"
}
}
Create tenant
Register a new consuming application. Requires admin authentication.
| Field | Type | Description |
|---|---|---|
| name required | string | Unique human-readable identifier for the tenant |
curl -X POST http://localhost:3002/api/tenants \ -H "Authorization: Bearer $ADMIN_KEY" \ -H "Content-Type: application/json" \ -d '{"name": "deckdrop"}'
{
"success": true,
"data": {
"id": "a1b2c3d4-...",
"name": "deckdrop",
"api_key": "sk_commently_abc123..."
}
}
api_key is returned only in this response. Store it securely — it cannot be retrieved later.
List tenants
List all registered tenants with their key prefixes. Requires admin authentication.
curl http://localhost:3002/api/tenants \
-H "Authorization: Bearer $ADMIN_KEY"
{
"success": true,
"data": [
{
"id": "a1b2c3d4-...",
"name": "deckdrop",
"api_key_prefix": "sk_commently_abc1...",
"created_at": "2026-06-01T10:00:00Z"
}
]
}
Rotate API key
Generate a new API key for a tenant. The previous key is invalidated immediately. Requires admin authentication.
curl -X POST http://localhost:3002/api/tenants/$TENANT_ID/rotate-key \
-H "Authorization: Bearer $ADMIN_KEY"
{
"success": true,
"data": {
"api_key": "sk_commently_newkey..."
}
}
Delete tenant
Permanently delete a tenant and all its comments. This action cannot be undone. Requires admin authentication.
curl -X DELETE "http://localhost:3002/api/tenants/$TENANT_ID" \ -H "Authorization: Bearer $ADMIN_KEY"
{
"success": true,
"data": { "id": "a1b2c3d4-...", "deleted": true }
}
Create comment
Post a new comment or reply. Requires tenant API key.
| Field | Type | Description |
|---|---|---|
| resource_id required | string | Opaque ID of the resource being commented on (max 500 chars) |
| author_id required | string | Your app's user identifier (max 500 chars) |
| author_name required | string | Display name of the comment author |
| body required | string | Comment content (1–5000 characters) |
| context_key | string | Sub-anchor within the resource, e.g. slide:3 (max 200 chars) |
| author_role | string | Role label, e.g. owner, viewer |
| parent_id | uuid | ID of parent comment for threading (must be on same resource, no nested replies) |
curl -X POST http://localhost:3002/api/comments \ -H "X-API-Key: $API_KEY" \ -H "Content-Type: application/json" \ -d '{ "resource_id": "deck-uuid-123", "context_key": "slide:3", "author_id": "viewer@gmail.com", "author_name": "Dana", "author_role": "viewer", "body": "Love this chart!" }'
{
"success": true,
"data": {
"id": "c9a2f1b3-...",
"resource_id": "deck-uuid-123",
"context_key": "slide:3",
"author_id": "viewer@gmail.com",
"author_name": "Dana",
"author_role": "viewer",
"body": "Love this chart!",
"parent_id": null,
"created_at": "2026-06-05T10:30:00Z"
}
}
List comments
Fetch comments for a resource. Replies are nested under their parent (one level deep). Results are in chronological order with cursor-based pagination.
| Param | Type | Description |
|---|---|---|
| resource_id required | string | The resource to fetch comments for |
| context_key | string | Filter to a specific context, e.g. slide:3 |
| cursor | uuid | Pagination cursor (ID of last item from previous page) |
| limit | integer | Max results per page (default 50, max 100) |
curl "http://localhost:3002/api/comments?resource_id=deck-uuid-123&context_key=slide:3" \ -H "X-API-Key: $API_KEY"
{
"success": true,
"data": {
"comments": [
{
"id": "c9a2f1b3-...",
"resource_id": "deck-uuid-123",
"context_key": "slide:3",
"author_id": "viewer@gmail.com",
"author_name": "Dana",
"author_role": "viewer",
"body": "Love this chart!",
"parent_id": null,
"created_at": "2026-06-05T10:30:00Z",
"replies": [
{
"id": "d4e5f6a7-...",
"author_id": "itay@wix.com",
"author_name": "Itay",
"author_role": "owner",
"body": "Thanks! Data is from Q3 report",
"parent_id": "c9a2f1b3-...",
"created_at": "2026-06-05T10:32:00Z"
}
]
}
],
"pagination": {
"next_cursor": "c9a2f1b3-...",
"has_more": false
}
}
}
Get comment counts
Get total comment counts for one or more resources, optionally grouped by context key. Useful for rendering badges and indicators.
| Param | Type | Description |
|---|---|---|
| resource_id | string | Single resource ID |
| resource_ids | string | Comma-separated list of IDs (batch mode, max 50) |
| group_by | string | Set to context_key for per-context counts |
curl "http://localhost:3002/api/comments/counts?resource_id=deck-uuid-123&group_by=context_key" \ -H "X-API-Key: $API_KEY"
{
"success": true,
"data": {
"total": 8,
"groups": {
"slide:0": 2,
"slide:3": 5,
"slide:7": 1
}
}
}
curl "http://localhost:3002/api/comments/counts?resource_ids=deck-1,deck-2,deck-3" \ -H "X-API-Key: $API_KEY"
{
"success": true,
"data": {
"counts": {
"deck-1": 8,
"deck-2": 3,
"deck-3": 0
}
}
}
Delete comment
Soft-delete a comment. Deleted comments are excluded from all list and count queries. Replies to a deleted parent remain visible — your app can render the parent as "[deleted]". Optionally pass X-Author-Id header for audit logging.
curl -X DELETE "http://localhost:3002/api/comments/$COMMENT_ID" \ -H "X-API-Key: $API_KEY" \ -H "X-Author-Id: admin@example.com"
{
"success": true,
"data": {
"id": "c9a2f1b3-...",
"deleted_at": "2026-06-05T11:00:00Z",
"deleted_by": "admin@example.com"
}
}
Edit comment
Edit a comment's body. Only the original author can edit, and only within the edit window (default 15 minutes, configurable via COMMENT_EDIT_WINDOW_MINUTES env var).
| Field | Type | Description |
|---|---|---|
| author_id required | string | Must match the comment's original author |
| body required | string | New comment body (1–5000 characters) |
curl -X PATCH "http://localhost:3002/api/comments/$COMMENT_ID" \ -H "X-API-Key: $API_KEY" \ -H "Content-Type: application/json" \ -d '{"author_id": "viewer@gmail.com", "body": "Updated comment text"}'
{
"success": true,
"data": {
"id": "c9a2f1b3-...",
"body": "Updated comment text",
"edited_at": "2026-06-05T10:35:00Z"
}
}
FORBIDDEN (403) if the author doesn't match, EDIT_WINDOW_EXPIRED (403) if the edit window has passed.
Add reaction
Add an emoji reaction to a comment. Duplicate reactions (same author + same emoji) are silently ignored.
| Field | Type | Description |
|---|---|---|
| author_id required | string | Who is reacting |
| emoji required | string | Emoji string, max 32 chars |
curl -X POST "http://localhost:3002/api/comments/$COMMENT_ID/reactions" \ -H "X-API-Key: $API_KEY" \ -H "Content-Type: application/json" \ -d '{"author_id": "user@example.com", "emoji": "👍"}'
{
"success": true,
"data": {
"id": "r1a2b3c4-...",
"comment_id": "c9a2f1b3-...",
"author_id": "user@example.com",
"emoji": "👍"
}
}
List reactions
Get reactions for a comment, grouped by emoji with counts and author lists.
{
"success": true,
"data": [
{ "emoji": "👍", "count": 3, "authors": ["alice@ex.com", "bob@ex.com", "carol@ex.com"] },
{ "emoji": "❤️", "count": 1, "authors": ["alice@ex.com"] }
]
}
Remove reaction
Remove a specific reaction. Pass author_id and emoji in the request body.
curl -X DELETE "http://localhost:3002/api/comments/$COMMENT_ID/reactions" \ -H "X-API-Key: $API_KEY" \ -H "Content-Type: application/json" \ -d '{"author_id": "user@example.com", "emoji": "👍"}'
Create webhook
Register a webhook URL to receive notifications when comments are created or deleted. Requires tenant API key.
| Field | Type | Description |
|---|---|---|
| url required | string | HTTPS endpoint to receive webhook payloads |
| events | string[] | Events to subscribe to (default: ["comment.created", "comment.deleted"]) |
| secret | string | HMAC secret — payloads are signed with X-Commently-Signature |
curl -X POST http://localhost:3002/api/webhooks \ -H "X-API-Key: $API_KEY" \ -H "Content-Type: application/json" \ -d '{"url": "https://myapp.com/webhook", "secret": "my-signing-secret"}'
{
"event": "comment.created",
"data": { "id": "...", "body": "...", ... },
"timestamp": "2026-06-05T10:30:00.000Z"
}
List webhooks
List all webhooks for the authenticated tenant.
Delete webhook
Remove a webhook registration.
Export resource comments
Export all comments (including deleted) for a resource. Requires tenant API key.
| Param | Type | Description |
|---|---|---|
| resource_id required | string | The resource to export |
{
"success": true,
"data": {
"resource_id": "deck-uuid-123",
"count": 42,
"comments": [ ... ]
}
}
Export tenant comments
Export all comments for a tenant across all resources. Requires admin authentication.
curl "http://localhost:3002/api/export/tenant/$TENANT_ID" \ -H "Authorization: Bearer $ADMIN_KEY"
Integration guide
Here's how to integrate Commently into your app. This example uses a Node.js backend, but Commently works with any language that can make HTTP requests.
1. Set up a Commently client
const COMMENTLY_URL = process.env.COMMENTLY_URL || 'http://localhost:3002'; const COMMENTLY_KEY = process.env.COMMENTLY_API_KEY; async function commently(method, path, body) { const res = await fetch(`${COMMENTLY_URL}${path}`, { method, headers: { 'X-API-Key': COMMENTLY_KEY, 'Content-Type': 'application/json', }, body: body ? JSON.stringify(body) : undefined, }); return res.json(); }
2. Post a comment from your app
// In your app's comment handler — after verifying the user is authenticated app.post('/api/my-app/comments', async (req, res) => { const user = req.user; // your auth const { resourceId, contextKey, body } = req.body; const result = await commently('POST', '/api/comments', { resource_id: resourceId, context_key: contextKey, author_id: user.email, author_name: user.displayName, author_role: user.role, // "owner", "viewer", etc. body, }); res.json(result); });
3. Fetch comments for a page
app.get('/api/my-app/comments', async (req, res) => { const { resourceId, contextKey, cursor } = req.query; const params = new URLSearchParams({ resource_id: resourceId }); if (contextKey) params.set('context_key', contextKey); if (cursor) params.set('cursor', cursor); const result = await commently('GET', `/api/comments?${params}`); res.json(result); });
4. Show comment count badges
// Get counts per slide for a deck const counts = await commently( 'GET', `/api/comments/counts?resource_id=${deckId}&group_by=context_key` ); // counts.data.groups → { "slide:0": 2, "slide:3": 5 } // Use these to render badges on each slide thumbnail // Batch mode — get total counts for a list of decks const batch = await commently( 'GET', `/api/comments/counts?resource_ids=${deckIds.join(',')}` ); // batch.data.counts → { "deck-1": 8, "deck-2": 3 }
Threading
Commently supports one level of replies. A comment can have replies, but a reply cannot have replies of its own.
// Create a reply await commently('POST', '/api/comments', { resource_id: 'deck-uuid-123', context_key: 'slide:3', author_id: 'bob@example.com', author_name: 'Bob', body: 'Agreed!', parent_id: 'c9a2f1b3-...', // ID of the parent comment });
parent_id must reference an existing, non-deleted comment on the same resource_id. Replies to replies (nested threading) are rejected with a VALIDATION_ERROR.
Context keys
Context keys let you anchor comments to sub-locations within a resource. The format is a free-form string — you define the convention that makes sense for your app.
| App type | context_key pattern | Example |
|---|---|---|
| Slide deck | slide:{index} | slide:3 |
| Blog post | paragraph:{number} | paragraph:12 |
| Tutorial | step:{slug} | step:setup-db |
| Code review | file:{path}:line:{n} | file:src/app.js:line:42 |
| Video | t:{seconds} | t:127 |
Error codes
| Code | HTTP Status | Description |
|---|---|---|
VALIDATION_ERROR | 400 | Missing or invalid request fields |
INVALID_JSON | 400 | Request body is not valid JSON |
UNAUTHORIZED | 401 | Missing, invalid, or expired API key |
FORBIDDEN | 403 | Not allowed (e.g. editing another user's comment) |
EDIT_WINDOW_EXPIRED | 403 | Comment edit window has expired |
NOT_FOUND | 404 | Resource, comment, or tenant not found |
CONFLICT | 409 | Duplicate tenant name |
RATE_LIMITED | 429 | Too many requests — try again later |
INTERNAL_ERROR | 500 | Unexpected server error |