Skip to content
prod 352bb92
Browse

1 · Prerequisites

Objective — collect every operator-supplied input, ensure the full CLI toolchain (incl. a DB-dump tool for backups before destructive ops), then create the private GitHub repo, hosting sites, and databases (and wire passwordless SSH to both servers) so nothing later in the phase depends on infrastructure or tooling that isn’t there yet.

Steps at a glance:

  1. Verify the local toolchain — Confirm versions before anything depends on them — and verify every CLI a later phase will call, not just the ones used on this page.
  2. Install editor extensions — Editor extensions are a machine-wide install, covered once — canonically — in Machine setup, including the editor-CLI guard and the single-PHP-LSP rule.
  3. Create remote infrastructure — Provision the repo, hosting sites, databases, and matching credential items now so later steps have somewhere to push, run, and resolve op:// references.
  4. Configure SSH access (you → hosting, hosting → GitHub) — This section wires two separate SSH paths — not GitHub login on your laptop (that is gh auth login in Machine setup for repo creation above).

Three kinds of prerequisite gate this phase: operator-supplied inputs (the accounts, tokens, keys, and licences only the human can provide), tools on your machine (CLIs that later phases call, not just this one), and infrastructure in the cloud (repo, hosting, databases, SSH). Capture and verify all three before you git init — a credential that wasn’t collected, a missing PHP version, a missing DB-dump tool, or an unprovisioned database surfaces far more painfully three steps later, often as a mid-phase stall.

Two failure modes this phase exists to prevent:

  • Scattered operator inputs. Hosting creds are needed in Phase 2, Stripe in Phase 6, production infra in Phase 4 — collecting them one-at-a-time stalls every later phase on a “go ask the human” round-trip. The Operator Provisioning Gate collects everything once, up front, into op://.
  • Silently-skipped tooling. A later phase that needs gh, op, or a DB-dump tool (mariadb-dump / mysqldump) for a backup-before-migrate:fresh will either stall or — worse — skip the safety step if the tool is missing. Step 1 verifies every CLI a later phase needs, not just the ones used here.

One early 👤 operator pass that collects everything the human must provide across the entire playbook — so later phases read from op:// non-blocking instead of stalling mid-phase to “go ask the human.” Prod infrastructure that can’t exist yet is recorded as a later-phase carry-forward, but its inputs (account, token, intended domain) are captured here.

Hosting-account readiness itself is verified later at Phase 3 · Shared-hosting readiness — capture the account here, verify it there.

  1. Walk the inventory — collect each input now, even when the resource it unlocks is a later phase. Mark needed-by-phase and status per row.

    InputNeeded byWhere it goesStatus
    Hosting account(s) — email/username per environment (staging and production may be different accounts)Phase 2 (this page)CLAUDE.local.md server mapping + op://<Project>/<env>/HOSTING_ACCOUNT
    Hosting API token / MCP — one per account (Hostinger tokens are per-account, no cross-account visibility)Phase 2 / 4op://<Project>/<env>/HOSTINGER_API_TOKEN (or shared infra vault)
    GitHub — org/owner + gh auth; server deploy key add (👤 click)Phase 2gh auth status; Deploy keys UI
    Domain / DNS / Cloudflare — registrar access, CF zone + scoped tokenPhase 2 / 5op://<Project>/<env>/CF_API_TOKEN + zone ID in CLAUDE.local.md
    Stripe / PayPal keys (test first; live-mode carry-forward)Phase 6op://<Project>/<env>/STRIPE_* etc.
    Transactional mail (SMTP / Resend / Postmark key)Phase 6 / 8op://<Project>/<env>/MAIL_*
    Sentry / PostHog DSN + keysPhase 7op://<Project>/<env>/SENTRY_DSN, POSTHOG_*
    Vendor licence / purchase code (CodeCanyon)Phase 3 / 6op://<Project>/license/PURCHASE_CODE
    1Password vault + service account (read for agent)All phasesvault <Project>; SA granted the vault
    • ✅ Every row is either captured to op:// or explicitly marked carry-forward with its needed-by phase — no scattered, uncollected inputs remain.
  2. Confirm hosting topology — one account, or several? Record the answer in CLAUDE.local.md, because it changes how every site/DB/token step below behaves.

    • Single account — one token/MCP covers both staging and production.

    • Multiple accounts — staging and production live under different hosting logins (separate tokens, no shared access). Capture the account per environment and a token per account (see §3 multi-account note). Hosting MCPs are per-account: a single connected MCP authed to the wrong account silently cannot create the other account’s site or DB.

    • ✅ Hosting topology recorded; per-environment account + token captured when they differ.

