mirror of
https://github.com/BetterCorp/BetterFrame.git
synced 2026-05-26 16:56:33 +00:00
Kiosk checks for stable firmware update before pairing. If available,
downloads + verifies + swaps binary and restarts. No auth needed.
Server: GET /api/firmware/public/check (stable channel, no auth)
GET /api/firmware/public/download/:id (rate-limited, no auth)
Kiosk: check_public() + apply_public() in firmware.rs. Called from
ui.rs worker thread before entering pairing loop. kiosk_app_version
made pub for access from ui.rs.
Also includes kiosk_id deserialization fix (Value instead of String).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
7 KiB
7 KiB
BSB Framework — Practical Learnings
Lessons learned building BetterFrame on BSB v9. Intended for updating bsbcode.dev/llms.txt so future LLM sessions don't repeat these mistakes.
Container Runtime
Base Image
- Image:
betterweb/service-base:9(Docker Hub) - Alpine-based (use
apknotapt-get) - Entrypoint:
/root/entrypoint.sh— runs as root, drops to unprivileged user - DO NOT set
USERin child Dockerfiles — BSB entrypoint handles privilege dropping - DO NOT override
CMDorENTRYPOINT— BSB handles startup
Production Environment
- Set
ENV NODE_ENV=productionandENV BSB_LIVE=truein Dockerfile - Without
BSB_LIVE=true, BSB warns about non-production and tries to write sec-config.yaml
Plugin Discovery
- BSB searches:
/home/bsb/node_modules/<package>/lib/plugins/<type>-<name>/index.js - Also searches:
/mnt/bsb-plugins/node_modules/...but this is a VOLUME — build-time COPY gets shadowed by empty anonymous volume at runtime - USE
/home/bsb/node_modules/for build-time plugin installation - Working directory is
/home/bsb
Plugin Registration in Config
services:
service-my-plugin:
package: my-npm-package-name # <-- REQUIRED for container mode
plugin: service-my-plugin
enabled: true
config: {}
Without package:, BSB only finds built-in plugins (config-default, events-default, log-default).
The package value must match the name field in the plugin's package.json.
Config Schema
Schema Extractor Limitations
bsb-plugin-cli buildextracts config schemas from TypeScript source statically- Cannot resolve cross-file imports — imported schema variables show as "not defined"
- Workaround: inline anyvali schema definitions in each plugin's ConfigSchema
- Type-only imports (
import type { ... }) are fine — only runtime value imports break extraction
// BAD — schema extractor can't resolve this:
import { dbConfigSchema } from "../../shared/db/config.js";
const ConfigSchema = av.object({ db: dbConfigSchema, ... });
// GOOD — inline the schema:
const ConfigSchema = av.object({
db: av.object({
driver: av.enum_(["sqlite", "postgres"] as const).default("postgres"),
host: av.string().default("postgres"),
// ...
}, { unknownKeys: "strip" }),
// ...
});
Nested Config Objects
- Config supports nested
av.object()— use for grouping related fields:
config:
db:
driver: postgres
host: postgres
host: 0.0.0.0
port: 18080
- Access via
this.config.db.driver,this.config.host
Config Defaults
- BSB does NOT apply anyvali schema defaults for keys missing from sec-config.yaml
- Always declare config values explicitly in sec-config.yaml
- Defaults in the schema are documentation, not runtime fallbacks
No Environment Variable Access
- Application code should never read
process.env - All config comes from sec-config.yaml via
this.config.* - For Docker/Coolify: use build-time
envsubston a template to generate sec-config.yaml - sec-config.yaml should be baked into the image or bind-mounted — not generated at runtime
Plugin Architecture
What Should Be a Plugin
- Services with own port, lifecycle, independent scaling
- Examples: HTTP server, WebSocket server, API server
What Should NOT Be a Plugin
- Database access layer (just a library — no port, no lifecycle)
- Shared utilities, crypto helpers, auth logic
- Anything that's just a function other plugins call
Cross-Plugin Communication
- Plugins should have zero runtime imports from other plugins
- Type-only imports (
import type { ... }) are acceptable - Communication between plugins: BSB event bus (emitBroadcast, emitReturnableEvent)
- Shared code goes in
src/shared/— imported by multiple plugins as a library
Plugin Init Order
initAfterPlugins/initBeforePluginscontrol order- Not needed when plugins are independent (each inits own DB, etc.)
Build System
Build Command
cross-env NODE_OPTIONS="--import tsx" bsb-plugin-cli build
- BSB build needs
tsxfor schema extraction from TypeScript source - Build output:
lib/directory with compiled JS +bsb-plugin.json
bsb-plugin.json
- Auto-generated by build, lists all discovered plugins
- Must match the plugins present in
src/plugins/ - If a plugin is removed, this file must be regenerated
Dev Mode
cross-env NODE_OPTIONS="--import tsx" bsb-plugin-cli dev
- Hot reload, runs TypeScript directly
- Creates
.bsbdevwatchfile for include/exclude patterns
Logging
Log Message Format
obs.log.info("text {tag}", { tag: value })— structured logging- Message strings MUST be string literals (BSB SmartLogMeta extracts placeholders from literal type)
- String concatenation (
"a " + "b") widens tostringand breaks SmartLogMeta placeholder extraction - Template literals in log messages work for the value, not the message key
// BAD:
obs.log.info("connecting to " + url, {});
// GOOD:
obs.log.info("connecting to {url}", { url });
Docker Deployment Pattern
Dockerfile Structure
# Builder — compile TypeScript + native deps
FROM node:24-trixie-slim AS builder
WORKDIR /app
# ... npm ci, npm run build ...
# Runtime — BSB container
FROM betterweb/service-base:9
# Install extras (Alpine)
RUN apk add --no-cache gettext ffmpeg
# Copy built plugin into BSB's node_modules
COPY --from=builder /app/server/package.json /home/bsb/node_modules/<pkg>/package.json
COPY --from=builder /app/server/bsb-plugin.json /home/bsb/node_modules/<pkg>/bsb-plugin.json
COPY --from=builder /app/server/lib /home/bsb/node_modules/<pkg>/lib
COPY --from=builder /app/node_modules /home/bsb/node_modules/<pkg>/node_modules
# Generate config from template
COPY sec-config.template.yaml /tmp/sec-config.template.yaml
RUN envsubst < /tmp/sec-config.template.yaml > /home/bsb/sec-config.yaml \
&& chmod 444 /home/bsb/sec-config.yaml \
&& rm /tmp/sec-config.template.yaml
ENV NODE_ENV=production
ENV BSB_LIVE=true
sec-config.template.yaml Pattern
- Template with
${VAR}placeholders (NOT${VAR:-default}— envsubst doesn't support defaults) - Defaults come from Dockerfile
ARGdeclarations - Secrets set as Coolify build args (not in git)
- Template committed to public repo (safe — no secrets)
- Generated sec-config.yaml baked into image at build time
Common Pitfalls
betterweb/service-base:nodeis v8,:9is v9 — use the correct tag- VOLUME declarations in base image shadow build-time COPY — don't write to
/mnt/bsb-plugins USER nodeblocks BSB entrypoint — entrypoint is at/root/entrypoint.sh, needs root access- Schema extractor can't follow imports — inline schemas in plugin ConfigSchema
package:required in sec-config — without it, BSB won't find external plugins- Alpine = apk, not apt-get — base image is Alpine Linux
- sec-config.yaml must be writable in dev, read-only in prod — BSB_LIVE=true skips write attempts