diff --git a/.env.example b/.env.example index 3f62ac7..5899e50 100644 --- a/.env.example +++ b/.env.example @@ -25,6 +25,10 @@ FORGECRM_JWT_SECRET= FORGECRM_ENCRYPTION_KEY= # Any random string; you paste the same value into Meta's webhook setup. META_WEBHOOK_VERIFY_TOKEN= +# Encrypts Instagram tokens and the Social section's AI keys. Any long random +# string: openssl rand -hex 32. Saving an Instagram credential is refused while +# it is empty. ⚠ Same rule as FORGECRM_ENCRYPTION_KEY: back it up, never change it. +SOCIAL_ENCRYPTION_KEY= # ─── Database ──────────────────────────────────────────────────────────────── # With the bundled compose stack, leave the host as the service name. diff --git a/CHANGELOG.md b/CHANGELOG.md index 3265d9f..dc07412 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,29 @@ re-run and leaves `.env` and your database alone. Pin to a specific release with ## [Unreleased] +## [3.1.1] — 2026-10-26 + +### Fixed + +- **Instagram could not be connected on a fresh install.** The Social section refuses to save + Instagram credentials without `SOCIAL_ENCRYPTION_KEY`, and nothing generated it, so every new install + needed a hand edit of `.env` that no page mentioned. The installer and `generate-secrets.sh` now + generate it with the other secrets; re-run `./install.sh` on an existing install to fill it in. + An existing value is never changed. +- The installer's closing "Next steps" named `Admin Settings → WhatsApp Accounts`, a menu that no + longer exists. It now says `Settings → WhatsApp → Accounts`, and adds where to connect Instagram. +- **The installer hung forever, silently, on Alpine Linux** (and in `docker:dind`) right after + "install name". BusyBox's `lsof` ignores its arguments and reports success, so every port looked + taken and the search for a free one never ended. It now uses `netstat` there, and stops with a clear + message if a hundred ports in a row read as busy. + +### Documentation + +- **The printable installation guide is rewritten for this release.** The previous PDF dated from + before 1.0.0 and sent people to Admin Settings → Domain, which 3.0.0 removed. New: the download + bundle, a chapter on connecting Instagram, and domain checks that use `curl`. Its HTML source is now + in `docs/guide/`, and `scripts/make-guide.sh` rebuilds the PDF from it. + ## [3.1.0] — 2026-10-26 **Fresh installs work again.** Every new install of 3.0.0 stopped while pulling images, because the @@ -400,7 +423,8 @@ Published as multi-architecture images (`linux/amd64` and `linux/arm64`) at - Playwright end-to-end tests are not run in CI — they need the full stack and browser binaries, so run them locally against a live install. -[Unreleased]: https://github.com/Forgemind-git/ForgeGrowth-OSS/compare/v3.1.0...HEAD +[Unreleased]: https://github.com/Forgemind-git/ForgeGrowth-OSS/compare/v3.1.1...HEAD +[3.1.1]: https://github.com/Forgemind-git/ForgeGrowth-OSS/compare/v3.1.0...v3.1.1 [3.1.0]: https://github.com/Forgemind-git/ForgeGrowth-OSS/compare/v3.0.0...v3.1.0 [3.0.0]: https://github.com/Forgemind-git/ForgeGrowth-OSS/compare/v1.0.1...v3.0.0 [2.0.0]: https://github.com/Forgemind-git/ForgeGrowth-OSS/compare/v1.0.1...v2.0.0 diff --git a/README.md b/README.md index 8984611..757383a 100644 --- a/README.md +++ b/README.md @@ -142,8 +142,8 @@ follow-up?"*. ### Already on a server with Docker? One command. ```bash -bash -c "$(curl -fsSL https://raw.githubusercontent.com/Forgemind-git/ForgeGrowth-OSS/v3.1.0/scripts/install.sh)" -- \ - --version v3.1.0 --domain crm.example.com --yes +bash -c "$(curl -fsSL https://raw.githubusercontent.com/Forgemind-git/ForgeGrowth-OSS/v3.1.1/scripts/install.sh)" -- \ + --version v3.1.1 --domain crm.example.com --yes ``` Swap `crm.example.com` for your domain. That single command is the entire install: it pulls the @@ -238,7 +238,7 @@ Two ways in, same result. Pick either. Everything in one file, with a folder for each operating system. -1. Download **[`forge-growth-v3.1.0.zip`](https://github.com/Forgemind-git/ForgeGrowth-OSS/raw/main/dist/forge-growth-v3.1.0.zip)** +1. Download **[`forge-growth-v3.1.1.zip`](https://github.com/Forgemind-git/ForgeGrowth-OSS/raw/main/dist/forge-growth-v3.1.1.zip)** (~145 KB). It is in this repository under [`dist/`](./dist), and also attached to the [releases page](https://github.com/Forgemind-git/ForgeGrowth-OSS/releases/latest) — two hosts, so one of them being down or rate-limiting you is not the end of the install. @@ -264,8 +264,8 @@ bash -c "$(curl -fsSL https://raw.githubusercontent.com/Forgemind-git/ForgeGrowt **On a server with a real domain**, pinned to a release and asking nothing at all: ```bash -bash -c "$(curl -fsSL https://raw.githubusercontent.com/Forgemind-git/ForgeGrowth-OSS/v3.1.0/scripts/install.sh)" -- \ - --version v3.1.0 --domain crm.example.com +bash -c "$(curl -fsSL https://raw.githubusercontent.com/Forgemind-git/ForgeGrowth-OSS/v3.1.1/scripts/install.sh)" -- \ + --version v3.1.1 --domain crm.example.com ``` --- diff --git a/SECURITY.md b/SECURITY.md index a7e90f9..d4c0159 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -17,7 +17,7 @@ re-run and leaves `.env` and your database alone: ```bash ./install.sh # move to the current release -./install.sh --version v3.1.0 # or pin to a specific one +./install.sh --version v3.1.1 # or pin to a specific one ``` ## Reporting a vulnerability diff --git a/backend/package-lock.json b/backend/package-lock.json index 164289b..c9722d8 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -1,12 +1,12 @@ { "name": "forgecrm-backend", - "version": "3.1.0", + "version": "3.1.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "forgecrm-backend", - "version": "3.1.0", + "version": "3.1.1", "license": "MIT", "dependencies": { "@anthropic-ai/sdk": "^0.99.0", diff --git a/backend/package.json b/backend/package.json index 113cea9..992448a 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,6 @@ { "name": "forgecrm-backend", - "version": "3.1.0", + "version": "3.1.1", "license": "MIT", "description": "ForgeChat backend API for WhatsApp chat viewer", "main": "src/index.js", diff --git a/dist/forge-growth-v3.1.0.zip b/dist/forge-growth-v3.1.1.zip similarity index 50% rename from dist/forge-growth-v3.1.0.zip rename to dist/forge-growth-v3.1.1.zip index b268f75..f193ba8 100644 Binary files a/dist/forge-growth-v3.1.0.zip and b/dist/forge-growth-v3.1.1.zip differ diff --git a/docs/ForgeGrowth-Installation-Guide.pdf b/docs/ForgeGrowth-Installation-Guide.pdf index 21d2e1e..3338509 100644 Binary files a/docs/ForgeGrowth-Installation-Guide.pdf and b/docs/ForgeGrowth-Installation-Guide.pdf differ diff --git a/docs/guide/installation-guide.html b/docs/guide/installation-guide.html new file mode 100644 index 0000000..50b7df4 --- /dev/null +++ b/docs/guide/installation-guide.html @@ -0,0 +1,593 @@ + + + + +Forge Growth — Installation Guide + + + + + +
+
FORGE GROWTH
+