Confirm versions before anything depends on them — and verify every CLI a later phase will call, not just the ones used on this page. CodeCanyon Laravel apps are typically pinned to a PHP/Composer range, and a mismatch fails at composer install (Phase 3), not here. The cost of a missing tool is worse than a wrong version: a phase that needs gh, op, or a DB-dump tool for a backup-before-migrate:fresh will stall — or silently skip the safety step — if the tool isn’t present. Ensure them all now.

  1. Print every tool version in one pass — required-for-a-later-phase tools included.

    Terminal window
    echo "=== Tool Versions ==="
    echo "PHP: $(php -v 2>/dev/null | head -1 || echo 'NOT INSTALLED')"
    echo "Composer: $(composer -V 2>/dev/null | head -1 || echo 'NOT INSTALLED')"
    echo "Node: $(node -v 2>/dev/null || echo 'NOT INSTALLED')"
    echo "NPM: $(npm -v 2>/dev/null || echo 'NOT INSTALLED')"
    echo "Git: $(git --version 2>/dev/null || echo 'NOT INSTALLED')"
    echo "GitHub CLI: $(gh --version 2>/dev/null | head -1 || echo 'NOT INSTALLED')"
    echo "1Password CLI: $(op --version 2>/dev/null || echo 'NOT INSTALLED')"
    echo "Herd: $(herd --version 2>/dev/null || echo 'NOT INSTALLED')"
    # DB-dump tool — Herd Pro ships MariaDB, so the binary is mariadb-dump (mysqldump was renamed).
    echo "DB dump: $(mariadb-dump --version 2>/dev/null | head -1 || mysqldump --version 2>/dev/null | head -1 || echo 'NOT INSTALLED')"
    echo "Atlas: $(atlas version 2>/dev/null | head -1 || echo 'NOT INSTALLED (optional)')"
    echo "Docker: $(docker --version 2>/dev/null || echo 'NOT INSTALLED (optional)')"
    # Expected: a version line for each required tool — no "NOT INSTALLED" on PHP/Composer/Node/NPM/Git/gh/op/Herd/DB-dump
    • ✅ Each required tool prints a version, not NOT INSTALLED — including gh, op, and a DB-dump tool (mariadb-dump or mysqldump).
  2. Install only what’s missing — run the block that matches the failure from step 1.

    PHP version from composer.json (match require.php or higher):

    Terminal window
    php -v | head -1
    # Expected: the PHP version required by composer.json, or higher
    # If missing or too old:
    herd use php@8.3
    # Alternative: brew install php@8.3

    Composer 2.x:

    Terminal window
    composer -V
    # Expected: Composer version 2.x
    # If Composer 1.x: composer self-update
    # If not installed:
    php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
    php composer-setup.php --install-dir=/usr/local/bin --filename=composer
    php -r "unlink('composer-setup.php');"

    Node 18+ / NPM 9+:

    Terminal window
    node -v && npm -v
    # Expected: v18+ and 9+
    # If too low:
    nvm install 20 && nvm use 20

    Git 2.x:

    Terminal window
    git --version
    # Expected: git version 2.x
    # If missing: xcode-select --install

    Laravel Herd:

    Terminal window
    herd --version && herd list
    # Expected: Herd version line + a table of linked sites (may be empty before this project)
    # If missing: download from https://herd.laravel.com

    GitHub CLI (gh) — used for the repo + later phases:

    Terminal window
    gh --version && gh auth status
    # Expected: a version line, and "Logged in to github.com"
    # If missing: brew install gh # then: gh auth login (Machine setup §1)

    1Password CLI (op) — reads/writes the credentials every phase consumes:

    Terminal window
    op --version && op whoami
    # Expected: a 2.x version, and an account/email (or a service-account name)
    # If missing: brew install --cask 1password-cli

    DB-dump tool — needed to back up the database before any destructive op (migrate:fresh, seed-reset). Herd Pro ships MariaDB, so the binary is mariadb-dump — Oracle renamed mysqldumpmariadb-dump. Make one of them resolve:

    Terminal window
    mariadb-dump --version 2>/dev/null || mysqldump --version 2>/dev/null || echo "NO DB-DUMP TOOL"
    # Expected: a version line from either binary
    # Option A — alias mysqldump → the Herd-bundled mariadb-dump (so scripts that call `mysqldump` work):
    HERD_BIN="$(dirname "$(command -v herd 2>/dev/null)")"
    ALIAS_LINE="alias mysqldump='$HERD_BIN/mariadb-dump'"
    ls "$HERD_BIN"/mariadb-dump 2>/dev/null && \
    grep -qF "$ALIAS_LINE" ~/.zshrc 2>/dev/null || echo "$ALIAS_LINE" >> ~/.zshrc
    source ~/.zshrc
    # Option B — install the standalone MySQL client (provides mysqldump globally):
    # brew install mysql-client # then add its bin to PATH per the brew caveat it prints
    • ✅ Every Required tool is present and within the app’s supported range — php, composer, node, npm, git, gh, op, Herd, and a DB-dump tool (mariadb-dump or mysqldump).
  3. Dev tooling — skip unless you use schema diff tooling or self-hosted Bytebase.

    Terminal window
    atlas version && atlas whoami # optional — brew install ariga/tap/atlas; atlas login if needed
    docker --version
    docker info >/dev/null 2>&1 && echo "Docker running" || echo "Docker not running"
    # Expected: Atlas/Docker may print NOT INSTALLED — both are genuinely optional.
    # (gh, op, and the DB-dump tool are REQUIRED and handled in step 2, not here.)
    • ✅ Optional tools noted. (op is now a required tool — see step 2 — not an optional one.)
  4. Record the discovered local toolchain in the project state file. Phase 3 verifies the vendor lock against these facts; Phase 4 confirms the deploy runtime; Phase 5 pins Composer before the first live deploy.

    Terminal window
    test -f Zaj-PROJECT.md || echo "⚠️ Zaj-PROJECT.md not seeded yet — Phase 1/Step 5 or Phase 2/Step 7 must create it"
    {
    echo ""
    echo "## Phase 2 Toolchain Snapshot — $(date +%F)"
    echo "- PHP: $(php -r 'echo PHP_VERSION;' 2>/dev/null || echo 'missing')"
    echo "- Composer: $(composer -V 2>/dev/null | head -1 || echo 'missing')"
    echo "- Node: $(node -v 2>/dev/null || echo 'missing')"
    echo "- NPM: $(npm -v 2>/dev/null || echo 'missing')"
    echo "- DB dump: $(mariadb-dump --version 2>/dev/null | head -1 || mysqldump --version 2>/dev/null | head -1 || echo 'missing')"
    } >> Zaj-PROJECT.md
    # Expected: non-secret local tool versions appended; secrets are not written

    If your team uses asdf/mise, align .tool-versions with the chosen PHP / Node / Composer targets. DB engine/version and collation stay in Zaj-PROJECT.md.

    • Zaj-PROJECT.md records the local toolchain facts that later phases verify against.
