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
https://api.freescout.net/wp-json/freescout/v1/modules.Manage » Modules. Per-module: paste license key → click Install Module → click Activate. End of flow.download_link that the FreeScout API mints per-activation. Drop the broken TF + bash automation. A thin re-run helper is documented below for re-creates.POST /polycast/receive returned HTTP 419 from 22:14 onward). No POST /modules/ajax ever fired. The operator most likely clicked Install, the JS handler short-circuited on the 419, and the spinner stalled silently. Re-login, then retry — the install flow itself is healthy on this instance.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):
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.…/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).Helper::downloadRemoteFile) to write the archive to Modules/<alias>.zip.Helper::unzip extracts into Modules/<ModuleName>/ (PHP ZipArchive extension required).Module::clearCache() then Module::findByAlias($alias) confirms the unzip produced a valid module.Module::activateLicense($alias, $license) writes (alias, license) to the modules DB table for future check-license cron runs.Modules/<alias>.zip archive.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").
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:
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.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).infisical secrets get --path /MooseQuest/freescout/<SLUG>_LICENSE_KEY (see slug table below).module:migrate <alias> + module:enable <alias> + freescout:module-laroute <alias> internally). On success, the module's nav entry appears within ~1s.Recommended install order (respects internal dependencies + minimizes nav clutter while testing):
customfields)tags)workflows)savedreplies)customfolders)apiwebhooks)reports)oauthlogin)slack)customization)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.
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.
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.
WpApi::$lastErrorCause: 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.
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
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.
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).
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."
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.
freescout.net/wp-json/freescout/v1/modules?action=get_version only after a successful activate_license call binding the license to a specific url (APP_URL). Calling it from CI would (a) require us to reproduce FreeScout's WpApi POST signature in a shell script, (b) burn one of the license's domain activations on every CI run, and (c) the download_link is one-shot anyway. Rejected.module:migrate may fail on schema drift. We would also need to manually re-run install from the UI to register each license against the new domain — meaning B doesn't actually save us a step from C, just adds a stale-zip refresh chore. Rejected.Action items closing #984:
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.scripts/ops/install-freescout-modules.sh (the broken break-glass).scripts/ops/freescout-modules-walkthrough.sh per the pseudocode above.