A multi-tenant portal platform composed of independent services unified by a single theme. API-first, HTMX-first, schema-validated.
BetterPortal is a protocol first, a Node SDK second. The wire protocol (HTTP + HTMX + JSON) is specified in spec/ so services and themes can be implemented in any language. The reference implementations in this repo (under framework/nodejs, themes/nodejs, etc.) are one SDK that speaks the protocol — and are built on BSB (Better Service Base). PHP, Python, Go, Rust, etc. SDKs are welcome (none exist yet).
BetterPortal lets you compose a tenant-aware web portal out of small, independent services that each own their own data, config, and views. A theme service renders the host shell; HTMX swaps content from each business service directly in the browser. There is no client-side framework, no proxy layer, no shared monolith.
┌─────────────┐
│ Browser │
└──┬──┬───┬───┘
shell + nav │ │ │ HTMX fragments / SSE
┌─────────┘ │ │ (CORS, direct)
▼ │ ▼
┌──────────────┐ │ ┌─────────────┐
│ Theme(s) │ │ │ Service A │
│ port 3100 │ │ │ port 3200 │
│ bootstrap1 │ │ └─────────────┘
└──────────────┘ │ ┌─────────────┐
└──►│ Service B │
│ port 3300 │
└─────────────┘
Theme serves the HTML shell + nav/style/brand/fragment-list endpoints.
Each service is its own origin; the browser calls them directly via
HTMX. The theme NEVER proxies content from a service.
- Microservices without microservice pain. Each service is its own deployable, but the portal feels like one application to the user.
- No SPA framework. Server-rendered HTML. HTMX swaps fragments in. State lives in the URL, server, and DOM. That's all.
- No iframes. Composition is fragment swaps, not nested browsing contexts.
- Schema-validated everywhere. Inputs and outputs go through anyvali. Nothing untyped crosses a boundary.
- File-based routing. Drop a folder under
bp-routes/— it becomes a route, validated and themed. - Per-tenant config. Encrypted secrets, scoped sync, admin UI.
| Layer | Where | What |
|---|---|---|
| Protocol | spec/ |
Normative HTTP + HTMX + JSON contracts. Any language can implement. |
| Node SDK | framework/nodejs/, plugins/nodejs/betterportal-bsb/ |
Reference TypeScript implementation of the protocol, built on BSB. |
| Reference services | auth/nodejs/, themes/nodejs/, services/nodejs/admin/config-manager/ |
Concrete services built with the Node SDK. |
| Examples | services/nodejs/examples/hello-view/ |
A minimal Node service demonstrating views, fragments, SSE. |
When the SDK and the spec disagree, the spec wins — file a bug.
BetterPortal/
├── spec/ # NORMATIVE protocol specification (language-neutral)
├── bp-config.yaml # platform config (tenants/apps/routes/services/menu/fragments)
├── framework/nodejs/ # Node SDK: contracts, runtime, codegen, h3 adapter
├── plugins/nodejs/betterportal-bsb/ # BSB ↔ Node SDK integration (BPService base class)
├── themes/nodejs/
│ ├── bootstrap1/ # reference theme: Bootstrap 5 + HTMX shell
│ └── embedded/ # reference theme: lightweight embedded renderer
├── auth/nodejs/ # optional OIDC-style auth platform service
├── services/nodejs/
│ ├── examples/hello-view/ # example business service (clock SSE, profile fragment, showcase)
│ └── admin/config-manager/ # admin UI: tenants, services, routes, menu, fragments, preview
├── llms.txt # detailed developer guide (humans + LLMs)
└── docs/ # architecture overview + ADRs
npm install
# build (workspace-aware)
npm run -ws build
# in three terminals (or background, your call):
cd themes/nodejs/bootstrap1 && npm start # http://localhost:3100
cd services/nodejs/admin/config-manager && npm start # http://localhost:3300
cd services/nodejs/examples/hello-view && npm start # http://localhost:3200
# wait for /.well-known/bp/manifest on each, then open http://localhost:3100The default bp-config.yaml ships with the betterportal tenant + betterportal-web app pre-configured. The sidebar menu exposes the showcase routes (hello-view) and admin tools (config-manager).
- Scaffold a directory under
services/nodejs/<category>/<name>/(mirrorservices/nodejs/examples/hello-view/). - Declare the plugin in
src/plugins/<plugin-name>/index.tsextendingBPServicefrom@betterportal/plugin-bsb. - Drop routes under
bp-routes/<routeDir>/index.ts. ExportResponseSchema,handleGetviacreateHandler(...). Add theme renderers in_theme.<themeId>/index.tsx. - Run
npx bp-codegen(regenerates.bp-generated/registry.ts). npm run build && npm start— visit/.well-known/bp/manifest.- Register in
bp-config.yamlunder a tenant'sservices[](or as a platform service) and bind a route inapps[].routes[].
The full developer guide lives in llms.txt — written for LLM agents but equally useful for humans new to the codebase. It covers file conventions, HTMX patterns, design constraints, the auth model, SSE, and codegen in detail.
| Concept | What it is |
|---|---|
| Tenant | Isolation boundary; owns services + branding. |
| App | A site under a tenant; has theme, routes, menu, fragments. |
| Service | BSB plugin exposing typed views + manifest, registered under a tenant. |
| Platform service | Shared cross-tenant service (e.g., auth); tenants opt in. |
| Route | path → service + view + targetPath binding in an app. |
| View | bp-routes/<dir>/index.ts — schemas + handler. Theme renderers live alongside in _theme.<themeId>/index.tsx. |
| Fragment | HTML island at a named location (nav, footer). File: _<location>.<id>.tsx. Optional SSE renderer: _<location>.<id>.sse.tsx. |
| Menu | Per-app tree (groups + links) driving the sidebar nav. |
- No iframes.
- No SPA framework, no client-side router, no client-side state library, no client-side data-fetching library.
- Server never emits absolute URLs — client rewrites root-relative
/footo the correct service origin viadata-bp-service. - HTML-as-API. Services emit HTML fragments; the browser inserts them.
- Cookies are theme-origin only. Auth tokens travel via
hx-headers, never cross-origin cookies. - Codegen is mandatory. Hand-written registries are not supported.
See llms.txt § 1b for the full list.
| Package | Purpose |
|---|---|
@betterportal/framework |
Contracts, runtime, codegen CLI (bp-codegen), h3 adapter, schema helpers. |
@betterportal/plugin-bsb |
BPService base class; wires h3, CORS, observability into BSB. |
@betterportal/theme-bootstrap1 |
Default theme: Bootstrap 5 + HTMX shell, theme designer, nav/brand/style/fragment refresh endpoints. |
@betterportal/theme-embedded |
Lightweight embedded renderer for headless / external embeds. |
@betterportal/auth |
Optional JWT auth platform service. |
@betterportal/config-manager |
Admin UI for tenants, services, routes, menu, fragments, preview. |
@betterportal/hello-view |
Example business service. |
All packages live in this repo as a single npm workspace. Versioning is unified.
Pre-release. APIs are stabilizing. Expect breaking changes between minor versions until 1.0.
PRs welcome. Before opening one:
- Read
llms.txtend-to-end — especially § 1b Design constraints. npm run -ws buildshould pass on every workspace.- New routes require
npx bp-codegenbefore commit. - Don't break the no-iframes / no-SPA / no-absolute-URLs rules.
Dual-licensed:
- GNU AGPL-3.0-only — for open-source use. If you run a modified BetterPortal as a network service, you must offer the modified source to your users.
- Commercial license — for use without AGPL obligations. Contact BetterCorp.
See LICENSE for the full AGPL text and package.json for SPDX identifiers.