ToolMinimumRequired?If missing
PHP8.1Yesherd use php@8.3 or brew install php@8.3 — match composer.json require.php
Composer2.xYescomposer self-update (from 1.x), or the getcomposer.org installer
Node18Yesnvm install 20 && nvm use 20
NPM9Yesships with Node
Git2.xYesxcode-select --install
GitHub CLI (gh)anyYesbrew install gh then gh auth login (Machine setup §1) — needed to create the repo + later phases
1Password CLI (op)2.xYesbrew install --cask 1password-cli — every phase reads creds from op://; pairs with Project constitution §5
DB-dump tool (mariadb-dump or mysqldump)anyYesHerd Pro ships mariadb-dump (mysqldump was renamed) — alias mysqldumpmariadb-dump, or brew install mysql-client. Needed for backups before migrate:fresh / destructive ops
Herd (or Valet/Sail/XAMPP)anyYesdownload from herd.laravel.com
AtlasanyOptionalbrew install ariga/tap/atlas then atlas login — or free alternatives: mariadb-dump --no-data, TablePlus, DBeaver
DockeranyOptionalonly for self-hosted Bytebase; skip if using Bytebase Cloud

Editor extensions are a machine-wide install, covered once — canonically — in Machine setup, including the editor-CLI guard and the single-PHP-LSP rule. The repo also commits that same set as .vscode/extensions.json (Create the project), so opening the folder prompts you to install anything missing — there is one canonical list, not a second one to drift.