Installation
Guide

+

Every way to install it — a server from published images or the download bundle, a + server from source, a Mac or Windows laptop — then attaching a domain and connecting WhatsApp and + Instagram. Every command can be copied as-is.

+ + + +
Repositorygithub.com/Forgemind-git/ForgeGrowth-OSS
Releasev3.1.1
+
+ +

Contents

+ + + + + + + + + + + + +
0Before you start — what the machine needs
1Server, from published images — the usual route; nothing is compiled
2Attaching a domain — both kinds of server
3Connecting WhatsApp — Meta credentials and the webhook
4Connecting Instagram — the Social section
5Server, from source — git clone, build locally
6Local install — macOS
7Local install — Windows
8ngrok — WhatsApp against a laptop
9Upgrading, backup, removal
10When it goes wrong — real errors and what they mean
+ +

Which sections do I need?

+ + + + + + + +
SituationRead
Installing on a customer's server1 → 2a → 3 and/or 4
Server already runs Traefik, nginx or another site1 → 2b → 3 and/or 4
You intend to change the code5
Trying it on your own laptop6 or 7, then 8
Already installed, moving to a newer version9
+ +
One rule that saves the most time +The directory you are standing in is what selects the install. Every command in this guide is run +from the install directory, because one machine can hold several installs and they are told apart by +nothing else.
+ + +

0Before you start

+

Everything runs in containers. You do not install Node, PostgreSQL, Redis or MinIO on the machine.

+ + + + + + + +
RequirementDetail
Dockerwith the Compose v2 plugin. docker compose version must work — note the space, not docker-compose
ShellLinux, macOS, or Windows through WSL2 / Git Bash. There is no PowerShell installer
RAM2 GB only if building from source (section 5). The published-image route has no build step
Disk~3 GB. The first download is about 350 MB
PortsOne for the web UI (8080 by default; the installer moves to the next free one). Ports 80 and 443 as well, if this machine is to obtain its own HTTPS certificate
+ +

Check the machine is ready

+
docker compose version
+docker run --rm hello-world
+

If the second command prints a greeting, Docker works. If either fails, fix that before continuing — +every failure after this point will otherwise be blamed on the installer.

+ +

What gets created

+

The installer makes one directory — forge-growth — holding a handful of small files and no source code:

+
forge-growth/
+  .env                   your settings and every generated secret
+  .forgegrowth-install   which version this install is pinned to
+  docker-compose.yml     downloaded; overwritten on every upgrade
+  caddy/Caddyfile        only used when this machine handles HTTPS itself
+  up.sh  down.sh         start and stop
+  install.sh             a copy of the installer, for upgrades
+

Your data is not in that folder. It lives in Docker volumes, which is why ./down.sh is safe +and docker compose down -v destroys everything:

+
docker volume ls | grep forgegrowth
+ + +

1Server, from published images

+

The normal way to install on a customer's machine. Nothing is cloned, nothing is compiled, and every secret is generated for you.

+ +

The machine owns ports 80 and 443, and DNS already points at it

+
bash -c "$(curl -fsSL https://raw.githubusercontent.com/Forgemind-git/ForgeGrowth-OSS/v3.1.1/scripts/install.sh)" -- \
+  --version v3.1.1 --domain crm.example.com --yes
+

That is the whole install. It pulls the images, generates every secret, obtains a Let's Encrypt +certificate, starts the stack, fetches the public URL to prove it answers, and prints the admin +password once. --yes means it asks nothing.

+

Both halves of the pin matter: the URL fixes which script runs, and --version +fixes what that script then downloads and pulls. The choice is remembered, so a later bare re-run +cannot drift onto a newer build.

+ +

Something else already owns 80 and 443 (Traefik, nginx, another site)

