Skip to content

feat(selfhosted): add optional self-hosted variant with SQLite persistence#19

Open
kilo-code-bot[bot] wants to merge 1 commit intomainfrom
session/agent_7767eee6-1728-49db-8729-07a75a14e375
Open

feat(selfhosted): add optional self-hosted variant with SQLite persistence#19
kilo-code-bot[bot] wants to merge 1 commit intomainfrom
session/agent_7767eee6-1728-49db-8729-07a75a14e375

Conversation

@kilo-code-bot
Copy link

@kilo-code-bot kilo-code-bot bot commented Mar 20, 2026

Summary

  • Self-hosted app: Added a separate Next.js server app at selfhosted/ that stores artifact payloads in SQLite and serves them via UUID links (/{uuid}). The static fragment-based app at the root is unchanged.
  • SQLite persistence: better-sqlite3 database with artifacts table storing payloads mapped by UUID v4, with created_at, updated_at, last_viewed_at, and expires_at fields.
  • REST API: POST /api/artifacts (create), GET /api/artifacts/:id (read + TTL refresh), DELETE /api/artifacts/:id, PUT /api/artifacts/:id (update), POST /api/cleanup (remove expired rows).
  • 24h sliding TTL: Every successful GET/view extends expires_at by 24 hours. Expired entries return 404 and are cleaned up lazily on read.
  • Shared viewer: New SelfHostedViewerShell component (src/components/selfhosted-viewer-shell.tsx) accepts a pre-fetched PayloadEnvelope prop and renders the same artifact stage, toolbar, and renderer slots as the fragment-based viewer. Supports copy, download, markdown print-to-PDF, artifact bundle switching, and all 5 artifact kinds.
  • Server-rendered route: selfhosted/src/app/artifact/[id]/page.tsx fetches the payload from SQLite, validates it, and passes it to the shell component.
  • Docker deployment: Dockerfile (standalone build) and docker-compose.yml with persistent volume.
  • Skill: skills/selfhosted-agent-render/SKILL.md — full deployment, API usage, TTL, cleanup, and auth guidance for agents.
  • Docs: Updated README.md, docs/architecture.md, docs/deployment.md, docs/dependency-notes.md, and AGENTS.md to document the optional self-hosted mode.

New files

