License & updates
FortPass activates against Medel Platforms with a per-install license key. Once activated, the same client is used to check for new releases and apply them in one click from the admin panel.
Architecture
install/steps/2.php /admin/license
| |
| verify(key) |
v v
\ /
\ /
\ /
v v
app/MedelLicenseClient.php
| |
v v
POST /verify GET /updates?current=X.Y.Z
\ /
v v
https://medel.es/api/v1/license
|
v
settings.license_cache
settings.app_version
The client
app/MedelLicenseClient.php is a thin curl wrapper. The same file lives in install/lib/ so the wizard can use it before the runtime is bootstrapped.
| Method | Endpoint | Returns |
|---|---|---|
verify() | POST /verify | { ok, error?, expires_at? } |
checkUpdates($current) | GET /updates?current=X.Y.Z | { ok, has_update, release: { version, download_url, file_hash, notes? } } |
downloadLatest($current, $dst) | Downloads download_url | Path of the downloaded ZIP, or null. SHA-256 verified. |
Runtime license guard
app/license.php exposes license_enforce_or_die($con), called at the bottom of config.php. If the cache says the license is invalid and the grace window has passed, every non-recovery route is replaced by a "License required" page that links to /admin/license.
Cache windows
| Window | Duration | Behavior |
|---|---|---|
fresh after success | 12 hours | The cached ok=true is trusted without re-verifying. The first request after the window expires triggers a live verify. |
fresh after hard fail | 5 minutes | Bad verify (explicit invalid response) is cached briefly so a fixed key recovers quickly without spamming the API. |
grace on network error | 24 hours (from last cached_at) | If the live verify cannot reach the API, the previous cached verdict (good or bad) is honoured. Prevents a Medel Platforms outage from breaking every buyer at once. |
/admin/license for recovery.
Whitelisted routes
The guard never blocks the routes the buyer needs to recover from a broken state:
/install/*— the install wizard itself./admin/license— recovery panel for entering a new key or re-verifying./manifest.json,/sitemap.xml,/robots.txt— machine endpoints.- AJAX actions:
adminLicenseVerify,adminLicenseUpdate,adminUpdateCheck,adminUpdateApply,adminLogout.
Update flow
The buyer's flow is exactly two clicks: Check now → Apply update. The page at /admin/license automatically runs the check on load so the action button is enabled if there is an update available.
Apply pipeline
- Maintenance mode ON —
settings.maintenance_mode = 1, public visitors see the maintenance page (HTTP 503). - Backup database —
backups/pre-update-<ver>-<ts>.sqlusingmysqldumpif available, otherwise a PHP fallback that walks every table. - Backup file tree —
backups/pre-update-<ver>-<ts>.zip, honoring the same exclusion list used by the copy step. - Download — the release ZIP from the URL returned by the API, with SHA-256 verification.
- Extract — into
backups/extract-<ts>/, descending into a single wrapper folder if the ZIP has one. - Copy over — every file in the extracted tree is written into the live tree, respecting
updater_excluded_paths(). - Run schema changes —
updater_run_update_sql()executesupdate/update.sqlin its entirety. Each release ships its own self-contained script. - Bump version —
settings.app_versionand theVERSIONfile are updated to the new value. - Cleanup — remove the downloaded ZIP and the extraction folder.
- Maintenance mode OFF.
Rollback
Any failure inside the pipeline throws a RuntimeException. The catch handler:
- Logs the error and the message.
- Extracts the pre-update file backup ZIP over the live tree, undoing any partial file copy.
- Disables maintenance mode.
- Returns the captured log to the admin UI for display.
The pre-update SQL backup is not auto-restored — the admin can use it to roll back the database manually if a migration corrupted data.
What is preserved across updates
updater_excluded_paths() in app/updater.php lists every path that is never overwritten:
| Path | Why it survives |
|---|---|
config.php | Buyer's APP_NAME, APP_URL, demo flags. |
app/db.php | Database credentials. |
app/.installed | Install marker. |
app/mail_log/ | Mail log (runtime state). |
assets/img/uploads/ | Every buyer-uploaded image (blog covers, OG images, PWA logos, branding, avatars). |
uploads/vault/ | Encrypted Documents-module attachments (uploads/vault/<uid>/*.enc). |
backups/ | All previous backups stay intact. |
install/ | The buyer's wizard is never overwritten after first install. |
Shipping a release (dev workflow)
- Make your code changes.
- If there are schema changes, edit
update/update.sqlwith the SQL needed to bring the previous release up to this one. If nothing changed, leave the file with just the header. - (Optional) Regenerate
install/sql/schema.sqlwithmysqldump --no-dataso fresh installs land directly on the latest schema. - Bump
VERSION(e.g.echo 1.1.0 > VERSION). - Double-click
build-dist.batto package_dist/fortpass-v1.1.0.zip. - Upload the ZIP to Medel Platforms as the new release with its SHA-256.
Admin recovery panel
/admin/license has two halves:
- License — current status banner (green/red), masked key, last check timestamp, source (
cache_fresh,verify,cache_grace). A textarea lets the admin paste a new key and save (re-verified live) or click Re-verify to refresh the cache without changing the key. - Updates — current vs latest version, Check now button (auto-fired on page load) and Apply update when an update is available. The log area streams the apply pipeline's progress in real time.
license_enforce_or_die() so it stays reachable even when the license is broken. That's the whole point: the buyer must always be able to fix their own state.