Config, Health, Themes, Push & Recommendations
Config
Section titled “Config”GET /config.json
Section titled “GET /config.json”Unauthenticated. Public runtime config for frontend bootstrapping:
{ "apiUrl": "...", "domain": "...", "siteTitle": "...", "registrationIsOpen": true }Health
Section titled “Health”GET /health
Section titled “GET /health”CORS-open (Access-Control-Allow-Origin: *, so the setup wizard can poll it cross-origin during install) liveness check. Returns 200 with { status: "ok", timestamp, services: { mongodb: "connected" } }, or 503 with status: "error" if MongoDB is disconnected.
Themes (/themes/*)
Section titled “Themes (/themes/*)”Three built-in themes (system, kowloon-light, kowloon-dark) are seeded on module load if missing, and the seed self-heals -- any previously-seeded author: "system" theme no longer in the current built-in list is deleted automatically. Custom admin-authored themes are never touched by this.
GET /themes
Section titled “GET /themes”Public. Returns all themes (built-in ones first) plus defaultThemeId.
GET /themes/:id
Section titled “GET /themes/:id”Public. 404 if not found.
POST /themes
Section titled “POST /themes”Admin only (401/403). Body:
{ "id": "...", "name": "...", "colorScheme": "...", "description": "...", "colors": { ... }, "postColors": { ... } }id/name/colorScheme required. 409 on duplicate id.
PUT /themes/:id
Section titled “PUT /themes/:id”Admin only. 403 if the target is a built-in theme -- built-ins are immutable. Whitelisted fields: name, description, colorScheme, colors, postColors.
DELETE /themes/:id
Section titled “DELETE /themes/:id”Admin only. 403 if built-in.
PATCH /themes/default
Section titled “PATCH /themes/default”Admin only. Body { themeId } -- must reference an existing theme. Sets the defaultTheme setting.
Push (/push/*)
Section titled “Push (/push/*)”POST /push/register
Section titled “POST /push/register”Auth required. Body:
{ "token": "...", "provider": "expo", "platform": "android" }(provider defaults to "expo" -- the other accepted value is "native"; platform defaults to "android", also accepts "ios"/"web".)
Upserts by token -- one row per device. Re-registering the same token under a different user reassigns ownership, which is what makes device account-switching work correctly. 400 missing token.
POST /push/unregister
Section titled “POST /push/unregister”Auth required. Body { token }. Deletes the token row, scoped to the current user.
Recommendations (/recommendations)
Section titled “Recommendations (/recommendations)”The public "Discover" surface -- see also project_recommendations_discover internal notes if you're the server operator curating shelves via /admin/recommendations.
GET /recommendations
Section titled “GET /recommendations”Unauthenticated OK. Returns active RecommendationSections in order, each resolved to its live, currently-visible items.
Curated Recommendation references are resolved against Post (via FeedItems), Circle, Group, Bookmark, Page, or FederatedServer. Items whose target was deleted, or whose visibility narrowed since curation, are silently dropped -- nothing about a recommendation is snapshotted except the tier it had at add-time; the live object is always the source of truth at read time.
Viewer-aware tiering matches /posts//circles -- local-authenticated sees public+server, everyone else sees public only.
Sections with source: "hybrid" or "heuristic" and a contentType get algorithmic backfill (getHeuristicPicks) up to targetCount, excluding already-curated refs. Empty shelves (nothing curated and nothing to backfill) are omitted from the response entirely, rather than returned empty.
Media-type shelves get their first attachment's kind (image/video/audio) resolved and exposed as mediaKind/mediaUrl on each item.
Also returns a top-level background value (the discoverBackground setting) for the Discover page's backdrop.
{ "@context": "...", "type": "Collection", "background": "...", "sections": [ { "id": "...", "name": "...", "slug": "...", "summary": "...", "contentType": "...", "source": "...", "targetCount": 10, "order": 0, "items": [ ... ] } ]}