From 01d9098af27612b791fc82e5cd4e38b9074bb41b Mon Sep 17 00:00:00 2001 From: Mitchell R Date: Tue, 26 May 2026 04:16:33 +0200 Subject: [PATCH] chore: gitignore doc files + remove from tracking Co-Authored-By: Claude Opus 4.6 (1M context) --- .gitignore | Bin 639 -> 802 bytes BSB_LEARNINGS.md | 203 --------------------------- server/src/shared/db/MULTI_TENANT.md | 33 ----- 3 files changed, 236 deletions(-) delete mode 100644 BSB_LEARNINGS.md delete mode 100644 server/src/shared/db/MULTI_TENANT.md diff --git a/.gitignore b/.gitignore index e7324ce66d8fa572aad7920cd1c3e8c7878a7520..f2ca9a55f69829dcce4444f5b645940485a9ceee 100644 GIT binary patch literal 802 zcmZuvOK#gR5an8c?l6Tntx-{Duwx@BkhFm1tPP4B%dGejNXm)3D0+yVuqWxzlGCE- zg7YCC-8P#ha`#OoxVY+cPYv|Bn5D8UTaAKHGcB4z$A}EnWSuD9~CtJM; zGe_}cv$PDDc#oH<$}bJkY??!pe895UpGMop>+KE3KKX%brzhxEX#!ER{2V3qOUmzV zVZ@ZQ>*CzV+djS~bWwYEJKhaz9ZO@8{g6N23sj?$1bOY&wwW#Wd@|~+ttN3zlwH85?#P<^?V0WcaBO28cBrr zZ_@?ir7-65@m3D`1lqy9P7h-XE17azAIc(#QC2exb>@w#hD$nmM@jvl`|8d+hcZEnBf2`^-Mkx~E;vEK{sx1&a(>VCSo?DGN-p>`zQkjD)IbwPc^z TGcoOn<3M&xOecY9oB;Hvwi8C5|wp|X0hu{f$5|Y_&kt!~Z zu=C5UCc3u}2A2m@{uKo}eo6AZOAqVSG|# zp~BAzOA)=RB;;{gZ84jlD>5I!!I@LVM?S8`Zt4^DV)4o6DPb%9R0gTNsSJjv+(qn{ zq1`qu`I#itqz*rtmA*}zcwcJj{`%W32n}3$LX|D>_GNUCO7JP2-d`xxQ-K&hlHM&n zTnB9;-kI4ip?d~#rli/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