docs: self-hosting guide#140
Conversation
Single Node + SQLite self-host of @relaycast/engine via the relaycast-engine bin: install/run, config (flags + env), first-workspace bootstrap over the API, client connection (SDK/WS/MCP), local-fs files, production (Caddy/nginx/systemd/ Docker), upgrades/backups, and an honest limitations + what-you-dont-get-vs-hosted section. Linked from the README. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Pullfrog stalled The agent stopped emitting events for 301s and was killed by the activity-timeout watchdog. 55 events were processed before the failure. Recent agent stderr
|
|
Warning Review limit reached
More reviews will be available in 48 minutes and 54 seconds. Learn how PR review limits work. Your organization has run out of usage credits. Purchase more in the billing tab. ⌛ How to resolve this issue?After more reviews become available, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available. Please see our Fair Usage Limits Policy for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthroughAdds README quick-start and a new docs/self-hosting.md for running Relaycast as a single Node + SQLite process. Also introduces Relay-side A2A part coercion, runtime guards for MCP WebSocket events, and safer event normalization and validation in the TypeScript SDK and agent route. ChangesSelf-Hosting Guide + Runtime Safety
🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs:
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Code Review
This pull request introduces a comprehensive self-hosting guide for Relaycast, adding a new documentation file docs/self-hosting.md and updating the README.md with a quick-start section. The documentation covers prerequisites, installation, configuration, production setup (including reverse proxies, systemd, and Docker), and limitations. Feedback on the documentation suggests correcting the systemd service path to /usr/local/bin/ to prevent execution failures, and optimizing the provided Dockerfile using a multi-stage build to reduce image size and improve security.
|
|
||
| ```ini | ||
| [Service] | ||
| ExecStart=/usr/bin/relaycast-engine --db /var/lib/relaycast/relaycast.db --port 8787 --base-url https://relay.example.com |
There was a problem hiding this comment.
On most Linux distributions, globally installed npm packages (via npm install -g) are placed in /usr/local/bin/ rather than /usr/bin/. Using /usr/bin/relaycast-engine will likely cause the systemd service to fail to start with a 203/EXEC error. Updating this path to /usr/local/bin/relaycast-engine ensures compatibility with standard Node.js installations.
| ExecStart=/usr/bin/relaycast-engine --db /var/lib/relaycast/relaycast.db --port 8787 --base-url https://relay.example.com | |
| ExecStart=/usr/local/bin/relaycast-engine --db /var/lib/relaycast/relaycast.db --port 8787 --base-url https://relay.example.com |
| FROM node:20-bookworm-slim | ||
| RUN apt-get update && apt-get install -y --no-install-recommends build-essential python3 \ | ||
| && rm -rf /var/lib/apt/lists/* | ||
| RUN npm install -g @relaycast/engine | ||
| VOLUME /data | ||
| EXPOSE 8787 | ||
| ENV RELAYCAST_DB_PATH=/data/relaycast.db PORT=8787 | ||
| ENTRYPOINT ["relaycast-engine"] |
There was a problem hiding this comment.
The current Dockerfile installs build dependencies (build-essential and python3) which are required to compile the native better-sqlite3 module during installation, but are not needed at runtime. Keeping these in the final image significantly increases the image size and the security attack surface.
Using a multi-stage build allows you to compile the dependencies in a builder stage and copy only the compiled node modules and binary to a clean runtime stage, keeping the final production image slim and secure.
| FROM node:20-bookworm-slim | |
| RUN apt-get update && apt-get install -y --no-install-recommends build-essential python3 \ | |
| && rm -rf /var/lib/apt/lists/* | |
| RUN npm install -g @relaycast/engine | |
| VOLUME /data | |
| EXPOSE 8787 | |
| ENV RELAYCAST_DB_PATH=/data/relaycast.db PORT=8787 | |
| ENTRYPOINT ["relaycast-engine"] | |
| FROM node:20-bookworm-slim AS builder | |
| RUN apt-get update && apt-get install -y --no-install-recommends build-essential python3 && rm -rf /var/lib/apt/lists/* | |
| RUN npm install -g @relaycast/engine | |
| FROM node:20-bookworm-slim | |
| COPY --from=builder /usr/local/lib/node_modules /usr/local/lib/node_modules | |
| COPY --from=builder /usr/local/bin/relaycast-engine /usr/local/bin/relaycast-engine | |
| VOLUME /data | |
| EXPOSE 8787 | |
| ENV RELAYCAST_DB_PATH=/data/relaycast.db PORT=8787 | |
| ENTRYPOINT ["relaycast-engine"] |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a1a8ff4141
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| VOLUME /data | ||
| EXPOSE 8787 | ||
| ENV RELAYCAST_DB_PATH=/data/relaycast.db PORT=8787 | ||
| ENTRYPOINT ["relaycast-engine"] |
There was a problem hiding this comment.
Persist the upload directory in the Docker example
With this Dockerfile/run command, only /data is mounted, but the Node adapter writes uploaded blobs to process.cwd()/relaycast-files by default and the image never sets WORKDIR /data or a file-directory option. In the documented Docker deployment, uploaded files therefore land in an unmounted /relaycast-files inside the container and are lost when the container is recreated, even though the database is persisted under /data.
Useful? React with 👍 / 👎.
| - **Real-time WebSocket**: `ws://your-host:8787/v1/ws?token=<rk_live_ or at_live_>` | ||
| (an agent token streams that agent's events; a workspace key streams the | ||
| workspace event stream). |
There was a problem hiding this comment.
Enable workspace stream before documenting rk_live_ WebSockets
For a fresh self-host started by this guide, rk_live_ WebSocket connections do not stream by default: startServer leaves workspaceStreamEnabled unset, and /v1/ws rejects workspace keys unless the workspace stream override is enabled. Users following this URL with the workspace key they just created will get a 404 until they call PUT /v1/workspace/stream with { "enabled": true } (or they use an agent token), so the connection instructions need to include that prerequisite.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@docs/self-hosting.md`:
- Line 13: Update the broken in-page link by making the fragment and heading
match: either change the link text "[Limitations](`#limitations`)" to use the
generated anchor "[Limitations](`#8-limitations`)" or rename the heading "## 8.
Limitations" to "## Limitations" so the existing "[Limitations](`#limitations`)"
link resolves; ensure the adjusted fragment exactly matches the heading slug
used by GitHub (e.g., "8-limitations") and update any other references if
present.
- Around line 48-50: The fenced code block containing "Relaycast self-host
listening on http://localhost:8787 (db: ./relaycast.db)" lacks a language
identifier and triggers MD040; update that fenced block to include a language
(e.g., add "text" or "bash" after the opening ``` fence) so the block becomes
```text (or ```bash) before the content to satisfy the linter.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 1095739f-f1b5-4ba2-9ac5-7020139c73fc
📒 Files selected for processing (2)
README.mddocs/self-hosting.md
| ``` | ||
| Relaycast self-host listening on http://localhost:8787 (db: ./relaycast.db) | ||
| ``` |
There was a problem hiding this comment.
Add a language identifier to the fenced block.
The code fence at Line 48 has no language, which trips markdown linting (MD040). Use text (or bash if you want it treated as command output).
Suggested patch
-```
+```text
Relaycast self-host listening on http://localhost:8787 (db: ./relaycast.db)</details>
<!-- suggestion_start -->
<details>
<summary>📝 Committable suggestion</summary>
> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
```suggestion
🧰 Tools
🪛 markdownlint-cli2 (0.22.1)
[warning] 48-48: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/self-hosting.md` around lines 48 - 50, The fenced code block containing
"Relaycast self-host listening on http://localhost:8787 (db: ./relaycast.db)"
lacks a language identifier and triggers MD040; update that fenced block to
include a language (e.g., add "text" or "bash" after the opening ``` fence) so
the block becomes ```text (or ```bash) before the content to satisfy the linter.
|
Preview deployed!
This preview shares the staging database and will be cleaned up when the PR is merged or closed. Run E2E testsnpm run e2e -- https://pr140-api.relaycast.dev --ciOpen observer dashboard |
|
Reviewed PR #140 and fixed the issues I found. Changes made:
Local checks run:
Lint completed with warnings only in pre-existing engine files. |
|
✅ pr-reviewer applied fixes — committed and pushed Reviewed PR #140 and fixed the issues I found. Changes made:
Local checks run:
Lint completed with warnings only in pre-existing engine files. |
There was a problem hiding this comment.
✅ pr-reviewer applied fixes — committed and pushed 8d2533e to this PR. The notes below describe what changed.
Reviewed PR #140 and fixed the issues I found.
Changes made:
- Corrected
docs/self-hosting.mdso workspace-key WebSocket streams are documented as disabled by default and requirePUT /v1/workspace/stream. - Added the restart limitation for the in-memory workspace-stream override.
- Fixed TypeScript build failures in SDK/MCP/engine caused by Zod type inference across package boundaries by adding local schema/event narrowing.
Local checks run:
npm run -w @relaycast/sdk buildnpx turbo build --filter=@relaycast/enginenpx turbo test --filter=@relaycast/sdk --filter=@relaycast/mcp --filter=@relaycast/enginenpx turbo lint --filter=@relaycast/sdk --filter=@relaycast/mcp --filter=@relaycast/engine
Lint completed with warnings only in pre-existing engine files.
There was a problem hiding this comment.
3 issues found across 2 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="docs/self-hosting.md">
<violation number="1" location="docs/self-hosting.md:13">
P2: Broken in-page link: `#limitations` won't resolve because the target heading is `## 8. Limitations`, which GitHub renders as anchor `#8-limitations`. Either update the link to `#8-limitations` or rename the heading to `## Limitations`.</violation>
<violation number="2" location="docs/self-hosting.md:162">
P2: The `ExecStart` path should be `/usr/local/bin/relaycast-engine`. On most Linux distributions, `npm install -g` installs binaries to `/usr/local/bin/`, not `/usr/bin/`. Using `/usr/bin/relaycast-engine` will cause systemd to fail with a `203/EXEC` error.</violation>
<violation number="3" location="docs/self-hosting.md:179">
P2: The Docker example only mounts `/data` for the database, but uploaded files are written to `<cwd>/relaycast-files` by default. Since `WORKDIR` is not set (defaults to `/`), uploaded files land in `/relaycast-files` inside the container — an unmounted path — and are lost when the container is recreated. Either set `WORKDIR /data` or add a `RELAYCAST_FILES_PATH` env pointing into the mounted volume.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
There was a problem hiding this comment.
🧹 Nitpick comments (2)
packages/mcp/src/resources/ws-bridge.ts (1)
6-15: ⚡ Quick winReplace ad-hoc event runtime guards with a local Zod schema in
packages/mcp/src/resources/ws-bridge.ts
packages/mcpalready depends on and uses Zod (safeParse/parsepatterns). Define a local Zod schema for the websocket event (includingtypeand any string fields you read) and usesafeParse/parseto narrow instead oftypeof-based helpers likeisResourceEvent/getStringEventField(and the resulting early return inWsBridge.start).🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/mcp/src/resources/ws-bridge.ts` around lines 6 - 15, Replace the ad-hoc runtime guards isResourceEvent and getStringEventField with a local Zod schema: define a z.object schema that requires type: z.string() and any other string fields you read from events, import z from 'zod' locally, and then use schema.safeParse(event) (or schema.parse where appropriate) inside WsBridge.start to validate and narrow the event instead of typeof checks and early returns; remove isResourceEvent/getStringEventField and update references in WsBridge.start to use the parsed result (result.success ? result.data : handle invalid).packages/engine/src/engine/a2a.ts (1)
187-189: ⚡ Quick win
asRelayA2aPartscast doesn’t introduce the reported runtime risk (schema already enforces per-kindpayloads)
translateA2aToRelayparses input withJsonRpcRequestSchema.safeParse/JsonRpcResponseSchema.parse, which useA2aPartSchemaas az.discriminatedUnion('kind'):
kind: 'file'requires afileobject (includingfile.name)kind: 'data'requires adatarecordSo
extractTextFromParts/extractAttachmentsFromPartsshouldn’t receive parts with missingfile/datawhen coming from these parsed paths. Optional:asRelayA2aPartscan be simplified to take the already-typed parts directly (drop theunknown+Array.isArray+ cast).🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/engine/src/engine/a2a.ts` around lines 187 - 189, The asRelayA2aParts function currently accepts unknown and performs an Array.isArray check and cast, which is unnecessary because translateA2aToRelay already validates input via JsonRpcRequestSchema/JsonRpcResponseSchema using the discriminated A2aPartSchema; update asRelayA2aParts to accept RelayA2aPart[] directly (remove the unknown parameter, Array.isArray guard, and cast) so downstream helpers like extractTextFromParts and extractAttachmentsFromParts receive correctly-typed parts without redundant runtime checks; ensure any callers (e.g., translateA2aToRelay) are updated to pass the typed array.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@packages/engine/src/engine/a2a.ts`:
- Around line 187-189: The asRelayA2aParts function currently accepts unknown
and performs an Array.isArray check and cast, which is unnecessary because
translateA2aToRelay already validates input via
JsonRpcRequestSchema/JsonRpcResponseSchema using the discriminated
A2aPartSchema; update asRelayA2aParts to accept RelayA2aPart[] directly (remove
the unknown parameter, Array.isArray guard, and cast) so downstream helpers like
extractTextFromParts and extractAttachmentsFromParts receive correctly-typed
parts without redundant runtime checks; ensure any callers (e.g.,
translateA2aToRelay) are updated to pass the typed array.
In `@packages/mcp/src/resources/ws-bridge.ts`:
- Around line 6-15: Replace the ad-hoc runtime guards isResourceEvent and
getStringEventField with a local Zod schema: define a z.object schema that
requires type: z.string() and any other string fields you read from events,
import z from 'zod' locally, and then use schema.safeParse(event) (or
schema.parse where appropriate) inside WsBridge.start to validate and narrow the
event instead of typeof checks and early returns; remove
isResourceEvent/getStringEventField and update references in WsBridge.start to
use the parsed result (result.success ? result.data : handle invalid).
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 9bb71ad2-0a2a-4385-85bc-bf7fcd418475
📒 Files selected for processing (9)
.trajectories/index.jsondocs/self-hosting.mdpackages/engine/src/engine/a2a.tspackages/engine/src/routes/agent.tspackages/mcp/src/resources/ws-bridge.tspackages/sdk-typescript/src/agent.tspackages/sdk-typescript/src/relay.tspackages/sdk-typescript/src/setup.tspackages/sdk-typescript/src/ws.ts
💤 Files with no reviewable changes (1)
- .trajectories/index.json
✅ Files skipped from review due to trivial changes (2)
- packages/sdk-typescript/src/ws.ts
- packages/sdk-typescript/src/setup.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- docs/self-hosting.md
There was a problem hiding this comment.
1 issue found across 9 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="packages/engine/src/routes/agent.ts">
<violation number="1" location="packages/engine/src/routes/agent.ts:25">
P3: Reuse the shared `AgentTypeSchema` instead of duplicating the enum here. This keeps agent registration validation aligned with `packages/types/src/agent.ts`.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| }); | ||
|
|
||
| const capabilitiesSchema = z.record(z.string(), z.unknown()); | ||
| const agentTypeSchema = z.enum(['agent', 'human', 'system']); |
There was a problem hiding this comment.
P3: Reuse the shared AgentTypeSchema instead of duplicating the enum here. This keeps agent registration validation aligned with packages/types/src/agent.ts.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/engine/src/routes/agent.ts, line 25:
<comment>Reuse the shared `AgentTypeSchema` instead of duplicating the enum here. This keeps agent registration validation aligned with `packages/types/src/agent.ts`.</comment>
<file context>
@@ -23,10 +22,11 @@ const skillSchema = z.object({
});
const capabilitiesSchema = z.record(z.string(), z.unknown());
+const agentTypeSchema = z.enum(['agent', 'human', 'system']);
const registerAgentSchema = z.object({
</file context>
|
Reviewed PR #140 and applied the needed fixes. Changed:
Local verification:
|
|
✅ pr-reviewer applied fixes — committed and pushed Reviewed PR #140 and applied the needed fixes. Changed:
Local verification:
|
There was a problem hiding this comment.
✅ pr-reviewer applied fixes — committed and pushed 190df7d to this PR. The notes below describe what changed.
Reviewed PR #140 and applied the needed fixes.
Changed:
- Restored
.trajectories/index.jsonto avoid unrelated metadata deletion. - Fixed the broken
docs/self-hosting.mdin-page anchor. - Updated the systemd
ExecStartpath to/usr/local/bin/relaycast-engine. - Added
WORKDIR /datato the Docker example so default file uploads persist under the mounted volume.
Local verification:
npm run -w @relaycast/sdk buildnpm run -w @relaycast/mcp buildnpm run -w @relaycast/engine buildnpx turbo test --filter=@relaycast/sdk --filter=@relaycast/mcp --filter=@relaycast/enginenpx turbo lint --filter=@relaycast/sdk --filter=@relaycast/mcp --filter=@relaycast/enginepassed with existing engine warnings only.

Adds
docs/self-hosting.md— the guide for running Relaycast yourself as a single Node + SQLite process via therelaycast-enginebin (@relaycast/engine). No Cloudflare, no external services.Covers, accurate to what actually ships (1.1.7):
npx @relaycast/engineor globalrelaycast-engine.--db/--port/--base-url/--env) + env (RELAYCAST_DB_PATH/PORT/RELAYCAST_ENV); migrations run on boot; telemetry off by default.POST /v1/workspaces→rk_live_…), register agents (at_live_…)./v1/ws) / MCP (/mcp+x-relay-api-key) / CLI./_relayfilesURLs (mirrors the hosted presigned-R2 flow).Linked from the README (new Self-Hosting section).
Verified the documented quick-start end-to-end: the server boots, and the workspace-create curl returns the
rk_live_key +workspace_idexactly as shown.🤖 Generated with Claude Code