# 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 `apk` not `apt-get`) - Entrypoint: `/root/entrypoint.sh` — runs as root, drops to unprivileged user - **DO NOT** set `USER` in child Dockerfiles — BSB entrypoint handles privilege dropping - **DO NOT** override `CMD` or `ENTRYPOINT` — BSB handles startup ### Production Environment - Set `ENV NODE_ENV=production` and `ENV BSB_LIVE=true` in 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//lib/plugins/-/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 ```yaml 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 build` extracts 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 ```typescript // 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: ```yaml 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 `envsubst` on 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` / `initBeforePlugins` control order - Not needed when plugins are independent (each inits own DB, etc.) --- ## Build System ### Build Command ```bash cross-env NODE_OPTIONS="--import tsx" bsb-plugin-cli build ``` - BSB build needs `tsx` for 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 ```bash cross-env NODE_OPTIONS="--import tsx" bsb-plugin-cli dev ``` - Hot reload, runs TypeScript directly - Creates `.bsbdevwatch` file 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 to `string` and breaks SmartLogMeta placeholder extraction - Template literals in log messages work for the value, not the message key ```typescript // BAD: obs.log.info("connecting to " + url, {}); // GOOD: obs.log.info("connecting to {url}", { url }); ``` --- ## Docker Deployment Pattern ### Dockerfile Structure ```dockerfile # 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//package.json COPY --from=builder /app/server/bsb-plugin.json /home/bsb/node_modules//bsb-plugin.json COPY --from=builder /app/server/lib /home/bsb/node_modules//lib COPY --from=builder /app/node_modules /home/bsb/node_modules//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 `ARG` declarations - 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 1. **`betterweb/service-base:node` is v8, `:9` is v9** — use the correct tag 2. **VOLUME declarations in base image shadow build-time COPY** — don't write to `/mnt/bsb-plugins` 3. **`USER node` blocks BSB entrypoint** — entrypoint is at `/root/entrypoint.sh`, needs root access 4. **Schema extractor can't follow imports** — inline schemas in plugin ConfigSchema 5. **`package:` required in sec-config** — without it, BSB won't find external plugins 6. **Alpine = apk, not apt-get** — base image is Alpine Linux 7. **sec-config.yaml must be writable in dev, read-only in prod** — BSB_LIVE=true skips write attempts