Add optional self-hosted UUID-based artifact server#27
Conversation
Introduces a separate self-hosted server variant that stores agent-render
payloads in SQLite under UUID v4 keys, enabling short stable links like
/{uuid} that bypass fragment length limits and URL mangling on chat platforms.
The existing static fragment-based viewer remains the default product and
is fully preserved. The self-hosted mode reuses the same frontend by
injecting the stored payload via window.__AGENT_RENDER_PAYLOAD__ on the
server-rendered page, feeding into the existing decode/render pipeline
with full feature parity.
Includes REST API (CRUD), 24h sliding TTL, Docker Compose support,
selfhosted-agent-render skill, and comprehensive documentation updates.
https://claude.ai/code/session_01NeNkCL34w4hCodzArQhZgJ
|
Warning Rate limit exceeded
Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 2 minutes and 6 seconds. ⌛ How to resolve this issue?After the wait time has elapsed, 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 have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (3)
📝 WalkthroughWalkthroughAdds an optional self-hosted UUID mode: a standalone Node.js HTTP server that serves the static frontend, provides REST CRUD for artifact payloads persisted in SQLite under UUID keys, injects payloads into the viewer for /:uuid routes, and enforces a 24-hour sliding TTL. Changes
Sequence DiagramsequenceDiagram
participant Browser as Browser Client
participant Server as Self-hosted Server
participant DB as SQLite DB
Browser->>Server: GET /:uuid
Server->>DB: SELECT artifact WHERE id = :uuid
DB-->>Server: artifact row / not found
alt not found or expired
Server-->>Browser: 404 (error HTML)
else valid artifact
Server->>Server: inject payload into cached index.html\n(window.__AGENT_RENDER_PAYLOAD__ = ...)
Server->>DB: UPDATE last_viewed_at, expires_at (refresh TTL)
DB-->>Server: update confirmation
Server-->>Browser: 200 with injected HTML
Browser->>Browser: ViewerShell reads __AGENT_RENDER_PAYLOAD__ and renders UI
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~55 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 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.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 643e0cca61
ℹ️ 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".
| * via `window.__AGENT_RENDER_PAYLOAD__` on load. | ||
| */ | ||
| function injectPayload(html: string, payload: string): string { | ||
| const script = `<script>window.__AGENT_RENDER_PAYLOAD__=${JSON.stringify(payload)};</script>`; |
There was a problem hiding this comment.
Escape payload before injecting into HTML script tag
Embedding JSON.stringify(payload) directly inside a <script> block lets attacker-controlled payloads break out of the string with </script>..., which enables script execution whenever someone opens a UUID link. This is reachable because POST /api/artifacts accepts arbitrary strings and GET /:uuid injects them into HTML, so a crafted artifact can trigger stored XSS in any browser visiting that link.
Useful? React with 👍 / 👎.
| `UPDATE artifacts SET last_viewed_at = ?, expires_at = ?, updated_at = ? WHERE id = ?`, | ||
| ) | ||
| .run(now, newExpiresAt, now, id); |
There was a problem hiding this comment.
Preserve updated_at when serving artifact reads
getArtifact currently writes updated_at = now for every successful read, so plain views/GETs mutate the record as if the payload changed. In environments that poll GET /api/artifacts/:id or render via GET /:uuid, this makes updated_at unusable for detecting real content edits and can cause incorrect sync or cache-invalidation behavior.
Useful? React with 👍 / 👎.
- Escape </script> sequences in injected payload to prevent stored XSS - Add path traversal guard in serveStatic to keep resolved paths within the output directory - Stop mutating updated_at on read-only GET operations (only refresh last_viewed_at and expires_at) - Run Docker container as non-root nodejs user https://claude.ai/code/session_01NeNkCL34w4hCodzArQhZgJ
Deploying agent-render with
|
| Latest commit: |
c329bae
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://53a13ea4.agent-render.pages.dev |
| Branch Preview URL: | https://claude-add-selfhosted-uuid-m.agent-render.pages.dev |
Code Review SummaryStatus: No Issues Found | Recommendation: Merge Issue ResolutionThe previous WARNING at Other Observations (not in diff)Issues found in unchanged code that cannot receive inline comments:
Files Reviewed (1 file in diff)
Incremental review of changes since commit Reviewed by minimax-m2.5-20260211 · 387,611 tokens Reviewed by minimax-m2.5-20260211 · 79,457 tokens |
There was a problem hiding this comment.
🧹 Nitpick comments (2)
selfhosted/db.ts (1)
17-49: Singleton pattern has a subtle limitation with differentdbPathvalues.The singleton caches the first connection. If
getDb()is called with differentdbPatharguments after the initial call, subsequent paths are ignored. This is probably fine for production (single server instance) but could cause confusion in tests.Consider documenting this behavior or throwing an error if called with a different path while a connection exists:
export function getDb(dbPath?: string): Database.Database { - if (db) return db; + const resolvedPath = dbPath ?? process.env.DB_PATH ?? "./data/agent-render.db"; + if (db) { + // Connection already exists; subsequent dbPath arguments are ignored + return db; + } - const resolvedPath = dbPath ?? process.env.DB_PATH ?? "./data/agent-render.db"; mkdirSync(dirname(resolvedPath), { recursive: true });The WAL mode and indexed
expires_atcolumn are good choices for this workload.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@selfhosted/db.ts` around lines 17 - 49, The getDb singleton caches the first opened database and silently ignores later dbPath arguments, which can surprise tests; fix by recording the initial resolvedPath when you first open the DB (store it in a new module-level variable like initialDbPath) and, at the top of getDb, compare the new resolvedPath to that stored value and throw a clear error (or return the existing db only if paths match) when they differ; update references to the module-level db and use resolvedPath in the comparison so callers are immediately informed if they attempt to open a different file.selfhosted/Dockerfile (1)
14-27: Copynode_modulesfrom builder stage instead of reinstalling in production.The production stage runs
npm ci --omit=devto install better-sqlite3, which relies on prebuilt binaries being available for download. While better-sqlite3 v12.8.0 has prebuilt binaries for Linux + Node 20, this approach is unnecessarily inefficient and creates a reliability risk if network access is unavailable or prebuild servers are inaccessible.Since the builder stage already runs
npm ciand builds all dependencies, copynode_modulesdirectly from the builder:COPY --from=builder /app/node_modules ./node_modulesThis eliminates the redundant installation, reduces build time, and removes the network/prebuild dependency for production deployments.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@selfhosted/Dockerfile` around lines 14 - 27, The Dockerfile currently runs npm ci --omit=dev in the production stage which reinstalls dependencies (including better-sqlite3) and creates network/prebuild fragility; instead, copy the already-built node_modules from the builder stage into production by replacing the production-stage npm ci step with a COPY from the builder (reference the existing COPY --from=builder patterns and the npm ci --omit=dev invocation and the better-sqlite3 dependency) so production uses /app/node_modules produced in the builder and avoids reinstalling dependencies at runtime.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@selfhosted/db.ts`:
- Around line 17-49: The getDb singleton caches the first opened database and
silently ignores later dbPath arguments, which can surprise tests; fix by
recording the initial resolvedPath when you first open the DB (store it in a new
module-level variable like initialDbPath) and, at the top of getDb, compare the
new resolvedPath to that stored value and throw a clear error (or return the
existing db only if paths match) when they differ; update references to the
module-level db and use resolvedPath in the comparison so callers are
immediately informed if they attempt to open a different file.
In `@selfhosted/Dockerfile`:
- Around line 14-27: The Dockerfile currently runs npm ci --omit=dev in the
production stage which reinstalls dependencies (including better-sqlite3) and
creates network/prebuild fragility; instead, copy the already-built node_modules
from the builder stage into production by replacing the production-stage npm ci
step with a COPY from the builder (reference the existing COPY --from=builder
patterns and the npm ci --omit=dev invocation and the better-sqlite3 dependency)
so production uses /app/node_modules produced in the builder and avoids
reinstalling dependencies at runtime.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: c6d253fd-a25d-40b1-b26b-fd5caacadfc2
📒 Files selected for processing (3)
selfhosted/Dockerfileselfhosted/db.tsselfhosted/server.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- selfhosted/server.ts
Remove early return in ViewerShell useEffect when consuming an injected payload so the hashchange listener is still registered for subsequent navigation (sample links, back/forward, manual URL edits). Add tsx to devDependencies so selfhosted:dev script works out of the box. https://claude.ai/code/session_01NeNkCL34w4hCodzArQhZgJ
The self-hosted mode's core use case is payloads that exceed the ~8 KB fragment budget. Add a skipFragmentBudget option to decodeFragmentAsync and use it when decoding server-injected payloads so large artifacts render correctly. The decoded payload size limit (200K) is still enforced. https://claude.ai/code/session_01NeNkCL34w4hCodzArQhZgJ
There was a problem hiding this comment.
🧹 Nitpick comments (1)
src/lib/payload/fragment.ts (1)
259-262: Consider exportingDecodeOptionsfor API discoverability.The type is well-documented internally, but callers of
decodeFragmentAsynccurrently have to guess the option shape. Exporting it would improve API ergonomics without any downside.♻️ Suggested change
-type DecodeOptions = { +/** + * Options for async fragment decoding. + */ +export type DecodeOptions = { /** Skip the fragment transport size budget check (for server-injected payloads). */ skipFragmentBudget?: boolean; };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/lib/payload/fragment.ts` around lines 259 - 262, The DecodeOptions type is currently internal; export it so callers can import the options shape used by decodeFragmentAsync. Change the declaration of DecodeOptions to an exported type (export type DecodeOptions = { ... }) and update any local references/imports if necessary (e.g., places that import or annotate decodeFragmentAsync options) so the public API exposes the type for discoverability without altering runtime behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@src/lib/payload/fragment.ts`:
- Around line 259-262: The DecodeOptions type is currently internal; export it
so callers can import the options shape used by decodeFragmentAsync. Change the
declaration of DecodeOptions to an exported type (export type DecodeOptions = {
... }) and update any local references/imports if necessary (e.g., places that
import or annotate decodeFragmentAsync options) so the public API exposes the
type for discoverability without altering runtime behavior.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 1856c70b-1c8e-4d37-ba52-336619446c7e
📒 Files selected for processing (2)
src/components/viewer-shell.tsxsrc/lib/payload/fragment.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- src/components/viewer-shell.tsx
Static-only users should not be forced to install a native SQLite module. Moving it to optionalDependencies means npm install succeeds even without native build tools, keeping the core static product install lightweight. https://claude.ai/code/session_01NeNkCL34w4hCodzArQhZgJ
| req.destroy(); | ||
| reject(new Error("Request body too large.")); | ||
| } | ||
| chunks.push(chunk); |
There was a problem hiding this comment.
WARNING: Missing return after reject causes chunk accumulation after body limit exceeded
In readBody, when the body exceeds 1 MB limit, reject() is called on line 74 but execution falls through to chunks.push(chunk) on line 76 because there is no return statement. The oversized chunk is still pushed after the promise is rejected.
Prevents the oversized chunk from being pushed into the buffer after the promise is already rejected and the request is destroyed. https://claude.ai/code/session_01NeNkCL34w4hCodzArQhZgJ
Use the same typeof/length check for skipping syncHash as for consuming the injected payload, so a truthy non-string value cannot suppress the initial hash sync. https://claude.ai/code/session_01NeNkCL34w4hCodzArQhZgJ
Summary
This PR adds an optional self-hosted server mode to
agent-renderthat stores artifact payloads in SQLite and serves them under UUID-based links. The default static/fragment-based product remains unchanged. This new mode is designed for use cases where payloads exceed the ~8 KB fragment budget or when links are shared on platforms that mangle long URLs.Key Changes
Self-hosted HTTP server (
selfhosted/server.ts): Node.js HTTP server that serves static files, exposes a REST API for artifact CRUD operations, and renders the viewer with injected payloads for UUID routesPOST /api/artifacts— create a new artifact with a UUID and 24-hour TTLGET /api/artifacts/:id— retrieve artifact (refreshes TTL)PUT /api/artifacts/:id— update artifact payloadDELETE /api/artifacts/:id— delete artifactPOST /api/cleanup— batch-remove expired artifactsGET /:uuid— render viewer with stored payload injected viawindow.__AGENT_RENDER_PAYLOAD__SQLite storage layer (
selfhosted/db.ts): Database abstraction usingbetter-sqlite3with automatic table/index creation, UUID v4 artifact IDs, and sliding 24-hour TTLPayload validation (
selfhosted/validate.ts): Server-side validation that payloads are non-empty strings under 500 KBTTL utilities (
selfhosted/ttl.ts): Helper functions for computing expiration timestamps and checking expiry statusViewer integration (
src/components/viewer-shell.tsx): ModifiedViewerShellto check forwindow.__AGENT_RENDER_PAYLOAD__on mount and use it as the payload source when present, allowing the same decode/render pipeline to work for both fragment and UUID modesDeployment options:
selfhosted/docker-compose.yml,selfhosted/Dockerfile) for containerized deploymentselfhosted/tsconfig.json) for server compilationComprehensive test suite (
tests/selfhosted/): Unit tests for database operations, TTL behavior, and payload validationDocumentation: Updated README, deployment guide, architecture docs, and added a dedicated skill document (
skills/selfhosted-agent-render/SKILL.md) with API reference, deployment instructions, and auth guidanceImplementation Details
httpmodule (no external framework) for minimal dependenciescreated_at,updated_at,last_viewed_at, andexpires_attimestampsout/) alongside the API, so both modes coexist in one deployment</head>to avoid DOM parsing issueshttps://claude.ai/code/session_01NeNkCL34w4hCodzArQhZgJ
Summary by CodeRabbit
New Features
Documentation
Tests
Chores