If you skipped AI dev environment setup, install the canonical set now (idempotent), then continue:

Terminal window
CODE="$(command -v code || command -v cursor)"
for ext in bmewburn.vscode-intelephense-client onecentlin.laravel-blade \
onecentlin.laravel5-snippets amiralizadeh9480.laravel-extra-intellisense \
eamodio.gitlens mikestead.dotenv editorconfig.editorconfig; do
"$CODE" --install-extension "$ext"
done
# Expected: each reports "already installed" or installs cleanly
ExtensionIDPurpose
PHP Intelephensebmewburn.vscode-intelephense-clientPHP IntelliSense
Laravel Blade Snippetsonecentlin.laravel-bladeBlade syntax
Laravel Extra Intellisenseamiralizadeh9480.laravel-extra-intellisenseRoutes, views autocomplete
GitLenseamodio.gitlensEnhanced Git history
DotENVmikestead.dotenv.env syntax highlighting
EditorConfigeditorconfig.editorconfigShared editor settings
  • ✅ The canonical set is installed (PHP IntelliSense, Blade, Laravel IntelliSense, GitLens, DotENV, EditorConfig).

Skip this freely — none of it blocks setup; the terminal tools work regardless.

Provision the repo, hosting sites, and databases now so later steps have somewhere to push and run. Repo creation is agent-first (gh); site creation is agent-first only when your host exposes an MCP server authed to the account that owns this project’s servers.

  1. Create the private, empty GitHub repo (default: gh CLI).

    Vendor code is licensed, not yours to publish — use Title Case for the repo name ([ProjectName], not [projectname]). The remote must stay empty (no README, .gitignore, or license) or your first push collides.

    Audit before create — GitHub repo names are case-insensitive ([ProjectName] and [projectname] collide). Mirror the SSH audit pattern in §4:

    Terminal window
    ORG="<ORG>"
    REPO_NAME="[ProjectName]" # Title Case
    gh auth status # Expected: Logged in to github.com — if not, run gh auth login (Machine setup §1)
    if gh repo view "$ORG/$REPO_NAME" --json name,isEmpty,pushedAt 2>/dev/null; then
    echo "STOP: repo already exists — rename old, pick new Title-Case name, or confirm wipe"
    gh repo view "$ORG/$REPO_NAME" --json isEmpty,pushedAt,defaultBranchRef
    else
    echo "OK: name available"
    gh repo create "$ORG/$REPO_NAME" --private
    fi
    gh repo view "$ORG/$REPO_NAME" --json sshUrl -q .sshUrl
    # Expected: git@github.com:<ORG>/<ProjectName>.git — repo exists with zero commits/files

    Apply the repo-settings baseline while the repo is still empty. develop is the intended default branch for this playbook, but GitHub can only enforce it after develop exists on the remote in Commit & freeze.

    Terminal window
    gh repo edit "$ORG/$REPO_NAME" \
    --enable-issues \
    --enable-projects \
    --enable-wiki=false \
    --delete-branch-on-merge
    gh repo view "$ORG/$REPO_NAME" \
    --json nameWithOwner,isPrivate,isEmpty,sshUrl,hasIssuesEnabled,hasProjectsEnabled,hasWikiEnabled,deleteBranchOnMerge,defaultBranchRef
    # Expected: correct Title-Case repo, private=true, empty=true, Issues/Projects on,
    # Wiki off, deleteBranchOnMerge=true; defaultBranchRef may be null until develop is pushed
    If repo existsAction
    Old/unrelated projectgh repo rename <name>_old --repo <ORG>/<name> then create fresh empty repo
    Same project, wrong historyPick a new Title-Case name or explicit wipe after operator approval
    Empty repo you ownReuse its SSH URL — do not recreate
    • ✅ A private, completely empty repo exists; the repo-settings baseline is applied; you have its SSH URL; and develop is recorded as the default branch to enforce after the first push.

    Record the repo owner/name, SSH URL, intended default branch (develop), and release branch (main) in Zaj-PROJECT.md.

  2. Create the non-production + production sites (default: per-account hosting MCP when authed to the right account).

    Pre-flight: print the confirmed mapping from Zaj-PROJECT.md or _onboarding-summary.md — including the hosting account per environment row (non-production and production may be different accounts; see multi-account below). If environment keys, URLs, IPs, aliases, or accounts are missing, stop and return to Project constitution §1.

    0. Account pre-check (do this first). Confirm the MCP you’ll use is authed to the account that owns this environment’s server — otherwise Option 1 silently no-ops on the wrong account.

    Terminal window
    # List sites the connected MCP can see, then assert THIS project's domain is among them.
    # (Run hosting_listWebsitesV1 via the MCP; the shell shape below is the equivalent assertion.)
    # ✅ <DOMAIN> appears → MCP is authed to the right account → use Option 1
    # ❌ <DOMAIN> absent → MCP is on the WRONG account → use Option 2 (panel) or this account's API token
    echo "Expected: this project's domain (<DOMAIN>) listed by the MCP for THIS environment's account."
    • ✅ The MCP lists this project’s domain for the target environment’s account — confirmed before any create call.

    Option 1 — 🤖 agent + hosting MCP (default only when the pre-check passed). Use the MCP server for the correct account (e.g. Hostinger MCP in Cursor/Claude global config — run mcp_auth first if tools return 401):

    1. List hosting orders / existing sites (hosting_listOrdersV1, hosting_listWebsitesV1) — confirm the MCP responds and shows this account’s plan.
    2. Productionhosting_createWebsiteV1 with <DOMAIN> + the hosting plan order_id (and datacenter_code on the first site of a new plan).
    3. Each non-production row — create the URL/domain recorded in Zaj-PROJECT.md. It may be a subdomain under production, a separate domain, or a custom host such as qa, uat, client-demo, or sandbox. If that row is on a different account than production, switch to that account’s MCP/token first.
    4. Enable/confirm SSL in the panel if the MCP does not auto-issue — Hostinger usually provisions Let’s Encrypt once DNS points at the account.
    Terminal window
    NON_PROD_URL="https://nonprod.example.com" # replace with the selected Zaj-PROJECT.md row URL
    PROD_URL="https://<DOMAIN>"
    PROD_ORIGIN_IP="<production-origin-ip-from-Zaj-PROJECT>"
    dig +short "$(printf '%s' "$NON_PROD_URL" | sed -E 's#https?://##; s#/.*##')" A
    dig +short "$(printf '%s' "$PROD_URL" | sed -E 's#https?://##; s#/.*##')" A | grep -Fx "$PROD_ORIGIN_IP"
    curl -I "$NON_PROD_URL"
    curl -I "$PROD_URL"
    # Expected: both A records return the intended hosting IP; HTTPS responds 200 or 301 after origin SSL exists
    • ✅ Both sites resolve over HTTPS; both IPs recorded. Production is not orange-clouded until DNS points at the current origin and origin TLS is valid.
  3. Create the databases and seed credential items. One empty DB + a user per environment row (local / each non-production target / production) with distinct 20+ char passwords; grant ALL PRIVILEGES; create the matching 1Password items now so later op:// references cannot point at missing items.

    This is the canonical DB provisioning point: create the local DB plus every remote environment DB/user here, and seed the per-environment credential structure before any .env.tpl references it. The LOCAL DB is then verified, charset-checked, and confirmed wired for the installer in Phase 3 · Local database. Remote DBs are first consumed by the selected non-production deploy in Phase 5 and by production cutover in Phase 12. Record non-secret DB names, engine family, charset, collation, and item pointers in Zaj-PROJECT.md; passwords stay in 1Password or the gitignored vault.

    Naming convention (adjust [project] to your slug):

    EnvironmentDatabaseUser
    Local[project]_local_db[project]_local_user
    Staging[project]_staging_db[project]_staging_user
    Production[project]_production_db[project]_production_user

    Option A — 1Password CLI (seed per-environment items now, then paste passwords when the panel shows them):

    Terminal window
    PROJECT_VAULT="[PROJECT]" # the project vault name
    PROJECT_SLUG="[project]" # lowercase DB prefix
    seed_credential_item() {
    ENV_ITEM="$1"
    DB_HOST="$2"
    DB_DATABASE="$3"
    DB_USERNAME="$4"
    op item get "$ENV_ITEM" --vault "$PROJECT_VAULT" >/dev/null 2>&1 \
    || op item create --category "Secure Note" --title "$ENV_ITEM" --vault "$PROJECT_VAULT" \
    "DB_CONNECTION[text]=mysql" \
    "DB_HOST[text]=$DB_HOST" \
    "DB_PORT[text]=3306" \
    "DB_DATABASE[text]=$DB_DATABASE" \
    "DB_USERNAME[text]=$DB_USERNAME" \
    "DB_PASSWORD[password]=REPLACE_ME_PASTE_FROM_PANEL" \
    "APP_KEY[password]=SET_AT_INSTALL" \
    "env_file[password]=SET_AT_DEPLOY" >/dev/null
    op item get "$ENV_ITEM" --vault "$PROJECT_VAULT" --fields label=DB_DATABASE
    }
    seed_credential_item "Local" "127.0.0.1" "${PROJECT_SLUG}_local_db" "${PROJECT_SLUG}_local_user"
    seed_credential_item "Staging" "127.0.0.1" "${PROJECT_SLUG}_staging_db" "${PROJECT_SLUG}_staging_user"
    seed_credential_item "Production" "127.0.0.1" "${PROJECT_SLUG}_production_db" "${PROJECT_SLUG}_production_user"
    # Expected: each command prints only the non-secret DB_DATABASE field; no password is printed

    The item exists now even when its real password is pasted later. When the hosting panel shows the staging password, update the existing field:

    Terminal window
    op item edit "Staging" --vault "$PROJECT_VAULT" \
    "DB_PASSWORD[password]=<PASTE_STAGING_PASSWORD>" >/dev/null
    op item get "Staging" --vault "$PROJECT_VAULT" --fields label=DB_DATABASE
    # Expected: edit exits 0; verification prints only the non-secret database name

    Repeat later for production with its different password using op item edit "Production" ...; do not create a second item and do not resurrect the legacy Databases item.

    Option B — credentials.md in the gitignored vault: add Local, Staging, and Production Database sections with the same field names (DB_CONNECTION, DB_HOST, DB_PORT, DB_DATABASE, DB_USERNAME, DB_PASSWORD, APP_KEY, env_file).

    • ✅ Three empty databases exist; Local, Staging, and Production credential items exist with the flat field set; non-secret names/engine/collation/item pointers are in Zaj-PROJECT.md; staging + production creds are stored in 1Password or Admin-Local/1-Project/2-Vault/credentials.md.