+
bash -c "$(curl -fsSL https://raw.githubusercontent.com/Forgemind-git/ForgeGrowth-OSS/v3.1.1/scripts/install.sh)" -- \
+  --version v3.1.1 --url https://crm.example.com --port 8080 --yes
+

Use --url, not --domain. It tells the installer HTTPS is terminated +elsewhere, so it does not start a second web server to fight over the ports. Then follow section 2b.

+ +

No domain yet — reach it by IP

+
bash -c "$(curl -fsSL https://raw.githubusercontent.com/Forgemind-git/ForgeGrowth-OSS/v3.1.1/scripts/install.sh)" -- \
+  --version v3.1.1 --url http://203.0.113.10:8080 --yes
+
A plain-HTTP address is fine to start with, but it cannot connect WhatsApp or Instagram +Meta refuses any webhook address that is not https://. Add a domain later (section 2) — +no reinstall is needed.
+ +

Or: the download bundle

+

For handing an install to someone who will not paste a command from the internet. Download +forge-growth-v3.1.1.zip from the releases page, unzip it, open the folder for the +operating system (linux, macos or windows), then:

+
chmod +x *.sh
+./install.sh
+

The installer continues in that folder — what you unzipped is the install — and the bundle +stays on its own version. It still needs the internet the first time, to pull the images.

+ +

All the flags

+ + + + + + + + + + + + +
FlagMeaning
--domain <host>Serve HTTPS here, certificate included. Needs ports 80 + 443 free
--url <origin>Public address when something else terminates HTTPS. Include the port if people use one
--port <n>Host port for the UI (default 8080, or the next free one)
--version <ref>Pin the downloaded files and the image tag together. v3.1.1 and 3.1.1 both work
--dir <path>Install here instead of ./forge-growth
--admin-email <addr>First admin account (default admin@<your domain>)
--admin-password <pw>First admin password (default: generated and printed)
--tls-email <addr>Certificate contact. internal self-signs, for a domain with no public DNS
--sourceBuild from a checkout rather than pulling images
--yes, -yNever ask. With no address flag: this machine's public address, or localhost
+ +

Sign in

+

The installer prints the URL, the admin email and the password. To read the password again:

+
cd forge-growth
+grep '^BOOTSTRAP_ADMIN_PASSWORD=' .env
+ +

Managing the stack afterwards

+ + + + + + + + +
CommandDoes
./up.shStart, then verify the public URL really answers
./down.shStop. Keeps all data
./install.shUpgrade in place
docker compose logs -f backendFollow the backend log
docker compose lsList every install on this machine
docker compose down -vDeletes all data. Take a dump first
+
Use up.sh and down.sh, not docker compose up -d +They perform the only two checks that catch the failures this software actually suffers: a site that +returns 404 while every container reports healthy, and a second folder quietly adopting this +install's database. Raw Compose skips both.
+ + +

2Attaching a domain

+

Which instructions apply depends on one thing: does this install own ports 80 and 443, or does something else? The installer worked it out and recorded the answer.

+
cd forge-growth
+grep '^TLS_MODE=' .env
+ + + + +
ValueMeaningFollow
TLS_MODE=caddyThis install owns the ports and obtains its own certificate2a
TLS_MODE=proxySomething else owns them; that thing must route the domain here2b
+ +

2a — This install owns ports 80 and 443

+
    +
  1. Point the domain's A record at the server's IP address, and wait until it resolves.
  2. +
  3. Re-run the installer with the domain. Your data and secrets are kept: +
    cd forge-growth
    +./install.sh --domain crm.example.com
  4. +
  5. The bundled Caddy obtains the certificate, and the installer confirms it works before reporting success.
  6. +
+
Behind Cloudflare's orange cloud? +The certificate challenge stops at the CDN and never reaches this machine. Switch the record to +"DNS only" while the certificate is issued, or use 2b with the CDN supplying the certificate.
+ +

2b — Something else owns the ports

+

Your existing proxy must accept the hostname and forward it to this install's web +container on port 80. First tell the install its public address, so the API accepts requests from it:

+
cd forge-growth
+./install.sh --url https://crm.example.com
+

Traefik, running in Docker on the same host. Create an overlay file outside the install +directory — docker-compose.yml is re-downloaded on every upgrade, so labels added there are +eventually lost:

+
services:
+  web:
+    networks: [default, proxy_default]
+    labels:
+      - traefik.enable=true
+      - traefik.docker.network=proxy_default
+      # ONE service, named explicitly on every router below.
+      - traefik.http.services.forgegrowth.loadbalancer.server.port=80
+      - "traefik.http.routers.forgegrowth.rule=Host(`crm.example.com`)"
+      - traefik.http.routers.forgegrowth.entrypoints=websecure
+      - traefik.http.routers.forgegrowth.tls=true
+      - traefik.http.routers.forgegrowth.tls.certresolver=myresolver
+      - traefik.http.routers.forgegrowth.service=forgegrowth
+networks:
+  proxy_default:
+    external: true
+

Save it as /opt/forge-growth-proxy.yml, replace proxy_default with the network +Traefik is on, and myresolver with your certificate resolver. Find the network with:

+
docker inspect <traefik-container> \
+  --format '{{range $k,$v := .NetworkSettings.Networks}}{{$k}}{{"\n"}}{{end}}'
+

Wire it in through .env — never an exported shell variable — then restart:

+
cd forge-growth
+echo 'COMPOSE_FILE=docker-compose.yml:/opt/forge-growth-proxy.yml' >> .env
+./up.sh
+
Two mistakes here cost an afternoon each +One service per router. Declaring a loadbalancer.server.port for each router makes +Traefik unable to choose, so it discards every router on that container and each hostname returns 404. +No certresolver behind a CDN. If the domain is proxied by Cloudflare or similar, the ACME +challenge can never succeed, and its retries are rate-limited per account — enough to stop every other +domain on the server from renewing. Use tls=true with no resolver and let the CDN supply the +certificate.
+ +

