Skip to content

API Endpoints

SvelteKit API routes (+server.ts files under src/routes/api/). Sync endpoints use D1 database with client-side encryption.

POST /api/feeds

Fetch developer news feeds from multiple sources. Returns items grouped by source for client-side curation. No auth required.

Request:

{
  "languages": ["JavaScript", "Rust"],
  "frameworks": ["React"],
  "tools": ["Docker"],
  "topics": ["AI/ML & LLMs"],
  "resolvedMappings": {
    "Homelab": {
      "subreddits": ["selfhosted", "homelab"],
      "lobstersTags": ["selfhosted"],
      "devtoTags": ["homelab"]
    }
  }
}

Response:

{
  "feeds": {
    "hn": [{ "title": "...", "url": "...", "discussionUrl": "...", "score": 100, "source": "HN", "date": 1706000000000 }],
    "lobsters": [{ "title": "...", "url": "...", "score": 50, "source": "Lobsters" }],
    "reddit": [{ "title": "...", "url": "...", "score": 200, "source": "r/rust" }],
    "github": [{ "title": "...", "url": "...", "score": 1000, "source": "GitHub (Rust)" }],
    "devto": [{ "title": "...", "url": "...", "score": 30, "source": "Dev.to" }]
  }
}

Feed items have title, url, score, and source. HN, Lobsters, and Reddit items also include discussionUrl (link to comments when the primary URL is an article) and date (Unix timestamp in milliseconds). Items older than 7 days are filtered out server-side.

The client curates HN/Lobsters items via a lightweight call to the configured curation provider before including in the prompt. Reddit, GitHub, and Dev.to are already profile-targeted.


Sync Endpoints

The following endpoints support the sync system.

POST /api/profile/create

Create or re-upload a profile. Used for initial share. If the profile already exists, the password must match before the update is applied.

Request:

{
  "id": "uuid",
  "name": "Profile Name",
  "password_hash": "salt:hash",
  "encrypted_api_key": "base64...",
  "salt": "base64...",
  "languages": ["JavaScript"],
  "frameworks": ["React"],
  "tools": ["Docker"],
  "topics": ["AI/ML"],
  "depth": "standard",
  "custom_focus": "optional text"
}

encrypted_api_key Format

The encrypted_api_key field contains all API keys (Anthropic, DeepSeek, Gemini, Perplexity, Serper) encrypted as a JSON object. The field name is singular for backward compatibility, but it stores multiple keys.

Response (201):

{
  "id": "uuid",
  "name": "Profile Name"
}

GET /api/profile/{id}?password_hash=...

Get full profile data including encrypted API keys. Requires password. Supports include_data=true query param to also return encrypted diffs and stars.

Response:

{
  "id": "uuid",
  "name": "Profile Name",
  "encrypted_api_key": "base64...",
  "salt": "base64...",
  "languages": [],
  "frameworks": [],
  "tools": [],
  "topics": [],
  "depth": "standard",
  "custom_focus": null,
  "resolved_sources": null,
  "content_hash": null,
  "content_updated_at": "2026-01-26T04:13:00Z"
}

With include_data=true, the response also includes encrypted_diffs and encrypted_stars arrays.

PUT /api/profile/{id}

Update profile metadata. Requires password in request body.

Request:

{
  "password_hash": "salt:hash",
  "name": "New Name",
  "languages": ["TypeScript"],
  "frameworks": ["Svelte"],
  "tools": [],
  "topics": [],
  "depth": "detailed",
  "custom_focus": "...",
  "resolved_sources": {}
}

Only fields present in the request are updated. Allowed fields: name, languages, frameworks, tools, topics, depth, custom_focus, resolved_sources.

Response:

{ "success": true }

DELETE /api/profile/{id}?password_hash=...

Delete a profile and all associated diffs and stars (cascade). Requires password.

Response:

{ "success": true }

GET /api/profile/{id}/status

Check if profile exists and get content hashes. No auth required.

Response:

