The Clawstore backend is a single Hono app deployed as a Cloudflare Worker at api.useclawstore.com. It owns all read and write paths for the registry: authentication, publishing, search, agent metadata, tarball serving, yank, reports. The CLI and the web frontend both consume it — the web Worker through a Cloudflare service binding, the CLI over public HTTPS.
There is no second backend. The discovery website does not talk to D1 directly; it goes through the same API as clawstore install.
+-----------+ +-----------+
| clawstore | | clawstore |
| CLI | | web |
+-----+-----+ +-----+-----+
| |
| HTTPS | Cloudflare
| api.useclawstore.com | service binding
| |
v v
+--------------------------------+
| Clawstore API |
| (Hono on Worker) |
| |
| /api/auth/* Better Auth |
| /v1/* Clawstore API |
+--------+----------+------------+
| | |
v v v
+-----+ +-----+ +-----+
| D1 | | R2 | | KV |
+-----+ +-----+ +-----+
Same code path serves the CLI and the web frontend. Service-binding calls from the web Worker are sub-millisecond; public HTTPS calls from the CLI are standard Worker ingress.
Authentication is handled by Better Auth via the better-auth-cloudflare wrapper. Sessions are stored in D1; rate-limit counters are stored in KV; R2 is available as a Better Auth binding but Clawstore's own code uses R2 directly.
All Better Auth endpoints are mounted under /api/auth/*. The web frontend uses the GitHub OAuth flow; Better Auth handles the redirect, token exchange, session cookie creation, and CSRF protection. Clawstore adds no routes under /api/auth.
| Method | Path | Description |
|---|---|---|
GET |
/api/auth/sign-in/social/github |
Start the GitHub OAuth flow |
GET |
/api/auth/callback/github |
OAuth callback (internal) |
GET |
/api/auth/get-session |
Return the current session, if any |
POST |
/api/auth/sign-out |
Destroy the session |
Refer to Better Auth's documentation for the full endpoint surface. Clawstore relies on Better Auth's defaults.
The CLI authenticates via the OAuth 2.0 Device Authorization flow, implemented by Better Auth's deviceAuthorization plugin.
- CLI calls
POST /api/auth/device/code→ receivesdevice_code,user_code,verification_uri_complete. - CLI opens the browser to the verification URL on the web frontend (
/device?user_code=...). - The operator signs in with GitHub (if needed) and clicks Approve on the device page, which calls
POST /api/auth/device/approve. - CLI polls
POST /api/auth/device/token→ receivesaccess_tokenonce approved. - The token is stored in
~/.clawstore/auth.jsonwith0600permissions and presented viaAuthorization: Bearer <token>on every subsequent request.
Clawstore adds no custom token routes — Better Auth's bearer and device authorization plugins handle the full lifecycle.
All Clawstore-owned routes are under /v1/. Breaking changes ship as a new prefix (/v2/) and the CLI's clawstore doctor warns operators when their client is too old to speak the current prefix. The CLI's registry base URL is configurable so enterprise or private deployments can point at alternate hosts.
Better Auth routes live under /api/auth/* and are versioned independently by the Better Auth project.
| Method | Path | Auth | Description |
|---|---|---|---|
GET |
/v1/health |
none | Liveness check. Returns { ok: true, version }. |
GET |
/v1/categories |
none | Return the curated category list: [{ id, name, icon }]. |
| Method | Path | Auth | Description |
|---|---|---|---|
GET |
/v1/me |
required | Return the authenticated user's identity and profile: { id, name, email, image, scope, ownedAgentCount, profile: { bio, website, location, avatarUrl, displayName } }. |
Public profiles for any user. Profile data is seeded from GitHub on first sign-in and editable by the owner.
| Method | Path | Auth | Description |
|---|---|---|---|
GET |
/v1/users/:username |
none | Public profile: { githubLogin, displayName, avatarUrl, bio, website, location, createdAt, agents: [{ scope, name, tagline, avgRating, downloadCount }] }. |
PUT |
/v1/users/:username/profile |
required | Update the caller's own profile. Body: { bio?, website?, location?, displayName? }. Returns 403 if :username does not match the caller's scope. |
:scope in the URL is the publisher's GitHub username (no @ — the client adds it back when displaying). :name is the agent name within the scope.
| Method | Path | Auth | Description |
|---|---|---|---|
GET |
/v1/agents |
none | List or search agents. See query params below. |
GET |
/v1/agents/:scope/:name |
none | Agent detail including the current latest version manifest. |
GET |
/v1/agents/:scope/:name/versions |
none | Version history for an agent (paginated). |
GET |
/v1/agents/:scope/:name/versions/:version |
none | A specific version's metadata and full agent.json. |
Query params for GET /v1/agents:
| Param | Type | Description |
|---|---|---|
q |
string | Search query. Matched with LIKE against name, tagline, and tags. |
category |
string | Filter by category id. |
tag |
string | Filter by tag. Repeatable. |
channel |
community|official |
Filter by channel. Defaults to all visible channels. |
scope |
string | Filter by publisher scope. |
sort |
recent|name|downloads|rating |
Sort order. Defaults to recent. |
limit |
int | Page size. 1–100. Defaults to 20. |
cursor |
string | Pagination cursor returned by the previous response. |
MVP search is LIKE with indexes on name, tagline, and tags. See Data Model for the indexing strategy and the reason FTS5 is deliberately off the table.
| Method | Path | Auth | Description |
|---|---|---|---|
GET |
/v1/agents/:scope/:name/versions/:version/tarball |
none | Download the tarball. Content-addressed URL; Cache-Control: public, max-age=31536000, immutable. |
GET |
/v1/agents/:scope/:name/versions/:version/assets/:assetId |
none | Serve a store asset (icon, screenshot) by ID. Same cache headers. |
Downloads increment a counter on the version row. Counters are bucketed per day and not blocked on — the request returns whether or not the counter write succeeds.
| Method | Path | Auth | Description |
|---|---|---|---|
POST |
/v1/agents/publish |
token | Authenticated tarball upload. Request body is multipart/form-data with one file part (tarball, the .tgz) and one JSON part (metadata, redundant manifest for quick rejection). Server validates, extracts assets, writes the version row, returns the live URL. |
POST |
/v1/agents/:scope/:name/versions/:version/yank |
token | Yank a published version. Owner-only. Body: { reason? }. Idempotent — yanking an already-yanked version is a no-op. |
POST |
/v1/agents/:scope/:name/versions/:version/unyank |
token | Reverse a yank. Maintainer-only (owners cannot unyank their own yanked versions — avoids accidental re-exposure of bad content). |
The publish endpoint runs, in order:
- Authentication — verify the bearer token, resolve to a user.
- Ownership — if the
idalready exists, verify the caller is the owner. If it doesn't, claim it. - Version monotonicity — the new version must be strictly greater than any existing published version for this
id. - Deterministic validation — run
packages/validatoragainst the uploaded tarball. Identical to theclawstore validatelogic the author already ran locally. - Executable scan + secret scan — hard fail on any finding.
- Plugin reachability — for each
dependencies.plugins[], confirm the spec resolves (ClawHub entry exists, npm package exists, git URL responds). - Extract assets — stream the tarball, write the full
.tgztotarballs/:scope/:name/:version.tgzin R2, extract icon and screenshots toassets/:scope/:name/:version/...with immutable cache headers. - Persist — write the version row to D1, update the agent's
latestpointer (unless this is a pre-release), insert any new owner claim. - Return — response body includes the canonical URLs for the tarball, each asset, and the version detail endpoint.
Failures at any step abort the publish. Partial writes are cleaned up — the R2 uploads are idempotent and the D1 insert is the last step.
| Method | Path | Auth | Description |
|---|---|---|---|
POST |
/v1/updates |
token (optional) | Bulk update check. Request body: { installs: [{ id, version }] }. Response: { updates: [{ id, from, to, channel, yanked }] }. Used by clawstore update check. |
This is a single round-trip for the common case where an operator has 10 installed agents and wants to check all of them at once.
| Method | Path | Auth | Description |
|---|---|---|---|
POST |
/v1/reports |
none (optional auth) | File a report against an agent or version. Body: { agentId, versionId?, reason, details }. Rate-limited hard. |
GET |
/v1/reports |
maintainer | List pending reports. Maintainer-only. |
POST |
/v1/reports/:id/resolve |
maintainer | Resolve a report. |
Reports land in the database for maintainer triage. There is no pre-publish human review — moderation is entirely post-hoc. See Documentation hub § Open publish.
Authenticated users can leave one review per agent. Authors cannot review their own agents. Reviews carry a 1–5 star rating and optional text.
| Method | Path | Auth | Description |
|---|---|---|---|
GET |
/v1/agents/:scope/:name/reviews |
none | List reviews for an agent. Paginated, sorted by created_at DESC. Response includes avgRating and reviewCount in metadata. |
POST |
/v1/agents/:scope/:name/reviews |
required | Create a review. Body: { rating, title?, body? }. Returns 409 if the user already reviewed this agent. Returns 403 if the user is the agent owner. |
PUT |
/v1/agents/:scope/:name/reviews/:id |
required | Update the caller's own review. Body: { rating?, title?, body? }. Returns 403 if the review belongs to another user. |
DELETE |
/v1/agents/:scope/:name/reviews/:id |
required | Delete the caller's own review, or any review if the caller is a maintainer. |
On every write operation (create, update, delete), the API recalculates agents.avg_rating and agents.review_count inline.
Agent detail responses (GET /v1/agents/:scope/:name) include avgRating, reviewCount, and downloadCount alongside existing fields.
Every successful JSON response is a plain object. Collection endpoints return:
{
"items": [...],
"nextCursor": "..."
}Resource endpoints return the resource directly, not wrapped in a data envelope.
{
"error": {
"code": "version_not_monotonic",
"message": "Version 0.3.1 is not greater than the current published 0.3.1",
"details": { "id": "@someone/calorie-coach", "current": "0.3.1", "requested": "0.3.1" }
}
}Error codes are snake_case and stable — the CLI pattern-matches them for user-friendly messages. The HTTP status code matches semantically: 400 for validation, 401 for unauthenticated, 403 for unauthorized, 404 for missing, 409 for conflict, 413 for size limits, 422 for validator findings, 429 for rate limited, 5xx for server errors.
Rate limits are enforced via Better Auth's KV-backed rate limiter for auth endpoints, and a Hono middleware with its own KV counters for the rest. All limits are per-IP + per-token.
| Endpoint class | Limit | Window |
|---|---|---|
/api/auth/* |
100 | 1 minute |
POST /v1/agents/publish |
20 | 1 hour (per user) |
POST /v1/reports |
10 | 1 hour (per IP) |
POST/PUT/DELETE /v1/.../reviews |
30 | 1 hour (per user) |
All other GET /v1/* (read) |
600 | 1 minute |
All other POST /v1/* (write) |
60 | 1 minute |
Limits may be tuned after launch based on observed traffic. They exist primarily to make abuse expensive, not to enforce a paid tier.
useclawstore.com and api.useclawstore.com are separate Cloudflare Workers bound to the same D1, R2, and KV resources via SST links. The reasons this split exists rather than a single path-routed Worker:
- Independent caching. Tarball downloads from
api.useclawstore.comget aggressive long-TTL CDN caching; the web app onuseclawstore.comgets short-TTL HTML caching. A unified domain would require per-path cache rules, which is fragile. - CORS is trivial. The web app explicitly allows
useclawstore.comas the origin forapi.useclawstore.com. A single domain would need no CORS but makes local dev setup messier. - CLI base URL is a constant.
clawstoreCLI ships withhttps://api.useclawstore.com/v1hardcoded (overridable via env var). Future path changes to the web app never affect CLI clients. - The web Worker calls the API via a service binding, not HTTPS. Same-binding calls are sub-millisecond regardless of the URL shape.
- Data Model — the D1 schema that backs these routes
- Agent Package — the tarball format publish validates against
- Documentation hub