nginx, running outside Docker

+
server {
+    listen 443 ssl;
+    server_name crm.example.com;
+    ssl_certificate     /etc/letsencrypt/live/crm.example.com/fullchain.pem;
+    ssl_certificate_key /etc/letsencrypt/live/crm.example.com/privkey.pem;
+    client_max_body_size 100M;          # media uploads; nginx defaults to 1M
+    location / {
+        proxy_pass http://127.0.0.1:8080;      # WEB_PORT from .env
+        proxy_set_header Host              $host;
+        proxy_set_header X-Forwarded-Proto $scheme;
+        proxy_set_header X-Forwarded-Host  $host;
+        proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
+        proxy_http_version 1.1;
+        proxy_set_header Upgrade    $http_upgrade;
+        proxy_set_header Connection "upgrade";
+        proxy_read_timeout 600s;
+    }
+}
+ +

2c — Prove it actually works

+

A page that loads in your browser shows the interface was served. It does not show the API behind it +is reachable — and that is the half which breaks sign-in. Check both:

+
curl -s -o /dev/null -w '%{http_code}\n' https://crm.example.com/
+200
+curl -s https://crm.example.com/api/auth/me
+{"error":"Unauthorized"}
+ + + + + + + +
ResultMeaning
200, then {"error":"Unauthorized"}Working. The proxy reaches this install, and the API answers
404 on the first commandThe proxy has no route for the domain — usually a typo in the host rule, or COMPOSE_FILE missing from .env
An HTML page from the second commandSomething else is answering — another app, or another install on this machine
A certificate errorRouting is right; the certificate is missing or still being issued
Could not resolve hostDNS
+

./up.sh runs the first check for you every time it starts the stack.

+ + +

3Connecting WhatsApp

+

Attach a WhatsApp Business number. This needs an https:// address, so finish section 2 first.

+ +

Step 1 — collect the values from Meta

+

At developers.facebook.com → your app → WhatsApp → API Setup:

+ + + + + + +
ValueWhere
Phone number IDAPI Setup, under the number
WhatsApp Business Account IDSame screen
App IDApp dashboard header
Access tokenThe one on API Setup expires in 24 hours — create a System User token in Business Settings for anything permanent
+ +

Step 2 — add the number in the app

+

Settings → WhatsApp → Accounts → Add account. Paste the display number (digits only, no ++ and no spaces), the Phone Number ID, the WABA ID, the App ID and the token. The token is +encrypted at rest with AES-256-GCM.

+ +

Step 3 — read your webhook verify token

+
cd forge-growth
+grep '^META_WEBHOOK_VERIFY_TOKEN=' .env
+ +

Step 4 — test the handshake before touching Meta

+

This is the exact request Meta sends when you press Verify and save. Running it yourself turns a +slow, silent failure into a one-second answer:

+
cd forge-growth
+TOKEN=$(grep '^META_WEBHOOK_VERIFY_TOKEN=' .env | cut -d= -f2-)
+curl -s "https://crm.example.com/api/webhook/whatsapp?hub.mode=subscribe&hub.verify_token=$TOKEN&hub.challenge=42"
+42
+

If it prints 42, Meta will verify successfully. Anything else — fix it here first.

+ +

Step 5 — point Meta at it

+

WhatsApp → Configuration → Webhook → Edit:

+ + + + +
FieldValue
Callback URLhttps://crm.example.com/api/webhook/whatsapp
Verify tokenthe META_WEBHOOK_VERIFY_TOKEN from step 3
+

Save, then under Webhook fields → Manage, subscribe to messages. That single field covers +messages, delivery statuses, echoes and template events.

+
The path is /api/webhook/whatsapp +Not /api/webhook. A mismatch gives a silent 404 at verification time and no useful message from Meta.
+ +

Step 6 — confirm

+ +
One callback address per WhatsApp Business Account +Every phone number under one WABA shares a single callback URL. Pointing it at a new install takes +inbound messages away from whatever had it before — instantly and with no warning. If you are moving a +live number, plan it as a cutover, not a configuration change.
+ + +

4Connecting Instagram

+

The Social tab — Instagram DM automations, AI agents, the inbox and analytics — runs on a Meta app of its own. This also needs an https:// address.

+ +

Step 1 — check the encryption key exists

+
cd forge-growth
+grep -c '^SOCIAL_ENCRYPTION_KEY=.' .env
+1
+

The installer generates it. If this prints 0 — an install made before 3.1.1 — re-run +./install.sh, which fills it in and keeps everything else. Without it, the app refuses to +save Instagram credentials.

+ +

Step 2 — register these addresses in your Meta app

+ + + + + +
SettingValue
Instagram OAuth redirecthttps://crm.example.com/api/social/instagram/oauth/callback
Webhook callback (Instagram)https://crm.example.com/api/social/instagram/webhook
Privacy policy URLYour own business's privacy policy page
+ +

Step 3 — enter the app credentials and connect

+

Settings → Instagram → Connection → App credentials. Enter the App ID and secret, the +Instagram App ID and secret, and a webhook verify token of your choosing — paste the same token into +the app's webhook setup. Then connect each Instagram account from the same screen.

+ +

Step 4 — turn on syncing

+

Posts, insights and comments sync in the background, which is off until you switch it on:

+
cd forge-growth
+echo 'SOCIAL_JOBS_ENABLED=1' >> .env
+./up.sh
+ +

Optional

+ + + +

5Server, from source

+

Use this only if you intend to change the code. It builds the images on the machine, which needs about 2 GB of free memory and a few minutes.

