6 · Commit & secure
Objective — make the working install durable and safe: commit the installer’s output (especially the storage/installed marker), document any author patches in the shipped vendor tree, verify deploy symlinks and shared_files, then lock down /install and /update with layered defense.
Background
Section titled “Background”The app works — now make the state durable and safe. This page commits the installer’s artifacts, records vendor patches that would break on composer update, confirms the deploy symlinks survive a release, and slams the installer routes shut.
1. Review what the installer changed
Section titled “1. Review what the installer changed”See the installer’s footprint before staging anything.
-
Check the working tree.
Terminal window git status# Expected: modified storage/**/.gitignore, new storage/installed, maybe lock files + public/build/- ✅ The installer’s modified + new files are visible.
-
Backfill any missing
storage/**ignore files (version-aware — Laravel 11+ and debugbar add new subdirs).Terminal window for dir in storage/app storage/app/public storage/app/private \storage/framework storage/framework/cache storage/framework/cache/data \storage/framework/sessions storage/framework/testing storage/framework/views \storage/logs storage/debugbar; do[ -d "$dir" ] && [ ! -f "$dir/.gitignore" ] && printf '*\n!.gitignore\n' > "$dir/.gitignore"done# Expected: each existing storage subdir ends up with its "* / !.gitignore" file- ✅ Every existing
storage/**directory carries a.gitignore.
- ✅ Every existing
2. Stage, commit, push
Section titled “2. Stage, commit, push”Stage the marker, the installer-touched ignore files, lock files, and built assets, then push.
-
Stage the install artifacts.
Terminal window git add -A storage/git add composer.lock package-lock.json public/build/ 2>/dev/null# Expected: marker, ignore files, lock files, and build output staged- ✅ All install artifacts are staged.
-
Commit and push — single bundle for a first run, split by concern for a mixed rerun.
Terminal window git commit -m "Phase 3: Local dev environment + installer complete"git push origin "$(git rev-parse --abbrev-ref HEAD)"git status # working tree clean# Expected: commit created, branch pushed, "nothing to commit, working tree clean"- ✅ The commit is pushed and the working tree is clean.
For a first run, a single commit reads cleanly. For a rerun that mixes a previous session’s staged work with today’s changes, split by concern:
| Strategy | When |
|---|---|
| Single bundle | First run, or small rerun of the same concern |
| Split by concern | Previous session’s work is topically different (e.g. Boost install vs schema rebuild) |
3. Compare shipped vs fresh vendor
Section titled “3. Compare shipped vs fresh vendor”CodeCanyon authors frequently patch packages inside vendor/. Those patches vanish on composer update — so document them now.
-
Skip if there’s nothing to compare — log the skip reason if
vendor/was composer-installed from the lock file.Terminal window find _Source -maxdepth 4 -type d -name vendor 2>/dev/null | head -1 || echo "no shipped vendor — skip"# Expected: a shipped-vendor path, or "no shipped vendor — skip"- ✅ Whether a shipped
vendor/exists to diff is established.
- ✅ Whether a shipped
-
Install a fresh copy in a temp dir and diff against the shipped tree.
Terminal window TEMP_DIR=$(mktemp -d)cp composer.json composer.lock "$TEMP_DIR/"(cd "$TEMP_DIR" && composer install --no-scripts --no-autoloader 2>&1 | tail -5)diff -rq vendor/ "$TEMP_DIR/vendor/" 2>/dev/null | grep -v "^Only in $TEMP_DIR" | head -30[ -n "$TEMP_DIR" ] && [ -d "$TEMP_DIR" ] && rm -rf "$TEMP_DIR"# Expected: a list of differing/added files (author patches), or no output if clean- ✅ Author patches and additions are surfaced.
-
Record both in
_CUSTOMIZATIONS.mdat the project root — package, file, what changed, and a reminder to re-apply on update. This is the file the deploy pipeline reads before ever runningcomposer update.- ✅
_CUSTOMIZATIONS.mddocuments every vendor patch (or the documented skip reason).
- ✅
3b. Investigate (don’t delete) files tracked under storage/
Section titled “3b. Investigate (don’t delete) files tracked under storage/”Laravel’s storage/ holds runtime data, and Deployer swaps the release’s storage/ for a symlink on every deploy — so files committed under storage/ never reach the running app via the release copy. But deleting them blindly is dangerous: many vendor packages read runtime config from storage_path('<file>'), and git rm-ing such a file can break the package silently on the next run. The goal here is to investigate, not delete.
-
List the suspect files — everything tracked under
storage/that isn’t a standard marker.Terminal window ALL=$(git ls-files storage/ 2>/dev/null)SUSPECT=$(echo "$ALL" | grep -vE '\.gitkeep$|\.gitignore$|^storage/installed$')[ -z "$SUSPECT" ] && echo "✅ Only standard markers tracked — nothing to investigate" \|| { echo "Investigate:"; echo "$SUSPECT" | sed 's/^/ ⚠️ /'; }# Expected: either the all-clear, or a short list of files to scan- ✅ The suspect list (if any) is known.
-
For each suspect, scan for a
storage_path()reference across vendor, in-tree packages, and project code.Terminal window FILE="storage/<SUSPECT_FILE>"; BASENAME=$(basename "$FILE")grep -rn "$BASENAME\|storage_path.*$BASENAME" vendor/ packages/ app/ config/ routes/ resources/ 2>/dev/null | head -10wc -c "$FILE" 2>/dev/null; head -3 "$FILE" 2>/dev/null# Expected: any line that reads storage_path('<basename>') means the file is LIVE- ✅ Each suspect is classified by where it is read.
4. Verify deployment symlinks & shared files
Section titled “4. Verify deployment symlinks & shared files”Page 3 created public/storage (and any addon symlinks) locally. Deployer must recreate them on every release, and any shared_files entry that doesn’t exist on disk breaks the first deploy.
-
Confirm the local symlinks exist.
Terminal window ls -la public/ | grep "^l" # expect storage (+ packages/addons if used)# Expected: at least the storage symlink, plus any addon symlinks from page 3- ✅
public/storage(and any addon symlinks) are present.
- ✅
-
Verify every
deploy.phpshared_filesentry exists on disk.Terminal window php -r '$src = file_get_contents("deploy.php");$c = ["shared_files" => []];foreach (["set", "add"] as $fn) {if (preg_match_all("/\\b".$fn."\\s*\\(\\s*[\'\"]shared_files[\'\"]\\s*,\\s*\\[(.*?)\\]\\s*\\)/s", $src, $ms, PREG_SET_ORDER)) {foreach ($ms as $m) {preg_match_all("/[\'\"]([^\'\"]+)[\'\"]/", $m[1], $p);$c["shared_files"] = array_merge($c["shared_files"], $p[1]);}}}$c["shared_files"] = array_values(array_unique($c["shared_files"]));if (!$c["shared_files"]) { fwrite(STDERR, "❌ ABORT: no shared_files paths parsed from deploy.php\n"); exit(1); }foreach (($c["shared_files"] ?? []) as $f) echo $f, "\n";' | while IFS= read -r f; do[ -f "$f" ] && echo "✅ $f" || echo "❌ $f — MISSING; first deploy will fail"done# Expected: at least one path parsed; a ✅ per shared_files entry; fix any ❌ before deploying- ✅ Every
shared_filesentry resolves to a real file.
- ✅ Every
-
Prove the storage symlink serves a real file end-to-end.
Terminal window TEST="storage/app/public/test-$(date +%s).txt"; echo "ok-$(date +%s)" > "$TEST"URL="https://[PROJECT_NAME].test/storage/$(basename "$TEST")"[ "$(curl -s "$URL")" = "$(cat "$TEST")" ] && echo "✅ symlink + nginx wired" || echo "❌ chain broken"rm -f "$TEST"# Expected: "✅ symlink + nginx wired" (HTTP 200 with matching content)- ✅ The served content matches the file — symlink + nginx chain is wired.
5. Lock down /install and /update
Section titled “5. Lock down /install and /update”Left open, these routes let anyone reset the app or alter its schema. Use layered defense — two independent layers that block before PHP boots.
-
Layer 1 —
.htaccess. Add these right afterRewriteEngine Oninpublic/.htaccess, before the Laravel rewrite block.# Block installer + updater routes at Apache level — PHP never loads.RewriteRule ^install(/.*)?$ - [F,L]RewriteRule ^update(/.*)?$ - [F,L]RewriteRule ^upgrade-script$ - [F,L]RewriteRule ^update-manual(/.*)?$ - [F,L]# If your app has a separate /updater package, uncomment:# RewriteRule ^updater(/.*)?$ - [F,L]- ✅ The installer/updater rewrite rules sit before the Laravel block.
-
Commit the
.htaccesschange.Terminal window git add public/.htaccess && git commit -m "Block /install and /update routes at Apache level"# Expected: the .htaccess rule commit is created- ✅
public/.htaccessis committed.
- ✅
^install(/.*)?$ matches /install, /install/, and /install/anything — but not /install-extension (a legitimate admin route in some apps). [F] returns 403 instantly.
Now add the edge layer — a Cloudflare WAF rule that blocks the same paths for all public internet traffic.
-
Layer 2 — Cloudflare WAF. Check the rule count first (free plan allows 5 custom rules), then add a rule expression that blocks the same path prefixes (
http.request.uri.path contains "/install",/update, …) with ablockaction.- ✅ A Cloudflare WAF rule blocks the installer/updater paths at the edge.
-
Verify both layers from the terminal.
Terminal window curl -sI -o /dev/null -w "install: HTTP %{http_code}\n" https://[PROJECT_NAME].test/installcurl -sI -o /dev/null -w "update: HTTP %{http_code}\n" https://[PROJECT_NAME].test/update# Expected: 403 (Apache) or blocked at the edge — a 200 means a layer isn't active- ✅ Both routes return 403 / are blocked.
To temporarily allow access for a legitimate update: comment out the .htaccess rules via SSH (and pause the WAF rule), run the update, then restore both.
Checklist
Section titled “Checklist”Do not mark this step done until every box below is checked.
- 🤖 Marker tracked —
storage/installedtracked; allstorage/**/.gitignorefiles present. - 🤖 Committed + pushed — Phase-3 changes committed and pushed; working tree clean.
- 🤖 Patches documented —
_CUSTOMIZATIONS.mdrecords vendor patches (or the documented skip reason). - 🤖 Deploy paths verified — every
deploy.phpshared_filesentry exists; storage symlink passes the HTTP content test. - 🔀 Routes locked —
/installand/updatereturn 403 / are blocked at both layers (👤 WAF + 🤖.htaccess).