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.
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:
- Workshop routes (hardcoded in
index.js) — The/workshop/*editor UI for managing structures, routes, templates, databases, and files. - User routes (dynamic, loaded from DB) — Caught by the
app.all("*")handler at the bottom ofindex.js. At request time it matches the path against theroutesin-memory array (rebuilt bybuildRoutes()), finds the matching route record, and executes the user'shandlerJS 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 structurerequire('db')— getter for named database instances.require('db')(alias)returns{ library, sql }:sqlis the rawbetter-sqlite3instance for that db, andlibraryismodule.exportsfrom that db's own "library" script (arbitrary JS, edited via/workshop/:structure_id/db/:db_id/library, run once per request withsql,console, andfetchbound in its own vm context — seerunLibraryinindex.js). Handlers typically call helper functions offlibraryrather than writing raw SQL inline.require('push')— web-push libraryrequire('files')—{ saveFile }helperconsole.log— writes to the app'slogstable (not stdout)fetch,setTimeout,clearTimeoutroute(url)— prefixes a URL with the structure'sroute_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-clone—hx-gettarget for the clone-structure modal.data-bliss-copy— only set for GET routes, viaembedHTML(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-routecurrently 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)'scontext, 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 amigrationstable).
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
templatestable and resolved at render time viagetTemplater()/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/.