{
  "exists": true,
  "diffs_hash": "hex...",
  "stars_hash": "hex...",
  "content_updated_at": "2026-01-26T04:13:00Z"
}

Returns { "exists": false } if the profile doesn't exist.

POST /api/profile/{id}/content

Download encrypted content. Requires password. Supports selective download — pass local hashes to skip unchanged collections.

Request:

{
  "password_hash": "salt:hash",
  "diffs_hash": "hex...",
  "stars_hash": "hex..."
}

The diffs_hash and stars_hash fields are optional. When provided, the server skips fetching that collection if the hash matches, returning diffs_skipped: true or stars_skipped: true instead.

Response:

{
  "diffs": [{ "id": "uuid", "encrypted_data": "base64..." }],
  "stars": [{ "id": "uuid", "encrypted_data": "base64..." }],
  "diffs_skipped": false,
  "stars_skipped": false,
  "content_hash": null,
  "salt": "base64...",
  "profile": {
    "name": "...",
    "languages": [],
    "frameworks": [],
    "tools": [],
    "topics": [],
    "depth": "standard",
    "custom_focus": null
  }
}

POST /api/profile/{id}/sync

Upload content and deletions. Requires password. The server enforces a cap of 50 diffs per profile, deleting the oldest beyond that limit.

Request:

{
  "password_hash": "salt:hash",
  "diffs": [{ "id": "uuid", "encrypted_data": "base64 or JSON" }],
  "stars": [{ "id": "uuid", "encrypted_data": "base64..." }],
  "deleted_diff_ids": ["uuid"],
  "deleted_star_ids": ["uuid"],
  "diffs_hash": "hex...",
  "stars_hash": "hex...",
  "resolved_sources": {},
  "profile": { "name": "...", "languages": [], "..." : "..." }
}

Public Diffs

For public diffs, encrypted_data contains plaintext JSON (starts with {) instead of an encrypted base64 blob. The server detects this format and serves public diffs via /api/diff/{id}/public.

Response:

{
  "success": true,
  "diffs_hash": "hex...",
  "stars_hash": "hex...",
  "synced": {
    "diffs": 5,
    "stars": 3,
    "deleted_diffs": 1,
    "deleted_stars": 0
  }
}

GET /api/profile/{id}/sync?diffs_hash=...&stars_hash=...

Quick sync check — compares client hashes against server to determine if sync is needed. No auth required.

Response:

{
  "needs_sync": true,
  "diffs_sync_needed": true,
  "stars_sync_needed": false,
  "server_diffs_hash": "hex...",
  "server_stars_hash": "hex...",
  "server_updated_at": "2026-01-26T04:13:00Z"
}

POST /api/profile/{id}/password

Change sync password. Re-encrypts all data (all API keys, diffs, stars) with the new password client-side, then uploads everything in an atomic batch.

Request:

{
  "old_password_hash": "salt:hash",
  "new_password_hash": "salt:hash",
  "new_encrypted_api_key": "base64...",
  "new_salt": "base64...",
  "diffs": [{ "id": "uuid", "encrypted_data": "base64..." }],
  "stars": [{ "id": "uuid", "encrypted_data": "base64..." }]
}

Response:

{
  "success": true,
  "message": "Password updated successfully"
}

GET /api/diff/{id}/public

Retrieve a publicly shared diff. Returns 404 if the diff doesn't exist or is private. See Public Diff Sharing for details. No auth required.

Response:

{
  "id": "abc123",
  "content": "# Your Dev Digest\n\n...",
  "title": "Weekly Update",
  "generated_at": "2026-01-28T10:00:00Z",
  "profile_name": "Chris"
}

Cache Headers: Cache-Control: public, max-age=86400, Cache-Tag: diff-{id}


GET /api/share/{id}

Get public profile info for import flow. No auth required.

Response:

{
  "id": "uuid",
  "name": "Profile Name",
  "languages": [],
  "frameworks": [],
  "tools": [],
  "topics": [],
  "depth": "standard",
  "password_salt": "base64..."
}