File Purpose
selfhosted/package.json Separate app dependencies (includes better-sqlite3, uuid)
selfhosted/next.config.ts Server app config with @shared/@ webpack aliases to parent src/
selfhosted/tsconfig.json Path aliases for @shared/*, @self/*
selfhosted/src/lib/db.ts SQLite singleton with WAL mode, artifacts table
selfhosted/src/lib/artifacts.ts CRUD: createArtifact, getArtifact, deleteArtifact, updateArtifact, cleanupExpired
selfhosted/src/app/api/artifacts/route.ts POST create endpoint
selfhosted/src/app/api/artifacts/[id]/route.ts GET/DELETE/PUT endpoints
selfhosted/src/app/api/cleanup/route.ts POST cleanup endpoint
selfhosted/src/app/artifact/[id]/page.tsx Server-rendered viewer page
selfhosted/src/app/page.tsx Self-hosted home page with API usage docs
selfhosted/src/app/layout.tsx Root layout with fonts + theme
selfhosted/styles/globals.css Copy of root app's CSS
selfhosted/Dockerfile Standalone Docker build
selfhosted/docker-compose.yml Docker Compose with persistent volume
src/components/selfhosted-viewer-shell.tsx Client shell accepting envelope prop
skills/selfhosted-agent-render/SKILL.md Agent-facing deployment and usage guide

Modified files

File Change
README.md Added self-hosted variant section
docs/architecture.md Added self-hosted architecture section
docs/deployment.md Added self-hosted deployment instructions
docs/dependency-notes.md Added self-hosted dependency notes
AGENTS.md Added self-hosted key files, updated docs alignment list
tsconfig.json Excluded selfhosted/ from root typecheck
eslint.config.mjs Added selfhosted/** to eslint ignores

Testing

  • ✅ Root app lint: npm run lint — passed
  • ✅ Root app typecheck: npm run typecheck — passed
  • ✅ Root app tests: npm run test — 62 tests passed
  • ✅ Root app build: npm run build — static export succeeded
  • ✅ Selfhosted typecheck: cd selfhosted && npm run typecheck — passed
  • ✅ Selfhosted build: cd selfhosted && npm run build — standalone build succeeded

Architecture

The selfhosted app reuses the parent app's viewer components via webpack aliases:

  • @shared and @ both resolve to ../src/ (parent's source)
  • @self resolves to ./src/ (selfhosted's own modules)
  • CSS is imported directly from the parent's src/app/globals.css

This keeps the static app completely untouched while sharing rendering code.

…tence

Add a separate self-hosted Next.js app at selfhosted/ that reuses the
existing viewer/renderers while storing payloads in SQLite under UUID v4.

Key additions:
- SQLite database module with artifacts table (better-sqlite3)
- REST API: POST/GET/DELETE/PUT /api/artifacts, POST /api/cleanup
- 24-hour sliding TTL with lazy expiry on read
- Server-rendered /artifact/[id] route reusing shared viewer components
- SelfHostedViewerShell component accepting envelope prop
- Docker Compose deployment config with persistent volume
- selfhosted-agent-render skill documentation

The static fragment-based app at the root is unchanged.
Both apps pass lint, typecheck, tests, and build successfully.

Updated docs:
- README.md, docs/architecture.md, docs/deployment.md
- docs/dependency-notes.md, AGENTS.md
@cloudflare-workers-and-pages
Copy link

Deploying agent-render with  Cloudflare Pages  Cloudflare Pages

Latest commit: 005903a
Status: ✅  Deploy successful!
Preview URL: https://23e6172c.agent-render.pages.dev
Branch Preview URL: https://session-agent-7767eee6-1728.agent-render.pages.dev

View logs

return null;
}

if (new Date(row.expires_at + "Z").getTime() < Date.now()) {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: Timezone inconsistency in expiration check

This line treats expires_at (a SQLite datetime in local time) as UTC by appending "Z", but compares against Date.now() which is UTC. This can cause expiration logic to be off by the server's timezone offset. For example, if the server is in UTC+5, artifacts may appear to expire 5 hours early or late.

The cleanup function at line 93 correctly uses datetime('now') (SQLite local time) for comparison. Consider aligning this check to use the same timezone semantics.

@kilo-code-bot
Copy link
Author

kilo-code-bot bot commented Mar 20, 2026

Code Review Summary

Status: 1 Issue Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 1
SUGGESTION 0
Issue Details (click to expand)

WARNING

File Line Issue
selfhosted/src/lib/artifacts.ts 39 Timezone inconsistency in expiration check - treats SQLite local datetime as UTC, causing potential offset errors
Files Reviewed (27 files)
  • AGENTS.md - docs update
  • README.md - docs update
  • docs/architecture.md - docs update
  • docs/dependency-notes.md - docs update
  • docs/deployment.md - docs update
  • eslint.config.mjs - config update
  • selfhosted/src/lib/db.ts - database schema
  • selfhosted/src/lib/artifacts.ts - CRUD operations (1 issue)
  • selfhosted/src/app/api/artifacts/route.ts - POST endpoint
  • selfhosted/src/app/api/artifacts/[id]/route.ts - GET/DELETE/PUT endpoints
  • selfhosted/src/app/api/cleanup/route.ts - cleanup endpoint
  • selfhosted/src/app/artifact/[id]/page.tsx - server-rendered viewer
  • selfhosted/src/app/page.tsx - home page
  • src/components/selfhosted-viewer-shell.tsx - shared viewer shell
  • Other selfhosted config files

General Notes:

The self-hosted variant implementation is well-structured. The database schema, CRUD operations, and API endpoints follow good practices. UUID validation, payload format validation, and the sliding TTL mechanism are properly implemented. The architecture correctly reuses viewer components from the parent app via webpack aliases.

The only issue found is a timezone handling inconsistency in the artifact expiration check that could cause edge cases depending on the server's timezone configuration.


Reviewed by minimax-m2.5-20260211 · 419,788 tokens

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants