1 · Technical readiness (MUST)
Objective — run the seven MUST gates that verify the deployed system against reality (not against your local machine) so a real customer never hits a problem that a pre-launch check would have caught.
Background
Section titled “Background”These gates run in order — each one assumes the previous passed. The point is to test the system as it actually runs in production, because a clean local machine proves nothing about the deployed release.
1. Verify git & deployment
Section titled “1. Verify git & deployment”Confirm production runs the exact committed code: clean git status, production commit hash matches the deployed release, the current symlink points at the newest release, critical symlinks (storage, bootstrap/cache, public/build, public/storage) are intact, no dev-only files leaked into the release, and the scheduler cron entry exists.
-
Confirm the deployed tree matches the release.
Terminal window git status --porcelain # Expected: nothinggit fetch --allreadlink current # Expected: points at newest releases/* dirphp artisan schedule:list # Expected: scheduler entry present- ✅ Working tree is clean,
currentpoints at the newest release, and the scheduler entry is present.
- ✅ Working tree is clean,
-
Confirm dev-only files did not leak into the release. Build/CI scaffolding, the deploy script, and the test suite must not exist on the production server — their presence means the export filter is misconfigured.
Terminal window # On the production server, inside the current release dir — all should be absentssh <SSH_PRODUCTION_ALIAS> 'cd ~/domains/<DOMAIN>/deploy/current && for f in Admin-Local .github deploy.php tests; do[ -e "$f" ] && echo "LEAKED: $f (must not exist on prod)" || echo "OK: $f absent"done'grep -nE 'export-ignore' .gitattributes 2>/dev/null || true# Expected: every entry reports OK: … absent; export-ignore covers local-only paths- ✅ Every denylist entry reports
OK: … absent. If any leaked, add the path to.gitattributeswithexport-ignore(or your deploy exclude list), redeploy, and re-run:
.gitattributes # .gitattributes — keep dev scaffolding out of the production export/Admin-Local export-ignore/.github export-ignore/deploy.php export-ignore/tests export-ignoreAlso sweep other common dev artifacts:
Terminal window ssh <SSH_PRODUCTION_ALIAS> 'cd ~/domains/<DOMAIN>/deploy/current && find . -maxdepth 3 \( \-name ".env.example" -o -name ".env.local" -o -name ".playwright-mcp" -o \-name "phpunit.xml" -o -name "playwright-report" -o -name "test-results" \) -print'# Expected: no dev-only files on production- ✅ Production contains no test reports, local MCP state, unneeded examples, or dev-only harness files.
- ✅ Every denylist entry reports
2. Scan codebase health & security
Section titled “2. Scan codebase health & security”Validate config/route/view caching, boot the app, then run static analysis and the zero-tolerance security sweep: hardcoded secrets, SQL injection (DB::raw / whereRaw / selectRaw must be parameterized), mass-assignment guards on every model, CSRF on every POST form, and audited raw-output ({!! !!}) sites.
-
Cache config and run static analysis.
Terminal window php artisan config:cache && php artisan config:clear./vendor/bin/phpstan analyse --memory-limit=512M # Expected: ≤ 5 non-critical- ✅ Config caches and clears without error; PHPStan reports ≤ 5 non-critical findings.
3. Confirm database confidence
Section titled “3. Confirm database confidence”Migrations are the source of truth — identical migration files, all applied, mean identical schemas. Verify migration parity across local, staging, and production (same count, 0 pending, all applied), foreign-key integrity, zero orphaned records, and zero test data on production.
-
Check migration parity on every environment.
Terminal window php artisan migrate:status # Expected: all "Ran", 0 pending — on every environment- ✅ Every environment shows all migrations “Ran” with 0 pending, foreign keys intact, and no test data on production.
-
Run data-quality assertions against the live database. These catch “schema is fine, data is broken” launch failures.
-- Run per environment; each query must return 0 rows (or 0 count) on productionSELECT email, COUNT(*) c FROM users GROUP BY email HAVING c > 1; -- duplicate emailsSELECT COUNT(*) FROM users WHERE email IS NULL OR name IS NULL; -- required-field NULLsSELECT COUNT(*) FROM users WHERE email LIKE '%@example.%' -- residual test data (prod)OR email LIKE 'test%@%';Terminal window php artisan tinker --execute='foreach (["users","orders","subscriptions","payments"] as $t) { if (Schema::hasTable($t)) echo $t.": ".DB::table($t)->count().PHP_EOL; }'# Expected: counts make sense; no seed/test/demo customers remain on production- ✅ Duplicate-email and NULL-violation queries return zero rows; production carries zero test rows; launch-critical required fields are populated.
4. Audit environment security
Section titled “4. Audit environment security”On production, confirm APP_ENV=production, APP_DEBUG=false, an APP_KEY starting with base64:, the correct APP_URL, and that .env was never committed. Verify live payment keys (not test), SMTP sends, error-tracking captures, a valid TLS certificate, an HTTP→HTTPS 301, the full canonical security-header set from Phase 7 — all six (HSTS, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Content-Security-Policy, Permissions-Policy) at Grade A on securityheaders.com (not a subset; SSL Labs (TLS) A/A+ separately) — and the health endpoint.
-
Confirm the HTTPS redirect and health endpoint.
Terminal window curl -I http://<your-domain> # Expected: 301 → httpscurl -s -o /dev/null -w "%{http_code}" https://<your-domain>/up # Expected: 200- ✅ HTTP returns a 301 to HTTPS,
/upreturns 200, and the production env values + live keys + headers are all confirmed.
- ✅ HTTP returns a 301 to HTTPS,
5. Run end-to-end testing
Section titled “5. Run end-to-end testing”Walk the real user journeys against staging or production with payment test keys: signup (target < 90s), email verification, login, core CRUD, payment success and decline, mobile on a physical device, profile/password changes, file upload, and password reset. Then check API status codes, the browser console (no JS/CORS errors), data isolation between users, XSS/SQL-injection escaping in form fields, and login lockout.
-
Walk the payment paths with the provider’s test cards.
Purpose Test card Result Success 4242 4242 4242 4242Payment succeeds Decline 4000 0000 0000 0002Card declined 3D Secure 4000 0025 0000 3155Authentication required - ✅ Each card produces its expected result, and every journey — signup, login, CRUD, mobile, uploads, password reset — completes cleanly with no console errors and correct data isolation.
-
Measure a performance baseline against numeric thresholds. “Feels fast” is not a gate; these are.
Terminal window curl -s -w "Time: %{time_total}s\n" -o /dev/null "https://<your-domain>/"# Expected: under 5 s acceptable ceiling (NO-GO above ~10 s)Metric Target Page load < 3 s ideal · < 5 s acceptable (NO-GO above ~10 s) API response < 500 ms PageSpeed — Mobile > 50 PageSpeed — Desktop > 70 PageSpeed — Best Practices > 80 Flow Launch threshold Signup → verified < 90 seconds excluding inbox delay Login → dashboard usable < 3 seconds on desktop broadband First core create action < 5 minutes from account creation Mobile checkout/sign-up No horizontal scroll; no blocked CTA Critical API responses 2xx/3xx expected; no hidden 4xx/5xx - ✅ Page load is under the acceptable ceiling, the API responds under 500 ms, the three PageSpeed scores clear their targets, and journey notes include timings — not just screenshots.
6. Wire monitoring & observability
Section titled “6. Wire monitoring & observability”Confirm error tracking receives a deliberately thrown test exception within a minute, alert rules exist (new issue, high volume, critical), uptime monitors watch the homepage, login, and a /health endpoint, log rotation keeps 14+ days, and recent backups exist. Define alert thresholds (error rate, response time, disk) and trigger one test alert end-to-end.
-
Expose a health endpoint that checks DB, cache, and queue connectivity.
// routes/api.php — health endpoint checks DB, cache, and queue configRoute::get('/health', function () {try {DB::connection()->getPdo();Cache::put('health_check', true, 10);$cache = Cache::get('health_check') === true ? 'ok' : 'error';$queue = config('queue.default') ?: 'unset';return response()->json(['status' => 'ok','database' => 'connected','cache' => $cache,'queue' => $queue,]);} catch (\Exception $e) {return response()->json(['status' => 'error', 'message' => $e->getMessage()], 500);}});- ✅ Error tracking captures a test exception within a minute, uptime monitors watch homepage/login/
/health, log rotation keeps 14+ days, and one end-to-end test alert fired.
- ✅ Error tracking captures a test exception within a minute, uptime monitors watch homepage/login/
7. Complete final signoff
Section titled “7. Complete final signoff”Run the tiered go/no-go on the next page. Tier 1 must be 13/13 or the launch does not happen.
7b. Verify Cloudflare production settings (SHOULD — non-blocking)
Section titled “7b. Verify Cloudflare production settings (SHOULD — non-blocking)”Confirm that the zone’s SSL mode, TLS floor, HSTS, WAF rule count, and DNSSEC status all match the values configured in Phase 4. This step reports drift only — a mismatch should be fixed before accepting live traffic, but it does not pause the launch countdown.
-
Run the five Cloudflare API checks.
Terminal window # Confirm token is set (skip API curls in an interactive shell until loaded)if [ -z "${CF_API_TOKEN:-}" ]; thenecho "Token missing — load from secure vault (op run / op item get …), then re-run §1"elseexport CF_ZONE_ID=<your-zone-id># SSL mode — expect: full or strictcurl -s -H "Authorization: Bearer $CF_API_TOKEN" \"https://api.cloudflare.com/client/v4/zones/$CF_ZONE_ID/settings/ssl" \| jq -r '.result.value'# Min TLS version — expect: 1.2 or 1.3curl -s -H "Authorization: Bearer $CF_API_TOKEN" \"https://api.cloudflare.com/client/v4/zones/$CF_ZONE_ID/settings/min_tls_version" \| jq -r '.result.value'# HSTS — expect: {"enabled":true,...}curl -s -H "Authorization: Bearer $CF_API_TOKEN" \"https://api.cloudflare.com/client/v4/zones/$CF_ZONE_ID/settings/security_header" \| jq -r '.result.value.strict_transport_security'# WAF custom rule count — expect: matches Phase 4 count (typically 3+)curl -s -H "Authorization: Bearer $CF_API_TOKEN" \"https://api.cloudflare.com/client/v4/zones/$CF_ZONE_ID/rulesets" \| jq '.result[] | select(.phase == "http_request_firewall_custom") | .rules | length'# DNSSEC status — expect: activecurl -s -H "Authorization: Bearer $CF_API_TOKEN" \"https://api.cloudflare.com/client/v4/zones/$CF_ZONE_ID/dnssec" \| jq -r '.result.status'fi- ✅ SSL
fullorstrict, TLS1.2+, HSTS enabled, WAF rule count ≥ Phase 4 target, DNSSECactive. Any mismatch → fix before first real user signup.
- ✅ SSL
7c. Detect schema drift across environments (SHOULD)
Section titled “7c. Detect schema drift across environments (SHOULD)”Compare local, staging, and production schema snapshots before first traffic. This is separate from migrate:status: two environments can both show “all ran” while manual SQL or installer behavior created drift.
-
Method 1 — export and diff schema-only baselines.
Terminal window mysqldump --no-data --skip-comments <local-db> > /tmp/schema-local.sqlssh <SSH_STAGING_ALIAS> 'mysqldump --no-data --skip-comments <staging-db>' > /tmp/schema-staging.sqlssh <SSH_PRODUCTION_ALIAS> 'mysqldump --no-data --skip-comments <production-db>' > /tmp/schema-production.sqldiff -u /tmp/schema-staging.sql /tmp/schema-production.sql | sed -n '1,220p'# Expected: empty diff except known charset/auto-increment noise that is documented- ✅ Any schema drift is explained, fixed through migrations, or recorded as an intentional vendor/install difference before launch.
-
Method 2 — run Atlas CLI as a cross-check. Load database URLs from your vault or environment first; never inline passwords in commands, docs, shell history, or screenshots.
Terminal window atlas schema inspect -u "$STAGING_DB_URL" --format '{{ sql . }}' > /tmp/staging.sqlatlas schema inspect -u "$PROD_DB_URL" --format '{{ sql . }}' > /tmp/prod.sqldiff /tmp/staging.sql /tmp/prod.sql# Expected: empty diff except known charset/auto-increment noise that is documentedInterpret the direction carefully: if production has extra columns that staging and local do not have, someone ran raw SQL on production. Investigate before launch. If
mysqldumpand Atlas disagree, trust Atlas and keep digging until the difference is explained.
-
Hand off to the tiered signoff. Confirm Tier 1 is 13/13 on the next page before opening registration.
- ✅ Tier 1 reads 13/13, so the launch is cleared to proceed.
Checklist
Section titled “Checklist”Do not mark this step done until every box below is checked.
- 🤖 Git & deploy verified — clean tree,
currentsymlink correct, scheduler entry present. - 🤖 Security sweep clean — zero vulnerable instances; PHPStan ≤ 5 non-critical.
- 🤖 Database confidence — migrations at parity, 0 pending, no test data on production.
- 🤖 Env audit passed —
APP_DEBUG=false,base64:key, HTTPS 301, security headers, live keys. - 🔀 End-to-end journeys pass — payment test cards clear; 👤 mobile verified on a physical device.
- 🤖 Monitoring live — test exception captured, uptime monitors green, backups recent.
- 🤖 Production infra re-verified (SHOULD) — Cloudflare SSL/TLS/HSTS/WAF/DNSSEC match Phase 4 (step 7b); SEO
sitemap.xml+robots.txtreachable and SPF/DKIM/DMARC re-checked. Drift fixed before live traffic. - 🤖 Signoff ready — Tier 1 confirmed 13/13 on the next page.