Skip to content
prod e051e98
Browse

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.

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.

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.

  1. Confirm the deployed tree matches the release.

    Terminal window
    git status --porcelain # Expected: nothing
    git fetch --all
    readlink current # Expected: points at newest releases/* dir
    php artisan schedule:list # Expected: scheduler entry present
    • ✅ Working tree is clean, current points at the newest release, and the scheduler entry is present.
  2. 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 absent
    ssh <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 .gitattributes with export-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-ignore

    Also 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.

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.

  1. 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.

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.

  1. 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.
  2. 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 production
    SELECT email, COUNT(*) c FROM users GROUP BY email HAVING c > 1; -- duplicate emails
    SELECT COUNT(*) FROM users WHERE email IS NULL OR name IS NULL; -- required-field NULLs
    SELECT 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.

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.

  1. Confirm the HTTPS redirect and health endpoint.

    Terminal window
    curl -I http://<your-domain> # Expected: 301 → https
    curl -s -o /dev/null -w "%{http_code}" https://<your-domain>/up # Expected: 200
    • ✅ HTTP returns a 301 to HTTPS, /up returns 200, and the production env values + live keys + headers are all confirmed.

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.

  1. Walk the payment paths with the provider’s test cards.

    PurposeTest cardResult
    Success4242 4242 4242 4242Payment succeeds
    Decline4000 0000 0000 0002Card declined
    3D Secure4000 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.
  2. 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)
    MetricTarget
    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
    FlowLaunch 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-upNo horizontal scroll; no blocked CTA
    Critical API responses2xx/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.

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.

  1. Expose a health endpoint that checks DB, cache, and queue connectivity.

    // routes/api.php — health endpoint checks DB, cache, and queue config
    Route::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.

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.

  1. 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:-}" ]; then
    echo "Token missing — load from secure vault (op run / op item get …), then re-run §1"
    else
    export CF_ZONE_ID=<your-zone-id>
    # SSL mode — expect: full or strict
    curl -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.3
    curl -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: active
    curl -s -H "Authorization: Bearer $CF_API_TOKEN" \
    "https://api.cloudflare.com/client/v4/zones/$CF_ZONE_ID/dnssec" \
    | jq -r '.result.status'
    fi
    • ✅ SSL full or strict, TLS 1.2+, HSTS enabled, WAF rule count ≥ Phase 4 target, DNSSEC active. Any mismatch → fix before first real user signup.

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.

  1. Method 1 — export and diff schema-only baselines.

    Terminal window
    mysqldump --no-data --skip-comments <local-db> > /tmp/schema-local.sql
    ssh <SSH_STAGING_ALIAS> 'mysqldump --no-data --skip-comments <staging-db>' > /tmp/schema-staging.sql
    ssh <SSH_PRODUCTION_ALIAS> 'mysqldump --no-data --skip-comments <production-db>' > /tmp/schema-production.sql
    diff -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.
  2. 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.sql
    atlas schema inspect -u "$PROD_DB_URL" --format '{{ sql . }}' > /tmp/prod.sql
    diff /tmp/staging.sql /tmp/prod.sql
    # Expected: empty diff except known charset/auto-increment noise that is documented

    Interpret 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 mysqldump and Atlas disagree, trust Atlas and keep digging until the difference is explained.

  1. 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.

Do not mark this step done until every box below is checked.

  • 🤖 Git & deploy verified — clean tree, current symlink 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 passedAPP_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.txt reachable and SPF/DKIM/DMARC re-checked. Drift fixed before live traffic.
  • 🤖 Signoff ready — Tier 1 confirmed 13/13 on the next page.