bliss/CLAUDE.md
Your Name 03a9d2143d Allow fetch in db library vm contexts
Db library scripts are basically a JS module scoped to the db, and
handlers already get fetch — but runLibrary's own vm context only
bound sql/console, so any library calling fetch (e.g. the chat db's
OpenAI call) threw ReferenceError. Since library calls are often
fire-and-forget inside setTimeout (no rejection handler), that
crashed the whole process. Bind fetch in all three places a library
script runs: request-time (runLibrary), the library-editor save eval,
and repl.

Also add `bliss link` to bliss-cli for jumping straight to a
structure/route/template/db's workshop editor URL, including
resolving a db by its require('db')(alias) name.
2026-08-02 23:19:24 -04:00

6.7 KiB

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Commands

node index.js          # Start the server (port 3000)
npx prettier --write . # Format code

No build step. No test suite. Restart the server to pick up changes to index.js or db.js; changes to Eta templates in views/ are picked up live (cache is disabled).

Requires a .env file with VAPID_PUBLIC_KEY and VAPID_PRIVATE_KEY for web push.

What This Is

Bliss is a browser-based low-code platform where users build mini web apps ("Structures") entirely through a /workshop UI. Each Structure has routes, templates, and SQLite databases — all stored in the main app database and executed at runtime via Node's vm module.

Architecture

Two layers of routes

The Express app has two kinds of routes:

  1. Workshop routes (hardcoded in index.js) — The /workshop/* editor UI for managing structures, routes, templates, databases, and files.
  2. User routes (dynamic, loaded from DB) — Caught by the app.all("*") handler at the bottom of index.js. At request time it matches the path against the routes in-memory array (rebuilt by buildRoutes()), finds the matching route record, and executes the user's handler JS string inside a sandboxed VM context.

buildRoutes() must be called after any route is created/updated to refresh the in-memory matcher array.

Sandboxed execution (bootstrapContext)

User route handlers run in vm.createContext() with a restricted API:

  • require('eta') — Eta template instance scoped to the structure
  • require('db') — getter for named database instances. require('db')(alias) returns { library, sql }: sql is the raw better-sqlite3 instance for that db, and library is module.exports from that db's own "library" script (arbitrary JS, edited via /workshop/:structure_id/db/:db_id/library, run once per request with sql, console, and fetch bound in its own vm context — see runLibrary in index.js). Handlers typically call helper functions off library rather than writing raw SQL inline.
  • require('push') — web-push library
  • require('files'){ saveFile } helper
  • console.log — writes to the app's logs table (not stdout)
  • fetch, setTimeout, clearTimeout
  • route(url) — prefixes a URL with the structure's route_prefix

Handlers call res.render(templateName, context) or ws.render(templateName, context) to render Eta templates. These are injected onto req/res after the context is created (not available inside bootstrapContext itself).

WebSocket routes are bootstrapped once by bootstrapWebsocketHandler and stored in wsRoutes; subsequent saves only update the handler function without re-registering the ws path.

Template rendering (bootstrapTemplateWithHTMXetc)

Every HTML response passes through this function, which uses Cheerio to inject HTMX, Tailwind, Hyperscript, and bliss_inspector.js into the <head>, and attaches data-bliss-route / data-bliss-clone / data-bliss-copy attributes to <body> (or to fragment children for HTMX partial swaps). These attributes drive the in-page editor overlay.

  • data-bliss-route — link to the workshop editor page for whatever produced this element.
  • data-bliss-clonehx-get target for the clone-structure modal.
  • data-bliss-copy — only set for GET routes, via embedHTML(url) (index.js): <div hx-get="{url}" hx-trigger="load"></div>. This is the actual page-nesting primitive — the inspector's 📋 button copies this snippet to the clipboard, and pasting it into any other template lazy-loads that route as a live child page via HTMX. This is how Structures compose: pages nesting pages nesting pages, each one an independently addressable, independently editable route.

The inspector (bliss_inspector.js) and where it's headed

Currently the 🔍 overlay (injected into every rendered page, see above) only highlights [data-bliss-route] elements and offers links out to the editor, a copy-embed button, and a clone-structure modal — it does not let you edit anything in place yet.

The long-term goal is for the inspector to become a real-time, in-page structure editor: instead of jumping out to /workshop/..., you'd edit a nested page's route/template directly where it's embedded and see it update live. Two things that design needs and don't exist yet:

  • Each embedded view needs to carry its source (which structure/route/template produced it) — data-bliss-route currently only gives a link out, not enough info to edit inline.
  • Each embedded view needs to carry its args — the context/params it was rendered with (e.g. res.render(template, context)'s context, URL/query params) — so the in-place editor can show and modify what was actually passed in, not just the output.

Cross-fragment interaction (nested pages talking to each other) isn't a bespoke Bliss feature — it rides on standard HTMX/Hyperscript behavior since every nested page lives in the same DOM: hx-swap-oob for out-of-band updates (already used for head injection), and bubbling custom events (Hyperscript send/trigger, hx-trigger="eventName from:body") for one nested page to notify siblings/ancestors.

Database layer (db.js)

  • Main app DB: ./dbs/0.sqlite — stores structures, routes, templates, dbs metadata, files, logs.
  • User databases: ./dbs/{id}.sqlite — one SQLite file per user-created DB, opened on demand and cached via LRU (max 25 connections).
  • Eta template instances are also LRU-cached per structure (max 100). The cache must be invalidated if template content changes — currently the cache is not explicitly invalidated on update, relying on the LRU eviction policy.
  • Migrations in migrations/ are applied sequentially on startup (tracked in a migrations table).

Structure cloning

cloneStructure (in db.js) deep-copies a structure's routes, templates, scaffold pages, and optionally its databases (full copy vs. aliased reference). Aliased DBs share the same SQLite file across structures.

Views vs. user templates

  • views/ — Eta templates for the workshop UI itself (not user content).
  • User-created templates are stored as rows in the templates table and resolved at render time via getTemplater() / readFile.

Scaffold pages

Non-GET routes (POST, PUT, DELETE, WS) get auto-generated scaffold pages — test forms or WebSocket test UIs — stored in scaffold_pages. These are served at /workshop/:structure_id/route/:route_id/preview.

Frontend stack (workshop UI)

HTMX + Hyperscript + Tailwind (CDN, preflight disabled) + bliss_inspector.js (the in-page editor overlay). All served from public/js/.