+
git clone https://github.com/Forgemind-git/ForgeGrowth-OSS.git
+cd ForgeGrowth-OSS
+./scripts/install.sh --domain crm.example.com
+

The same flags apply as in section 1. Without a domain:

+
./scripts/install.sh --yes
+

Managing a source install

+ + + + + + + +
CommandDoes
./scripts/up.shStart and verify the public URL
./scripts/up.sh --buildRebuild from the checkout, then start
./scripts/down.shStop, keeping data
git pull && ./scripts/install.shUpgrade
./scripts/uninstall.sh --purgeRemove, including all data
+
A source install does not upgrade by pulling images +The images are built locally. Changes reach the running site only when you rebuild. A +git pull alone moves the files on disk and nothing else, which looks exactly like a feature +failing to arrive.
+

Running the tests

+
cd backend  && REQUIRE_DB=1 npm test && npm run lint
+cd frontend && npm run test:unit && npm run lint
+

Without REQUIRE_DB=1, the database-backed third of the backend tests quietly skip when no database is reachable.

+ + +

6Local install — macOS

+

Works on both Apple Silicon and Intel. The published images carry linux/amd64 and linux/arm64, so nothing is emulated.

+

Step 1 — Docker

+

Install Docker Desktop, OrbStack or Colima, start it, then confirm:

+
docker compose version
+

Step 2 — install

+
cd ~
+bash -c "$(curl -fsSL https://raw.githubusercontent.com/Forgemind-git/ForgeGrowth-OSS/v3.1.1/scripts/install.sh)" -- \
+  --version v3.1.1 --yes
+

--yes means "localhost, ask me nothing". It creates ~/forge-growth and serves on +port 8080 — or the next free port, which it tells you.

+

Step 3 — sign in

+
cd ~/forge-growth
+grep -E '^(BOOTSTRAP_ADMIN_EMAIL|BOOTSTRAP_ADMIN_PASSWORD)=' .env
+

Open the URL it printed and sign in with those.

+

Everyday commands

+
cd ~/forge-growth
+./up.sh                          # start
+./down.sh                        # stop, keeping data
+docker compose logs -f backend   # follow the log
+open ~/forge-growth              # reveal in Finder (Cmd+Shift+. shows .env)
+ + +

7Local install — Windows

+

The installer is a shell script, so it runs inside a Unix shell. WSL2 is the recommended route; Git Bash also works.

+

Step 1 — install WSL2

+

In PowerShell as Administrator, then reboot when prompted:

+
wsl --install
+

Open Ubuntu from the Start menu and create your Linux user.

+

Step 2 — Docker Desktop

+

Install Docker Desktop, then enable Settings → Resources → WSL integration for your distro. Confirm from inside the Ubuntu terminal:

+
docker compose version
+

Step 3 — install, from the Ubuntu terminal

+
cd ~
+bash -c "$(curl -fsSL https://raw.githubusercontent.com/Forgemind-git/ForgeGrowth-OSS/v3.1.1/scripts/install.sh)" -- \
+  --version v3.1.1 --yes
+
Install into the Linux filesystem, not the Windows drive +Use ~/forge-growth. A location under /mnt/c/... is dramatically slower and confuses file-watching. cd ~ first, every time.
+

Step 4 — sign in

+
cd ~/forge-growth
+grep -E '^(BOOTSTRAP_ADMIN_EMAIL|BOOTSTRAP_ADMIN_PASSWORD)=' .env
+

Open the printed URL in your normal Windows browser — WSL2 forwards localhost.

+

Reaching the files from Windows

+

Run explorer.exe . from the Ubuntu terminal, or type this in Explorer's address bar:

+
\\wsl$\Ubuntu\home\<your-user>\forge-growth
+ + +

8ngrok — WhatsApp against a laptop

+

Meta will not accept a localhost or plain-HTTP callback, so a laptop needs a public HTTPS address borrowed from a tunnel.

+
Use a Meta test number, never a live one +Every Meta app includes a free test number on its own WhatsApp Business Account, so its callback is +independent of any production number. A laptop also sleeps, and inbound messages arriving then are lost +rather than queued.
+

Step 1 — install and authenticate

+
brew install ngrok                       # macOS; on Windows install it inside WSL2
+ngrok config add-authtoken <your-token>  # dashboard.ngrok.com → Your Authtoken
+

Step 2 — claim a static domain

+

dashboard.ngrok.com → Domains → New Domain. The free tier includes one. Without it the URL changes on +every restart and the Meta webhook has to be re-pointed each time.

+

Step 3 — start the tunnel

+

Use the port your install actually reported, and leave that terminal running:

+
ngrok http --url=your-name.ngrok-free.app 8080
+

Step 4 — confirm the tunnel reaches the app

+
curl -s -o /dev/null -w '%{http_code}\n' https://your-name.ngrok-free.app/
+200
+

Step 5 — run Meta's handshake yourself

+
cd ~/forge-growth
+TOKEN=$(grep '^META_WEBHOOK_VERIFY_TOKEN=' .env | cut -d= -f2-)
+curl -s "https://your-name.ngrok-free.app/api/webhook/whatsapp?hub.mode=subscribe&hub.verify_token=$TOKEN&hub.challenge=42"
+42
+

This works before any WhatsApp account is configured — the check falls back to the token in +.env — which separates "my tunnel is wrong" from "my credentials are wrong".

+

Step 6 — keep using localhost for the interface

+

The tunnel is for Meta. Keep opening the app at the localhost address the installer printed.

+

Step 7 — configure Meta and test

+ + + + + +
FieldValue
Callback URLhttps://your-name.ngrok-free.app/api/webhook/whatsapp
Verify tokenthe value from step 5
Subscribe tomessages
+