A public repo of CodeCanyon source violates the license, and an initialized remote causes a non-fast-forward collision on your first push — so the repo must be both private and empty.

4. Configure SSH access (you → hosting, hosting → GitHub)

Section titled “4. Configure SSH access (you → hosting, hosting → GitHub)”

This section wires two separate SSH paths — not GitHub login on your laptop (that is gh auth login in Machine setup for repo creation above).

HopWho connects to whomWhy
A — Hosting SSHYour machine → staging + production serversDeploy, scp, ServerSync, and manual server work without passwords
B — GitHub SSH (on the server)Each server → git@github.comgit pull / Deployer on the host can reach the private repo

Steps 1–4 set up hop A (~/.ssh/config, ssh-copy-id). Step 5 verifies hop B (and adds a server-side deploy key in GitHub if needed). Repeat the block for production after staging.

Audit ~/.ssh/config first — a prior project’s host block may already point at the same IP.

  1. Check for, or generate, a key.

    Terminal window
    ls -la ~/.ssh/id_ed25519.pub 2>/dev/null \
    || ssh-keygen -t ed25519 -C "your-email@example.com" # Enter for default path
    # Expected: the .pub path prints, or a new ed25519 key pair is created
    • ~/.ssh/id_ed25519.pub exists.
  2. Audit ~/.ssh/config for existing host blocks before adding new ones — prior CodeCanyon projects often left reusable aliases on the same VPS or shared host.

    Terminal window
    test -f ~/.ssh/config || touch ~/.ssh/config && chmod 600 ~/.ssh/config
    grep -nE '^Host ' ~/.ssh/config
    grep -B1 -A6 -E '^Host ' ~/.ssh/config 2>/dev/null \
    | grep -E '^Host |HostName|User|Port|IdentityFile'
    # Match against the environment IPs you recorded in Zaj-PROJECT.md
    grep -nE '<NON_PROD_IP>|<PRODUCTION_IP>' ~/.ssh/config 2>/dev/null || true
    # Expected: either no hits (add fresh blocks next) or a Host block already pointing at those IPs
    What you findAction
    Block matches this project’s IP, user, port, and keyReuse that Host name as <NON_PROD_SSH> / <PRODUCTION_SSH> in Zaj-PROJECT.md and deploy config — do not duplicate the stanza.
    Alias exists, wrong IP (same server re-provisioned)Update HostName (and Port/User if needed) in place.
    Alias exists for another live projectAdd a project-scoped alias (for example, [project]-staging) so both projects keep working.
    No matching blockProceed to the next step and add new entries.
    • ✅ You know whether to reuse, update, or create host aliases — and the chosen non-secret alias/host/user/port facts are recorded in Zaj-PROJECT.md for later steps.
  3. Add or update host entries in ~/.ssh/config (one per server, only when step 2 did not already cover them), then lock the file down.

    Host <NON_PROD_SSH>
    HostName <NON_PROD_IP>
    User <SSH_USER>
    Port <SSH_PORT>
    IdentityFile ~/.ssh/id_ed25519
    AddKeysToAgent yes
    Terminal window
    chmod 600 ~/.ssh/config
    # Expected: no output (permissions set)
    • ssh <NON_PROD_SSH> resolves the alias without a full host string.
  4. Copy your key to the server, then confirm it no longer prompts for a password.

    Terminal window
    ssh-copy-id -i ~/.ssh/id_ed25519.pub -p <SSH_PORT> <SSH_USER>@<NON_PROD_IP>
    ssh <NON_PROD_SSH> "echo 'Non-production SSH OK' && php -v | head -1"
    # Expected: "Non-production SSH OK" + a PHP version — and NO password prompt
    • ✅ The server responds without asking for a password.

    Use the same non-interactive proof every later phase uses before reporting SSH as missing:

    Terminal window
    SSH_ALIAS="<NON_PROD_SSH>"
    PHPBIN="<versioned-php-path-if-known-or-php>"
    grep -nE '^Host |HostName|User|Port|IdentityFile' ~/.ssh/config 2>/dev/null
    ssh -o BatchMode=yes -o ConnectTimeout=8 "$SSH_ALIAS" "pwd && $PHPBIN -v | head -1"
    # Expected: connection exits 0 and prints remote path + PHP version without an interactive password prompt
  5. Verify the server can reach GitHub (needed for git pull on deploy).

    Terminal window
    ssh <NON_PROD_SSH> "ssh -T git@github.com 2>&1 | head -2"
    # Expected: "Hi <name>! You've successfully authenticated..."
    • ✅ GitHub authenticates. If it returns Permission denied, the server has no key on GitHub — remediate per server:

      Terminal window
      ssh <STAGING_SSH>
      ssh-keygen -t ed25519 -C "server-staging"
      # Press Enter for default path; passphrase optional
      cat ~/.ssh/id_ed25519.pub
      ssh -T git@github.com 2>&1 | head -2
      exit
      # Expected after GitHub step: "Hi <name>! You've successfully authenticated..."

      👤 Add the copied key to GitHub → Settings → SSH and GPG keys → New SSH key, then re-run the test from the server. Repeat for production with -C "server-production".

