Raxx · internal docs

internal · gated ↑ index

FreeScout paid module install — runbook

Status: authoritative as of 2026-05-03 Applies to: tickets.raxx.app (FreeScout 1.8.218 on AWS Lightsail raxx-tickets) Resolves: #984 Related: #707, #975, #979


TL;DR


How module install actually works (verified by reading core)

Every paid module install goes through one HTTP POST: POST /modules/ajax with action=install, alias=<canonical-alias>, license=<key>. Server flow (see app/Http/Controllers/ModulesController.php lines 224-309 and app/Misc/WpApi.php):

  1. Activate license. Server POSTs to https://api.freescout.net/wp-json/freescout/v1/modules?action=activate_license with {license, module_alias, url=APP_URL, v=app.version}. Response status=valid is required.
  2. Mint download URL. Server GETs …/modules?action=get_version&license=…&module_alias=…&url=…. Response includes a download_link — a one-shot, signed URL on freescout.net (EDD/Tagras issues these per-activation; they expire; they are not crawlable).
  3. Download zip. Server uses Guzzle (Helper::downloadRemoteFile) to write the archive to Modules/<alias>.zip.
  4. Unzip. Helper::unzip extracts into Modules/<ModuleName>/ (PHP ZipArchive extension required).
  5. Cache + verify. Module::clearCache() then Module::findByAlias($alias) confirms the unzip produced a valid module.
  6. Persist license. Module::activateLicense($alias, $license) writes (alias, license) to the modules DB table for future check-license cron runs.
  7. Cleanup. Deletes the Modules/<alias>.zip archive.
  8. Activate (separate click). UI then runs freescout:module-install <alias> server-side, which runs migrations and creates the public/modules/<alias> symlink → ../../Modules/<ModuleName>/Resources/assets/.

Validation cadence. A daily cron (freescout:module-check-licenses) re-validates every active license against …/modules?action=check_license. If freescout.net is unreachable or the license is invalid, the module is auto-deactivated server-side. A domain change requires Deactivate License on the old domain before Install on the new one ("No activations left for this license key").


The supported procedure (Option C)

Prerequisites — verify once before touching modules:

ssh -i /tmp/lightsail_us_east_1.pem admin@54.146.13.200
cd /var/www/html/freescout
php -m | grep -E '^(zip|curl|fileinfo|openssl)$'         # all 4 present?
curl -sS -o /dev/null -w '%{http_code}\n' https://api.freescout.net/wp-json/  # 403 is fine — endpoint exists
sudo -u www-data ls -la Modules/ public/modules/         # owned by www-data, mode 755
sudo grep '^APP_URL' .env                                # must be the real public URL

All four extensions, both directories owned www-data:www-data, and APP_URL=https://tickets.raxx.app were verified healthy on 2026-05-03 22:00 UTC.

Per-module install — repeat for each of the 10:

  1. Browse to https://tickets.raxx.app/login and authenticate. If the tab has been open >2 hours, fully reload after login — stale CSRF is the #1 cause of "the button does nothing." Confirm reload by checking the page footer timestamp.
  2. Navigate to Manage » Modules. The page lists every module returned by the freescout.net API (currently 71 modules; our 10 are interleaved alphabetically by display name, not by alias).
  3. For the target module row, click Install Module. A small modal/inline panel opens with a single text field labeled License Key.
  4. Paste the license from infisical secrets get --path /MooseQuest/freescout/<SLUG>_LICENSE_KEY (see slug table below).
  5. Click Install Module. The button spinner runs 3-15 seconds while the server hits the freescout.net API twice and downloads the zip. On success, a green toast "Module successfully installed!" appears.
  6. The same row now shows an Activate button. Click it. This runs the migration step (module:migrate <alias> + module:enable <alias> + freescout:module-laroute <alias> internally). On success, the module's nav entry appears within ~1s.
  7. Move to the next module. Workflows depends on Tags + Custom Fields — install those two first.

Recommended install order (respects internal dependencies + minimizes nav clutter while testing):

  1. Custom Fields (customfields)
  2. Tags (tags)
  3. Workflows (workflows)
  4. Saved Replies (savedreplies)
  5. Custom Folders (customfolders)
  6. API & Webhooks (apiwebhooks)
  7. Reports (reports)
  8. OAuth & Social Login (oauthlogin)
  9. Slack (slack)
  10. Customization & Rebranding (customization)

Slug → canonical alias table

Critical: the marketing-site slugs (which the vault path uses) differ from the API aliases the install endpoint expects. The web UI hides this — it knows its own aliases. Any future automation must use the API alias column.

