diff --git a/BSB_LEARNINGS.md b/BSB_LEARNINGS.md deleted file mode 100644 index 3094fb8..0000000 --- a/BSB_LEARNINGS.md +++ /dev/null @@ -1,203 +0,0 @@ -# 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 diff --git a/server/src/shared/db/MULTI_TENANT.md b/server/src/shared/db/MULTI_TENANT.md deleted file mode 100644 index 878a687..0000000 --- a/server/src/shared/db/MULTI_TENANT.md +++ /dev/null @@ -1,33 +0,0 @@ -# Multi-Tenant Architecture - -## Current Design - -- **Single admin user** — global admin, full access to all tenants -- **No per-tenant logins** — one admin manages everything -- **Tenant = data isolation boundary** — each tenant gets its own PG schema -- **Admin switches tenants** via dropdown in topbar (session-stored) -- **User management deferred** — if/when we want per-tenant user logins, that's a separate feature - -## How It Works - -1. `PUBLIC_MIGRATIONS` create `tenants` + `global_admins` tables in `public` schema -2. Each tenant gets a PG schema: `tenant_` (e.g. `tenant_acme`) -3. `TENANT_MIGRATIONS` run inside each tenant schema (full table set per tenant) -4. Admin creates tenants from the admin UI -5. Middleware sets `search_path = tenant_` per request based on selected tenant -6. All repo queries automatically scope to the active tenant's schema - -## What's NOT Happening - -- No per-tenant admin users (single global admin for now) -- No tenant-specific auth (global session, tenant is just a context switch) -- No tenant billing/limits enforcement (max_kiosks/max_cameras columns exist but unenforced) -- No tenant API keys (all API keys are global) - -## Future: Per-Tenant Users - -When needed, add: -- Per-tenant `users` table (already in TENANT_MIGRATIONS) -- Login scoped to tenant (tenant slug in login URL or selection) -- Role-based access per tenant -- Separate from global admin