{"id":50375139,"url":"https://github.com/nexfortisme/relay","last_synced_at":"2026-05-30T09:02:49.764Z","repository":{"id":355935823,"uuid":"1219980517","full_name":"nexfortisme/relay","owner":"nexfortisme","description":"This is a slop-job of a LLM chat application that I think I would actually use. 100% local, 100% your data.","archived":false,"fork":false,"pushed_at":"2026-05-13T16:48:42.000Z","size":2293,"stargazers_count":0,"open_issues_count":1,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-13T18:31:17.040Z","etag":null,"topics":["go","slop","sqlite","vue3"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/nexfortisme.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2026-04-24T12:23:41.000Z","updated_at":"2026-05-13T16:48:53.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/nexfortisme/relay","commit_stats":null,"previous_names":["nexfortisme/relay"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/nexfortisme/relay","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nexfortisme%2Frelay","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nexfortisme%2Frelay/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nexfortisme%2Frelay/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nexfortisme%2Frelay/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/nexfortisme","download_url":"https://codeload.github.com/nexfortisme/relay/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nexfortisme%2Frelay/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33686018,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-05-30T02:00:06.278Z","response_time":92,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["go","slop","sqlite","vue3"],"created_at":"2026-05-30T09:02:49.548Z","updated_at":"2026-05-30T09:02:49.751Z","avatar_url":"https://github.com/nexfortisme.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Relay\n\nRelay is a full-stack AI application with a Go backend and Vue 3 frontend. The main experience is realtime chat with token-by-token streaming over WebSocket, SQLite persistence, file attachments, and tool calling through Model Context Protocol (MCP) plus first-party Relay tools. Built-in **Feeds** and **Notebooks** workspaces let the assistant read RSS/Atom inboxes, summarize items, search uploaded documents, and query or edit notebook CSV data.\n\n## Features\n\n### Chat\n\n- Streaming assistant output with incremental `token` events and optional separate `thinking` / reasoning stream\n- Conversation CRUD: create, rename, archive, restore, delete\n- Retry and stop for in-flight or failed assistant generations; requeue failed messages\n- Attachments from the composer (including images suited for multimodal models) with optimistic UI that reconciles to server-backed messages\n- Markdown rendering for assistant content in the UI\n- Usage-style metadata where the model/provider exposes it (e.g. token counts)\n\n### Feeds\n\n- Subscribe to RSS/Atom URLs with a preview **check feed** flow before saving\n- Configurable polling interval, automatic periodic refresh in the backend, manual refresh in the UI\n- Backfill options when adding a feed (e.g. latest / since date / fuller history with limits)\n- Read and starred state; unread, starred, and all-item views\n- On-demand or automatic **LLM summaries** for items (automatic summary is gated on feed settings); expanded / re-summary actions in the UI\n- Feed settings store auto-add-to-notebook preferences and an optional notebook target\n\n### Notebooks\n\n- Create notebooks with description, notebook-specific system prompt, and skill prompt fields\n- Upload documents, CSV files, and images; background jobs index content into per-notebook SQLite databases under `DATA_DIR`\n- PDF text extraction, page image rendering, and optional LLM page descriptions for scanned or mixed-content pages\n- CSV table viewer plus notebook tools for querying, inserting, updating, and deleting CSV rows\n- Notebook-linked chat conversations that can search notebook documents and use notebook CSV tools\n\n### Dashboard and navigation\n\n- **Home** launcher for quick jumps (e.g. open Feeds, start Chat with prefilled draft)\n- **Chat**, **Feeds**, and **Notebooks** are fully wired to the API\n- **Scheduled** and **My Data** routes exist as placeholders for future features\n\n### Account and settings\n\n- Cookie-based JWT access tokens with refresh tokens; register, login, logout, `/auth/me`\n- Optional **`DISABLE_AUTH`** single-user mode (all requests treated as the seeded root user) for local dev\n- Root user seeded from **`ROOT_USERNAME`** / **`ROOT_PASSWORD`** (change before any real deployment)\n- Per-user persisted settings over the API: default LLM base URL, model id, optional API key, and custom **system prompt**\n\n### Models and tools\n\n- OpenAI-compatible chat completions (LM Studio, Ollama gateways, hosted APIs, etc.); **`LLM_URL`** or **`LLM_BASE_URL`**\n- **Composite tool runtime**: tools are merged from the internal MCP HTTP server and Relay tools such as `list_feeds` and `get_feed_items`\n- Notebook-linked chats also expose notebook tools: `notebook_search_docs`, `notebook_query_csv`, `notebook_insert_csv_row`, `notebook_update_csv_row`, and `notebook_delete_csv_row`\n- Internal MCP tools (streamable HTTP on **`MCP_SERVER_ADDRESS`**, exposed at **`MCP_URL`**): `web_search` (SearxNG when configured), `get_weather`, `get_time`\n- **`fetch_url`** / **`fetch_urls`** are registered on the MCP server but call an external **fetcher** MCP relay (`FETCHER_MCP_ENDPOINT`, default `http://localhost:3000/mcp`) for JS-heavy pages — run that stack if you rely on browsing-style fetch\n\n### Operations\n\n- **`GET /health`** on the main HTTP server for liveness checks\n- When `web/dist` is present beside the backend, Gin serves static assets and **SPA fallback** for non-API routes (Docker / production-style single port)\n\n## Architecture\n\n```text\nweb (Vue 3 + Pinia + Vue Router + Vite)\n        |  HTTP / WS\n        v\napi (Gin + SQLite + WebSocket chat + feed scheduler + notebook indexer)\n        |\n        +-- internal MCP (:8090)  streamable HTTP tools\n        |\n        +-- per-notebook SQLite databases under DATA_DIR\n        |\n        +-- optional FETCHER MCP (browse/fetch_url) via FETCHER_MCP_ENDPOINT\n```\n\n- **`api/`**: HTTP API, WebSocket stream, SQLite, LLM client, attachments, MCP client/runtime, RSS/Atom feed fetch + summarize, notebook indexing/tools\n- **`web/`**: Split Pinia stores (auth, chat, conversations, notebooks, settings, UI), routed views; chat streaming/cache merge helpers in `src/lib/conversationStreamMessages.ts`\n- **`resources/`**: prompt and resource markdown used by backend packages\n- **`scripts/dev.sh`**: installs deps and runs **`air`** for the API and **`bun dev`** for the frontend\n\n## Requirements\n\n- Go (current stable)\n- Bun\n- Air (`go install github.com/air-verse/air@latest`)\n- Node.js `^20.19.0 || \u003e=22.12.0` (e.g. `nvm use 24` before Bun commands if needed)\n\n## Quick Start\n\n1. Copy environment variables:\n\n   ```bash\n   cp example.env .env\n   ```\n\n2. Configure at least:\n\n   ```bash\n   LLM_URL=http://localhost:1234/v1\n   LLM_MODEL=your-model-name\n   ```\n\n3. Start both services:\n\n   ```bash\n   ./scripts/dev.sh\n   ```\n\n4. Open the app:\n\n   - Frontend: `http://localhost:5173`\n   - Backend API: `http://localhost:8091/api`\n\n## Development Commands\n\n### Run everything\n\n```bash\n./scripts/dev.sh\n```\n\n### Backend (`api/`)\n\n```bash\ngo mod download\nair\ngo test ./...\ngo build ./...\n```\n\n### Frontend (`web/`)\n\n```bash\nnvm use 24\nbun install\nbun dev\nbun test:unit --run\nbun run lint\nbun run type-check\nbun run build\n# optional when type-check already ran:\nbun run build-only\n```\n\n## Configuration\n\nKey environment variables:\n\n- **`LLM_URL`** or **`LLM_BASE_URL`**: OpenAI-compatible API base URL (one of them required)\n- **`LLM_MODEL`**: default model id when settings do not override\n- **`API_PORT`**: backend HTTP port (default `8091`)\n- **`MCP_SERVER_ADDRESS`**: internal MCP bind address (default `:8090`)\n- **`MCP_URL`**: MCP endpoint consumed by the chat runtime (default `http://localhost:8090/mcp`)\n- **`FETCHER_MCP_ENDPOINT`**: URL of the relay-fetcher MCP server used by `fetch_url` / `fetch_urls` (default `http://localhost:3000/mcp`)\n- **`SEARXNG_URL`**: base URL for a SearxNG instance; enables meaningful `web_search` results\n- **`WEB_ORIGIN`**: browser origin allowed by CORS (default `http://localhost:5173`)\n- **`VITE_API_BASE_DEV`**: frontend API base during Vite dev (default `http://localhost:8091/api`)\n- **`VITE_API_BASE`**: frontend API base in production builds (default `/api`)\n- **`SQLITE_PATH`**: main SQLite database file (default `../.relay/data/relay.db` relative to API cwd with `./scripts/dev.sh`)\n- **`DATA_DIR`**: notebook databases, snapshots, and other larger local data (default `../.relay/data` relative to API cwd — repo `.relay/data` with `./scripts/dev.sh`)\n- **`JWT_TOKEN`**, **`JWT_REFRESH_TOKEN`**: secrets for signing and hashing tokens\n- **`DISABLE_AUTH`**: disable auth wall and act as root user\n- **`ROOT_USERNAME`**, **`ROOT_PASSWORD`**: seeded admin account\n- **`COOKIE_SECURE`**: require HTTPS for cookies when true\n- **`VITE_MAX_UPLOAD_BYTES`**, **`VITE_MAX_IMAGE_BYTES`**, **`VITE_MAX_TOKEN_COUNT`**: limits mirrored in backend where applicable\n\nSee **`example.env`** for comments and defaults.\n\n## Docker Deployment\n\nBuild an image that includes the compiled frontend and backend:\n\n```bash\ndocker build -t relay:latest .\n```\n\nRun with SQLite persisted on a host volume:\n\n```bash\ndocker run --rm -p 8091:8091 -v relay-data:/data \\\n  -e LLM_URL=http://host.docker.internal:1234/v1 \\\n  -e LLM_MODEL=your-model-name \\\n  -e DATA_DIR=/data \\\n  relay:latest\n```\n\nRun in the background (detached) with `.env` and persistent SQLite:\n\n```bash\ndocker run -d \\\n  --name relay \\\n  -p 8091:8091 \\\n  -v relay-data:/data \\\n  --env-file .env \\\n  -e SQLITE_PATH=/data/relay.db \\\n  -e DATA_DIR=/data \\\n  --restart unless-stopped \\\n  relay:latest\n```\n\nNotes:\n\n- The container defaults **`SQLITE_PATH`** to `/data/relay.db`.\n- Set **`DATA_DIR=/data`** when using notebooks so notebook databases and snapshots survive image rebuilds.\n- Mount `/data` so the database and notebook data survive image rebuilds.\n- The bundled UI is served from the backend; only **`8091`** (or your chosen **`API_PORT`**) needs to be published unless you terminate TLS elsewhere.\n\n## API Surface\n\nRoutes under **`/api`** unless noted.\n\nAuth (no prior session required):\n\n- **`POST /auth/register`**\n- **`POST /auth/login`**\n- **`POST /auth/logout`**\n- **`POST /auth/refresh`**\n\nAuthenticated:\n\n- **`GET /auth/me`**\n- **`GET` / `PUT /settings`** — per-user LLM URL, model, API key, system prompt\n\nConversations:\n\n- **`POST /conversations`**\n- **`GET /conversations`** — supports `includeArchived`\n- **`PATCH /conversations/:id`**\n- **`PATCH /conversations/:id/archive`**\n- **`PATCH /conversations/:id/restore`**\n- **`DELETE /conversations/:id`**\n- **`POST /conversations/:id/suggest-title`**\n\nMessages and streaming:\n\n- **`GET /conversations/:id/messages`**\n- **`POST /conversations/:id/messages`** — JSON body or multipart with `files[]`\n- **`POST /conversations/:id/messages/failed`**\n- **`POST /conversations/:id/messages/:messageId/requeue`**\n- **`GET /conversations/:id/stream`** — WebSocket upgrade\n- **`POST /conversations/:id/stop`**\n\nFiles:\n\n- **`GET /files/:id/download`** — attachment bytes for authenticated owner\n\nNotebooks:\n\n- **`POST /notebooks`**\n- **`GET /notebooks`**\n- **`GET /notebooks/:notebookId`**\n- **`PATCH /notebooks/:notebookId`**\n- **`DELETE /notebooks/:notebookId`**\n- **`POST /notebooks/:notebookId/files`**\n- **`GET /notebooks/:notebookId/files`**\n- **`DELETE /notebooks/:notebookId/files/:fileId`**\n- **`GET /notebooks/:notebookId/files/:fileId/download`**\n- **`GET /notebooks/:notebookId/files/:fileId/pages/:pageNum/image`**\n- **`GET /notebooks/:notebookId/jobs/count`**\n- **`GET /notebooks/:notebookId/conversations`**\n- **`POST /notebooks/:notebookId/conversations`**\n- **`GET /notebooks/:notebookId/csv/:fileId`**\n\nFeeds:\n\n- **`GET /feeds`**\n- **`POST /feeds/check`** — resolve and describe a subscription URL without persisting\n- **`POST /feeds`**\n- **`GET /feeds/items`** — list items; query **`view`** = `unread` (default), `starred`, or `all`; optional **`feedId`** to scope one subscription\n- **`GET /feeds/items/:id`**\n- **`PATCH /feeds/items/:id`** — e.g. read / starred toggles\n- **`POST /feeds/items/:id/summarize`**\n- **`POST /feeds/:id/mark-read`**\n- **`PATCH /feeds/:id`**\n- **`DELETE /feeds/:id`**\n\nOther:\n\n- **`GET /health`** — process liveness (not under `/api`)\n\nExcept for the auth bootstrap routes above, **`/api/*`** routes expect a valid session cookie (or **`DISABLE_AUTH`**).\n\n### WebSocket event types\n\n- `token`\n- `thinking`\n- `done`\n- `stopped`\n- `error`\n- `ping`\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnexfortisme%2Frelay","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fnexfortisme%2Frelay","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnexfortisme%2Frelay/lists"}