Vault slug (/MooseQuest/freescout/<SLUG>_LICENSE_KEY) API alias (used by Modules/ dir + UI POST + artisan) Display name Current version Notes
api-webhooks apiwebhooks API & Webhooks Module 1.0.101 required for any agent integrations
reports reports Reports Module 1.0.57
auth oauthlogin OAuth & Social Login Module 1.0.26 NOT auth — that slug is unused upstream
custom-fields customfields Custom Fields Module 1.0.52 install before Workflows
tags tags Tags Module 1.0.45 install before Workflows
workflows workflows Workflows Module 1.0.87 depends on customfields + tags
customization customization Customization & Rebranding Module 1.0.33 logo/favicon/footer rebranding
saved-replies savedreplies Saved Replies Module 1.0.55
slack slack Slack Integration Module 1.0.24
folders customfolders Custom Folders Module 1.0.31

Confirmed live 2026-05-03 23:05 UTC against https://api.freescout.net/wp-json/freescout/v1/modules?v=1.8.218 (returned 71 modules).

Action needed: rename the vault entries to match the API aliases (/MooseQuest/freescout/{apiwebhooks,oauthlogin,customfields,savedreplies,customfolders}_LICENSE_KEY). Track in a follow-up issue. Doing this once now prevents every future re-create from suffering the same slug-mismatch confusion.


Common failure modes + diagnosis

F1. Operator clicks Install Module, nothing happens, no error

Cause: stale CSRF / expired session. Symptom in /var/log/apache2/freescout-access.log: POST /polycast/receive HTTP/1.1 419 from the operator's IP, repeating every 60s. No POST /modules/ajax from that IP. Fix: logout + login + hard reload /modules/list. Verified on 2026-05-03 22:14 — Mac browser had been open since the rebuild and was hitting 419 continuously; iPhone session at 22:20 was healthy but operator did not run install from there.

F2. "Empty license key" toast

Cause: license field validated client-side as empty (whitespace, smart quotes from translation tool, etc.). Symptom: POST /modules/ajax returns JSON {status:"error", msg:"Empty license key"}. Fix: copy license direct from infisical CLI, paste into a plain text editor first to strip smart quotes, then paste into the field.

F3. "Error occurred. Please try again later." with WpApi::$lastError

Cause: freescout.net API unreachable, slow, or the license is invalid/expired. Diagnose:

ssh -i /tmp/lightsail_us_east_1.pem admin@54.146.13.200
sudo tail -50 /var/www/html/freescout/storage/logs/laravel-$(date -u +%Y-%m-%d).log
curl -sS -o /dev/null -w 'http=%{http_code} time=%{time_total}\n' https://api.freescout.net/wp-json/

Fix: if freescout.net is reachable but returns an error message about the license, double-check we're activating against the same domain we paid for (tickets.raxx.app). If we changed domains, deactivate the license on the prior domain via the modules page on whichever instance still holds it, or open a ticket with FreeScout support quoting the receipt ID.

F4. "Error occurred downloading the module"

Cause: the download_link was minted but Helper::downloadRemoteFile failed — usually disk space, a perms issue on Modules/, or a network blip. Symptom: the page falls back to "Please [download] module manually and extract into /var/www/html/freescout/Modules" with a clickable link to the just-minted URL. Fix: click the manual link from the same browser session (the URL is signed and time-limited — do not save it for later). Save the zip locally, scp it up, then sudo -u www-data unzip <slug>.zip -d /var/www/html/freescout/Modules/ and run sudo -u www-data php artisan freescout:module-install <alias> to migrate. If the link expired, click Install Module again to mint a fresh one.

Cause: public/modules/<alias> exists as a directory (someone copied files into it instead of letting the installer symlink). FreeScout's installer refuses to overwrite a real dir with a symlink. Symptom: module shows under Modules/ but won't activate. Fix:

ssh -i /tmp/lightsail_us_east_1.pem admin@54.146.13.200
sudo rm -rf /var/www/html/freescout/public/modules/<alias>
cd /var/www/html/freescout && sudo -u www-data php artisan freescout:module-install <alias>
sudo -u www-data php artisan freescout:clear-cache

F6. Module installs but admin UI throws "Whoops, something went wrong"

Cause: schema migration partially applied; Laravel's compiled view cache or route cache is stale. Fix:

