[{"data":1,"prerenderedAt":151},["ShallowReactive",2],{"doc-self-host":3},{"html":4,"headings":5},"\u003Cp>Everything you need to run HelpBites yourself, from a laptop to production. No\nprior knowledge of the codebase assumed.\u003C\u002Fp>\n\u003Cp>HelpBites is one support inbox for every app you run. It ships as a single Nuxt\napp plus a handful of stateful services you run alongside it. This guide covers\nwhat those services are, how to wire them up, every environment variable, and\nthree ways to deploy: local dev, Dokploy (the blessed path), and plain Docker\nCompose behind your own reverse proxy.\u003C\u002Fp>\n\u003Cul>\n\u003Cli>New here? Start with \u003Ca href=\"#quick-start-local\">Quick start (local)\u003C\u002Fa>.\u003C\u002Fli>\n\u003Cli>Going to production? Read \u003Ca href=\"#architecture\">Architecture\u003C\u002Fa> then jump to\n\u003Ca href=\"#deploy-on-dokploy\">Dokploy\u003C\u002Fa> or \u003Ca href=\"#deploy-with-plain-docker-compose\">Plain Docker Compose\u003C\u002Fa>.\u003C\u002Fli>\n\u003Cli>Just need a variable? \u003Ca href=\"#environment-reference\">Environment reference\u003C\u002Fa>.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Chr>\n\u003Ch2 id=\"architecture\">Architecture\u003Ca class=\"heading-anchor\" href=\"#architecture\" aria-label=\"Link to this section\">#\u003C\u002Fa>\u003C\u002Fh2>\n\u003Cp>One app container, four stateful services. The app is the only holder of the\nParse master key and the only thing your users and operators ever talk to.\u003C\u002Fp>\n\u003Cdiv class=\"code-block\">\u003Cbutton type=\"button\" class=\"copy-btn\" data-copy aria-label=\"Copy to clipboard\">Copy\u003C\u002Fbutton>\u003Cpre class=\"shj-lang-plain\">\u003Ccode>                         ┌───────────────────────────────┐\n   browser widget  ──────▶                               │\n   operator panel  ──────▶   app  (Nuxt + Nitro, :3000)  │\n   your own API    ──────▶                               │\n                         └──┬───────┬────────┬────────┬──┘\n                            │       │        │        │\n              master key    │       │        │        │  hybrid search\n                 ┌──────────▼──┐ ┌──▼────┐ ┌─▼─────┐ ┌▼──────────┐\n                 │  sb-parse   │ │sb-redis│ │  S3   │ │  sb-meili │\n                 │ (Parse 9)   │ │ (7)   │ │bucket │ │ (Meili)   │\n                 └──────┬──────┘ └───────┘ └───────┘ └───────────┘\n                        │  SSE fan-out,     attachments  knowledge\n                 ┌──────▼──────┐ rate limits, + knowledge  search\n                 │  sb-mongo   │ email throttle  docs\n                 │ (Mongo 7)   │\n                 └─────────────┘\u003C\u002Fcode>\u003C\u002Fpre>\u003C\u002Fdiv>\n\u003Cdiv class=\"table-wrap\">\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>Service\u003C\u002Fth>\n\u003Cth>Image\u003C\u002Fth>\n\u003Cth>Role\u003C\u002Fth>\n\u003Cth>Required?\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\n\u003Ctr>\n\u003Ctd>\u003Cstrong>app\u003C\u002Fstrong>\u003C\u002Ftd>\n\u003Ctd>built from \u003Ccode>Dockerfile\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>The panel, the \u003Ccode>\u002Fv1\u002F*\u003C\u002Fcode> API, the widget. Listens on \u003Ccode>:3000\u003C\u002Fcode>. Applies the DB schema on every boot.\u003C\u002Ftd>\n\u003Ctd>Yes\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Cstrong>sb-mongo\u003C\u002Fstrong>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>mongo:7\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Parse's datastore. All conversations, projects, knowledge, users.\u003C\u002Ftd>\n\u003Ctd>Yes\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Cstrong>sb-parse\u003C\u002Fstrong>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>parseplatform\u002Fparse-server:9.10.0\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Data layer (classes, CLPs, sessions). Only the app holds its master key.\u003C\u002Ftd>\n\u003Ctd>Yes\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Cstrong>sb-redis\u003C\u002Fstrong>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>redis:7-alpine\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>SSE fan-out (pub\u002Fsub), rate limits, email throttle.\u003C\u002Ftd>\n\u003Ctd>Yes\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Cstrong>sb-meili\u003C\u002Fstrong>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>getmeili\u002Fmeilisearch:v1.53\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Hybrid knowledge + full-text search. App falls back to a Parse keyword scan if it's down.\u003C\u002Ftd>\n\u003Ctd>Recommended\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Cstrong>S3 bucket\u003C\u002Fstrong>\u003C\u002Ftd>\n\u003Ctd>any S3-compatible (MinIO, Garage, RustFS, AWS S3)\u003C\u002Ftd>\n\u003Ctd>Image attachments and uploaded knowledge documents.\u003C\u002Ftd>\n\u003Ctd>For attachments\u002Fuploads\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\n\u003C\u002Ftable>\u003C\u002Fdiv>\n\u003Cp>Optional integrations (all off unless configured): a \u003Cstrong>LLM endpoint\u003C\u002Fstrong> for AI\nauto-reply, \u003Cstrong>Mailgun\u003C\u002Fstrong> for email notifications, \u003Cstrong>APNs\u003C\u002Fstrong> for native operator\npush, and a \u003Cstrong>remote embeddings\u003C\u002Fstrong> service if you don't want to run the model\nin-process.\u003C\u002Fp>\n\u003Cp>\u003Cstrong>Why the \u003Ccode>sb-\u003C\u002Fcode> prefix?\u003C\u002Fstrong> On a shared Dokploy network, other projects run\nservices literally named \u003Ccode>parse\u003C\u002Fcode>, \u003Ccode>redis\u003C\u002Fcode>, or \u003Ccode>mongo\u003C\u002Fcode>. Docker DNS would resolve a\nbare name to any of them. The prefix keeps HelpBites talking to its own services.\u003C\u002Fp>\n\u003Cp>\u003Cstrong>Scaling note:\u003C\u002Fstrong> every write publishes on one Redis channel and each app\nprocess fans it out to its own SSE clients, so you can run \u003Cstrong>multiple \u003Ccode>app\u003C\u002Fcode>\nreplicas\u003C\u002Fstrong> behind a load balancer. Rate limits are shared through Redis. The one\nper-process cost is the local embedding model (cached on a volume per instance) —\nif you scale out, either accept the per-replica model cache or point\n\u003Ccode>EMBEDDING_ENDPOINT\u003C\u002Fcode> at a shared embeddings service.\u003C\u002Fp>\n\u003Chr>\n\u003Ch2 id=\"prerequisites\">Prerequisites\u003Ca class=\"heading-anchor\" href=\"#prerequisites\" aria-label=\"Link to this section\">#\u003C\u002Fa>\u003C\u002Fh2>\n\u003Cul>\n\u003Cli>\u003Cstrong>Docker\u003C\u002Fstrong> + the Compose plugin (\u003Ccode>docker compose version\u003C\u002Fcode> ≥ v2).\u003C\u002Fli>\n\u003Cli>A \u003Cstrong>domain\u003C\u002Fstrong> and TLS for production. Dokploy or a reverse proxy terminates TLS;\nno HelpBites service publishes a host port on its own.\u003C\u002Fli>\n\u003Cli>For building\u002Fhacking locally: \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbun.sh\" target=\"_blank\" rel=\"noopener\">Bun\u003C\u002Fa> 1.x\u003C\u002Fstrong>.\u003C\u002Fli>\n\u003Cli>Optional accounts, only if you want the matching feature: an S3 provider, a\nMailgun account, an OpenAI-style LLM endpoint, an Apple developer account (push).\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>Nothing else. Mongo, Parse, Redis, and Meili all run as containers from the\nprovided Compose file.\u003C\u002Fp>\n\u003Chr>\n\u003Ch2 id=\"quick-start-local\">Quick start (local)\u003Ca class=\"heading-anchor\" href=\"#quick-start-local\" aria-label=\"Link to this section\">#\u003C\u002Fa>\u003C\u002Fh2>\n\u003Cp>The fastest path to a running app on your machine. Uses the dev Compose overlay,\nwhich publishes ports on \u003Ccode>localhost\u003C\u002Fcode> and adds a Parse dashboard and RustFS.\u003C\u002Fp>\n\u003Cdiv class=\"code-block\">\u003Cbutton type=\"button\" class=\"copy-btn\" data-copy aria-label=\"Copy to clipboard\">Copy\u003C\u002Fbutton>\u003Cpre class=\"shj-lang-bash\">\u003Ccode>\u003Cspan class=\"shj-syn-func\">git\u003C\u002Fspan> clone https:\u002F\u002Fgithub.com\u002Felrumo\u002Fhelp-bites-oss.git help-bites\n\u003Cspan class=\"shj-syn-func\">cd\u003C\u002Fspan> help-bites\n\u003Cspan class=\"shj-syn-func\">cp\u003C\u002Fspan> .env.example .env          \u003Cspan class=\"shj-syn-cmnt\"># defaults already point at localhost\u003C\u002Fspan>\n\u003Cspan class=\"shj-syn-func\">bun\u003C\u002Fspan> \u003Cspan class=\"shj-syn-kwd\">install\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\u003C\u002Fdiv>\n\u003Cp>Bring up the backing services (note the \u003Ccode>--profile dev\u003C\u002Fcode> — it adds RustFS and the\nParse dashboard):\u003C\u002Fp>\n\u003Cdiv class=\"code-block\">\u003Cbutton type=\"button\" class=\"copy-btn\" data-copy aria-label=\"Copy to clipboard\">Copy\u003C\u002Fbutton>\u003Cpre class=\"shj-lang-bash\">\u003Ccode>\u003Cspan class=\"shj-syn-func\">docker\u003C\u002Fspan> compose\u003Cspan class=\"shj-syn-kwd\"> -f\u003C\u002Fspan> docker-compose.yml\u003Cspan class=\"shj-syn-kwd\"> -f\u003C\u002Fspan> docker-compose.dev.yml --profile dev up\u003Cspan class=\"shj-syn-kwd\"> -d\u003C\u002Fspan> sb-mongo sb-parse sb-redis sb-meili sb-rustfs\u003C\u002Fcode>\u003C\u002Fpre>\u003C\u002Fdiv>\n\u003Cp>Apply the schema and search setup (both idempotent — safe to re-run any time):\u003C\u002Fp>\n\u003Cdiv class=\"code-block\">\u003Cbutton type=\"button\" class=\"copy-btn\" data-copy aria-label=\"Copy to clipboard\">Copy\u003C\u002Fbutton>\u003Cpre class=\"shj-lang-bash\">\u003Ccode>\u003Cspan class=\"shj-syn-func\">bun\u003C\u002Fspan> run schema:apply\n\u003Cspan class=\"shj-syn-func\">bun\u003C\u002Fspan> run search:setup\u003C\u002Fcode>\u003C\u002Fpre>\u003C\u002Fdiv>\n\u003Cp>Run the app in dev mode:\u003C\u002Fp>\n\u003Cdiv class=\"code-block\">\u003Cbutton type=\"button\" class=\"copy-btn\" data-copy aria-label=\"Copy to clipboard\">Copy\u003C\u002Fbutton>\u003Cpre class=\"shj-lang-bash\">\u003Ccode>\u003Cspan class=\"shj-syn-func\">bun\u003C\u002Fspan> run dev\u003C\u002Fcode>\u003C\u002Fpre>\u003C\u002Fdiv>\n\u003Cp>Open \u003Cstrong>\u003Ca href=\"http:\u002F\u002Flocalhost:3100\" target=\"_blank\" rel=\"noopener\">http:\u002F\u002Flocalhost:3100\u003C\u002Fa>\u003C\u002Fstrong>. The first visit shows a sign-up form — that\ncreates your first operator account. From there: create a project, upload\nknowledge, grab the widget snippet.\u003C\u002Fp>\n\u003Cp>What you get locally:\u003C\u002Fp>\n\u003Cdiv class=\"table-wrap\">\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>URL\u003C\u002Fth>\n\u003Cth>What\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\n\u003Ctr>\n\u003Ctd>\u003Ca href=\"http:\u002F\u002Flocalhost:3100\" target=\"_blank\" rel=\"noopener\">http:\u002F\u002Flocalhost:3100\u003C\u002Fa>\u003C\u002Ftd>\n\u003Ctd>The app (panel + API + widget)\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ca href=\"http:\u002F\u002Flocalhost:3100\u002F_docs\u002Fscalar\" target=\"_blank\" rel=\"noopener\">http:\u002F\u002Flocalhost:3100\u002F_docs\u002Fscalar\u003C\u002Fa>\u003C\u002Ftd>\n\u003Ctd>Interactive API docs\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ca href=\"http:\u002F\u002Flocalhost:4040\" target=\"_blank\" rel=\"noopener\">http:\u002F\u002Flocalhost:4040\u003C\u002Fa>\u003C\u002Ftd>\n\u003Ctd>Parse dashboard (\u003Ccode>admin\u003C\u002Fcode> \u002F \u003Ccode>admin\u003C\u002Fcode>)\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ca href=\"http:\u002F\u002Flocalhost:9001\" target=\"_blank\" rel=\"noopener\">http:\u002F\u002Flocalhost:9001\u003C\u002Fa>\u003C\u002Ftd>\n\u003Ctd>RustFS console (\u003Ccode>rustfsadmin\u003C\u002Fcode> \u002F \u003Ccode>rustfsadmin\u003C\u002Fcode>)\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ca href=\"http:\u002F\u002Flocalhost:7700\" target=\"_blank\" rel=\"noopener\">http:\u002F\u002Flocalhost:7700\u003C\u002Fa>\u003C\u002Ftd>\n\u003Ctd>Meilisearch\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\n\u003C\u002Ftable>\u003C\u002Fdiv>\n\u003Cp>Seed some demo knowledge and run the end-to-end smoke test against the running\napp:\u003C\u002Fp>\n\u003Cdiv class=\"code-block\">\u003Cbutton type=\"button\" class=\"copy-btn\" data-copy aria-label=\"Copy to clipboard\">Copy\u003C\u002Fbutton>\u003Cpre class=\"shj-lang-bash\">\u003Ccode>\u003Cspan class=\"shj-syn-func\">bun\u003C\u002Fspan> run seed:knowledge\n\u003Cspan class=\"shj-syn-func\">bun\u003C\u002Fspan> run smoke\u003C\u002Fcode>\u003C\u002Fpre>\u003C\u002Fdiv>\n\u003Cblockquote>\n\u003Cp>The dev overlay (\u003Ccode>docker-compose.dev.yml\u003C\u002Fcode>) is \u003Cstrong>never\u003C\u002Fstrong> used in production — it\nexists only to expose localhost ports so \u003Ccode>bun run dev\u003C\u002Fcode> and the Parse dashboard\ncan reach the stack.\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\u003Chr>\n\u003Ch2 id=\"deploy-on-dokploy\">Deploy on Dokploy\u003Ca class=\"heading-anchor\" href=\"#deploy-on-dokploy\" aria-label=\"Link to this section\">#\u003C\u002Fa>\u003C\u002Fh2>\n\u003Cp>This is what HelpBites is built for. Dokploy runs the base \u003Ccode>docker-compose.yml\u003C\u002Fcode>\nas a &quot;Docker Compose&quot; application and attaches domains per service — \u003Cstrong>no service\npublishes a host port\u003C\u002Fstrong>.\u003C\u002Fp>\n\u003Ch3 id=\"1-create-the-application\">1. Create the application\u003Ca class=\"heading-anchor\" href=\"#1-create-the-application\" aria-label=\"Link to this section\">#\u003C\u002Fa>\u003C\u002Fh3>\n\u003Col>\n\u003Cli>In Dokploy, \u003Cstrong>Create → Compose\u003C\u002Fstrong> (Docker Compose app).\u003C\u002Fli>\n\u003Cli>Point it at your Git repository (Dokploy re-clones on each deploy) or paste\nthe compose. Use the repo's \u003Ccode>docker-compose.yml\u003C\u002Fcode> as-is.\u003C\u002Fli>\n\u003Cli>Set the build context so the \u003Ccode>app\u003C\u002Fcode> service's \u003Ccode>build: .\u003C\u002Fcode> finds the \u003Ccode>Dockerfile\u003C\u002Fcode>.\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Cblockquote>\n\u003Cp>\u003Cstrong>Persistence:\u003C\u002Fstrong> a git-sourced deploy re-clones the code directory on every\ndeploy, so nothing durable may live under it. All state lives in the named\nvolumes (\u003Ccode>mongo-data\u003C\u002Fcode>, \u003Ccode>redis-data\u003C\u002Fcode>, \u003Ccode>meili-data\u003C\u002Fcode>, \u003Ccode>model-cache\u003C\u002Fcode>) — those\nsurvive redeploys. Don't bind-mount app state into the repo tree.\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\u003Ch3 id=\"2-set-environment-variables\">2. Set environment variables\u003Ca class=\"heading-anchor\" href=\"#2-set-environment-variables\" aria-label=\"Link to this section\">#\u003C\u002Fa>\u003C\u002Fh3>\n\u003Cp>Paste your production values into Dokploy's \u003Cstrong>Environment\u003C\u002Fstrong> tab (this becomes the\n\u003Ccode>.env\u003C\u002Fcode> the \u003Ccode>app\u003C\u002Fcode> service reads via \u003Ccode>env_file: .env\u003C\u002Fcode>). See the\n\u003Ca href=\"#environment-reference\">Environment reference\u003C\u002Fa> and the\n\u003Ca href=\"#example-production-env\">production \u003Ccode>.env\u003C\u002Fcode> example\u003C\u002Fa> below. At minimum set:\n\u003Ccode>APP_URL\u003C\u002Fcode>, \u003Ccode>PARSE_ID\u003C\u002Fcode>, \u003Ccode>PARSE_MASTER_KEY\u003C\u002Fcode>, \u003Ccode>MEILI_MASTER_KEY\u003C\u002Fcode>, and the \u003Ccode>S3_*\u003C\u002Fcode>\ngroup.\u003C\u002Fp>\n\u003Cp>The compose file injects the internal service URLs for you, so \u003Cstrong>do not\u003C\u002Fstrong> set\nthese in production — they're overridden anyway:\u003C\u002Fp>\n\u003Cdiv class=\"code-block\">\u003Cbutton type=\"button\" class=\"copy-btn\" data-copy aria-label=\"Copy to clipboard\">Copy\u003C\u002Fbutton>\u003Cpre class=\"shj-lang-yaml\">\u003Ccode>\u003Cspan class=\"shj-syn-var\">PARSE_URL\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-syn-var\">http\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">:\u003C\u002Fspan>\u002F\u002F\u003Cspan class=\"shj-syn-var\">sb-parse\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">:\u003C\u002Fspan>\u003Cspan class=\"shj-syn-num\">1337\u003C\u002Fspan>\u002Fparse\n\u003Cspan class=\"shj-syn-var\">REDIS_URL\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-syn-var\">redis\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">:\u003C\u002Fspan>\u002F\u002F\u003Cspan class=\"shj-syn-var\">sb-redis\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">:\u003C\u002Fspan>\u003Cspan class=\"shj-syn-num\">6379\u003C\u002Fspan>\n\u003Cspan class=\"shj-syn-var\">MEILI_URL\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-syn-var\">http\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">:\u003C\u002Fspan>\u002F\u002F\u003Cspan class=\"shj-syn-var\">sb-meili\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">:\u003C\u002Fspan>\u003Cspan class=\"shj-syn-num\">7700\u003C\u002Fspan>\n\u003Cspan class=\"shj-syn-var\">TRANSFORMERS_CACHE\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">:\u003C\u002Fspan> \u002Fmodels\n\u003Cspan class=\"shj-syn-var\">NITRO_PORT\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-syn-str\">\"3000\"\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\u003C\u002Fdiv>\n\u003Ch3 id=\"3-attach-domains\">3. Attach domains\u003Ca class=\"heading-anchor\" href=\"#3-attach-domains\" aria-label=\"Link to this section\">#\u003C\u002Fa>\u003C\u002Fh3>\n\u003Cp>From the \u003Cstrong>Domains\u003C\u002Fstrong> tab, attach domains by \u003Cstrong>service name + container port\u003C\u002Fstrong>:\u003C\u002Fp>\n\u003Cdiv class=\"table-wrap\">\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>Service\u003C\u002Fth>\n\u003Cth>Container port\u003C\u002Fth>\n\u003Cth>Domain\u003C\u002Fth>\n\u003Cth>Notes\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\n\u003Ctr>\n\u003Ctd>\u003Ccode>app\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>3000\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>support.yourdomain.com\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>The whole product. Required.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>sb-meili\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>7700\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>help-search.yourdomain.com\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Only if you run a Meilisearch UI elsewhere pointing at it. \u003Cstrong>Exposes the API — see below.\u003C\u002Fstrong>\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>sb-parse\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>1337\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Cstrong>Don't\u003C\u002Fstrong>\u003C\u002Ftd>\n\u003Ctd>The app talks to Parse internally. Parse accepts its master key from any address (\u003Ccode>PARSE_SERVER_MASTER_KEY_IPS\u003C\u002Fcode>), and \u003Ccode>\u002Fparse\u002Flogin\u003C\u002Fcode> has none of the app's rate limits, so a public \u003Ccode>sb-parse\u003C\u002Fcode> is guarded by the master key alone.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\n\u003C\u002Ftable>\u003C\u002Fdiv>\n\u003Cp>Dokploy terminates TLS and routes to the container port. Point \u003Ccode>APP_URL\u003C\u002Fcode> at the\n\u003Ccode>app\u003C\u002Fcode> domain.\u003C\u002Fp>\n\u003Cblockquote>\n\u003Cp>\u003Cstrong>If you expose \u003Ccode>sb-meili\u003C\u002Fcode> publicly:\u003C\u002Fstrong> the compose runs it with\n\u003Ccode>MEILI_ENV=production\u003C\u002Fcode>, and \u003Ccode>\u002Fhealth\u003C\u002Fcode> is unauthenticated. Set a strong\n\u003Ccode>MEILI_MASTER_KEY\u003C\u002Fcode> — it's the only thing protecting your search data and\nsettings — and give the app its own scoped \u003Ccode>MEILI_API_KEY\u003C\u002Fcode> (see\n\u003Ca href=\"#meilisearch\">Meilisearch\u003C\u002Fa>). Better still, restrict the domain to the\naddresses that need it (a Traefik \u003Ccode>ipAllowList\u003C\u002Fcode> middleware), or don't attach a\ndomain to it at all if you don't need external search access.\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\u003Ch3 id=\"4-deploy\">4. Deploy\u003Ca class=\"heading-anchor\" href=\"#4-deploy\" aria-label=\"Link to this section\">#\u003C\u002Fa>\u003C\u002Fh3>\n\u003Cp>Hit \u003Cstrong>Deploy\u003C\u002Fstrong>. On boot the \u003Ccode>app\u003C\u002Fcode> container runs, in order:\u003C\u002Fp>\n\u003Cdiv class=\"code-block\">\u003Cbutton type=\"button\" class=\"copy-btn\" data-copy aria-label=\"Copy to clipboard\">Copy\u003C\u002Fbutton>\u003Cpre class=\"shj-lang-bash\">\u003Ccode>\u003Cspan class=\"shj-syn-func\">bun\u003C\u002Fspan> scripts\u002Fensure-schema.mjs --apply      \u003Cspan class=\"shj-syn-cmnt\"># create\u002Fpatch classes, CLPs, indexes\u003C\u002Fspan>\n\u003Cspan class=\"shj-syn-func\">bun\u003C\u002Fspan> scripts\u002Fmigrate-workspaces.mjs --apply \u003Cspan class=\"shj-syn-cmnt\"># an older install moves into one workspace\u003C\u002Fspan>\n\u003Cspan class=\"shj-syn-func\">bun\u003C\u002Fspan> scripts\u002Fmigrate-changelog-public.mjs --apply \u003Cspan class=\"shj-syn-cmnt\"># projects whose board published the changelog get their own switch\u003C\u002Fspan>\n\u003Cspan class=\"shj-syn-func\">bun\u003C\u002Fspan> scripts\u002Fensure-search.mjs              \u003Cspan class=\"shj-syn-cmnt\"># create Meili indexes + settings, backfill workspaceId\u003C\u002Fspan>\n\u003Cspan class=\"shj-syn-func\">bun\u003C\u002Fspan> .output\u002Fserver\u002Findex.mjs               \u003Cspan class=\"shj-syn-cmnt\"># start the app\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\u003C\u002Fdiv>\n\u003Cp>All are idempotent. \u003Ccode>ensure-search\u003C\u002Fcode> never fails the boot (Meili is\noptional). There is no separate migration step: the schema is reconciled, and an\ninstall from before workspaces is moved into one, on every boot (see\n\u003Ca href=\"#upgrading-to-workspaces\">Upgrades\u003C\u002Fa>). With more than one app container, let one\nof them migrate and set \u003Ccode>RUN_MIGRATIONS=0\u003C\u002Fcode> on the rest.\u003C\u002Fp>\n\u003Cp>Then open your \u003Ccode>app\u003C\u002Fcode> domain and \u003Ca href=\"#first-run\">create the first operator\u003C\u002Fa>.\u003C\u002Fp>\n\u003Chr>\n\u003Ch2 id=\"deploy-with-plain-docker-compose\">Deploy with plain Docker Compose\u003Ca class=\"heading-anchor\" href=\"#deploy-with-plain-docker-compose\" aria-label=\"Link to this section\">#\u003C\u002Fa>\u003C\u002Fh2>\n\u003Cp>Not on Dokploy? Same compose, you just supply the reverse proxy. Because no\nservice publishes a host port, put a proxy in front of \u003Ccode>app:3000\u003C\u002Fcode>.\u003C\u002Fp>\n\u003Ch3 id=\"1-configure\">1. Configure\u003Ca class=\"heading-anchor\" href=\"#1-configure\" aria-label=\"Link to this section\">#\u003C\u002Fa>\u003C\u002Fh3>\n\u003Cp>The quick way: \u003Ccode>scripts\u002Finstall.sh help.example.com\u003C\u002Fcode> writes \u003Ccode>.env\u003C\u002Fcode> with generated\nsecrets, bundled storage and the bundled Caddy (\u003Ccode>--profile proxy\u003C\u002Fcode>) for HTTPS. Point DNS for\n\u003Ccode>help.example.com\u003C\u002Fcode> and \u003Ccode>files.help.example.com\u003C\u002Fcode> at the server first; Caddy gets the\ncertificates by itself. Without a domain it prepares a trial on \u003Ccode>http:\u002F\u002Flocalhost:3100\u003C\u002Fcode>.\u003C\u002Fp>\n\u003Cp>By hand:\u003C\u002Fp>\n\u003Cdiv class=\"code-block\">\u003Cbutton type=\"button\" class=\"copy-btn\" data-copy aria-label=\"Copy to clipboard\">Copy\u003C\u002Fbutton>\u003Cpre class=\"shj-lang-bash\">\u003Ccode>\u003Cspan class=\"shj-syn-func\">git\u003C\u002Fspan> clone https:\u002F\u002Fgithub.com\u002Felrumo\u002Fhelp-bites-oss.git \u003Cspan class=\"shj-syn-kwd\">&#38;&#38;\u003C\u002Fspan> \u003Cspan class=\"shj-syn-func\">cd\u003C\u002Fspan> help-bites-oss\n\u003Cspan class=\"shj-syn-func\">cp\u003C\u002Fspan> .env.example .env\n\u003Cspan class=\"shj-syn-var\">$EDITOR\u003C\u002Fspan> .env      \u003Cspan class=\"shj-syn-cmnt\"># replace every change-me value (see Environment reference)\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\u003C\u002Fdiv>\n\u003Cp>Set \u003Ccode>APP_URL\u003C\u002Fcode> to your public HTTPS URL. Leave \u003Ccode>PARSE_URL\u003C\u002Fcode>, \u003Ccode>REDIS_URL\u003C\u002Fcode>,\n\u003Ccode>MEILI_URL\u003C\u002Fcode> as the compose provides them (internal service DNS). Production refuses\nto start while \u003Ccode>PARSE_MASTER_KEY\u003C\u002Fcode> or \u003Ccode>MEILI_MASTER_KEY\u003C\u002Fcode> still holds the \u003Ccode>change-me\u003C\u002Fcode>\nplaceholder.\u003C\u002Fp>\n\u003Ch3 id=\"2-start-the-stack\">2. Start the stack\u003Ca class=\"heading-anchor\" href=\"#2-start-the-stack\" aria-label=\"Link to this section\">#\u003C\u002Fa>\u003C\u002Fh3>\n\u003Cdiv class=\"code-block\">\u003Cbutton type=\"button\" class=\"copy-btn\" data-copy aria-label=\"Copy to clipboard\">Copy\u003C\u002Fbutton>\u003Cpre class=\"shj-lang-bash\">\u003Ccode>\u003Cspan class=\"shj-syn-func\">docker\u003C\u002Fspan> compose up\u003Cspan class=\"shj-syn-kwd\"> -d\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\u003C\u002Fdiv>\n\u003Cp>This starts \u003Ccode>sb-mongo\u003C\u002Fcode>, \u003Ccode>sb-parse\u003C\u002Fcode>, \u003Ccode>sb-redis\u003C\u002Fcode>, \u003Ccode>sb-meili\u003C\u002Fcode>, and \u003Ccode>app\u003C\u002Fcode> (pulled from\nDocker Hub as \u003Ccode>elrumo\u002Fhelpbites\u003C\u002Fcode>; add \u003Ccode>-f docker-compose.build.yml --build\u003C\u002Fcode> to build it\nfrom your checkout instead). \u003Ccode>parse-dashboard\u003C\u002Fcode> stays down (it's behind the \u003Ccode>dev\u003C\u002Fcode> profile), and\nso does the bundled \u003Ccode>sb-rustfs\u003C\u002Fcode> unless you opt in with \u003Ccode>--profile rustfs\u003C\u002Fcode> (see\n\u003Ca href=\"#s3-storage\">S3 storage\u003C\u002Fa>).\u003C\u002Fp>\n\u003Ch3 id=\"3-put-a-proxy-in-front\">3. Put a proxy in front\u003Ca class=\"heading-anchor\" href=\"#3-put-a-proxy-in-front\" aria-label=\"Link to this section\">#\u003C\u002Fa>\u003C\u002Fh3>\n\u003Cp>The \u003Ccode>app\u003C\u002Fcode> container listens on \u003Ccode>3000\u003C\u002Fcode> but publishes nothing to the host. Add a\nreverse proxy on the same Docker network. A minimal \u003Cstrong>Caddy\u003C\u002Fstrong> example\n(auto-TLS) — save as \u003Ccode>Caddyfile\u003C\u002Fcode> and run Caddy on the compose network:\u003C\u002Fp>\n\u003Cdiv class=\"code-block\">\u003Cbutton type=\"button\" class=\"copy-btn\" data-copy aria-label=\"Copy to clipboard\">Copy\u003C\u002Fbutton>\u003Cpre class=\"shj-lang-plain\">\u003Ccode>support.yourdomain.com {\n    reverse_proxy app:3000\n}\u003C\u002Fcode>\u003C\u002Fpre>\u003C\u002Fdiv>\n\u003Cp>Or an \u003Cstrong>nginx\u003C\u002Fstrong> server block if you terminate TLS elsewhere:\u003C\u002Fp>\n\u003Cdiv class=\"code-block\">\u003Cbutton type=\"button\" class=\"copy-btn\" data-copy aria-label=\"Copy to clipboard\">Copy\u003C\u002Fbutton>\u003Cpre class=\"shj-lang-plain\">\u003Ccode>server {\n    listen 80;\n    server_name support.yourdomain.com;\n\n    location \u002F {\n        proxy_pass http:\u002F\u002Fapp:3000;\n        proxy_http_version 1.1;\n        proxy_set_header Host $host;\n        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n        proxy_set_header X-Forwarded-Proto $scheme;\n\n        # Server-Sent Events (\u002Fv1\u002Fevents) — do not buffer, keep it open.\n        proxy_buffering off;\n        proxy_read_timeout 3600s;\n    }\n}\u003C\u002Fcode>\u003C\u002Fpre>\u003C\u002Fdiv>\n\u003Cblockquote>\n\u003Cp>\u003Cstrong>SSE matters.\u003C\u002Fstrong> HelpBites pushes live updates over \u003Ccode>\u002Fv1\u002Fevents\u003C\u002Fcode> (Server-Sent\nEvents). Any proxy in front must \u003Cstrong>disable response buffering\u003C\u002Fstrong> and allow long\nread timeouts on that path, or messages will arrive in laggy bursts. Caddy\nhandles this automatically; nginx needs \u003Ccode>proxy_buffering off\u003C\u002Fcode> as shown.\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\u003Ch3 id=\"4-point-a-bucket-at-it\">4. Point a bucket at it\u003Ca class=\"heading-anchor\" href=\"#4-point-a-bucket-at-it\" aria-label=\"Link to this section\">#\u003C\u002Fa>\u003C\u002Fh3>\n\u003Cp>If you're not using AWS S3, the simplest self-hosted option is the bundled RustFS\n(\u003Ccode>docker compose --profile rustfs up -d\u003C\u002Fcode>) — or point at a separate\nMinIO\u002FGarage\u002FRustFS deployment, then set the \u003Ccode>S3_*\u003C\u002Fcode> variables to match. See\n\u003Ca href=\"#s3-storage\">S3 storage\u003C\u002Fa>.\u003C\u002Fp>\n\u003Chr>\n\u003Ch2 id=\"environment-reference\">Environment reference\u003Ca class=\"heading-anchor\" href=\"#environment-reference\" aria-label=\"Link to this section\">#\u003C\u002Fa>\u003C\u002Fh2>\n\u003Cp>The app reads a \u003Ccode>.env\u003C\u002Fcode> file (Compose passes it to the \u003Ccode>app\u003C\u002Fcode> service via\n\u003Ccode>env_file: .env\u003C\u002Fcode>). Copy \u003Ccode>.env.example\u003C\u002Fcode> and fill it in. Below, \u003Cstrong>Required\u003C\u002Fstrong> means\nthe app won't function without it; \u003Cstrong>Recommended\u003C\u002Fstrong> means a core feature degrades\nwithout it; \u003Cstrong>Optional\u003C\u002Fstrong> means a feature stays off until you set it.\u003C\u002Fp>\n\u003Ch3 id=\"core\">Core\u003Ca class=\"heading-anchor\" href=\"#core\" aria-label=\"Link to this section\">#\u003C\u002Fa>\u003C\u002Fh3>\n\u003Cdiv class=\"table-wrap\">\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>Variable\u003C\u002Fth>\n\u003Cth>Required\u003C\u002Fth>\n\u003Cth>Default\u003C\u002Fth>\n\u003Cth>Notes\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\n\u003Ctr>\n\u003Ctd>\u003Ccode>APP_URL\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Cstrong>Yes\u003C\u002Fstrong> (prod)\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>http:\u002F\u002Flocalhost:3100\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Public origin of this deployment. Used by \u003Ccode>widget.js\u003C\u002Fcode>, email CTAs, and the API examples in the panel. Set to your real HTTPS domain.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>ADMIN_URL\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Optional\u003C\u002Ftd>\n\u003Ctd>falls back to \u003Ccode>APP_URL\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Only set if the operator panel lives on a different host than \u003Ccode>APP_URL\u003C\u002Fcode>. Used in operator email links (e.g. password reset, new-message CTAs).\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>SETUP_TOKEN\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Recommended\u003C\u002Ftd>\n\u003Ctd>\u003Cem>(empty: open)\u003C\u002Fem>\u003C\u002Ftd>\n\u003Ctd>While no operator exists, the first sign-up must enter this value, so whoever reaches a fresh deploy first can't claim it. Any long random string; unused once the first operator exists. \u003Ccode>scripts\u002Finstall.sh help.example.com\u003C\u002Fcode> generates one.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>TRUST_PROXY_HOPS\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Optional\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>1\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>How many reverse proxies stand in front of the app. Per-IP rate limits read the client from \u003Ccode>X-Forwarded-For\u003C\u002Fcode> counting this many entries from the right, since the left-most entry is whatever the client sent. \u003Ccode>1\u003C\u002Fcode> fits Dokploy's Traefik; set \u003Ccode>2\u003C\u002Fcode> behind e.g. Cloudflare → Traefik.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>NITRO_PORT\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>No\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>3000\u003C\u002Fcode> (set by compose)\u003C\u002Ftd>\n\u003Ctd>Port the app listens on inside the container. Leave it.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>APP_ROLE\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Optional\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>all\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>all\u003C\u002Fcode> runs everything in one process. The hosted overlay splits \u003Ccode>web\u003C\u002Fcode>, \u003Ccode>worker\u003C\u002Fcode> (schedulers, the job queue, pushes: whatever must run once) and \u003Ccode>embedder\u003C\u002Fcode> (the local model, served at \u003Ccode>\u002Fv1\u002Finternal\u002Fembeddings\u003C\u002Fcode>).\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>RUN_MIGRATIONS\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Optional\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>1\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Run the schema, workspace and search migrations on boot. With several app containers, keep it on one and set \u003Ccode>0\u003C\u002Fcode> on the rest.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>SOURCE_CODE_URL\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Optional\u003C\u002Ftd>\n\u003Ctd>the upstream repository\u003C\u002Ftd>\n\u003Ctd>Where the panel's \u003Cstrong>Source code\u003C\u002Fstrong> link and \u003Ccode>\u002Fsource\u003C\u002Fcode> point. The AGPL asks a modified deployment to offer its users the source it runs: set your fork. With \u003Ccode>GIT_SHA\u003C\u002Fcode> baked in at build time, a GitHub URL points at the exact commit.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>DEFAULT_WORKSPACE_NAME\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Optional\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>HelpBites\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>The workspace an install from before workspaces moves into.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>ALLOW_PRIVATE_NETWORK\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Optional\u003C\u002Ftd>\n\u003Ctd>off\u003C\u002Ftd>\n\u003Ctd>URLs typed into the panel (assistant LLM endpoints, the workspace's AI provider, error-tracker URLs, webhooks, database syncs) may not point at private or loopback addresses unless this is \u003Ccode>1\u003C\u002Fcode>. Self-hosting a LiteLLM or GlitchTip next to the app? Set it. Never set it on a shared, hosted service. Server-side env URLs (\u003Ccode>LITELLM_API_BASE_URL\u003C\u002Fcode>, \u003Ccode>GLITCHTIP_API_URL\u003C\u002Fcode>) are not affected.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>HELPBITES_EDITION\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Build arg\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>community\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>cloud\u003C\u002Fcode> builds the hosted edition (adds \u003Ccode>ee\u002F\u003C\u002Fcode>). See \u003Ca href=\"https:\u002F\u002Fgithub.com\u002Felrumo\u002Fhelp-bites-oss\u002Fblob\u002Fmain\u002Fdocs\u002FEDITIONS.md\" target=\"_blank\" rel=\"noopener\">EDITIONS.md\u003C\u002Fa>.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>NODE_ENV\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>No\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>production\u003C\u002Fcode> (set by Dockerfile)\u003C\u002Ftd>\n\u003Ctd>—\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\n\u003C\u002Ftable>\u003C\u002Fdiv>\n\u003Ch3 id=\"parse--mongodb\">Parse + MongoDB\u003Ca class=\"heading-anchor\" href=\"#parse--mongodb\" aria-label=\"Link to this section\">#\u003C\u002Fa>\u003C\u002Fh3>\n\u003Cp>The app is the only master-key holder. In production, Compose sets \u003Ccode>PARSE_URL\u003C\u002Fcode>\nfor you.\u003C\u002Fp>\n\u003Cdiv class=\"table-wrap\">\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>Variable\u003C\u002Fth>\n\u003Cth>Required\u003C\u002Fth>\n\u003Cth>Default\u003C\u002Fth>\n\u003Cth>Notes\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\n\u003Ctr>\n\u003Ctd>\u003Ccode>PARSE_ID\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Cstrong>Yes\u003C\u002Fstrong>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>helpbites\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Parse application id. Must match the \u003Ccode>sb-parse\u003C\u002Fcode> service.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>PARSE_MASTER_KEY\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Cstrong>Yes\u003C\u002Fstrong>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>change-me-master\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Cstrong>Change this.\u003C\u002Fstrong> The master key; grants full data access. Only the app should have it.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>PARSE_MAINTENANCE_KEY\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>No\u003C\u002Ftd>\n\u003Ctd>unset\u003C\u002Ftd>\n\u003Ctd>Keeps the original dates of rows imported from another HelpBites; must equal \u003Ccode>PARSE_SERVER_MAINTENANCE_KEY\u003C\u002Fcode> on Parse. See \u003Ca href=\"\u002Fdocs\u002Ftransfer\">TRANSFER.md\u003C\u002Fa>.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>PARSE_URL\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Cstrong>Yes\u003C\u002Fstrong>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>http:\u002F\u002Flocalhost:1337\u002Fparse\u003C\u002Fcode> (compose overrides to \u003Ccode>http:\u002F\u002Fsb-parse:1337\u002Fparse\u003C\u002Fcode>)\u003C\u002Ftd>\n\u003Ctd>Where the app reaches Parse. Don't set in prod.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>PARSE_SERVER_DATABASE_URI\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Cstrong>Yes\u003C\u002Fstrong>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>mongodb:\u002F\u002Fsb-mongo:27017\u002Fhelpbites\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Mongo connection string the \u003Ccode>sb-parse\u003C\u002Fcode> container reads. Point at an external Mongo\u002FAtlas here if you don't run \u003Ccode>sb-mongo\u003C\u002Fcode>.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\n\u003C\u002Ftable>\u003C\u002Fdiv>\n\u003Ch3 id=\"redis\">Redis\u003Ca class=\"heading-anchor\" href=\"#redis\" aria-label=\"Link to this section\">#\u003C\u002Fa>\u003C\u002Fh3>\n\u003Cdiv class=\"table-wrap\">\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>Variable\u003C\u002Fth>\n\u003Cth>Required\u003C\u002Fth>\n\u003Cth>Default\u003C\u002Fth>\n\u003Cth>Notes\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\n\u003Ctr>\n\u003Ctd>\u003Ccode>REDIS_URL\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Cstrong>Yes\u003C\u002Fstrong>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>redis:\u002F\u002Flocalhost:6379\u003C\u002Fcode> (compose overrides to \u003Ccode>redis:\u002F\u002Fsb-redis:6379\u003C\u002Fcode>)\u003C\u002Ftd>\n\u003Ctd>SSE fan-out, rate limits, email throttle. Rate limiting fails \u003Cstrong>open\u003C\u002Fstrong> if Redis is down (requests are allowed).\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\n\u003C\u002Ftable>\u003C\u002Fdiv>\n\u003Ch3 id=\"search-meilisearch\">Search (Meilisearch)\u003Ca class=\"heading-anchor\" href=\"#search-meilisearch\" aria-label=\"Link to this section\">#\u003C\u002Fa>\u003C\u002Fh3>\n\u003Cdiv class=\"table-wrap\">\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>Variable\u003C\u002Fth>\n\u003Cth>Required\u003C\u002Fth>\n\u003Cth>Default\u003C\u002Fth>\n\u003Cth>Notes\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\n\u003Ctr>\n\u003Ctd>\u003Ccode>MEILI_URL\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Recommended\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>http:\u002F\u002Flocalhost:7700\u003C\u002Fcode> (compose overrides to \u003Ccode>http:\u002F\u002Fsb-meili:7700\u003C\u002Fcode>)\u003C\u002Ftd>\n\u003Ctd>Hybrid knowledge + full-text search. If unreachable, the app falls back to a Parse keyword scan.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>MEILI_MASTER_KEY\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Recommended\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>change-me-meili\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Cstrong>Set a strong value in production\u003C\u002Fstrong>, especially if \u003Ccode>sb-meili\u003C\u002Fcode> is exposed via a domain — the container runs with \u003Ccode>MEILI_ENV=production\u003C\u002Fcode>. \u003Ccode>sb-meili\u003C\u002Fcode> needs it; the app only as a fallback for \u003Ccode>MEILI_API_KEY\u003C\u002Fcode>.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>MEILI_API_KEY\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Recommended\u003C\u002Ftd>\n\u003Ctd>\u003Cem>(empty: the master key)\u003C\u002Fem>\u003C\u002Ftd>\n\u003Ctd>The app's scoped Meilisearch key, printed by \u003Ccode>bun run search:key\u003C\u002Fcode> (see \u003Ca href=\"#meilisearch\">Meilisearch\u003C\u002Fa>).\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\n\u003C\u002Ftable>\u003C\u002Fdiv>\n\u003Ch3 id=\"knowledge-embeddings\">Knowledge embeddings\u003Ca class=\"heading-anchor\" href=\"#knowledge-embeddings\" aria-label=\"Link to this section\">#\u003C\u002Fa>\u003C\u002Fh3>\n\u003Cp>Semantic search runs a CLIP text encoder in-process by default (via\ntransformers.js on the onnxruntime-node backend). You have three modes: \u003Cstrong>local\nmodel\u003C\u002Fstrong> (default), \u003Cstrong>remote endpoint\u003C\u002Fstrong>, or \u003Cstrong>lexical-only\u003C\u002Fstrong>.\u003C\u002Fp>\n\u003Cdiv class=\"table-wrap\">\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>Variable\u003C\u002Fth>\n\u003Cth>Required\u003C\u002Fth>\n\u003Cth>Default\u003C\u002Fth>\n\u003Cth>Notes\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\n\u003Ctr>\n\u003Ctd>\u003Ccode>ICON_EMBEDDING_MODEL\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>No\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>Xenova\u002Fclip-vit-large-patch14\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>The local embedding model.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>ICON_EMBEDDING_DEVICE\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>No\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>cpu\u003C\u002Fcode> (empty = cpu)\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>cpu\u003C\u002Fcode> or \u003Ccode>cuda\u003C\u002Fcode> \u003Cstrong>only\u003C\u002Fstrong> (Linux + GPU). \u003Ccode>wasm\u003C\u002Fcode>\u002F\u003Ccode>webgpu\u003C\u002Fcode> are browser-only and throw &quot;Unsupported device&quot;.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>TRANSFORMERS_CACHE\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>No\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>.data\u002Fmodels\u003C\u002Fcode> (compose sets \u003Ccode>\u002Fmodels\u003C\u002Fcode> on a volume)\u003C\u002Ftd>\n\u003Ctd>Where the model is cached across boots.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>KNOWLEDGE_DISABLE_EMBEDDINGS\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>No\u003C\u002Ftd>\n\u003Ctd>\u003Cem>(off)\u003C\u002Fem>\u003C\u002Ftd>\n\u003Ctd>Set \u003Ccode>1\u003C\u002Fcode> to skip embeddings entirely and run lexical-only search. Fastest boot, no model download.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>EMBEDDING_ENDPOINT\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>No\u003C\u002Ftd>\n\u003Ctd>\u003Cem>(off)\u003C\u002Fem>\u003C\u002Ftd>\n\u003Ctd>Point at a remote OpenAI-style embeddings service instead of running the model in-process. Recommended if you run multiple app replicas.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>EMBEDDING_API_KEY\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>No\u003C\u002Ftd>\n\u003Ctd>\u003Cem>(off)\u003C\u002Fem>\u003C\u002Ftd>\n\u003Ctd>Auth for \u003Ccode>EMBEDDING_ENDPOINT\u003C\u002Fcode>.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\n\u003C\u002Ftable>\u003C\u002Fdiv>\n\u003Ch3 id=\"ai-support-agents-auto-reply\">AI support agents (auto-reply)\u003Ca class=\"heading-anchor\" href=\"#ai-support-agents-auto-reply\" aria-label=\"Link to this section\">#\u003C\u002Fa>\u003C\u002Fh3>\n\u003Cp>Off unless a project has an assistant with \u003Ccode>autoReply\u003C\u002Fcode> enabled \u003Cstrong>and\u003C\u002Fstrong> an LLM\nconfigured. OpenAI-compatible \u002F LiteLLM style.\u003C\u002Fp>\n\u003Cdiv class=\"table-wrap\">\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>Variable\u003C\u002Fth>\n\u003Cth>Required\u003C\u002Fth>\n\u003Cth>Default\u003C\u002Fth>\n\u003Cth>Notes\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\n\u003Ctr>\n\u003Ctd>\u003Ccode>LITELLM_API_BASE_URL\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Optional\u003C\u002Ftd>\n\u003Ctd>\u003Cem>(off)\u003C\u002Fem>\u003C\u002Ftd>\n\u003Ctd>Default LLM endpoint. Per-agent settings can override it.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>LITELLM_API_KEY\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Optional\u003C\u002Ftd>\n\u003Ctd>\u003Cem>(off)\u003C\u002Fem>\u003C\u002Ftd>\n\u003Ctd>Auth for the endpoint.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>LLM_DEFAULT_MODEL\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Optional\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>openai\u002Fmimo-v2.6-flash\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Model used when an agent doesn't specify one.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\n\u003C\u002Ftable>\u003C\u002Fdiv>\n\u003Ch3 id=\"error-tracking-glitchtip-or-bugsink\">Error tracking (GlitchTip or Bugsink)\u003Ca class=\"heading-anchor\" href=\"#error-tracking-glitchtip-or-bugsink\" aria-label=\"Link to this section\">#\u003C\u002Fa>\u003C\u002Fh3>\n\u003Cp>Optional, and either tracker (or both) can be set up. The bundled \u003Ccode>glitchtip\u003C\u002Fcode> Compose profile runs a Sentry-compatible\nGlitchTip instance with its own Postgres database. Attach a public domain to\n\u003Ccode>sb-glitchtip:8000\u003C\u002Fcode>; Sentry SDKs must be able to reach that domain. Follow the\nbootstrap and connection steps in \u003Ca href=\"\u002Fdocs\u002Ferrors\">ERRORS.md\u003C\u002Fa>. The \u003Ccode>bugsink\u003C\u002Fcode> profile\nruns a single-container Bugsink on SQLite; attach a public domain to\n\u003Ccode>sb-bugsink:8000\u003C\u002Fcode> the same way.\u003C\u002Fp>\n\u003Cdiv class=\"table-wrap\">\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>Variable\u003C\u002Fth>\n\u003Cth>Required\u003C\u002Fth>\n\u003Cth>Default\u003C\u002Fth>\n\u003Cth>Notes\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\n\u003Ctr>\n\u003Ctd>\u003Ccode>GLITCHTIP_URL\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Optional\u003C\u002Ftd>\n\u003Ctd>\u003Cem>(off)\u003C\u002Fem>\u003C\u002Ftd>\n\u003Ctd>Public GlitchTip URL, used in DSNs and operator links.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>GLITCHTIP_TOKEN\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Optional\u003C\u002Ftd>\n\u003Ctd>\u003Cem>(off)\u003C\u002Fem>\u003C\u002Ftd>\n\u003Ctd>API token. Can be entered in Connect error tracking instead, which wins over these.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>GLITCHTIP_ORG\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>With token\u003C\u002Ftd>\n\u003Ctd>\u003Cem>(off)\u003C\u002Fem>\u003C\u002Ftd>\n\u003Ctd>GlitchTip organisation slug.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>GLITCHTIP_TEAM\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>No\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>helpbites\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Team created or reused for HelpBites projects.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>GLITCHTIP_SECRET_KEY\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>With bundled service\u003C\u002Ftd>\n\u003Ctd>—\u003C\u002Ftd>\n\u003Ctd>Set a strong random value.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>GLITCHTIP_DB_PASSWORD\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>With bundled service\u003C\u002Ftd>\n\u003Ctd>—\u003C\u002Ftd>\n\u003Ctd>Postgres password for GlitchTip only.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>BUGSINK_URL\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Optional\u003C\u002Ftd>\n\u003Ctd>\u003Cem>(off)\u003C\u002Fem>\u003C\u002Ftd>\n\u003Ctd>Public Bugsink URL (its \u003Ccode>BASE_URL\u003C\u002Fcode>), used in operator links.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>BUGSINK_TOKEN\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Optional\u003C\u002Ftd>\n\u003Ctd>\u003Cem>(off)\u003C\u002Fem>\u003C\u002Ftd>\n\u003Ctd>Bugsink API token. Can be entered in Connect error tracking instead, which wins over these.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>BUGSINK_TEAM\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>No\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>HelpBites\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Team created or reused for HelpBites projects.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>BUGSINK_SECRET_KEY\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>With bundled service\u003C\u002Ftd>\n\u003Ctd>—\u003C\u002Ftd>\n\u003Ctd>Set a strong random value.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>ERRORS_WEBHOOK_BASE\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>No\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>APP_URL\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Reachable HelpBites base URL for alert callbacks. \u003Ccode>GLITCHTIP_WEBHOOK_BASE\u003C\u002Fcode> is still read as a fallback.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>NUXT_PUBLIC_SENTRY_DSN\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>No\u003C\u002Ftd>\n\u003Ctd>\u003Cem>(off)\u003C\u002Fem>\u003C\u002Ftd>\n\u003Ctd>A project DSN for HelpBites' own browser and server errors.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>NUXT_PUBLIC_PLAUSIBLE_SRC\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>No\u003C\u002Ftd>\n\u003Ctd>\u003Cem>(off)\u003C\u002Fem>\u003C\u002Ftd>\n\u003Ctd>The address of a \u003Ca href=\"https:\u002F\u002Fplausible.io\" target=\"_blank\" rel=\"noopener\">Plausible\u003C\u002Fa> script (\u003Ccode>https:\u002F\u002Fstats.example.com\u002Fjs\u002Fpa-….js\u003C\u002Fcode>) for page-view stats of HelpBites' own site and panel. Not loaded in the embeds or on invite and OAuth pages; the script's origin is added to the panel's script policy. The panel reads it at run time; the landing page and docs are prerendered, so also pass it as a Docker build arg to track them.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\n\u003C\u002Ftable>\u003C\u002Fdiv>\n\u003Cp>An external GlitchTip installation works too: set \u003Ccode>GLITCHTIP_URL\u003C\u002Fcode> and, if its\nAPI is on another origin, \u003Ccode>GLITCHTIP_API_URL\u003C\u002Fcode> (or the API URL field in Connect\nerror tracking).\u003C\u002Fp>\n\u003Ch3 id=\"s3-storage\">S3 storage\u003Ca class=\"heading-anchor\" href=\"#s3-storage\" aria-label=\"Link to this section\">#\u003C\u002Fa>\u003C\u002Fh3>\n\u003Cp>Holds image attachments and uploaded knowledge documents. Works with any\nS3-compatible provider. The app throws when an upload is attempted and these\naren't set, so configure them for any real deployment.\u003C\u002Fp>\n\u003Cdiv class=\"table-wrap\">\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>Variable\u003C\u002Fth>\n\u003Cth>Required\u003C\u002Fth>\n\u003Cth>Default\u003C\u002Fth>\n\u003Cth>Notes\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\n\u003Ctr>\n\u003Ctd>\u003Ccode>S3_ENDPOINT\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Cstrong>Yes\u003C\u002Fstrong>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>localhost\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Host of the S3 API (no scheme). e.g. \u003Ccode>s3.amazonaws.com\u003C\u002Fcode>, \u003Ccode>sb-rustfs\u003C\u002Fcode>, \u003Ccode>s3.eu-west-1.amazonaws.com\u003C\u002Fcode>.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>S3_PORT\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>No\u003C\u002Ftd>\n\u003Ctd>provider default\u003C\u002Ftd>\n\u003Ctd>Set for self-hosted (\u003Ccode>9000\u003C\u002Fcode> for RustFS). Omit for AWS.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>S3_USE_SSL\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>No\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>true\u003C\u002Fcode> (anything but \u003Ccode>false\u003C\u002Fcode>)\u003C\u002Ftd>\n\u003Ctd>Set \u003Ccode>false\u003C\u002Fcode> for plain-HTTP RustFS on a private network.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>S3_ACCESS_KEY\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Cstrong>Yes\u003C\u002Fstrong>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>rustfsadmin\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>—\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>S3_SECRET_KEY\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Cstrong>Yes\u003C\u002Fstrong>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>rustfsadmin\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Cstrong>Change this.\u003C\u002Fstrong>\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>S3_BUCKET\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Cstrong>Yes\u003C\u002Fstrong>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>help-bites\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Created automatically if it doesn't exist.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>S3_PUBLIC_URL\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Cstrong>Yes\u003C\u002Fstrong>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>http:\u002F\u002Flocalhost:9000\u002Fhelp-bites\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Public base URL of the bucket — this is what ends up in attachment URLs served to browsers.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>S3_REGION\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>No\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>us-east-1\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Region for the S3 client.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>S3_SET_PUBLIC_POLICY\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>No\u003C\u002Ftd>\n\u003Ctd>\u003Cem>(on)\u003C\u002Fem>\u003C\u002Ftd>\n\u003Ctd>On first use the app sets a public-read bucket policy (good for RustFS). Set \u003Ccode>false\u003C\u002Fcode> on AWS S3 where you manage the bucket policy \u002F ACLs yourself.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\n\u003C\u002Ftable>\u003C\u002Fdiv>\n\u003Cblockquote>\n\u003Cp>Attachments and knowledge docs are stored under unguessable, project-prefixed\nkeys. The bucket is public-read by design (attachment URLs are shared into\nconversations). Knowledge source documents are only ever served through an\nauthenticated admin route, not their public URL.\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\u003Ch3 id=\"email-mailgun\">Email (Mailgun)\u003Ca class=\"heading-anchor\" href=\"#email-mailgun\" aria-label=\"Link to this section\">#\u003C\u002Fa>\u003C\u002Fh3>\n\u003Cp>Optional. Enables the email send API and the email notifications to end users\nand operators. All three of the first must be set or email is skipped (the send\nAPI answers 503).\u003C\u002Fp>\n\u003Cdiv class=\"table-wrap\">\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>Variable\u003C\u002Fth>\n\u003Cth>Required\u003C\u002Fth>\n\u003Cth>Default\u003C\u002Fth>\n\u003Cth>Notes\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\n\u003Ctr>\n\u003Ctd>\u003Ccode>MAILGUN_API_KEY\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Optional\u003C\u002Ftd>\n\u003Ctd>\u003Cem>(off)\u003C\u002Fem>\u003C\u002Ftd>\n\u003Ctd>—\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>MAILGUN_DOMAIN\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Optional\u003C\u002Ftd>\n\u003Ctd>\u003Cem>(off)\u003C\u002Fem>\u003C\u002Ftd>\n\u003Ctd>Your Mailgun sending domain. A project can send through another domain of the same account (Project → Email → Settings).\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>MAILGUN_API_BASE_URL\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>No\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>https:\u002F\u002Fapi.eu.mailgun.net\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Use \u003Ccode>https:\u002F\u002Fapi.mailgun.net\u003C\u002Fcode> for US region.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>MAILGUN_FROM_EMAIL\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Optional\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>support@example.com\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>The default from-address. Projects can set their own address and from name.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>MAILGUN_WEBHOOK_SIGNING_KEY\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Optional\u003C\u002Ftd>\n\u003Ctd>\u003Cem>(off)\u003C\u002Fem>\u003C\u002Ftd>\n\u003Ctd>Turns on \u003Ccode>\u002Fv1\u002Fwebhooks\u002Fmailgun\u003C\u002Fcode>: delivery status in the email log, and automatic suppression of bounces, complaints and unsubscribes.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>EMAIL_LOG_RETENTION_DAYS\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>No\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>180\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Days of email log to keep; \u003Ccode>0\u003C\u002Fcode> keeps everything.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>CHANGELOG_LINK_SECRET\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>For changelog emails\u003C\u002Ftd>\n\u003Ctd>derived from \u003Ccode>HELPBITES_SECRET\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Signs the confirm and unsubscribe links in changelog subscription mail. Its own secret, so rotating a project secret never breaks unsubscribe links already sent. 32+ characters; a shorter one counts as unset. Unset, a key derived from \u003Ccode>HELPBITES_SECRET\u003C\u002Fcode> signs them (setting this later voids those links). With neither, subscribing answers 503 and the scheduler logs a warning and does not start.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>ENABLE_CHANGELOG_NOTIFY\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>No\u003C\u002Ftd>\n\u003Ctd>off\u003C\u002Ftd>\n\u003Ctd>Set \u003Ccode>true\u003C\u002Fcode> to run the changelog scheduler (every five minutes: the instant sweep and the Monday digest, for each project with \u003Ccode>changelog.notify\u003C\u002Fcode> on). Off, nothing is mailed to subscribers, so a deploy never starts before you say so, and a system that still sends the same digests can be switched off first. \u003Ccode>POST \u002Fv1\u002Fadmin\u002Fchangelog\u002Fnotify\u002Frun\u003C\u002Fcode> runs a beat by hand either way.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\n\u003C\u002Ftable>\u003C\u002Fdiv>\n\u003Ch3 id=\"native-push-apns\">Native push (APNs)\u003Ca class=\"heading-anchor\" href=\"#native-push-apns\" aria-label=\"Link to this section\">#\u003C\u002Fa>\u003C\u002Fh3>\n\u003Cp>Optional. Required only for the native operator app's push notifications.\u003C\u002Fp>\n\u003Cdiv class=\"table-wrap\">\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>Variable\u003C\u002Fth>\n\u003Cth>Required\u003C\u002Fth>\n\u003Cth>Default\u003C\u002Fth>\n\u003Cth>Notes\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\n\u003Ctr>\n\u003Ctd>\u003Ccode>APNS_KEY_ID\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Optional\u003C\u002Ftd>\n\u003Ctd>\u003Cem>(off)\u003C\u002Fem>\u003C\u002Ftd>\n\u003Ctd>Apple key id.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>APNS_TEAM_ID\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Optional\u003C\u002Ftd>\n\u003Ctd>\u003Cem>(off)\u003C\u002Fem>\u003C\u002Ftd>\n\u003Ctd>Apple team id.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>APNS_BUNDLE_ID\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Optional\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>com.helpbites.operator\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>The operator app's bundle id.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>APNS_KEY\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Optional\u003C\u002Ftd>\n\u003Ctd>\u003Cem>(off)\u003C\u002Fem>\u003C\u002Ftd>\n\u003Ctd>The full \u003Ccode>.p8\u003C\u002Fcode> private key body. Literal \u003Ccode>\\n\u003C\u002Fcode> sequences are accepted (handy for single-line env vars).\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>APNS_ENV\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>No\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>sandbox\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>sandbox\u003C\u002Fcode> or \u003Ccode>production\u003C\u002Fcode>.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\n\u003C\u002Ftable>\u003C\u002Fdiv>\n\u003Ch3 id=\"agent-seeding-cli-only\">Agent seeding (CLI only)\u003Ca class=\"heading-anchor\" href=\"#agent-seeding-cli-only\" aria-label=\"Link to this section\">#\u003C\u002Fa>\u003C\u002Fh3>\n\u003Cp>Defaults for \u003Ccode>bun run add-agent\u003C\u002Fcode>; command-line args override them. Not read by\nthe running app.\u003C\u002Fp>\n\u003Cdiv class=\"table-wrap\">\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>Variable\u003C\u002Fth>\n\u003Cth>Default\u003C\u002Fth>\n\u003Cth>Notes\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\n\u003Ctr>\n\u003Ctd>\u003Ccode>AGENT_USER\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>elias\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Default username for \u003Ccode>add-agent\u003C\u002Fcode>.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>AGENT_PASS\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>test1234\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Default password. Change or override on real use.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>AGENT_EMAIL\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Cem>(empty)\u003C\u002Fem>\u003C\u002Ftd>\n\u003Ctd>Optional.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\n\u003C\u002Ftable>\u003C\u002Fdiv>\n\u003Ch3 id=\"script-only\">Script-only\u003Ca class=\"heading-anchor\" href=\"#script-only\" aria-label=\"Link to this section\">#\u003C\u002Fa>\u003C\u002Fh3>\n\u003Cp>Read by helper scripts, not the app.\u003C\u002Fp>\n\u003Cdiv class=\"table-wrap\">\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>Variable\u003C\u002Fth>\n\u003Cth>Default\u003C\u002Fth>\n\u003Cth>Notes\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\n\u003Ctr>\n\u003Ctd>\u003Ccode>BASE\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>http:\u002F\u002Flocalhost:3100\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Target URL for \u003Ccode>bun run smoke\u003C\u002Fcode> and \u003Ccode>bun run seed:knowledge\u003C\u002Fcode>. Point at a remote deployment to test it.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>SKIP_SSE\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Cem>(off)\u003C\u002Fem>\u003C\u002Ftd>\n\u003Ctd>Set to skip the SSE check in \u003Ccode>bun run smoke\u003C\u002Fcode>.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\n\u003C\u002Ftable>\u003C\u002Fdiv>\n\u003Ch3 id=\"example-production-env\">Example production \u003Ccode>.env\u003C\u002Fcode>\u003Ca class=\"heading-anchor\" href=\"#example-production-env\" aria-label=\"Link to this section\">#\u003C\u002Fa>\u003C\u002Fh3>\n\u003Cdiv class=\"code-block\">\u003Cbutton type=\"button\" class=\"copy-btn\" data-copy aria-label=\"Copy to clipboard\">Copy\u003C\u002Fbutton>\u003Cpre class=\"shj-lang-bash\">\u003Ccode>\u003Cspan class=\"shj-syn-cmnt\"># --- app ---\u003C\u002Fspan>\n\u003Cspan class=\"shj-syn-var\">APP_URL\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">=\u003C\u002Fspan>https:\u002F\u002Fsupport.yourdomain.com\n\n\u003Cspan class=\"shj-syn-cmnt\"># --- Parse (change the master key!) ---\u003C\u002Fspan>\n\u003Cspan class=\"shj-syn-var\">PARSE_ID\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">=\u003C\u002Fspan>helpbites\n\u003Cspan class=\"shj-syn-var\">PARSE_MASTER_KEY\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">=&lt;\u003C\u002Fspan>\u003Cspan class=\"shj-syn-num\">64\u003C\u002Fspan>+ random chars\u003Cspan class=\"shj-syn-oper\">&gt;\u003C\u002Fspan>\n\u003Cspan class=\"shj-syn-var\">PARSE_SERVER_DATABASE_URI\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">=\u003C\u002Fspan>mongodb:\u002F\u002Fsb-mongo:\u003Cspan class=\"shj-syn-num\">27017\u003C\u002Fspan>\u002Fhelpbites\n\u003Cspan class=\"shj-syn-cmnt\"># PARSE_URL \u002F REDIS_URL \u002F MEILI_URL are injected by docker-compose — leave unset.\u003C\u002Fspan>\n\n\u003Cspan class=\"shj-syn-cmnt\"># --- Meilisearch (strong key; the endpoint may be public) ---\u003C\u002Fspan>\n\u003Cspan class=\"shj-syn-var\">MEILI_MASTER_KEY\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">=&lt;\u003C\u002Fspan>\u003Cspan class=\"shj-syn-num\">32\u003C\u002Fspan>+ random chars\u003Cspan class=\"shj-syn-oper\">&gt;\u003C\u002Fspan>\n\u003Cspan class=\"shj-syn-cmnt\"># The app's scoped key, from `docker compose exec app bun scripts\u002Fmeili-key.mjs` after the first deploy.\u003C\u002Fspan>\n\u003Cspan class=\"shj-syn-var\">MEILI_API_KEY\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">=&lt;\u003C\u002Fspan>printed key\u003Cspan class=\"shj-syn-oper\">&gt;\u003C\u002Fspan>\n\n\u003Cspan class=\"shj-syn-cmnt\"># --- Embeddings: run locally on CPU (default). To offload:\u003C\u002Fspan>\n\u003Cspan class=\"shj-syn-cmnt\"># EMBEDDING_ENDPOINT=https:\u002F\u002Fembeddings.yourdomain.com\u002Fv1\u002Fembeddings\u003C\u002Fspan>\n\u003Cspan class=\"shj-syn-cmnt\"># EMBEDDING_API_KEY=...\u003C\u002Fspan>\n\u003Cspan class=\"shj-syn-cmnt\"># ...or go lexical-only:\u003C\u002Fspan>\n\u003Cspan class=\"shj-syn-cmnt\"># KNOWLEDGE_DISABLE_EMBEDDINGS=1\u003C\u002Fspan>\n\n\u003Cspan class=\"shj-syn-cmnt\"># --- S3 (example: AWS) ---\u003C\u002Fspan>\n\u003Cspan class=\"shj-syn-var\">S3_ENDPOINT\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">=\u003C\u002Fspan>s3.eu-west-\u003Cspan class=\"shj-syn-num\">1.\u003C\u002Fspan>amazonaws.com\n\u003Cspan class=\"shj-syn-var\">S3_USE_SSL\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">=\u003C\u002Fspan>true\n\u003Cspan class=\"shj-syn-var\">S3_ACCESS_KEY\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">=&lt;\u003C\u002Fspan>key\u003Cspan class=\"shj-syn-oper\">&gt;\u003C\u002Fspan>\n\u003Cspan class=\"shj-syn-var\">S3_SECRET_KEY\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">=&lt;\u003C\u002Fspan>secret\u003Cspan class=\"shj-syn-oper\">&gt;\u003C\u002Fspan>\n\u003Cspan class=\"shj-syn-var\">S3_BUCKET\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">=\u003C\u002Fspan>help-bites-prod\n\u003Cspan class=\"shj-syn-var\">S3_PUBLIC_URL\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">=\u003C\u002Fspan>https:\u002F\u002Fhelp-bites-prod.s3.eu-west-\u003Cspan class=\"shj-syn-num\">1.\u003C\u002Fspan>amazonaws.com\n\u003Cspan class=\"shj-syn-var\">S3_REGION\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">=\u003C\u002Fspan>eu-west-\u003Cspan class=\"shj-syn-num\">1\u003C\u002Fspan>\n\u003Cspan class=\"shj-syn-var\">S3_SET_PUBLIC_POLICY\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">=\u003C\u002Fspan>false   \u003Cspan class=\"shj-syn-cmnt\"># manage the bucket policy in AWS yourself\u003C\u002Fspan>\n\n\u003Cspan class=\"shj-syn-cmnt\"># --- Email (optional) ---\u003C\u002Fspan>\n\u003Cspan class=\"shj-syn-var\">MAILGUN_API_KEY\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">=&lt;\u003C\u002Fspan>key\u003Cspan class=\"shj-syn-oper\">&gt;\u003C\u002Fspan>\n\u003Cspan class=\"shj-syn-var\">MAILGUN_DOMAIN\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">=\u003C\u002Fspan>mg.yourdomain.com\n\u003Cspan class=\"shj-syn-var\">MAILGUN_API_BASE_URL\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">=\u003C\u002Fspan>https:\u002F\u002Fapi.eu.mailgun.net\n\u003Cspan class=\"shj-syn-var\">MAILGUN_FROM_EMAIL\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">=\u003C\u002Fspan>support@yourdomain.com\n\n\u003Cspan class=\"shj-syn-cmnt\"># --- AI auto-reply (optional) ---\u003C\u002Fspan>\n\u003Cspan class=\"shj-syn-cmnt\"># LITELLM_API_BASE_URL=https:\u002F\u002Fllm.yourdomain.com\u003C\u002Fspan>\n\u003Cspan class=\"shj-syn-cmnt\"># LITELLM_API_KEY=&lt;key&gt;\u003C\u002Fspan>\n\u003Cspan class=\"shj-syn-cmnt\"># LLM_DEFAULT_MODEL=mimo-pro-v2\u003C\u002Fspan>\n\n\u003Cspan class=\"shj-syn-cmnt\"># --- Native push (optional) ---\u003C\u002Fspan>\n\u003Cspan class=\"shj-syn-cmnt\"># APNS_KEY_ID=...\u003C\u002Fspan>\n\u003Cspan class=\"shj-syn-cmnt\"># APNS_TEAM_ID=...\u003C\u002Fspan>\n\u003Cspan class=\"shj-syn-cmnt\"># APNS_BUNDLE_ID=com.helpbites.operator\u003C\u002Fspan>\n\u003Cspan class=\"shj-syn-cmnt\"># APNS_KEY=-----BEGIN PRIVATE KEY-----\\n...\\n-----END PRIVATE KEY-----\u003C\u002Fspan>\n\u003Cspan class=\"shj-syn-cmnt\"># APNS_ENV=production\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\u003C\u002Fdiv>\n\u003Cp>Generate secrets with, e.g.:\u003C\u002Fp>\n\u003Cdiv class=\"code-block\">\u003Cbutton type=\"button\" class=\"copy-btn\" data-copy aria-label=\"Copy to clipboard\">Copy\u003C\u002Fbutton>\u003Cpre class=\"shj-lang-bash\">\u003Ccode>\u003Cspan class=\"shj-syn-func\">openssl\u003C\u002Fspan> rand\u003Cspan class=\"shj-syn-kwd\"> -base\u003C\u002Fspan>64 \u003Cspan class=\"shj-syn-num\">48\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\u003C\u002Fdiv>\n\u003Chr>\n\u003Ch2 id=\"external-services\">External services\u003Ca class=\"heading-anchor\" href=\"#external-services\" aria-label=\"Link to this section\">#\u003C\u002Fa>\u003C\u002Fh2>\n\u003Cp>You can run everything in the provided Compose file, or point HelpBites at\nmanaged services. Mix and match.\u003C\u002Fp>\n\u003Ch3 id=\"mongodb\">MongoDB\u003Ca class=\"heading-anchor\" href=\"#mongodb\" aria-label=\"Link to this section\">#\u003C\u002Fa>\u003C\u002Fh3>\n\u003Cp>Provided as \u003Ccode>sb-mongo\u003C\u002Fcode> (Mongo 7). To use a managed Mongo (Atlas, etc.) instead,\ndrop \u003Ccode>sb-mongo\u003C\u002Fcode> and set \u003Ccode>PARSE_SERVER_DATABASE_URI\u003C\u002Fcode> to your connection string.\nParse needs \u003Ccode>disableIndexFieldValidation\u003C\u002Fcode> (the compose sets this via\n\u003Ccode>PARSE_SERVER_DATABASE_OPTIONS\u003C\u002Fcode>) so the schema script can declare indexes on\nMongo's own key names.\u003C\u002Fp>\n\u003Ch3 id=\"s3-storage-1\">S3 storage\u003Ca class=\"heading-anchor\" href=\"#s3-storage-1\" aria-label=\"Link to this section\">#\u003C\u002Fa>\u003C\u002Fh3>\n\u003Cp>Any S3-compatible bucket works. Two common setups:\u003C\u002Fp>\n\u003Cp>\u003Cstrong>Self-hosted\u003C\u002Fstrong> (RustFS\u002FMinIO\u002FGarage — the dev overlay bundles RustFS on\n\u003Ccode>localhost:9000\u003C\u002Fcode>):\u003C\u002Fp>\n\u003Cdiv class=\"code-block\">\u003Cbutton type=\"button\" class=\"copy-btn\" data-copy aria-label=\"Copy to clipboard\">Copy\u003C\u002Fbutton>\u003Cpre class=\"shj-lang-bash\">\u003Ccode>\u003Cspan class=\"shj-syn-var\">S3_ENDPOINT\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">=\u003C\u002Fspan>localhost\n\u003Cspan class=\"shj-syn-var\">S3_PORT\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">=\u003C\u002Fspan>\u003Cspan class=\"shj-syn-num\">9000\u003C\u002Fspan>\n\u003Cspan class=\"shj-syn-var\">S3_USE_SSL\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">=\u003C\u002Fspan>false\n\u003Cspan class=\"shj-syn-var\">S3_ACCESS_KEY\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">=\u003C\u002Fspan>rustfsadmin\n\u003Cspan class=\"shj-syn-var\">S3_SECRET_KEY\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">=\u003C\u002Fspan>rustfsadmin\n\u003Cspan class=\"shj-syn-var\">S3_BUCKET\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">=\u003C\u002Fspan>help-bites\n\u003Cspan class=\"shj-syn-var\">S3_PUBLIC_URL\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">=\u003C\u002Fspan>http:\u002F\u002Flocalhost:\u003Cspan class=\"shj-syn-num\">9000\u003C\u002Fspan>\u002Fhelp-bites\u003C\u002Fcode>\u003C\u002Fpre>\u003C\u002Fdiv>\n\u003Cp>\u003Cstrong>AWS S3\u003C\u002Fstrong> — omit \u003Ccode>S3_PORT\u003C\u002Fcode>, use SSL, set the region, and manage the bucket\npolicy yourself:\u003C\u002Fp>\n\u003Cdiv class=\"code-block\">\u003Cbutton type=\"button\" class=\"copy-btn\" data-copy aria-label=\"Copy to clipboard\">Copy\u003C\u002Fbutton>\u003Cpre class=\"shj-lang-bash\">\u003Ccode>\u003Cspan class=\"shj-syn-var\">S3_ENDPOINT\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">=\u003C\u002Fspan>s3.eu-west-\u003Cspan class=\"shj-syn-num\">1.\u003C\u002Fspan>amazonaws.com\n\u003Cspan class=\"shj-syn-var\">S3_USE_SSL\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">=\u003C\u002Fspan>true\n\u003Cspan class=\"shj-syn-var\">S3_BUCKET\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">=\u003C\u002Fspan>help-bites-prod\n\u003Cspan class=\"shj-syn-var\">S3_PUBLIC_URL\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">=\u003C\u002Fspan>https:\u002F\u002Fhelp-bites-prod.s3.eu-west-\u003Cspan class=\"shj-syn-num\">1.\u003C\u002Fspan>amazonaws.com\n\u003Cspan class=\"shj-syn-var\">S3_REGION\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">=\u003C\u002Fspan>eu-west-\u003Cspan class=\"shj-syn-num\">1\u003C\u002Fspan>\n\u003Cspan class=\"shj-syn-var\">S3_SET_PUBLIC_POLICY\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">=\u003C\u002Fspan>false\u003C\u002Fcode>\u003C\u002Fpre>\u003C\u002Fdiv>\n\u003Cp>The bucket is created on first use if missing. On non-AWS providers, the app also\napplies a public-read policy unless \u003Ccode>S3_SET_PUBLIC_POLICY=false\u003C\u002Fcode>.\u003C\u002Fp>\n\u003Cp>\u003Cstrong>Bundled RustFS\u003C\u002Fstrong> (\u003Ccode>--profile rustfs\u003C\u002Fcode>) — if you don't want to run an external\nbucket, the compose can bring up a \u003Ca href=\"https:\u002F\u002Frustfs.com\" target=\"_blank\" rel=\"noopener\">RustFS\u003C\u002Fa> instance\nalongside the app. It's \u003Cstrong>off by default\u003C\u002Fstrong>; the app uses your external S3 unless\nyou opt in. Enable it and point \u003Ccode>S3_*\u003C\u002Fcode> at it:\u003C\u002Fp>\n\u003Cdiv class=\"code-block\">\u003Cbutton type=\"button\" class=\"copy-btn\" data-copy aria-label=\"Copy to clipboard\">Copy\u003C\u002Fbutton>\u003Cpre class=\"shj-lang-bash\">\u003Ccode>\u003Cspan class=\"shj-syn-cmnt\"># start the stack with RustFS included\u003C\u002Fspan>\n\u003Cspan class=\"shj-syn-func\">docker\u003C\u002Fspan> compose --profile rustfs up\u003Cspan class=\"shj-syn-kwd\"> -d\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\u003C\u002Fdiv>\n\u003Cdiv class=\"code-block\">\u003Cbutton type=\"button\" class=\"copy-btn\" data-copy aria-label=\"Copy to clipboard\">Copy\u003C\u002Fbutton>\u003Cpre class=\"shj-lang-bash\">\u003Ccode>\u003Cspan class=\"shj-syn-cmnt\"># .env — use the bundled bucket\u003C\u002Fspan>\n\u003Cspan class=\"shj-syn-var\">S3_ENDPOINT\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">=\u003C\u002Fspan>sb-rustfs\n\u003Cspan class=\"shj-syn-var\">S3_PORT\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">=\u003C\u002Fspan>\u003Cspan class=\"shj-syn-num\">9000\u003C\u002Fspan>\n\u003Cspan class=\"shj-syn-var\">S3_USE_SSL\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">=\u003C\u002Fspan>false\n\u003Cspan class=\"shj-syn-var\">S3_ACCESS_KEY\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">=&lt;\u003C\u002Fspan>pick one\u003Cspan class=\"shj-syn-oper\">&gt;\u003C\u002Fspan>          \u003Cspan class=\"shj-syn-cmnt\"># sb-rustfs reuses these two\u003C\u002Fspan>\n\u003Cspan class=\"shj-syn-var\">S3_SECRET_KEY\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">=&lt;\u003C\u002Fspan>pick a strong one\u003Cspan class=\"shj-syn-oper\">&gt;\u003C\u002Fspan> \u003Cspan class=\"shj-syn-cmnt\"># (default rustfsadmin — change it)\u003C\u002Fspan>\n\u003Cspan class=\"shj-syn-var\">S3_BUCKET\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">=\u003C\u002Fspan>help-bites\n\u003Cspan class=\"shj-syn-var\">S3_PUBLIC_URL\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">=\u003C\u002Fspan>https:\u002F\u002Ffiles.yourdomain.com   \u003Cspan class=\"shj-syn-cmnt\"># a URL browsers can reach — see below\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\u003C\u002Fdiv>\n\u003Cp>Notes:\u003C\u002Fp>\n\u003Cul>\n\u003Cli>\u003Cstrong>\u003Ccode>sb-rustfs\u003C\u002Fcode> and the app share \u003Ccode>S3_ACCESS_KEY\u003C\u002Fcode> \u002F \u003Ccode>S3_SECRET_KEY\u003C\u002Fcode>.\u003C\u002Fstrong> Set them\nonce; RustFS boots with those credentials and the app authenticates with them.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>\u003Ccode>S3_PUBLIC_URL\u003C\u002Fcode> must be browser-reachable.\u003C\u002Fstrong> \u003Ccode>sb-rustfs\u003C\u002Fcode> only \u003Ccode>expose\u003C\u002Fcode>s its\nports on the internal network (like every other service here), so\n\u003Ccode>http:\u002F\u002Fsb-rustfs:9000\u003C\u002Fcode> works app→storage but not from a browser. Attach a\ndomain to \u003Ccode>sb-rustfs\u003C\u002Fcode> → \u003Cstrong>9000\u003C\u002Fstrong> (Dokploy Domains tab) and set \u003Ccode>S3_PUBLIC_URL\u003C\u002Fcode>\nto it. The console is on \u003Cstrong>9001\u003C\u002Fstrong> if you want a UI.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Data lives in four named volumes\u003C\u002Fstrong> (\u003Ccode>rustfs-data-0…3\u003C\u002Fcode>). RustFS runs as UID\n10001 and enforces a 4-path disk layout, so a one-shot \u003Ccode>sb-rustfs-init\u003C\u002Fcode>\ncontainer pre-owns the volumes before it starts. This mirrors RustFS's own\nsingle-node reference compose — back up these four volumes like any other state.\u003C\u002Fli>\n\u003Cli>In production there's no published port for \u003Ccode>sb-rustfs\u003C\u002Fcode>; the dev overlay\n(\u003Ccode>docker-compose.dev.yml\u003C\u002Fcode>, \u003Ccode>--profile dev\u003C\u002Fcode>) publishes \u003Cstrong>9000\u002F9001\u003C\u002Fstrong> on\n\u003Ccode>localhost\u003C\u002Fcode> for local use.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Ch3 id=\"meilisearch\">Meilisearch\u003Ca class=\"heading-anchor\" href=\"#meilisearch\" aria-label=\"Link to this section\">#\u003C\u002Fa>\u003C\u002Fh3>\n\u003Cp>Provided as \u003Ccode>sb-meili\u003C\u002Fcode> (v1.53). \u003Ccode>bun run search:setup\u003C\u002Fcode> (and the boot entrypoint)\ncreates the indexes, settings, and the userProvided embedder. If you expose it\npublicly, set a strong \u003Ccode>MEILI_MASTER_KEY\u003C\u002Fcode>. To use Meilisearch Cloud, set\n\u003Ccode>MEILI_URL\u003C\u002Fcode> and \u003Ccode>MEILI_MASTER_KEY\u003C\u002Fcode> and drop the \u003Ccode>sb-meili\u003C\u002Fcode> service.\u003C\u002Fp>\n\u003Cp>The app shouldn't hold the master key, which can mint keys and delete every\nindex. Give it a scoped one:\u003C\u002Fp>\n\u003Col>\n\u003Cli>\u003Ccode>docker compose exec app bun scripts\u002Fmeili-key.mjs\u003C\u002Fcode> (locally: \u003Ccode>bun run search:key\u003C\u002Fcode>). It creates the app's key, limited to the \u003Ccode>knowledge\u003C\u002Fcode>,\n\u003Ccode>conversations\u003C\u002Fcode> and \u003Ccode>endusers\u003C\u002Fcode> indexes and the actions the app uses, and\nprints it. Re-running prints the same key.\u003C\u002Fli>\n\u003Cli>Put \u003Ccode>MEILI_API_KEY=&lt;that key&gt;\u003C\u002Fcode> in \u003Ccode>.env\u003C\u002Fcode> and redeploy.\u003C\u002Fli>\n\u003Cli>Uncomment \u003Ccode>MEILI_MASTER_KEY: &quot;&quot;\u003C\u002Fcode> under the \u003Ccode>app\u003C\u002Fcode> service in\n\u003Ccode>docker-compose.yml\u003C\u002Fcode> and redeploy again: the app container no longer carries\nthe master key; \u003Ccode>sb-meili\u003C\u002Fcode> keeps it.\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Cp>Until \u003Ccode>MEILI_API_KEY\u003C\u002Fcode> is set the app uses \u003Ccode>MEILI_MASTER_KEY\u003C\u002Fcode> and logs a warning.\u003C\u002Fp>\n\u003Ch3 id=\"llm-endpoint-ai-auto-reply\">LLM endpoint (AI auto-reply)\u003Ca class=\"heading-anchor\" href=\"#llm-endpoint-ai-auto-reply\" aria-label=\"Link to this section\">#\u003C\u002Fa>\u003C\u002Fh3>\n\u003Cp>Any OpenAI-compatible \u002F LiteLLM endpoint. Set \u003Ccode>LITELLM_API_BASE_URL\u003C\u002Fcode>,\n\u003Ccode>LITELLM_API_KEY\u003C\u002Fcode>, and \u003Ccode>LLM_DEFAULT_MODEL\u003C\u002Fcode>. Auto-reply stays off until a project\nhas an assistant with \u003Ccode>autoReply\u003C\u002Fcode> enabled — the env vars just make it \u003Cem>possible\u003C\u002Fem>.\nPer-agent endpoint\u002Fkey\u002Fmodel settings override the defaults. See\n\u003Ca href=\"https:\u002F\u002Fgithub.com\u002Felrumo\u002Fhelp-bites-oss\u002Fblob\u002Fmain\u002Fdocs\u002FKNOWLEDGE.md\" target=\"_blank\" rel=\"noopener\">docs\u002FKNOWLEDGE.md\u003C\u002Fa> for how knowledge grounds the answers.\u003C\u002Fp>\n\u003Ch3 id=\"mailgun-email\">Mailgun (email)\u003Ca class=\"heading-anchor\" href=\"#mailgun-email\" aria-label=\"Link to this section\">#\u003C\u002Fa>\u003C\u002Fh3>\n\u003Cp>Set \u003Ccode>MAILGUN_API_KEY\u003C\u002Fcode>, \u003Ccode>MAILGUN_DOMAIN\u003C\u002Fcode>, and \u003Ccode>MAILGUN_FROM_EMAIL\u003C\u002Fcode>. Pick the\nregion base URL (\u003Ccode>MAILGUN_API_BASE_URL\u003C\u002Fcode>). Without all three, email notifications\nare simply skipped and the send API answers 503 — the app runs fine.\u003C\u002Fp>\n\u003Cp>For delivery status, set \u003Ccode>MAILGUN_WEBHOOK_SIGNING_KEY\u003C\u002Fcode> (Mailgun → Sending →\nWebhooks → HTTP webhook signing key) and add \u003Ccode>https:\u002F\u002F&lt;your domain&gt;\u002Fv1\u002Fwebhooks\u002Fmailgun\u003C\u002Fcode>\nfor delivered, opened, clicked, permanent and temporary failure, complaints and\nunsubscribes, on every domain your projects send from. See\n\u003Ca href=\"https:\u002F\u002Fgithub.com\u002Felrumo\u002Fhelp-bites-oss\u002Fblob\u002Fmain\u002Fdocs\u002FEMAIL.md#delivery-webhooks\" target=\"_blank\" rel=\"noopener\">docs\u002FEMAIL.md\u003C\u002Fa>.\u003C\u002Fp>\n\u003Ch3 id=\"apns-native-push\">APNs (native push)\u003Ca class=\"heading-anchor\" href=\"#apns-native-push\" aria-label=\"Link to this section\">#\u003C\u002Fa>\u003C\u002Fh3>\n\u003Cp>Only for the native operator app (\u003Ca href=\"https:\u002F\u002Fgithub.com\u002Felrumo\u002Fhelp-bites-oss\u002Ftree\u002Fmain\u002Fhelpbites-native\" target=\"_blank\" rel=\"noopener\">helpbites-native\u003C\u002Fa>).\nCreate an APNs auth key in your Apple developer account and set the \u003Ccode>APNS_*\u003C\u002Fcode>\ngroup. Push stays disabled until these are present.\u003C\u002Fp>\n\u003Chr>\n\u003Ch2 id=\"first-run\">First run\u003Ca class=\"heading-anchor\" href=\"#first-run\" aria-label=\"Link to this section\">#\u003C\u002Fa>\u003C\u002Fh2>\n\u003Col>\n\u003Cli>\u003Cstrong>Create the first operator.\u003C\u002Fstrong> Open \u003Ccode>APP_URL\u003C\u002Fcode> — a fresh install shows a\nsign-up form. That first sign-up becomes the first operator, so set\n\u003Ccode>SETUP_TOKEN\u003C\u002Fcode> (the form then asks for it) or finish it straight after\ndeploying: until then anyone who finds the URL can claim the instance. (Add\nteammates later from the Team page, or\n\u003Ccode>docker compose exec app bun scripts\u002Fadd-agent.mjs\u003C\u002Fcode>.) Admin API keys act as\ntheir owner, expire after 90 days unless you choose otherwise, and are\nrevoked with a password reset. Changing an account's email asks for the\npassword and tells the old address, and Parse locks an account for 15\nminutes after ten wrong passwords.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Create a project.\u003C\u002Fstrong> Panel → Projects → New project. You get a public key\n(\u003Ccode>pk_…\u003C\u002Fcode>) and a secret (\u003Ccode>sk_…\u003C\u002Fcode>). Set the site URL, allowed widget origins, the\nreply name, and notification emails.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Add knowledge (optional).\u003C\u002Fstrong> Upload a \u003Ccode>SUPPORT_FAQ.md\u003C\u002Fcode>, review detected FAQs,\norganize them, and assign them to an assistant. See \u003Ca href=\"https:\u002F\u002Fgithub.com\u002Felrumo\u002Fhelp-bites-oss\u002Fblob\u002Fmain\u002Fdocs\u002FKNOWLEDGE.md\" target=\"_blank\" rel=\"noopener\">docs\u002FKNOWLEDGE.md\u003C\u002Fa>.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Connect your app.\u003C\u002Fstrong> Mint identity tokens on your backend and drop in the\nwidget (or use the API directly). Full walkthrough:\n\u003Ca href=\"\u002Fdocs\u002Fconnect\">docs\u002FCONNECTING.md\u003C\u002Fa>. Interactive API reference lives at\n\u003Ccode>APP_URL\u002F_docs\u002Fscalar\u003C\u002Fcode>.\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Cp>Mint a test token against a running deployment without writing any host code:\u003C\u002Fp>\n\u003Cdiv class=\"code-block\">\u003Cbutton type=\"button\" class=\"copy-btn\" data-copy aria-label=\"Copy to clipboard\">Copy\u003C\u002Fbutton>\u003Cpre class=\"shj-lang-bash\">\u003Ccode>\u003Cspan class=\"shj-syn-func\">bun\u003C\u002Fspan> scripts\u002Fdev-token.mjs \u003Cspan class=\"shj-syn-oper\">&lt;\u003C\u002Fspan>projectId\u003Cspan class=\"shj-syn-oper\">&gt;\u003C\u002Fspan> \u003Cspan class=\"shj-syn-oper\">&lt;\u003C\u002Fspan>userId\u003Cspan class=\"shj-syn-oper\">&gt;\u003C\u002Fspan> \u003Cspan class=\"shj-syn-str\">\"Demo user\"\u003C\u002Fspan> demo@example.com\u003C\u002Fcode>\u003C\u002Fpre>\u003C\u002Fdiv>\n\u003Chr>\n\u003Ch2 id=\"operations\">Operations\u003Ca class=\"heading-anchor\" href=\"#operations\" aria-label=\"Link to this section\">#\u003C\u002Fa>\u003C\u002Fh2>\n\u003Ch3 id=\"schema-migrations\">Schema migrations\u003Ca class=\"heading-anchor\" href=\"#schema-migrations\" aria-label=\"Link to this section\">#\u003C\u002Fa>\u003C\u002Fh3>\n\u003Cp>There is no manual migration step. The \u003Ccode>app\u003C\u002Fcode> container runs\n\u003Ccode>ensure-schema.mjs --apply\u003C\u002Fcode> on \u003Cstrong>every boot\u003C\u002Fstrong>: it creates missing classes,\nadds missing fields, and re-asserts CLPs and indexes. Existing data is\npreserved. To preview pending changes without applying (exits \u003Ccode>2\u003C\u002Fcode> when work is\npending):\u003C\u002Fp>\n\u003Cdiv class=\"code-block\">\u003Cbutton type=\"button\" class=\"copy-btn\" data-copy aria-label=\"Copy to clipboard\">Copy\u003C\u002Fbutton>\u003Cpre class=\"shj-lang-bash\">\u003Ccode>\u003Cspan class=\"shj-syn-func\">bun\u003C\u002Fspan> run schema\u003C\u002Fcode>\u003C\u002Fpre>\u003C\u002Fdiv>\n\u003Ch3 id=\"backups\">Backups\u003Ca class=\"heading-anchor\" href=\"#backups\" aria-label=\"Link to this section\">#\u003C\u002Fa>\u003C\u002Fh3>\n\u003Cp>All durable state is in named Docker volumes. Back these up:\u003C\u002Fp>\n\u003Cdiv class=\"table-wrap\">\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>Volume\u003C\u002Fth>\n\u003Cth>Contains\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\n\u003Ctr>\n\u003Ctd>\u003Ccode>mongo-data\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Cstrong>Everything\u003C\u002Fstrong> — conversations, projects, users, knowledge. The critical one.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>meili-data\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Search index. Rebuildable with \u003Ccode>bun run reindex\u003C\u002Fcode> + re-indexing knowledge.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>redis-data\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Ephemeral (SSE\u002Frate limits). Safe to lose.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>model-cache\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Downloaded embedding model. Re-downloads if lost.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\n\u003C\u002Ftable>\u003C\u002Fdiv>\n\u003Cp>Plus your \u003Cstrong>S3 bucket\u003C\u002Fstrong> (attachments + knowledge docs) — back that up with your\nprovider's tooling. Example Mongo dump:\u003C\u002Fp>\n\u003Cdiv class=\"code-block\">\u003Cbutton type=\"button\" class=\"copy-btn\" data-copy aria-label=\"Copy to clipboard\">Copy\u003C\u002Fbutton>\u003Cpre class=\"shj-lang-bash\">\u003Ccode>\u003Cspan class=\"shj-syn-func\">docker\u003C\u002Fspan> compose \u003Cspan class=\"shj-syn-kwd\">exec\u003C\u002Fspan> sb-mongo mongodump --db helpbites --archive \u003Cspan class=\"shj-syn-oper\">&gt;\u003C\u002Fspan> helpbites-\u003Cspan class=\"shj-syn-var\">$(date +%F)\u003C\u002Fspan>.archive\u003C\u002Fcode>\u003C\u002Fpre>\u003C\u002Fdiv>\n\u003Cp>Restore into an empty database with the stack stopped except Mongo:\u003C\u002Fp>\n\u003Cdiv class=\"code-block\">\u003Cbutton type=\"button\" class=\"copy-btn\" data-copy aria-label=\"Copy to clipboard\">Copy\u003C\u002Fbutton>\u003Cpre class=\"shj-lang-bash\">\u003Ccode>\u003Cspan class=\"shj-syn-func\">docker\u003C\u002Fspan> compose up\u003Cspan class=\"shj-syn-kwd\"> -d\u003C\u002Fspan> sb-mongo\n\u003Cspan class=\"shj-syn-func\">docker\u003C\u002Fspan> compose \u003Cspan class=\"shj-syn-kwd\">exec\u003C\u002Fspan>\u003Cspan class=\"shj-syn-kwd\"> -T\u003C\u002Fspan> sb-mongo mongorestore --db helpbites --archive --drop \u003Cspan class=\"shj-syn-oper\">&lt;\u003C\u002Fspan> helpbites-\u003Cspan class=\"shj-syn-num\">2026\u003C\u002Fspan>-\u003Cspan class=\"shj-syn-num\">01\u003C\u002Fspan>-\u003Cspan class=\"shj-syn-num\">31.\u003C\u002Fspan>archive\n\u003Cspan class=\"shj-syn-func\">docker\u003C\u002Fspan> compose up\u003Cspan class=\"shj-syn-kwd\"> -d\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\u003C\u002Fdiv>\n\u003Cp>A nightly dump from cron is enough for most installs:\u003C\u002Fp>\n\u003Cdiv class=\"code-block\">\u003Cbutton type=\"button\" class=\"copy-btn\" data-copy aria-label=\"Copy to clipboard\">Copy\u003C\u002Fbutton>\u003Cpre class=\"shj-lang-plain\">\u003Ccode>0 3 * * * cd \u002Fopt\u002Fhelpbites &#38;&#38; docker compose exec -T sb-mongo mongodump --db helpbites --archive | gzip &gt; \u002Fvar\u002Fbackups\u002Fhelpbites-$(date +\\%F).archive.gz\u003C\u002Fcode>\u003C\u002Fpre>\u003C\u002Fdiv>\n\u003Ch3 id=\"rebuilding-the-search-index\">Rebuilding the search index\u003Ca class=\"heading-anchor\" href=\"#rebuilding-the-search-index\" aria-label=\"Link to this section\">#\u003C\u002Fa>\u003C\u002Fh3>\n\u003Cp>If Meili data is lost or you want to backfill:\u003C\u002Fp>\n\u003Cdiv class=\"code-block\">\u003Cbutton type=\"button\" class=\"copy-btn\" data-copy aria-label=\"Copy to clipboard\">Copy\u003C\u002Fbutton>\u003Cpre class=\"shj-lang-bash\">\u003Ccode>\u003Cspan class=\"shj-syn-func\">bun\u003C\u002Fspan> run search:setup   \u003Cspan class=\"shj-syn-cmnt\"># recreate indexes + settings\u003C\u002Fspan>\n\u003Cspan class=\"shj-syn-func\">bun\u003C\u002Fspan> run reindex        \u003Cspan class=\"shj-syn-cmnt\"># backfill conversations \u002F end-users \u002F FAQs (lexical)\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\u003C\u002Fdiv>\n\u003Cp>Knowledge FAQs re-embed as they're reindexed (unless embeddings are disabled).\u003C\u002Fp>\n\u003Ch3 id=\"scaling-out\">Scaling out\u003Ca class=\"heading-anchor\" href=\"#scaling-out\" aria-label=\"Link to this section\">#\u003C\u002Fa>\u003C\u002Fh3>\n\u003Cp>Run multiple \u003Ccode>app\u003C\u002Fcode> replicas behind your load balancer — SSE fan-out goes through\nRedis pub\u002Fsub, so a client connected to replica A still gets events from a write\non replica B. Rate limits are shared through Redis. If you scale the app, prefer\n\u003Ccode>EMBEDDING_ENDPOINT\u003C\u002Fcode> (a shared embeddings service) over each replica caching the\nmodel. Mongo, Redis, and Meili remain single logical instances (or their own\nmanaged clusters).\u003C\u002Fp>\n\u003Ch3 id=\"health-checks\">Health checks\u003Ca class=\"heading-anchor\" href=\"#health-checks\" aria-label=\"Link to this section\">#\u003C\u002Fa>\u003C\u002Fh3>\n\u003Cp>Mongo, Parse, Redis and \u003Ccode>app\u003C\u002Fcode> have a healthcheck; \u003Ccode>app\u003C\u002Fcode> waits for \u003Ccode>sb-parse\u003C\u002Fcode> and\n\u003Ccode>sb-redis\u003C\u002Fcode> to be healthy before starting. \u003Ccode>GET \u002Fv1\u002Fhealth\u002Flive\u003C\u002Fcode> only says the process\nis up (the container healthcheck uses it). \u003Ccode>GET \u002Fv1\u002Fhealth\u003C\u002Fcode> also checks Parse, Redis,\nMeilisearch, S3 and the worker: point your uptime monitor at that one.\u003C\u002Fp>\n\u003Ch3 id=\"logs\">Logs\u003Ca class=\"heading-anchor\" href=\"#logs\" aria-label=\"Link to this section\">#\u003C\u002Fa>\u003C\u002Fh3>\n\u003Cdiv class=\"code-block\">\u003Cbutton type=\"button\" class=\"copy-btn\" data-copy aria-label=\"Copy to clipboard\">Copy\u003C\u002Fbutton>\u003Cpre class=\"shj-lang-bash\">\u003Ccode>\u003Cspan class=\"shj-syn-func\">docker\u003C\u002Fspan> compose logs\u003Cspan class=\"shj-syn-kwd\"> -f\u003C\u002Fspan> app          \u003Cspan class=\"shj-syn-cmnt\"># the app\u003C\u002Fspan>\n\u003Cspan class=\"shj-syn-func\">docker\u003C\u002Fspan> compose logs\u003Cspan class=\"shj-syn-kwd\"> -f\u003C\u002Fspan> sb-parse     \u003Cspan class=\"shj-syn-cmnt\"># data layer\u003C\u002Fspan>\n\u003Cspan class=\"shj-syn-func\">docker\u003C\u002Fspan> compose logs --tail\u003Cspan class=\"shj-syn-oper\">=\u003C\u002Fspan>\u003Cspan class=\"shj-syn-num\">100\u003C\u002Fspan> app  \u003Cspan class=\"shj-syn-cmnt\"># recent\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\u003C\u002Fdiv>\n\u003Chr>\n\u003Ch2 id=\"verifying-a-deployment\">Verifying a deployment\u003Ca class=\"heading-anchor\" href=\"#verifying-a-deployment\" aria-label=\"Link to this section\">#\u003C\u002Fa>\u003C\u002Fh2>\n\u003Cp>The smoke test drives a real conversation end-to-end (auth, messages,\nattachments, SSE) against a running app. Point \u003Ccode>BASE\u003C\u002Fcode> at your deployment:\u003C\u002Fp>\n\u003Cdiv class=\"code-block\">\u003Cbutton type=\"button\" class=\"copy-btn\" data-copy aria-label=\"Copy to clipboard\">Copy\u003C\u002Fbutton>\u003Cpre class=\"shj-lang-bash\">\u003Ccode>\u003Cspan class=\"shj-syn-var\">BASE\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">=\u003C\u002Fspan>https:\u002F\u002Fsupport.yourdomain.com bun run smoke\u003C\u002Fcode>\u003C\u002Fpre>\u003C\u002Fdiv>\n\u003Cp>If SSE can't be reached from where you run it (e.g. through a strict proxy), skip\njust that check:\u003C\u002Fp>\n\u003Cdiv class=\"code-block\">\u003Cbutton type=\"button\" class=\"copy-btn\" data-copy aria-label=\"Copy to clipboard\">Copy\u003C\u002Fbutton>\u003Cpre class=\"shj-lang-bash\">\u003Ccode>\u003Cspan class=\"shj-syn-var\">SKIP_SSE\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">=\u003C\u002Fspan>\u003Cspan class=\"shj-syn-num\">1\u003C\u002Fspan> \u003Cspan class=\"shj-syn-var\">BASE\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">=\u003C\u002Fspan>https:\u002F\u002Fsupport.yourdomain.com bun run smoke\u003C\u002Fcode>\u003C\u002Fpre>\u003C\u002Fdiv>\n\u003Cp>Pure-logic unit tests (no running stack needed):\u003C\u002Fp>\n\u003Cdiv class=\"code-block\">\u003Cbutton type=\"button\" class=\"copy-btn\" data-copy aria-label=\"Copy to clipboard\">Copy\u003C\u002Fbutton>\u003Cpre class=\"shj-lang-bash\">\u003Ccode>\u003Cspan class=\"shj-syn-func\">bun\u003C\u002Fspan> test\u003C\u002Fcode>\u003C\u002Fpre>\u003C\u002Fdiv>\n\u003Cp>Quick manual checks:\u003C\u002Fp>\n\u003Cul>\n\u003Cli>\u003Ccode>APP_URL\u002F\u003C\u002Fcode> loads the landing page.\u003C\u002Fli>\n\u003Cli>\u003Ccode>APP_URL\u002F_docs\u002Fscalar\u003C\u002Fcode> renders the API docs.\u003C\u002Fli>\n\u003Cli>Sign-up creates an operator; the panel loads at \u003Ccode>\u002Finbox\u003C\u002Fcode>.\u003C\u002Fli>\n\u003Cli>Send a widget\u002FAPI message and confirm it appears live in the panel (SSE works).\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Chr>\n\u003Ch2 id=\"upgrades\">Upgrades\u003Ca class=\"heading-anchor\" href=\"#upgrades\" aria-label=\"Link to this section\">#\u003C\u002Fa>\u003C\u002Fh2>\n\u003Cp>Read \u003Ca href=\"https:\u002F\u002Fgithub.com\u002Felrumo\u002Fhelp-bites-oss\u002Fblob\u002Fmain\u002FCHANGELOG.md\" target=\"_blank\" rel=\"noopener\">CHANGELOG.md\u003C\u002Fa> first. Plain Compose pulls the new image; the\nentrypoint reconciles the schema on boot:\u003C\u002Fp>\n\u003Cdiv class=\"code-block\">\u003Cbutton type=\"button\" class=\"copy-btn\" data-copy aria-label=\"Copy to clipboard\">Copy\u003C\u002Fbutton>\u003Cpre class=\"shj-lang-bash\">\u003Ccode>\u003Cspan class=\"shj-syn-func\">git\u003C\u002Fspan> pull                 \u003Cspan class=\"shj-syn-cmnt\"># new compose file and Caddyfile, if any\u003C\u002Fspan>\n\u003Cspan class=\"shj-syn-func\">docker\u003C\u002Fspan> compose pull\n\u003Cspan class=\"shj-syn-func\">docker\u003C\u002Fspan> compose up\u003Cspan class=\"shj-syn-kwd\"> -d\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\u003C\u002Fdiv>\n\u003Cp>Pin a release with \u003Ccode>HELPBITES_VERSION=0.1.0\u003C\u002Fcode> in \u003Ccode>.env\u003C\u002Fcode> (default \u003Ccode>latest\u003C\u002Fcode>). To roll\nback, set the previous version and run \u003Ccode>docker compose up -d\u003C\u002Fcode> again. Back up first:\na rollback does not undo a schema change. Building from source instead: add\n\u003Ccode>-f docker-compose.build.yml\u003C\u002Fcode> and \u003Ccode>--build\u003C\u002Fcode>. On Dokploy, redeploy.\u003C\u002Fp>\n\u003Cp>Volumes persist, so data carries over. The schema script patches the DB forward\nautomatically. To also pull newer service images (Mongo\u002FParse\u002FRedis\u002FMeili),\nbump the pinned tags in \u003Ccode>docker-compose.yml\u003C\u002Fcode> deliberately and:\u003C\u002Fp>\n\u003Cdiv class=\"code-block\">\u003Cbutton type=\"button\" class=\"copy-btn\" data-copy aria-label=\"Copy to clipboard\">Copy\u003C\u002Fbutton>\u003Cpre class=\"shj-lang-bash\">\u003Ccode>\u003Cspan class=\"shj-syn-func\">docker\u003C\u002Fspan> compose pull \u003Cspan class=\"shj-syn-kwd\">&#38;&#38;\u003C\u002Fspan> \u003Cspan class=\"shj-syn-func\">docker\u003C\u002Fspan> compose up\u003Cspan class=\"shj-syn-kwd\"> -d\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\u003C\u002Fdiv>\n\u003Cblockquote>\n\u003Cp>Service image tags are pinned on purpose (\u003Ccode>mongo:7\u003C\u002Fcode>, \u003Ccode>parse-server:9.10.0\u003C\u002Fcode>,\n\u003Ccode>redis:7-alpine\u003C\u002Fcode>, \u003Ccode>meilisearch:v1.53.2\u003C\u002Fcode>). Upgrade them one at a time and test —\nParse and Meili occasionally have breaking changes between majors.\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\u003Ch3 id=\"upgrading-to-workspaces\">Upgrading to workspaces\u003Ca class=\"heading-anchor\" href=\"#upgrading-to-workspaces\" aria-label=\"Link to this section\">#\u003C\u002Fa>\u003C\u002Fh3>\n\u003Cp>Versions from before workspaces upgrade on their own, on the first boot of the\nnew image:\u003C\u002Fp>\n\u003Cul>\n\u003Cli>Everything moves into one workspace (\u003Ccode>DEFAULT_WORKSPACE_NAME\u003C\u002Fcode>). The oldest\noperator becomes its owner and every other operator an admin, which is the\naccess everyone had before.\u003C\u002Fli>\n\u003Cli>Error-tracker settings move from the install into the workspace.\u003C\u002Fli>\n\u003Cli>Search documents gain their workspace id in place. Nothing is re-embedded.\u003C\u002Fli>\n\u003Cli>The panel looks the same with one workspace: the switcher only appears once\nthere is a second.\u003C\u002Fli>\n\u003Cli>API keys keep working and now belong to that workspace.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>The migration is idempotent; running it again changes nothing. Two things now\nneed a setting that did not before:\u003C\u002Fp>\n\u003Cul>\n\u003Cli>An assistant's LLM endpoint or an error-tracker URL typed into the panel that\npoints at a private address (a LiteLLM or GlitchTip next to the app) needs\n\u003Ccode>ALLOW_PRIVATE_NETWORK=1\u003C\u002Fcode>.\u003C\u002Fli>\n\u003Cli>Outbound requests to those URLs now connect to the exact address that passed\nthe safety check. Behind an HTTP proxy that allows hosts by name, they will be\nrefused (the proxy is asked for an IP). No proxy is set by default.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Chr>\n\u003Ch2 id=\"licensing-notes-for-the-services\">Licensing notes for the services\u003Ca class=\"heading-anchor\" href=\"#licensing-notes-for-the-services\" aria-label=\"Link to this section\">#\u003C\u002Fa>\u003C\u002Fh2>\n\u003Cp>HelpBites itself is AGPL-3.0 (and \u003Ccode>ee\u002F\u003C\u002Fcode> commercial; see \u003Ca href=\"https:\u002F\u002Fgithub.com\u002Felrumo\u002Fhelp-bites-oss\u002Fblob\u002Fmain\u002Fdocs\u002FEDITIONS.md\" target=\"_blank\" rel=\"noopener\">EDITIONS.md\u003C\u002Fa>).\nThe services it runs next to have their own licenses, which matter mostly if you\noffer HelpBites as a service:\u003C\u002Fp>\n\u003Cdiv class=\"table-wrap\">\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>Service\u003C\u002Fth>\n\u003Cth>License\u003C\u002Fth>\n\u003Cth>What it means here\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\n\u003Ctr>\n\u003Ctd>Parse Server\u003C\u002Ftd>\n\u003Ctd>Apache-2.0\u003C\u002Ftd>\n\u003Ctd>No restrictions.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>MongoDB Community\u003C\u002Ftd>\n\u003Ctd>SSPL\u003C\u002Ftd>\n\u003Ctd>Fine as the private database of your service. It restricts offering MongoDB \u003Cem>itself\u003C\u002Fem> as a service.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>Redis 7.4+\u003C\u002Ftd>\n\u003Ctd>RSALv2 \u002F SSPL\u003C\u002Ftd>\n\u003Ctd>The base compose pins \u003Ccode>redis:7-alpine\u003C\u002Fcode>. The hosted overlay uses \u003Cstrong>Valkey 8\u003C\u002Fstrong> (BSD), the community fork.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>Meilisearch Community\u003C\u002Ftd>\n\u003Ctd>MIT\u003C\u002Ftd>\n\u003Ctd>No restrictions.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>RustFS\u003C\u002Ftd>\n\u003Ctd>Apache-2.0\u003C\u002Ftd>\n\u003Ctd>No restrictions.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>GlitchTip\u003C\u002Ftd>\n\u003Ctd>MIT\u003C\u002Ftd>\n\u003Ctd>Fine to offer to customers.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>Bugsink\u003C\u002Ftd>\n\u003Ctd>PolyForm Shield\u003C\u002Ftd>\n\u003Ctd>Fine for your own error tracking. Don't offer it to customers as part of a hosted service. The cloud edition does not give tenants the server's Bugsink.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\n\u003C\u002Ftable>\u003C\u002Fdiv>\n\u003Chr>\n\u003Ch2 id=\"troubleshooting\">Troubleshooting\u003Ca class=\"heading-anchor\" href=\"#troubleshooting\" aria-label=\"Link to this section\">#\u003C\u002Fa>\u003C\u002Fh2>\n\u003Cp>\u003Cstrong>App can't reach Parse \u002F &quot;master key rejected&quot;.\u003C\u002Fstrong> Parse 7+ refuses the master\nkey from non-loopback addresses unless allow-listed. The compose sets\n\u003Ccode>PARSE_SERVER_MASTER_KEY_IPS: &quot;0.0.0.0\u002F0,::\u002F0&quot;\u003C\u002Fcode> for the internal network — keep\nit. Also confirm \u003Ccode>PARSE_ID\u003C\u002Fcode> and \u003Ccode>PARSE_MASTER_KEY\u003C\u002Fcode> match between the app and\n\u003Ccode>sb-parse\u003C\u002Fcode>.\u003C\u002Fp>\n\u003Cp>\u003Cstrong>Services resolve to the wrong container.\u003C\u002Fstrong> On a shared network, use the \u003Ccode>sb-\u003C\u002Fcode>\nprefixed names (\u003Ccode>sb-parse\u003C\u002Fcode>, \u003Ccode>sb-redis\u003C\u002Fcode>, \u003Ccode>sb-mongo\u003C\u002Fcode>, \u003Ccode>sb-meili\u003C\u002Fcode>). A bare \u003Ccode>parse\u003C\u002Fcode>\nor \u003Ccode>redis\u003C\u002Fcode> may resolve to another project's service.\u003C\u002Fp>\n\u003Cp>\u003Cstrong>&quot;Unsupported device&quot; on boot \u002F embeddings.\u003C\u002Fstrong> \u003Ccode>ICON_EMBEDDING_DEVICE\u003C\u002Fcode> must be\n\u003Ccode>cpu\u003C\u002Fcode> (default) or \u003Ccode>cuda\u003C\u002Fcode> (Linux + GPU). \u003Ccode>wasm\u003C\u002Fcode>\u002F\u003Ccode>webgpu\u003C\u002Fcode> are browser-only. To\navoid the model entirely, set \u003Ccode>KNOWLEDGE_DISABLE_EMBEDDINGS=1\u003C\u002Fcode> or point\n\u003Ccode>EMBEDDING_ENDPOINT\u003C\u002Fcode> at a remote service.\u003C\u002Fp>\n\u003Cp>\u003Cstrong>Slow first boot.\u003C\u002Fstrong> The local embedding model downloads on first use and caches\nto \u003Ccode>TRANSFORMERS_CACHE\u003C\u002Fcode> (\u003Ccode>\u002Fmodels\u003C\u002Fcode> volume). Subsequent boots are fast. Disable\nembeddings or use a remote endpoint to skip the download.\u003C\u002Fp>\n\u003Cp>\u003Cstrong>Attachments\u002Fknowledge uploads fail with an S3 error.\u003C\u002Fstrong> \u003Ccode>S3_ENDPOINT\u003C\u002Fcode>,\n\u003Ccode>S3_ACCESS_KEY\u003C\u002Fcode>, and \u003Ccode>S3_SECRET_KEY\u003C\u002Fcode> must all be set. On AWS, set\n\u003Ccode>S3_SET_PUBLIC_POLICY=false\u003C\u002Fcode> and manage the bucket policy yourself (the app can't\nset a public policy on a locked-down AWS bucket). Confirm \u003Ccode>S3_PUBLIC_URL\u003C\u002Fcode>\nactually resolves to the bucket from a browser.\u003C\u002Fp>\n\u003Cp>\u003Cstrong>Search returns nothing \u002F degraded.\u003C\u002Fstrong> If Meili is down the app falls back to a\nParse keyword scan — functional but not hybrid. Check \u003Ccode>sb-meili\u003C\u002Fcode> health and\n\u003Ccode>MEILI_URL\u003C\u002Fcode>. Run \u003Ccode>bun run search:setup\u003C\u002Fcode> then \u003Ccode>bun run reindex\u003C\u002Fcode>.\u003C\u002Fp>\n\u003Cp>\u003Cstrong>Live updates lag or arrive in bursts.\u003C\u002Fstrong> Your proxy is buffering SSE. Disable\nresponse buffering on \u003Ccode>\u002Fv1\u002Fevents\u003C\u002Fcode> (\u003Ccode>proxy_buffering off\u003C\u002Fcode> in nginx; Caddy is fine\nby default) and allow long read timeouts.\u003C\u002Fp>\n\u003Cp>\u003Cstrong>Meili rejects requests \u002F 403.\u003C\u002Fstrong> In production it needs a key: the app sends\n\u003Ccode>MEILI_API_KEY\u003C\u002Fcode>, or \u003Ccode>MEILI_MASTER_KEY\u003C\u002Fcode> when that is unset. A scoped key made for\nanother master key (the master key changed) is refused; run \u003Ccode>meili-key.mjs\u003C\u002Fcode>\nagain with the current one. With only the master key, make sure the same value\nis set for both \u003Ccode>sb-meili\u003C\u002Fcode> and the app.\u003C\u002Fp>\n\u003Cp>\u003Cstrong>Email never sends.\u003C\u002Fstrong> All three of \u003Ccode>MAILGUN_API_KEY\u003C\u002Fcode>, \u003Ccode>MAILGUN_DOMAIN\u003C\u002Fcode>,\n\u003Ccode>MAILGUN_FROM_EMAIL\u003C\u002Fcode> must be set, and \u003Ccode>MAILGUN_API_BASE_URL\u003C\u002Fcode> must match your\nMailgun region (EU vs US).\u003C\u002Fp>\n\u003Chr>\n\u003Ch2 id=\"security-checklist\">Security checklist\u003Ca class=\"heading-anchor\" href=\"#security-checklist\" aria-label=\"Link to this section\">#\u003C\u002Fa>\u003C\u002Fh2>\n\u003Cp>Before you expose a deployment:\u003C\u002Fp>\n\u003Cul>\n\u003Cli class=\"task-item\">Changed \u003Ccode>PARSE_MASTER_KEY\u003C\u002Fcode> from the default to a long random value.\u003C\u002Fli>\n\u003Cli class=\"task-item\">Changed \u003Ccode>MEILI_MASTER_KEY\u003C\u002Fcode> (especially if \u003Ccode>sb-meili\u003C\u002Fcode> has a public domain), and gave the app a scoped \u003Ccode>MEILI_API_KEY\u003C\u002Fcode>.\u003C\u002Fli>\n\u003Cli class=\"task-item\">Set \u003Ccode>SETUP_TOKEN\u003C\u002Fcode> before the first visit, so only you can create the first operator.\u003C\u002Fli>\n\u003Cli class=\"task-item\">Changed \u003Ccode>S3_SECRET_KEY\u003C\u002Fcode> (RustFS reuses it as its root credential if self-hosting).\u003C\u002Fli>\n\u003Cli class=\"task-item\">\u003Ccode>APP_URL\u003C\u002Fcode> is your real HTTPS origin; TLS terminated by Dokploy\u002Fproxy.\u003C\u002Fli>\n\u003Cli class=\"task-item\">Did \u003Cstrong>not\u003C\u002Fstrong> attach a public domain to \u003Ccode>sb-parse\u003C\u002Fcode> unless you need it.\u003C\u002Fli>\n\u003Cli class=\"task-item\">Set restrictive \u003Cstrong>allowed widget origins\u003C\u002Fstrong> on each project (the widget only\nloads from listed origins; anything else gets no token).\u003C\u002Fli>\n\u003Cli class=\"task-item\">Project \u003Cstrong>secrets (\u003Ccode>sk_…\u003C\u002Fcode>) live only on your backend\u003C\u002Fstrong> — never shipped to a\nbrowser. Rotate from the project page if one leaks.\u003C\u002Fli>\n\u003Cli class=\"task-item\">The dev overlay (\u003Ccode>docker-compose.dev.yml\u003C\u002Fcode>) and the \u003Ccode>parse-dashboard\u003C\u002Fcode>\nservice are \u003Cstrong>not\u003C\u002Fstrong> running in production. Use the bundled RustFS in prod\nvia \u003Ccode>--profile rustfs\u003C\u002Fcode> (not the dev overlay), so it isn't published on\n\u003Ccode>localhost\u003C\u002Fcode> with dev-default credentials.\u003C\u002Fli>\n\u003Cli class=\"task-item\">Backups configured for \u003Ccode>mongo-data\u003C\u002Fcode> and your S3 bucket.\u003C\u002Fli>\n\u003Cli class=\"task-item\">On AWS S3, \u003Ccode>S3_SET_PUBLIC_POLICY=false\u003C\u002Fcode> and the bucket policy scoped to\nexactly what attachment serving needs.\u003C\u002Fli>\n\u003Cli class=\"task-item\">\u003Ccode>ALLOW_PRIVATE_NETWORK\u003C\u002Fcode> is off unless something the panel calls really\nlives on your private network.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Chr>\n\u003Cp>Related docs: \u003Ca href=\"https:\u002F\u002Fgithub.com\u002Felrumo\u002Fhelp-bites-oss\u002Fblob\u002Fmain\u002Fdocs\u002FEDITIONS.md\" target=\"_blank\" rel=\"noopener\">Editions\u003C\u002Fa> · \u003Ca href=\"\u002Fdocs\u002Fconnect\">Connecting an app\u003C\u002Fa> ·\n\u003Ca href=\"https:\u002F\u002Fgithub.com\u002Felrumo\u002Fhelp-bites-oss\u002Fblob\u002Fmain\u002Fdocs\u002FKNOWLEDGE.md\" target=\"_blank\" rel=\"noopener\">Knowledge &amp; AI assistants\u003C\u002Fa> · API reference at \u003Ccode>APP_URL\u002F_docs\u002Fscalar\u003C\u002Fcode>.\u003C\u002Fp>\n",[6,10,13,16,19,23,26,29,32,35,38,41,44,47,50,53,56,59,62,65,68,71,74,77,80,83,86,89,92,95,97,100,103,106,109,112,115,118,121,124,127,130,133,136,139,142,145,148],{"id":7,"text":8,"level":9},"architecture","Architecture",2,{"id":11,"text":12,"level":9},"prerequisites","Prerequisites",{"id":14,"text":15,"level":9},"quick-start-local","Quick start (local)",{"id":17,"text":18,"level":9},"deploy-on-dokploy","Deploy on Dokploy",{"id":20,"text":21,"level":22},"1-create-the-application","1. Create the application",3,{"id":24,"text":25,"level":22},"2-set-environment-variables","2. Set environment variables",{"id":27,"text":28,"level":22},"3-attach-domains","3. Attach domains",{"id":30,"text":31,"level":22},"4-deploy","4. Deploy",{"id":33,"text":34,"level":9},"deploy-with-plain-docker-compose","Deploy with plain Docker Compose",{"id":36,"text":37,"level":22},"1-configure","1. Configure",{"id":39,"text":40,"level":22},"2-start-the-stack","2. Start the stack",{"id":42,"text":43,"level":22},"3-put-a-proxy-in-front","3. Put a proxy in front",{"id":45,"text":46,"level":22},"4-point-a-bucket-at-it","4. Point a bucket at it",{"id":48,"text":49,"level":9},"environment-reference","Environment reference",{"id":51,"text":52,"level":22},"core","Core",{"id":54,"text":55,"level":22},"parse--mongodb","Parse + MongoDB",{"id":57,"text":58,"level":22},"redis","Redis",{"id":60,"text":61,"level":22},"search-meilisearch","Search (Meilisearch)",{"id":63,"text":64,"level":22},"knowledge-embeddings","Knowledge embeddings",{"id":66,"text":67,"level":22},"ai-support-agents-auto-reply","AI support agents (auto-reply)",{"id":69,"text":70,"level":22},"error-tracking-glitchtip-or-bugsink","Error tracking (GlitchTip or Bugsink)",{"id":72,"text":73,"level":22},"s3-storage","S3 storage",{"id":75,"text":76,"level":22},"email-mailgun","Email (Mailgun)",{"id":78,"text":79,"level":22},"native-push-apns","Native push (APNs)",{"id":81,"text":82,"level":22},"agent-seeding-cli-only","Agent seeding (CLI only)",{"id":84,"text":85,"level":22},"script-only","Script-only",{"id":87,"text":88,"level":22},"example-production-env","Example production .env",{"id":90,"text":91,"level":9},"external-services","External services",{"id":93,"text":94,"level":22},"mongodb","MongoDB",{"id":96,"text":73,"level":22},"s3-storage-1",{"id":98,"text":99,"level":22},"meilisearch","Meilisearch",{"id":101,"text":102,"level":22},"llm-endpoint-ai-auto-reply","LLM endpoint (AI auto-reply)",{"id":104,"text":105,"level":22},"mailgun-email","Mailgun (email)",{"id":107,"text":108,"level":22},"apns-native-push","APNs (native push)",{"id":110,"text":111,"level":9},"first-run","First run",{"id":113,"text":114,"level":9},"operations","Operations",{"id":116,"text":117,"level":22},"schema-migrations","Schema migrations",{"id":119,"text":120,"level":22},"backups","Backups",{"id":122,"text":123,"level":22},"rebuilding-the-search-index","Rebuilding the search index",{"id":125,"text":126,"level":22},"scaling-out","Scaling out",{"id":128,"text":129,"level":22},"health-checks","Health checks",{"id":131,"text":132,"level":22},"logs","Logs",{"id":134,"text":135,"level":9},"verifying-a-deployment","Verifying a deployment",{"id":137,"text":138,"level":9},"upgrades","Upgrades",{"id":140,"text":141,"level":22},"upgrading-to-workspaces","Upgrading to workspaces",{"id":143,"text":144,"level":9},"licensing-notes-for-the-services","Licensing notes for the services",{"id":146,"text":147,"level":9},"troubleshooting","Troubleshooting",{"id":149,"text":150,"level":9},"security-checklist","Security checklist",1792677543058]