Repeat for the production server.

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

  • 👤 Provisioning Gate captured — every operator input (hosting account(s) + token(s), GitHub, domain/DNS/CF, billing/mail/observability keys, vendor licence, 1Password vault/SA) is in op:// or explicitly marked carry-forward with its needed-by phase (Operator Provisioning Gate).
  • 🤖 Toolchain verifiedphp, composer, node, npm, git, gh, op, and a DB-dump tool (mariadb-dump or mysqldump) all report supported versions — no required tool NOT INSTALLED.
  • 🤖 Repo created + settings baseline applied — a private, empty GitHub repo exists (gh repo create or dashboard fallback), Issues/Projects are on, Wiki is off, delete-branch-on-merge is on, develop is recorded as the intended default branch, and you have its SSH URL.
  • 🤖 Sites reachable — staging + production both resolve over HTTPS, IPs recorded (per-account hosting MCP after the account pre-check passed, or panel Option 2).
  • 👤 Databases + credential items created — empty local + staging + production DBs (panel or per-account token — not shared-host SSH), Local / Staging / Production credential items exist, and each real password is stored in the matching item field.
  • 🔀 Hosting topology recorded — single vs multiple accounts; per-environment account + token captured when staging and production differ.
  • 🔀 SSH works both ways — hop A: passwordless from your machine to both servers; hop B: both servers can ssh -T git@github.com.