cd /var/www/html/freescout
sudo -u www-data php artisan freescout:clear-cache
sudo -u www-data php artisan module:migrate <alias>
sudo rm -rf bootstrap/cache/*.php   # nuke compiled config/route cache, keep .gitignore
sudo -u www-data php artisan freescout:after-app-update

If still broken: sudo -u www-data php artisan module:disable <alias> to back out, file an issue against the module on GitHub, retry after upgrading FreeScout core.

F7. Daily cron auto-deactivates a module

Cause: freescout:module-check-licenses ran while freescout.net was unreachable (Cloudflare blip, our outbound NAT down, etc.) or the license registration is corrupt. Symptom: module silently disappears from nav next morning. Log shows WpApi::checkLicense failure. Fix: confirm freescout.net is reachable from the instance, then re-run install from the UI (it's idempotent — same license key, same domain, Activate succeeds without re-downloading).


Why the operator's 2026-05-03 UI attempt did not install anything

Reconstructed from /var/log/apache2/freescout-access.log and storage/logs/laravel-2026-05-03.log:

time UTC source event interpretation
22:08 rebuild core install completes; Modules/, public/modules/ empty (only .gitkeep) clean slate
22:10-22:11 bash agent php artisan freescout:module-install <slug> --license=<key> ×10 every call exits with RuntimeException: The "--license" option does not exist (logged 10×). Filed as #984.
22:14 operator Mac (149.22.88.13) already-open /modules/list tab begins long-polling /polycast/receive session pre-dated the rebuild; CSRF token mismatch → 419 every 60s
22:20 operator iPhone (159.26.103.163) full login + GET /modules/list 200 healthy session — but no follow-up POST /modules/ajax from this IP
22:46-22:57 operator Mac continuous /polycast/receive 419s tab still stale; never reloaded
22:54 operator Mac POST /login 302 then GET / 200 new session, but went to dashboard root, not /modules/list
22:58 session last polycast 419 from Mac operator gave up

Conclusion: zero POST /modules/ajax requests were made. The operator was looking at a logged-out (or about-to-be-logged-out) page. When they clicked Install Module, the JavaScript made a POST that the server rejected with 419 before reaching the controller, and the JS error handler shows nothing visible (FreeScout 1.8.x has a known polycast-only retry-on-419 path; install POST inherits the same handler). The fix is "log out, log back in, reload the page, click Install again" — not "the install flow is broken."


Re-create helper (Option C-with-helper)

Auto-recreate is impossible — every install needs a human-paste step (license + click). What we can do is gate the recreate behind a one-shot interactive script that fetches the licenses from Infisical, prints them in install order, and walks the operator through the UI. Pseudocode in scripts/ops/freescout-modules-walkthrough.sh (to be added in the issue-#984 follow-up PR):

#!/usr/bin/env bash
# Usage: ./scripts/ops/freescout-modules-walkthrough.sh
# Prereq: infisical login completed
set -eu
ORDER="customfields tags workflows savedreplies customfolders apiwebhooks reports oauthlogin slack customization"
VAULT_BY_API_ALIAS=(
  "customfields:custom-fields"
  "tags:tags"
  "workflows:workflows"
  "savedreplies:saved-replies"
  "customfolders:folders"
  "apiwebhooks:api-webhooks"
  "reports:reports"
  "oauthlogin:auth"
  "slack:slack"
  "customization:customization"
)
echo "Open https://tickets.raxx.app/modules/list in a fresh logged-in browser tab."
for entry in "${VAULT_BY_API_ALIAS[@]}"; do
  api_alias="${entry%:*}"
  vault_slug="${entry#*:}"
  key=$(infisical secrets get --path /MooseQuest/freescout/ "${vault_slug^^//-/_}_LICENSE_KEY" --plain 2>/dev/null)
  echo
  echo "=== ${api_alias} (vault: ${vault_slug}) ==="
  echo "Find the row for the module on /modules/list, click Install Module."
  echo "Paste this license key:  ${key}"
  read -r -p "Press Enter when 'Module successfully installed!' shows. Then click Activate. Press Enter again when the module appears in the nav…"
done
echo "Done. Verify with: cd /var/www/html/freescout && sudo -u www-data php artisan module:list"

Total elapsed time: ~2-3 minutes per module = ~25-30 minutes for the full set.


Verdict on issue #984: A vs B vs C

Action items closing #984:

  1. Delete the terraform/freescout/modules.tf null_resource blocks that call the bogus --license and freescout:module-active commands. Replace with a local-exec echoing a pointer to this runbook + the walkthrough script.
  2. Delete scripts/ops/install-freescout-modules.sh (the broken break-glass).
  3. Add scripts/ops/freescout-modules-walkthrough.sh per the pseudocode above.
  4. Open a follow-up to rename Infisical paths from hyphenated marketing slugs to API aliases, so the walkthrough script and the FreeScout UI agree on names.
  5. Close #984 referencing this runbook.