Add your own phone under API Setup → To → Manage phone number list; a test number only messages +people on that list. Then watch every delivery arrive in ngrok's inspector at +http://127.0.0.1:4040 — it shows each request Meta makes with the full body, and lets you replay them.

+
Things that will bite +Closing the ngrok terminal kills the tunnel silently. The token on Meta's API Setup screen expires +after 24 hours — when inbound stops for no apparent reason, this is usually why.
+ + +

9Upgrading, backup, removal

+

Upgrading

+

Re-running the installer is the upgrade. From the install directory:

+
cd forge-growth
+./install.sh                    # stays on the version it is pinned to
+./install.sh --version v3.1.1   # move deliberately, and stay there
+

It re-downloads the compose file and scripts, pulls the images, applies any new database migrations +at start-up, and restarts. .env is never overwritten, and secrets are kept. For a source +install: git pull && ./scripts/install.sh.

+
Downgrading is not supported +Migrations run forward at start-up, so pinning back to an older release runs old code against a newer +schema. Take a dump first.
+ +

Backup — the things you cannot recreate

+
cd forge-growth
+grep -E '^(FORGECRM_ENCRYPTION_KEY|SOCIAL_ENCRYPTION_KEY|POSTGRES_PASSWORD)=' .env
+
Store these off the server, with the database +The two encryption keys are the only things that decrypt stored Meta, Google and Instagram credentials. +POSTGRES_PASSWORD is applied only when the database is first created, so a regenerated +.env cannot open an existing database. Lose any of them and the failure names the wrong +thing — an authentication error that reads like a network problem.
+

Database dump

+
cd forge-growth
+docker compose exec -T postgres pg_dump -U forgegrowth forgegrowth \
+  | gzip > backup-$(date +%F).sql.gz
+

Removal

+
cd forge-growth
+docker compose down -v        # stops and DELETES all data
+cd .. && rm -rf forge-growth
+ + +

10When it goes wrong

+ +

"pull access denied for minio/minio"

+

An install from before 3.1.0. The official MinIO image has been removed from Docker Hub; 3.1.0 and +later use a maintained build of the same server. Move to a current release — the Media Library is kept:

+
./install.sh --version v3.1.1
+ +

"no matching manifest for linux/arm64/v8"

+

An image with no build for your processor. Compose pulls concurrently and reports one failure against +every service, so the message names postgres, redis, minio and +caddy — all innocent. Check the two images this project publishes:

+
docker manifest inspect ghcr.io/forgemind-git/forgegrowth-web:latest | grep architecture
+

To carry on regardless, run everything under emulation: export DOCKER_DEFAULT_PLATFORM=linux/amd64.

+ +

"Secrets cannot be saved yet: the server has no SOCIAL_ENCRYPTION_KEY"

+

Instagram on an install made before 3.1.1. Re-run ./install.sh; it generates the key and changes nothing else.

+ +

It logs me out on every refresh, or says Unauthorized when I change tab

+

The login cookie is marked Secure and the site is being served over http://, so +the browser silently discards it. Reach the site over https://. If HTTPS is terminated by a +proxy this stack cannot see, set the flag explicitly:

+
cd forge-growth
+echo 'COOKIE_SECURE=true' >> .env
+./up.sh
+ +

Every container is healthy but the site returns 404

+

Nothing is routing the domain to the install. From inside the stack nothing is wrong, which is why +container health cannot detect it. Check that COMPOSE_FILE is in .env, then run the checks in section 2c.

+ +

"port 8080 is in use"

+

Not an error — the installer moved to the next free port and told you which. Find what holds a port:

+
lsof -nP -iTCP:8080 -sTCP:LISTEN     # macOS
+ss -lntp | grep 8080                 # Linux
+ +

A container keeps restarting

+

Usually a memory ceiling set too low — a container that reaches its limit is killed and restarted, +which looks like a container that never finishes starting.

+
cd forge-growth
+docker compose ps
+docker compose logs --tail 50 backend
+

Raise the ceiling in .env — BACKEND_MEM_LIMIT first; a busy install wants 1g.

+ +

A second install adopted the first one's database

+

Installs on one machine are separated by their project name and nothing else. Check which directory owns which:

+
docker compose ls
+grep '^COMPOSE_PROJECT_NAME=' forge-growth/.env
+

up.sh refuses to start when another directory already owns this install's name — the reason to use it rather than raw Compose.

+ +

Useful one-liners

