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:
- 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.
- 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.
- 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. - Configure SSH access (you → hosting, hosting → GitHub) — This section wires two separate SSH paths — not
GitHub login on your laptop (that is
gh auth loginin Machine setup for repo creation above).
Background
Section titled “Background”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:freshwill 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.
Operator Provisioning Gate
Section titled “Operator Provisioning Gate”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.
-
Walk the inventory — collect each input now, even when the resource it unlocks is a later phase. Mark
needed-by-phaseandstatusper row.Input Needed by Where it goes Status Hosting account(s) — email/username per environment (staging and production may be different accounts) Phase 2 (this page) CLAUDE.local.mdserver mapping +op://<Project>/<env>/HOSTING_ACCOUNT⬜ Hosting API token / MCP — one per account (Hostinger tokens are per-account, no cross-account visibility) Phase 2 / 4 op://<Project>/<env>/HOSTINGER_API_TOKEN(or shared infra vault)⬜ GitHub — org/owner + ghauth; server deploy key add (👤 click)Phase 2 gh auth status; Deploy keys UI⬜ Domain / DNS / Cloudflare — registrar access, CF zone + scoped token Phase 2 / 5 op://<Project>/<env>/CF_API_TOKEN+ zone ID inCLAUDE.local.md⬜ Stripe / PayPal keys (test first; live-mode carry-forward) Phase 6 op://<Project>/<env>/STRIPE_*etc.⬜ Transactional mail (SMTP / Resend / Postmark key) Phase 6 / 8 op://<Project>/<env>/MAIL_*⬜ Sentry / PostHog DSN + keys Phase 7 op://<Project>/<env>/SENTRY_DSN,POSTHOG_*⬜ Vendor licence / purchase code (CodeCanyon) Phase 3 / 6 op://<Project>/license/PURCHASE_CODE⬜ 1Password vault + service account (read for agent) All phases vault <Project>; SA granted the vault⬜ - ✅ Every row is either captured to
op://or explicitly marked carry-forward with itsneeded-byphase — no scattered, uncollected inputs remain.
- ✅ Every row is either captured to
-
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.
-
1. Verify the local toolchain
Section titled “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. 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.
-
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— includinggh,op, and a DB-dump tool (mariadb-dumpormysqldump).
- ✅ Each required tool prints a version, not
-
Install only what’s missing — run the block that matches the failure from step 1.
PHP version from
composer.json(matchrequire.phpor 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.3Composer 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=composerphp -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 20Git 2.x:
Terminal window git --version# Expected: git version 2.x# If missing: xcode-select --installLaravel 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.comGitHub 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-cliDB-dump tool — needed to back up the database before any destructive op (
migrate:fresh, seed-reset). Herd Pro ships MariaDB, so the binary ismariadb-dump— Oracle renamedmysqldump→mariadb-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" >> ~/.zshrcsource ~/.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-dumpormysqldump).
- ✅ Every Required tool is present and within the app’s supported range —
-
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 neededdocker --versiondocker 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. (
opis now a required tool — see step 2 — not an optional one.)
- ✅ Optional tools noted. (
-
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 writtenIf your team uses asdf/mise, align
.tool-versionswith the chosen PHP / Node / Composer targets. DB engine/version and collation stay inZaj-PROJECT.md.- ✅
Zaj-PROJECT.mdrecords the local toolchain facts that later phases verify against.
- ✅
| Tool | Minimum | Required? | If missing |
|---|---|---|---|
| PHP | 8.1 | Yes | herd use php@8.3 or brew install php@8.3 — match composer.json require.php |
| Composer | 2.x | Yes | composer self-update (from 1.x), or the getcomposer.org installer |
| Node | 18 | Yes | nvm install 20 && nvm use 20 |
| NPM | 9 | Yes | ships with Node |
| Git | 2.x | Yes | xcode-select --install |
GitHub CLI (gh) | any | Yes | brew install gh then gh auth login (Machine setup §1) — needed to create the repo + later phases |
1Password CLI (op) | 2.x | Yes | brew install --cask 1password-cli — every phase reads creds from op://; pairs with Project constitution §5 |
DB-dump tool (mariadb-dump or mysqldump) | any | Yes | Herd Pro ships mariadb-dump (mysqldump was renamed) — alias mysqldump→mariadb-dump, or brew install mysql-client. Needed for backups before migrate:fresh / destructive ops |
| Herd (or Valet/Sail/XAMPP) | any | Yes | download from herd.laravel.com |
| Atlas | any | Optional | brew install ariga/tap/atlas then atlas login — or free alternatives: mariadb-dump --no-data, TablePlus, DBeaver |
| Docker | any | Optional | only for self-hosted Bytebase; skip if using Bytebase Cloud |
2. Install editor extensions
Section titled “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. 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:
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| Extension | ID | Purpose |
|---|---|---|
| PHP Intelephense | bmewburn.vscode-intelephense-client | PHP IntelliSense |
| Laravel Blade Snippets | onecentlin.laravel-blade | Blade syntax |
| Laravel Extra Intellisense | amiralizadeh9480.laravel-extra-intellisense | Routes, views autocomplete |
| GitLens | eamodio.gitlens | Enhanced Git history |
| DotENV | mikestead.dotenv | .env syntax highlighting |
| EditorConfig | editorconfig.editorconfig | Shared 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.
3. Create remote infrastructure
Section titled “3. Create remote infrastructure”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.
-
Create the private, empty GitHub repo (default:
ghCLI).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 Casegh 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; thenecho "STOP: repo already exists — rename old, pick new Title-Case name, or confirm wipe"gh repo view "$ORG/$REPO_NAME" --json isEmpty,pushedAt,defaultBranchRefelseecho "OK: name available"gh repo create "$ORG/$REPO_NAME" --privatefigh repo view "$ORG/$REPO_NAME" --json sshUrl -q .sshUrl# Expected: git@github.com:<ORG>/<ProjectName>.git — repo exists with zero commits/filesApply the repo-settings baseline while the repo is still empty.
developis the intended default branch for this playbook, but GitHub can only enforce it afterdevelopexists on the remote in Commit & freeze.Terminal window gh repo edit "$ORG/$REPO_NAME" \--enable-issues \--enable-projects \--enable-wiki=false \--delete-branch-on-mergegh 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 pushedIf repo exists Action Old/unrelated project gh repo rename <name>_old --repo <ORG>/<name>then create fresh empty repoSame project, wrong history Pick a new Title-Case name or explicit wipe after operator approval Empty repo you own Reuse its SSH URL — do not recreate - ✅ A private, completely empty repo exists; the repo-settings baseline is applied; you have its SSH URL; and
developis 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) inZaj-PROJECT.md. - ✅ A private, completely empty repo exists; the repo-settings baseline is applied; you have its SSH URL; and
-
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.mdor_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 tokenecho "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_authfirst if tools return 401):- List hosting orders / existing sites (
hosting_listOrdersV1,hosting_listWebsitesV1) — confirm the MCP responds and shows this account’s plan. - Production —
hosting_createWebsiteV1with<DOMAIN>+ the hosting planorder_id(anddatacenter_codeon the first site of a new plan). - 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 asqa,uat,client-demo, orsandbox. If that row is on a different account than production, switch to that account’s MCP/token first. - 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 URLPROD_URL="https://<DOMAIN>"PROD_ORIGIN_IP="<production-origin-ip-from-Zaj-PROJECT>"dig +short "$(printf '%s' "$NON_PROD_URL" | sed -E 's#https?://##; s#/.*##')" Adig +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.
-
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 laterop://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.tplreferences 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 inZaj-PROJECT.md; passwords stay in 1Password or the gitignored vault.Naming convention (adjust
[project]to your slug):Environment Database User Local [project]_local_db[project]_local_userStaging [project]_staging_db[project]_staging_userProduction [project]_production_db[project]_production_userOption A — 1Password CLI (seed per-environment items now, then paste passwords when the panel shows them):
Terminal window PROJECT_VAULT="[PROJECT]" # the project vault namePROJECT_SLUG="[project]" # lowercase DB prefixseed_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/nullop 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 printedThe 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/nullop item get "Staging" --vault "$PROJECT_VAULT" --fields label=DB_DATABASE# Expected: edit exits 0; verification prints only the non-secret database nameRepeat later for production with its different password using
op item edit "Production" ...; do not create a second item and do not resurrect the legacyDatabasesitem.Option B —
credentials.mdin 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, andProductioncredential items exist with the flat field set; non-secret names/engine/collation/item pointers are inZaj-PROJECT.md; staging + production creds are stored in 1Password orAdmin-Local/1-Project/2-Vault/credentials.md.
- ✅ Three empty databases exist;
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).
| Hop | Who connects to whom | Why |
|---|---|---|
| A — Hosting SSH | Your machine → staging + production servers | Deploy, scp, ServerSync, and manual server work without passwords |
| B — GitHub SSH (on the server) | Each server → git@github.com | git 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.
-
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.pubexists.
- ✅
-
Audit
~/.ssh/configfor 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/configgrep -nE '^Host ' ~/.ssh/configgrep -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.mdgrep -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 IPsWhat you find Action Block matches this project’s IP, user, port, and key Reuse that Hostname as<NON_PROD_SSH>/<PRODUCTION_SSH>inZaj-PROJECT.mdand deploy config — do not duplicate the stanza.Alias exists, wrong IP (same server re-provisioned) Update HostName(andPort/Userif needed) in place.Alias exists for another live project Add a project-scoped alias (for example, [project]-staging) so both projects keep working.No matching block Proceed 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.mdfor later steps.
- ✅ You know whether to reuse, update, or create host aliases — and the chosen non-secret alias/host/user/port facts are recorded in
-
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_ed25519AddKeysToAgent yesTerminal window chmod 600 ~/.ssh/config# Expected: no output (permissions set)- ✅
ssh <NON_PROD_SSH>resolves the alias without a full host string.
- ✅
-
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/nullssh -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 -
Verify the server can reach GitHub (needed for
git pullon 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 optionalcat ~/.ssh/id_ed25519.pubssh -T git@github.com 2>&1 | head -2exit# 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.
Checklist
Section titled “Checklist”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 itsneeded-byphase (Operator Provisioning Gate). - 🤖 Toolchain verified —
php,composer,node,npm,git,gh,op, and a DB-dump tool (mariadb-dumpormysqldump) all report supported versions — no required toolNOT INSTALLED. - 🤖 Repo created + settings baseline applied — a private, empty GitHub repo exists (
gh repo createor dashboard fallback), Issues/Projects are on, Wiki is off, delete-branch-on-merge is on,developis 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/Productioncredential 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.