lightbase-edge
v0.1.11
Published
The one-liner: full-Node SSR on Cloudflare Pages with ZERO Cloudflare Workers. Drop into any codebase — bundles your server, publishes it to your own Pages CDN, registers a sha-pinned auto-deploy manifest with Lightbase, and generates the preflight-free z
Maintainers
Readme
lightbase-edge — the one-liner (v0.1.1, production/enterprise posture)
BismiLLAH Ar-Rahman Ar-Raheem. Drop this into any codebase and get full-Node SSR on Cloudflare Pages with ZERO Cloudflare Workers. Developers just code normally — no edge-function setup, no codebase changes beyond the build step. Lightbase edge does ALL the server-side work; Cloudflare Pages serves static assets unmetered from its CDN.
Quick start (drop-and-forget)
npm i -D lightbase-edge
npx lightbase-edge init # detects your framework, wires package.jsoninit writes lightbase.edge.json and wires your scripts:
"scripts": {
"lb:deploy": "lightbase-edge deploy",
"postbuild": "lightbase-edge deploy"
}From then on, npm run build is the only command you ever run — the
postbuild hook bundles, publishes and registers automatically. Without the
Lightbase env vars (a plain local build) the hook skips gracefully; on
Cloudflare Pages CI it deploys. Set these once in the Pages UI
(Settings → Environment variables — editable anytime, never baked into Git):
LIGHTBASE_URL=https://<lightbase-host>
LIGHTBASE_PROJECT_ID=<your-project-id>
LIGHTBASE_DEPLOY_TOKEN=lb_deploy_… # deploy-scope key (or admin key)
# optional:
LIGHTBASE_EDGE_ENV={"SECRET":"value"} # server-side-only app env (JSON string)
LIGHTBASE_EDGE_APP_ID=<app-id> # overrides lightbase.edge.json appId
PUBLIC_SITE_URL=https://<your-pages-url> # when building outside CF Pages CICF Pages build command: npm run build (that's all). GitHub CI is optional —
Pages' Git integration builds on every push; without any CI you can run the
same build anywhere and npx wrangler pages deploy <staticOut>.
The contract
| You write | What ships | Who runs it |
|---|---|---|
| A normal Node SSR app (Astro output:'server' + node adapter, Next output:'standalone', Express, Koa, plain http.createServer) | a portable CJS bundle published as static files on your own Pages deployment (/_lb/<app>/<hash>/server.js) | Lightbase edge — fetched at request time, sha256-pinned, executed in a hardened worker runtime; cached in RAM (never on instance disk) |
| Nothing else | _redirects + a universal shell + a service-worker reverse proxy | Cloudflare Pages CDN — static layer only, ZERO Workers quota |
lightbase.edge.json — full field reference
Written by init, safe to hand-edit. Every field is optional unless noted.
| Field | Type | Default | Meaning |
|---|---|---|---|
| appId | string (required) | from LIGHTBASE_EDGE_APP_ID or folder name | Stable app id ([a-z0-9-]). The manifest key on Lightbase and the /_lb/<appId>/ CDN path. |
| framework | string | auto-detected | astro | next | nuxt | express | custom. Drives defaults only. |
| entry | string | per framework | Path of the BUILT server entry (input to esbuild). Astro: dist/server/entry.mjs; Next: .next/standalone/server.js. |
| staticOut | string | dist | The folder that becomes your Pages deployment root. Everything Pages should serve must end up here. |
| clientDir | string | — | Astro standalone crucial setting: your build emits client assets into dist/client and the server into dist/server. Set "clientDir": "dist/client" and "staticOut": "dist-pages" — the deploy assembles dist-pages = client assets at the ROOT + /_lb/<app>/<hash>/server.js + bridge files. |
| assetPrefixes | string[] | framework defaults | Path prefixes served by the Pages CDN (bypass the SW reverse proxy). Astro defaults: /_astro/, /fonts/, /images/, /favicon. The deploy also auto-derives every top-level file/dir of staticOut as a prefix. |
| name | string | appId | Human name registered on Lightbase. |
| corsOpen | boolean | false | true = the SSR gateway answers any Origin (public demos). Keep false for production — origins are pinned automatically to your Pages URL. |
| timeoutMs | number | 30000 | Per-request SSR wall clock (max 300000). |
| enforced | boolean | false | true = deploy FAILS instead of skipping when Lightbase env vars are absent (use on required-deploy CI). |
Real-world Astro example (as used by all production integrations)
{
"appId": "my-app",
"name": "My App",
"framework": "astro",
"entry": "dist/server/entry.mjs",
"staticOut": "dist-pages",
"clientDir": "dist/client",
"assetPrefixes": ["/_astro/", "/fonts/", "/images/", "/favicon", "/icons/"],
"timeoutMs": 60000
}And the matching astro.config.mjs:
import { defineConfig } from 'astro/config';
import node from '@astrojs/node';
export default defineConfig({
output: 'server',
adapter: node({ mode: 'standalone' }),
});Astro emits dist/client/ + dist/server/; the postbuild hook assembles the
Pages root (dist-pages) automatically. In the CF Pages UI, set the build
output directory to your staticOut (e.g. dist-pages).
postbuild settings — the automatic part
init wires (and never clobbers an existing chain):
"scripts": {
"build": "astro build",
"postbuild": "lightbase-edge deploy"
}- Existing
postbuildchains are preserved:"<old> && lightbase-edge deploy". npm run buildon CF Pages → build → deploy → bridge generation, in one CI pass. New bundle hash ⇒ sha-pinned manifest update ⇒ instant real-time update on the edge (no waiting, no baking).- Local builds without Lightbase env vars print one line and exit 0.
Is hosting the bundle on my own Pages CDN secure? (yes — by design)
https://<app>.pages.dev/_lb/<app>/<hash>/server.js is a compiled bundle
of your own SSR app, served as a static file from YOUR deployment:
- No secrets inside. Env/secrets never enter the bundle — they ride
LIGHTBASE_EDGE_ENV(or the manifestenv), are sealed AES-256-GCM at rest on Lightbase, and are injected only into the sandbox at execution. Root keys stay server-side; the bundle is exactly as sensitive as the client JS you already ship. - Same trust level as your own static site. It is your code, on your origin, under your deployment's access controls. There is no third-party bucket involved — the CDN path is your own domain.
- sha256-pinned, fail-closed. Lightbase fetches files by hash and verifies every byte; a tampered or stale CDN file simply never executes.
- Not executable in the browser. It is a Node CJS server bundle
(
require, node builtins) — browsers cannot run it meaningfully; it is input to the Lightbase worker runtime only. - Enterprise posture. The execution sandbox has no disk, no network
sockets beyond the host-provided fetch, no
child_process, noworker_threads; per-request wall clocks; adaptive memory gates with relief (never usage caps); every manifest path is traversal-proofed (relative, no..); origins are allowlisted per app.
This is exactly the posture an Enterprise-grade financial platform needs: secrets server-side, code integrity hash-pinned, zero Workers to audit, and your CDN serves only your own code.
How the zero-Workers SSR bridge works
- First visit: Pages serves the shell (CDN-fast, branded loader). The
bootstrap fetches the SSR HTML for the current path from the Lightbase
gateway (one resilience retry), swaps the FULL document in (head + body,
scripts re-instantiated → frameworks hydrate normally), and — in
parallel — runs the parse-guard:
/sw.jsis verified to really be the Lightbase bridge before registration; a stale PWA service worker from an older deployment is unregistered + its caches cleared with one clean reload (no white pages, no ghosts). - Every request after that: the service worker reverse-proxies the request to the gateway with full fidelity — method, body, status codes, redirects (rewritten same-origin so login flows follow correctly), and a durable cookie jar (IndexedDB inside the worker, survives restarts). Static assets bypass the worker and hit the Pages CDN. A network hiccup answers a self-reloading 503 instead of a hard error.
- Zero Cloudflare Workers anywhere: the Pages project is a static
project (
_redirectsis asset-layer routing; the service worker runs in the browser, not on Cloudflare).
Wire protocol (preflight-free)
Some PaaS gateways (Zoho Catalyst AppSail among them) intercept CORS OPTIONS preflights at the edge and answer them without Access-Control headers — any browser fetch carrying a custom header dies in preflight (the symptom: blank pages). The bridge therefore sends ONLY CORS-safelisted requests and tunnels everything else through query parameters the gateway strips:
| Tunnel | Meaning | Example |
|---|---|---|
| __lbjar | the app's cookie jar (encoded) | __lbjar=sid%3Dabc%3Btheme%3Ddark |
| __lbmethod | effective HTTP method (PUT/PATCH/DELETE ride a POST) | __lbmethod=DELETE |
| __lbct | real content-type (JSON travels as text/plain) | __lbct=application/json |
Native headers still win when present, so server-to-server callers are
unaffected. The legacy x-lb-cookie header path is unchanged.
Commands
npx lightbase-edge init # detect framework → write lightbase.edge.json + wire scripts
npx lightbase-edge build # esbuild the server entry → portable CJS bundle
npx lightbase-edge deploy # bundle + publish + register + bridge (the one-liner)
npx lightbase-edge status # app status + stats from Lightbase
npx lightbase-edge logs # tail the app's structured log ringWhat deploy does (fully automatic)
- esbuild-bundles the server entry → portable CJS (node builtins external).
- Assembles the Pages root: client assets at the ROOT + the bundle under
_lb/<app>/<hash12>/(hash-versioned directory — instant real-time updates, free rollback history, no CDN staleness). - Registers the sha-pinned manifest with Lightbase (auto-deploy — new content hash ⇒ instant cache invalidation everywhere).
- Derives static asset prefixes from your build output (so the service worker knows exactly what is CDN-static vs SSR-dynamic).
- Generates the bridge files (
_redirects, shellindex.html,sw.js) LAST — they win anypublic/collision (e.g. an old PWA service worker).
Framework checklist
| Framework | Requirement |
|---|---|
| Astro | output: 'server' + @astrojs/node({ mode: 'standalone' }); set clientDir: "dist/client" + a dedicated staticOut |
| Next | output: 'standalone' |
| Express / Koa / custom | set entry in lightbase.edge.json to your server file |
| Nuxt | nitro node-server preset (entry = .output/server/index.mjs) |
Honest limits
- The very first visit needs JS (the shell is a CDN page); after that the service worker serves true SSR documents. JS-rendering crawlers — including Googlebot — index the SSR content.
- WebSockets are not bridged (SSE streams fine).
- Third-party-cookie browsers are unaffected: the cookie jar is first-party (localStorage + IndexedDB), never cross-site.
