bliss/CLAUDE.md
2026-08-08 17:57:14 -04:00

8.1 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.

Design philosophy: this is a hypermedia homestead

Bliss templates are not a web-app-with-an-API. There is no client-side framework, no JSON contract, no build step — the HTML is the application, and it composes by embedding more HTML (see "page-nesting primitive" below). Every template you write or edit should lean into that instead of fighting it:

  • Prefer <%~ %> (raw) over <%= %> (HTML-escaped) in Eta templates, always, unless you are interpolating into an HTML attribute value. Attribute values (id="...", data-*="...", href="...", style="...", value="...") are the one place escaping is not optional — an unescaped " there breaks the tag itself, not just a security nicety. Everywhere else (text content, <script> string literals, anything that isn't sitting inside attr="…") use <%~ %>. Arbitrary HTML/JS in user content is a feature here, not a bug to guard against — see the aim structure's message template for the canonical comment explaining why. When in doubt: default to raw, escape only when the HTML would literally break otherwise.
  • Prefer htmx attributes over hand-rolled JS for anything hypermedia-shaped — polling (hx-trigger="every 10s"), lazy loading (hx-trigger="load"), swapping (hx-swap-oob), cross-fragment events (hx-trigger="foo from:body") — instead of setTimeout/fetch/manual DOM patching. If htmx (or Hyperscript for small imperative glue) can express it declaratively, don't write the imperative version.

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/.