+
cd forge-growth
+grep '^BOOTSTRAP_ADMIN_PASSWORD=' .env    # admin password
+grep '^META_WEBHOOK_VERIFY_TOKEN=' .env   # WhatsApp webhook verify token
+grep '^TLS_MODE=' .env                    # caddy = owns 80/443, proxy = does not
+grep '^WEB_PORT=' .env                    # which port the UI is on
+docker compose ps                         # container states
+docker compose logs -f backend            # follow the backend
+docker compose ls                         # every install on this machine
+docker volume ls | grep forgegrowth       # where the data lives
+ + + diff --git a/docs/install.md b/docs/install.md index 95c5d91..648c855 100644 --- a/docs/install.md +++ b/docs/install.md @@ -14,8 +14,9 @@ New here? [`README.md`](../README.md) has the short version. This page is the fu > Installing on someone else's machine, or walking a non-technical person through it? > [`ForgeGrowth-Installation-Guide.pdf`](./ForgeGrowth-Installation-Guide.pdf) is the printable -> version — every route end to end, plus attaching a domain, connecting WhatsApp, and what the -> confusing errors actually mean. +> version — every route end to end, plus attaching a domain, connecting WhatsApp and Instagram, and +> what the confusing errors actually mean. Its source is [`guide/installation-guide.html`](./guide/installation-guide.html); +> `./scripts/make-guide.sh` regenerates it. ## Contents @@ -111,7 +112,7 @@ Download **`forge-growth-.zip`** from the [releases page](https://github.com/Forgemind-git/ForgeGrowth-OSS/releases/latest) — around 130 KB. ``` -forge-growth-v3.1.0/ +forge-growth-v3.1.1/ ├── READ-ME-FIRST.txt which folder to open ├── linux/ │ ├── START-HERE.md the steps, for this OS only @@ -129,7 +130,7 @@ differs. Pick the one for your OS and ignore the other two. ### Use it ```bash -cd forge-growth-v3.1.0/linux # or macos, or windows +cd forge-growth-v3.1.1/linux # or macos, or windows chmod +x *.sh ./install.sh ``` @@ -144,7 +145,7 @@ what you extracted *is* the install. | | | |---|---| -| **Pinned to its own version** | The bundled `.forgegrowth-install` names the version, so the first run cannot silently re-fetch `main`. A zip named v3.1.0 stays on v3.1.0 | +| **Pinned to its own version** | The bundled `.forgegrowth-install` names the version, so the first run cannot silently re-fetch `main`. A zip named v3.1.1 stays on v3.1.1 | | **Complete** | The file list is derived from `install.sh`'s own `fetch` calls and verified at build time, so it cannot fall behind the installer | | **Not a source checkout** | No `backend/Dockerfile` or `supabase/migrations`, so the installer correctly picks images mode and never tries to build | @@ -157,7 +158,7 @@ what you extracted *is* the install. From a checkout: ```bash -./scripts/make-bundle.sh v3.1.0 # writes dist/forge-growth-v3.1.0.zip +./scripts/make-bundle.sh v3.1.1 # writes dist/forge-growth-v3.1.1.zip ./scripts/make-bundle.sh # version from the current git tag ``` @@ -182,12 +183,12 @@ It asks one question — the address people will use — and derives everything What you want on a server you are handing to somebody: ```bash -bash -c "$(curl -fsSL https://raw.githubusercontent.com/Forgemind-git/ForgeGrowth-OSS/v3.1.0/scripts/install.sh)" -- \ - --version v3.1.0 --domain crm.example.com +bash -c "$(curl -fsSL https://raw.githubusercontent.com/Forgemind-git/ForgeGrowth-OSS/v3.1.1/scripts/install.sh)" -- \ + --version v3.1.1 --domain crm.example.com ``` **Both halves of the pinned form matter.** The URL fixes which *script* runs; `--version` fixes what -that script then downloads and pulls. Pin only one and you get a v3.1.0 script that fetches `main`. +that script then downloads and pulls. Pin only one and you get a v3.1.1 script that fetches `main`. Give it a domain whose DNS already points at the machine and it obtains a Let's Encrypt certificate on the way up, then **verifies the certificate actually works** before reporting success. @@ -271,7 +272,7 @@ pressing return. To skip them: | `--domain ` | serve HTTPS on this domain, certificate and renewal included | | `--tls-email ` | certificate contact (default: the admin email; `internal` self-signs) | | `--url ` | public origin the browser will use; sets CORS. **Include the port** if people reach it on one — `http://203.0.113.10:8080`, not `http://203.0.113.10` | -| `--version ` | pin the files and image tag together. `v3.1.0` and `3.1.0` both work. Sticky — later runs stay on it | +| `--version ` | pin the files and image tag together. `v3.1.1` and `3.1.1` both work. Sticky — later runs stay on it | | `--images` / `--source` | published images, or build from this checkout | | `--dir ` | install here rather than `./forge-growth` | | `--admin-email ` | first-run admin | @@ -470,7 +471,7 @@ overwritten — only empty or placeholder values are filled in — and every mig ```bash ./install.sh # images install: re-fetch, pull, restart git pull && ./scripts/install.sh # source install -./install.sh --version v3.1.0 # move deliberately, and stay there +./install.sh --version v3.1.1 # move deliberately, and stay there ``` A pinned install **stays pinned** — the version is remembered in `.forgegrowth-install`, so @@ -492,8 +493,8 @@ re-running the script later to change your domain cannot silently move you onto **`.env` and the database are a pair. Back them up together.** - `POSTGRES_PASSWORD` is applied **only** when Postgres first creates its data directory. -- `FORGECRM_ENCRYPTION_KEY` is the **only** thing that decrypts stored Meta, Google and - Instagram credentials. Losing it means re-entering all of them; changing it turns the +- `FORGECRM_ENCRYPTION_KEY` and `SOCIAL_ENCRYPTION_KEY` are the **only** things that decrypt stored + Meta, Google and Instagram credentials. Losing one means re-entering what it protects; changing it turns the existing rows into garbage rather than re-encrypting them. A restored database with a regenerated `.env` cannot be opened, **and the failure names the wrong diff --git a/docs/social-section.md b/docs/social-section.md index dd07a34..9f862c9 100644 --- a/docs/social-section.md +++ b/docs/social-section.md @@ -32,7 +32,7 @@ Required: | Variable | Purpose | |---|---| -| `SOCIAL_ENCRYPTION_KEY` | Encrypts stored secrets (AI keys, Instagram tokens). Saving any secret is refused until it is set. Use a long random string and never change it once secrets are saved. | +| `SOCIAL_ENCRYPTION_KEY` | Encrypts stored secrets (AI keys, Instagram tokens). Saving any secret is refused until it is set. **The installer generates it** (3.1.1 and later; re-run `./install.sh` on an older install). By hand: a long random string, `openssl rand -hex 32`. Never change it once secrets are saved. | | `SOCIAL_PUBLIC_BASE_URL` | Defaults to this install's own address (`https://$TLS_DOMAIN`, else the first `CORS_ORIGIN`). The OAuth callback is `/api/social/instagram/oauth/callback`. | The Meta app (Instagram, Webhooks). These can also be entered in @@ -56,8 +56,8 @@ Optional: `SOCIAL_REDIS_URL` (defaults to ForgeGrowth's), `SOCIAL_SYNC_INTERVAL_ ## Turning it on -1. Set `SOCIAL_ENCRYPTION_KEY` in `.env` and restart. The `social` schema is - created on boot. +1. Check `SOCIAL_ENCRYPTION_KEY` is set in `.env` — the installer generates it; if it is empty, + re-run `./install.sh`. The `social` schema is created on boot. 2. Settings → Instagram → Connection: enter the app credentials and connect each account. 3. Set `SOCIAL_JOBS_ENABLED=1` and restart the backend to start syncing. diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 5392f92..dd74b1c 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "forgecrm-frontend", - "version": "3.1.0", + "version": "3.1.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "forgecrm-frontend", - "version": "3.1.0", + "version": "3.1.1", "license": "MIT", "dependencies": { "@xyflow/react": "^12.12.0", diff --git a/frontend/package.json b/frontend/package.json index d5b7478..e0904b9 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "forgecrm-frontend", - "version": "3.1.0", + "version": "3.1.1", "license": "MIT", "description": "ForgeChat frontend — WhatsApp chat viewer", "type": "module", diff --git a/scripts/generate-secrets.sh b/scripts/generate-secrets.sh index ef56149..9e9dd17 100755 --- a/scripts/generate-secrets.sh +++ b/scripts/generate-secrets.sh @@ -20,6 +20,7 @@ echo "FORGECRM_ENCRYPTION_KEY=$(openssl rand -hex 32)" echo "META_WEBHOOK_VERIFY_TOKEN=$(openssl rand -hex 16)" echo "POSTGRES_PASSWORD=$(openssl rand -base64 24 | tr -d '\n=/+' )" echo "MINIO_ROOT_PASSWORD=$(openssl rand -base64 24 | tr -d '\n=/+' )" +echo "SOCIAL_ENCRYPTION_KEY=$(openssl rand -hex 32)" echo "" echo "# Keep FORGECRM_ENCRYPTION_KEY safe: it decrypts every stored Meta/Google/" >&2 echo "# gateway credential. Losing it means re-entering all of them." >&2 diff --git a/scripts/install.sh b/scripts/install.sh index fb6b199..acb2a31 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -602,7 +602,12 @@ fi port_in_use() { if command -v ss >/dev/null 2>&1; then ss -ltn 2>/dev/null | grep -qE "[:.]${1}[[:space:]]" - elif command -v lsof >/dev/null 2>&1; then + # ⚠ Not BusyBox's lsof (Alpine, docker:dind): it ignores every argument and + # exits 0, so every port read as busy and the free-port walk below never + # ended — a silent hang right after "install name". netstat is the one to + # trust there. + elif command -v lsof >/dev/null 2>&1 \ + && ! readlink -f "$(command -v lsof)" 2>/dev/null | grep -q busybox; then lsof -nP -iTCP:"${1}" -sTCP:LISTEN >/dev/null 2>&1 elif command -v netstat >/dev/null 2>&1; then netstat -an 2>/dev/null | grep -qE "[:.]${1}[[:space:]].*LISTEN" @@ -796,7 +801,13 @@ if [ -z "$WEB_PORT" ]; then # hand still fails below rather than being changed underneath you. if [ "$ours_running" = 0 ] && port_in_use "$WEB_PORT"; then busy=$WEB_PORT - while port_in_use "$WEB_PORT"; do WEB_PORT=$((WEB_PORT + 1)); done + while port_in_use "$WEB_PORT"; do + WEB_PORT=$((WEB_PORT + 1)) + # A port check that answers "busy" for everything would otherwise walk + # forever without a word. A hundred taken in a row is not a busy machine. + [ $((WEB_PORT - busy)) -lt 100 ] \ + || die "ports $busy-$WEB_PORT all read as in use, which is not believable. Pass --port ." + done warn "port $busy is in use — using $WEB_PORT instead" fi fi @@ -1094,6 +1105,11 @@ gen_if_needed FORGECRM_ENCRYPTION_KEY "openssl rand -hex 32" 'e gen_if_needed META_WEBHOOK_VERIFY_TOKEN "openssl rand -hex 16" 'paste into Meta webhook setup' gen_if_needed POSTGRES_PASSWORD "openssl rand -base64 24 | tr -d '\n=+/'" 'database' gen_if_needed MINIO_ROOT_PASSWORD "openssl rand -base64 24 | tr -d '\n=+/'" 'object storage' +# The Social section refuses to save Instagram credentials until this is set, +# and nothing else ever set it — so Instagram failed on every fresh install +# until somebody edited .env by hand. Only filled in when empty: changing it +# later turns every stored Instagram secret into garbage. +gen_if_needed SOCIAL_ENCRYPTION_KEY "openssl rand -hex 32" 'encrypts Instagram credentials' # The backend prefers MINIO_SECRET_KEY over MINIO_ROOT_PASSWORD. If one is set # and the other is not, the server and client disagree and every upload fails @@ -1389,9 +1405,10 @@ fi cat <&2; exit 1; } + +docker run --rm -v "$REPO_ROOT:/repo" -w /repo python:3.12-slim sh -ec " + apt-get update -qq >/dev/null + apt-get install -y -qq --no-install-recommends libpango-1.0-0 libpangoft2-1.0-0 fonts-dejavu-core >/dev/null + pip install -q --root-user-action=ignore weasyprint==68.1 + weasyprint '$SRC' '$OUT' + chown $(id -u):$(id -g) '$OUT' +" + +echo "wrote $OUT ($(du -h "$REPO_ROOT/$OUT" | cut -f1))"