3 · Credentials & branding
Objective — lock the account before anything public ships: rotate the default admin login, upload a coherent brand kit, resolve the server PHP binary, then strip the vendor’s demo content — because the default creds are a security hole and fake testimonials are a legal one.
Steps at a glance:
- Rotate the admin credentials — The email/password change happens in the running app’s profile page — a person logging into the panel.
- Generate the brand asset kit — If your project has the
image-gentoolchain wired (brand profile + deployment map from project setup), the pipeline produces everything section 3 of this page and the landing page need:. - Upload identity & branding — Most CodeCanyon apps cluster these under one
Settings → App/Settings → Brandingtab. The human signs in once; the agent drives non-secret uploads/fields from the existing session when browser automation is available. - Resolve the server PHP binary (SSH prerequisite) — Resolve it once and persist it so every later page (theme/error publishing, SMTP test, payment config) reuses it.
- Audit & replace demo content — Apply the same three-source protocol from page 2, scoped to content.
- Audit tracking files for infra-path drift — scan tracking files for hardcoded server paths, so branding work does not preserve stale staging or host-specific assumptions.
Background
Section titled “Background”This page is the MUST core of Phase 6. It does four things, in this order, and the order is not negotiable: rotate the admin credentials, produce the brand asset kit, resolve the server PHP binary, then strip the vendor’s demo content. Skipping the rotation or the demo-content audit are both ship-blockers — the first is a security hole, the second is a legal one. Everything here reads from the brand profile you built on the previous page. If Zaj-CUSTOMIZATIONS.md → ## Project Brand Profile is incomplete, stop and finish it — these tasks are “read the row, apply the value”, not “decide the value now”.
flowchart LR R["1 · Rotate creds<br/>(security first)"] --> A["2 · Brand assets<br/>(generate kit)"] A --> U["3 · Upload identity<br/>(config branding)"] U --> P["4 · PHP binary<br/>(SSH prereq)"] P --> D["5 · Demo content<br/>(replace, don't delete)"] classDef must fill:#0d7b6e22,stroke:#0d7b6e; classDef warn fill:#f4a26122,stroke:#f4a261; class R,U,D must; class A,P warn;1. Rotate the admin credentials
Section titled “1. Rotate the admin credentials”The email/password change happens in the running app’s profile page — a person logging into the panel. The agent can audit, prepare non-secret context, and verify non-secret evidence, but the login/password-change form itself is human-driven. Do not use a clipboard-bridged Playwright pass for browser password fields.
-
Change the email. Admin → Profile / Account Settings (the exact path is in your
vendor-capabilities.md § Admin menus; commonly/user/profileon Jetstream apps). Change from the vendor’sexample.comaddress to a real inbox you control — you need it for password resets.- ✅ The admin email is a real inbox you control, not the vendor’s
example.com.
- ✅ The admin email is a real inbox you control, not the vendor’s
-
Change the password. Same page → Change Password. Use a generated 20+ char password. Save it to your password manager and
credentials.mdbefore you submit.- ✅ A 20+ char password is set and already saved to the vault before submit.
The installer seeds demo users independently in every database (local, staging, production) — audit each one. Pick whichever query path your environment supports; the only invalid choice is “skip it because I have no SQL client”.
-
List every user via tinker (the universal fallback — works on any machine where the app runs).
Terminal window php artisan tinker --execute="\App\Models\User::select('id','name','email')->get()->each(function(\$u){echo \$u->id.'|'.\$u->email.'|'.\$u->name.PHP_EOL;});"# Expected: one id|email|name line per user- ✅ Every user row prints, so demo/test identities are visible.
-
List users on the selected non-production environment / production over SSH (respects the User model + soft-deletes).
Terminal window DEPLOY_TARGET="<selected-nonproduction-deploy-target>" # from Zaj-PROJECT.md, e.g. staging-primary/qa/uat/client-demoDEPLOY_PATH="~/domains/<DOMAIN>/deploy"SSH_ALIAS="<non-prod-alias-from-Zaj-PROJECT>"ssh "$SSH_ALIAS" "cd $DEPLOY_PATH/current && \php artisan tinker --execute=\"\App\Models\User::select('id','name','email')->get()->each(function(\$u){echo \$u->id.'|'.\$u->email.'|'.\$u->name.PHP_EOL;});\" 2>&1 | grep -v '^$'"# Expected: the same id|email|name listing, from the remote DB- ✅ For each result: delete demo/test identities (
demo@example.com,Emma Holden, etc.), rotate the password on any account you keep, and record the action incredentials.md.
- ✅ For each result: delete demo/test identities (
Store the new secret correctly — two valid homes, never leaving a rotated password only in /tmp/ or the clipboard:
- 1Password (preferred):
op item edit "superadmin" --vault "[PROJECT]-Local" "username=…" "password=…". Ifop vault listdoesn’t show your vault andop vault getreturns403 Forbidden, a service account lacks per-vault access — grant it in the web UI (Integrations → service account → Vaults → Add), or fall back tocredentials.md. credentials.md(fallback): edit with Python, notsed/echo— passwords contain$ / + & !that break shell escaping. Mark old defaults as “rotated”, don’t delete them — the audit trail shows which environments still need work.
Default credentials live in the database, not the code, so every environment with its own DB needs its own rotation — local included.
| Environment | When | Tracking entry | Entropy — and why |
|---|---|---|---|
| Local dev | Phase 3, or earliest Phase 6 touch | Superadmin (Local) | 12 chars, pronounceable — typed often |
| Selected non-production | This task | Superadmin (<env-key>), e.g. Superadmin (Staging) | 24 chars, random — never typed |
| Production | Phase 12, after first deploy + installer | Superadmin (Production) | 24 chars, random — stored in the vault |
If you copy a verified non-production DB → production at deploy time, production inherits that non-production admin password and needs no separate rotation. Document which path you took.
For AI-assisted rotation, do not use a clipboard bridge, browser_type, or browser_evaluate to fill password fields. The human owns the whole browser password-change transaction; the agent prepares/checks everything around it.
-
Generate or choose the replacement password without printing it.
Terminal window # 1Password path (preferred): human generates/saves the value in the vault UI.# Agent verification is non-secret only:op item get "Superadmin (<env-key>)" --vault "[PROJECT]-Local" --fields label=username >/dev/null \&& echo "✅ vault item exists (secret hidden)" || echo "❌ missing vault item"# Expected: vault item exists; no password value is printed- ✅ The replacement password exists in the vault or approved fallback, and no value was printed.
Then the human signs in, opens Profile / Account Settings, enters the current password and the replacement password, and submits the change. The agent resumes after the human reports the save or after the browser session redirects.
After saving, verify it took without asking the agent to type passwords. Capture non-secret evidence: the stored password hash changed from the pre-rotation hash, Hash::check("123456", ...) returns false for vendor defaults, and any auth-required page redirects to /login after logout/session invalidation. The human can perform one fresh sign-in to confirm the new credential works; the agent resumes after that session exists and records the evidence. (Jetstream logout is POST-only; GET /logout returns 405.)
2. Generate the brand asset kit
Section titled “2. Generate the brand asset kit”If your project has the image-gen toolchain wired (brand profile + deployment map from project setup), the pipeline produces everything section 3 of this page and the landing page need:
| Step | Output | Cost |
|---|---|---|
| Source logos | 3 SVGs (square / horizontal / vertical) | ~$0.24 |
| Derive variants | 12 SVGs (light / dark / mono / inverted × 3) | $0 (pure SVG) |
| Favicons | 16→1024 PNGs + .ico + site.webmanifest | $0 |
| Hero / feature imagery | per-scene PNGs | ~$0.039 each |
| Icons | MIT-licensed (Lucide via CDN) | $0 |
| Deploy | copies to vendor paths per asset-deployment-map.json | $0 |
Two human approval gates are built in — the agent pauses, a person reviews, then responds.
-
Pass both human approval gates before the pipeline deploys assets.
- ✅ The 3 source logos and first 3 hero images were each answered
approve,regenerate: <feedback>, orabort.
- ✅ The 3 source logos and first 3 hero images were each answered
No toolchain? Resize a single master with ImageMagick (magick "$MASTER" -resize ${size}x${size} …) into the sizes the panel asks for, build the multi-size favicon with -define icon:auto-resize=16,32,48,64, and make the Apple touch icon at 180×180 with no transparency (-background white -alpha remove). Commit the kit under Admin-Local/1-Project/1-Info/Brand/assets/ (same tree as Phase 8 · Brand kit); keep machine-readable inputs in Admin-Local/1-Project/1-Info/Brand/brand-profile.json.
3. Upload identity & branding
Section titled “3. Upload identity & branding”Most CodeCanyon apps cluster these under one Settings → App / Settings → Branding tab. These are panel uploads and field edits: the human supplies the logged-in session and any subjective approval, then the agent should drive non-secret fields/uploads when browser automation is available.
-
Fill app identity in
Settings → App.Admin field Brand-profile source Notes App name Identity → App nameExact capitalization — shows everywhere Tagline Identity → Tagline≤80 chars — mobile truncates Timezone App settings → TimezoneIANA, e.g. Asia/DubaiDefault currency App settings → CurrencyISO 4217 — also drives Stripe Default locale App settings → Localee.g. en_AE,ar_AEFirst day of week App settings → First daySunday ME/US, Monday EU/Asia - ✅ App name, tagline, timezone, currency, locale, and first-day are set from the brand profile.
-
Fill company information (feeds invoices, PDF exports, email footers).
-
Required: legal entity name (matches business registration), company address (use registered, not residential).
-
Recommended: support email (
support@[domain]), website URL. -
Optional: support phone, registration number, VAT/Tax ID, founding year.
-
✅ Legal entity name and address are real (no placeholder); a grep-able
REPLACE_ME_*marks any required-but-unknown field.
-
-
Collect every asset variant before uploading, then upload in the typical Froiden-family order.
Asset Light Dark Size Format Main logo (wide) ✅ ✅ ~400×100 SVG / transparent PNG Logo square / icon ✅ ✅ 512×512 SVG / PNG Login logo ✅ ✅ ~300×80 transparent PNG Email header logo ✅ — ~600×150 PNG (SVG fails in email) Invoice / PDF logo ✅ — 300×100 PNG 300 DPI Favicon one — 32×32 ICO / SVG Apple touch icon one — 180×180 PNG, no transparency PWA icon one — 512×512 PNG OG / social preview one — 1200×630 PNG / JPG Typical upload order: main logo light → dark → favicon → login logo (often a separate sub-tab) → email logo (Settings → Email) → app/PWA icon → invoice logo (Settings → Finance). After each upload click Save, refresh, and confirm the preview updates — some panels silently reject non-PNG or >500KB files.
- ✅ Every variant uploaded; each save refreshed and confirmed (no silent reject).
Verify in incognito at desktop + mobile breakpoints: logo in header (light + dark), favicon in tab, logo on /login, logo on a sample invoice, and the og:image meta tag in the landing page <head>. If the app uses a service worker, a new favicon may stay cached up to 24h — bump the PWA manifest version and note it in Zaj-CUSTOMIZATIONS.md so you don’t chase a ghost.
4. Resolve the server PHP binary (SSH prerequisite)
Section titled “4. Resolve the server PHP binary (SSH prerequisite)”Resolve it once and persist it so every later page (theme/error publishing, SMTP test, payment config) reuses it.
-
Reuse the path
deploy.phpalready captured, and verify it runs the required version.Terminal window PHP_BIN=$(grep "set('bin/php'" deploy.php | grep -oE "/[^'\"]+/php" | head -1)DEPLOY_TARGET="<selected-nonproduction-deploy-target>" # from Zaj-PROJECT.md, e.g. staging-primary/qa/uat/client-demoSSH_ALIAS="<non-prod-alias-from-Zaj-PROJECT>"ssh "$SSH_ALIAS" "test -x $PHP_BIN && $PHP_BIN -v | head -1" # expect the version composer.json requires# Expected: the PHP version line composer.json requires- ✅
$PHP_BINis executable and prints the versioncomposer.jsonrequires.
- ✅
-
If
deploy.phphas none, probe the known CloudLinux / Hostinger paths.Terminal window ssh "$SSH_ALIAS" 'for p in /opt/alt/php83/usr/bin/php /opt/cpanel/ea-php83/root/usr/bin/php /usr/local/bin/php83 /usr/bin/php; do[ -x "$p" ] && echo " $p → $($p -v 2>/dev/null | head -1)"; done'# Expected: each existing binary path with its version- ✅ A correct PHP binary path is identified.
-
Persist it for future sessions/phases.
Terminal window grep -q "NON_PROD_PHP_BIN" CLAUDE.local.md 2>/dev/null || \printf '\n## Server PHP binaries (Phase 6 prerequisite)\n- NON_PROD_ENV_KEY: <selected environment key from Zaj-PROJECT.md>\n- NON_PROD_PHP_BIN: %s\n- PRODUCTION_PHP_BIN: [fill in when Phase 12 runs]\n' \"$PHP_BIN" >> CLAUDE.local.md# Expected: the binary path is written to CLAUDE.local.md (idempotent)- ✅
NON_PROD_PHP_BINis recorded inCLAUDE.local.mdwith the matching non-production environment key.
- ✅
5. Audit & replace demo content
Section titled “5. Audit & replace demo content”Apply the same three-source protocol from page 2, scoped to content.
-
Inventory demo content from seeders and content tables.
Terminal window # Codebase — seedersgrep -rn "Faker\|demo\|seed" database/seeders/ 2>/dev/null | head -30# DB schema — content tablesmysql -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" -e \"SHOW TABLES LIKE '%review%'; SHOW TABLES LIKE '%testimonial%'; SHOW TABLES LIKE '%faq%'; SHOW TABLES LIKE '%feature%'; SHOW TABLES LIKE '%price%';"# Expected: seeder hits + the content tables (reviews/testimonials/faqs/features/prices)- ✅ A table in
Zaj-CUSTOMIZATIONS.mdrecords Category | Table | Current state | Decision (replace/remove/track post-launch) | Target source.
- ✅ A table in
Deleting demo rows is almost never enough — two mechanisms silently undo it:
flowchart TD Del["You DELETE the<br/>demo testimonial row"] --> T1{"Observer bound to<br/>language / tenant / setting?"} T1 -->|Yes| Re["Next activation re-inserts<br/>the fake row"] Del --> T2{"Blade @else falls back<br/>to @lang('landing.*')?"} T2 -->|Yes| Lang["Page renders the SAME<br/>fake name from lang/*.php"] Re --> Fix["✅ REPLACE via updateOrCreate<br/>on the vendor's exact key"] Lang --> Fix2["✅ ALSO hide the @else block<br/>(3-file deviation pattern)"] classDef bad fill:#e6394622,stroke:#e63946; classDef good fill:#0d7b6e22,stroke:#0d7b6e; class Re,Lang bad; class Fix,Fix2 good;-
Scan for both mechanisms before touching anything.
Terminal window ls app/Observers/ 2>/dev/null # observer re-seeding (Froiden family)grep -rn "firstOrCreate\|updateOrCreate" app/Observers/*.php 2>/dev/nullgrep -rn "@else" resources/views/ | head -30 # blade fallbacksgrep -rn "@lang('landing\|@lang('home" resources/views/ | head -30cat lang/en/landing.php 2>/dev/null | head -50 # hardcoded demo strings# Expected: any re-seeding observers + blade @else fallbacks + hardcoded lang strings- ✅ Each category is classified as DB-only, blade-fallback-only, or both in a new “Source classification” column.
-
Replace fake testimonials with
updateOrCreate, keyed on the vendor’s exact primary key (usuallyid, orlanguage_code + slot_index). These writes overwrite live content rows — snapshot the content tables first so a wrong key is reversible (mariadb-dump …reviews testimonials > backups/content-pre-edit-$(date +%F).sql; Herd Pro ships MariaDB, so the tool ismariadb-dump).Terminal window php artisan tinker --execute="\App\Models\FrontReviewSetting::updateOrCreate(['id' => 1],['reviewer_name' => '[PROJECT] beta customer (pending permission)','content' => '[Real testimonial pending — replace or hide the section]']);echo 'rows: ' . \App\Models\FrontReviewSetting::count();"NON_PROD_URL="https://<non-production-host>" # from Zaj-PROJECT.md, e.g. staging-primary/qa/uat/client-democurl -sS "$NON_PROD_URL" | grep -c "Michael Davis\|Greenview\|Lakeside" # expect 0# Expected: a rows count, then 0 fake-name matches in the rendered page- ✅ The fake testimonial is replaced (not deleted); the
curlcount is 0. If it still matches, the blade@elsefallback is rendering the fake name — hide that block with an attributed comment wrapper ({{-- [PROJECT] Phase 6 — hidden pending real content --}}).
- ✅ The fake testimonial is replaced (not deleted); the
For each remaining category (hero, features, FAQs): replace now (read the Livewire form → read the model → updateOrCreate for bulk, or drive the admin form via Playwright if there are post-save hooks → screenshot-verify → document the UI path), or capture the Phase 9 content task in Zaj-BACKLOG.md with the current placeholder, owner, trigger, and acceptance criterion. For more than a few rows, persist a script at Admin-Local/1-Project/5-Scripts/1-Setup/Content/DemoContentCleanup/ (with --dry-run + rollback) rather than nesting tinker inside SSH.
-
Verify the replacement survives an observer trigger.
Terminal window NON_PROD_URL="https://<non-production-host>" # from Zaj-PROJECT.mdcurl -sS "$NON_PROD_URL" | grep -iE "lorem ipsum|john doe|jane smith|faker" # expect nothing# Expected: no output — no demo markers remain- ✅ No demo markers render. Then fire the seeding observer once (activate/deactivate a language, or create/delete a tenant) and re-render — your replacement still holds. If a new demo row appears alongside yours, your
updateOrCreatekey didn’t match the observer’s — fix the key and re-run.
- ✅ No demo markers render. Then fire the seeding observer once (activate/deactivate a language, or create/delete a tenant) and re-render — your replacement still holds. If a new demo row appears alongside yours, your
6. Audit tracking files for infra-path drift
Section titled “6. Audit tracking files for infra-path drift”-
Run the drift scan — pull path and alias references, then cross-check against
deploy.phpand~/.ssh/config.Terminal window grep -rhnE '/home/[a-zA-Z0-9_-]+/domains/[^ "`]+|~/domains/[^ "`]+|/opt/alt/[^ "`]+' \CLAUDE.md CLAUDE.local.md \Admin-Local/1-Project/2-Vault/credentials.md \Zaj-PROJECT.md \Zaj-CUSTOMIZATIONS.md DECISIONS.md 2>/dev/null | sort -ugrep -E "deploy_path|domains/" deploy.php 2>/dev/null | head -10grep -B1 -A5 "Host.*[Ss]taging\|Host.*[Pp]roduction" ~/.ssh/config 2>/dev/null# Expected: tracked references next to the deploy.php / ssh-config truth- ✅ Tracking-file references are visible next to authoritative values.
-
Resolve each finding by type, then commit the fixes.
Drift type Action Wrong absolute path Fix immediately — it will mislead debugging Stale credentials reference Update to match the rotation tracking Stale domain name Fix immediately Outdated vendor gotcha Update the entry; link the vendor changelog Dead external link Update to current URL, or archive via archive.org - ✅ Every drift finding is fixed and committed.
Checklist
Section titled “Checklist”Do not mark this step done until every box below is checked.
- 👤 Creds rotated — admin email + password rotated on every environment with a DB; old credentials confirmed dead; new secret stored in 1Password or
credentials.md(header fixed if plaintext). - 🔀 Users audited — demo/test users deleted, kept users re-passworded, audit logged in
credentials.md. - 🔀 Branding uploaded — brand asset kit generated/collected and uploaded by the agent when browser automation is available; logo, favicon, login logo, invoice logo, and
og:imageall verified in incognito (light + dark, desktop + mobile). - 🤖 PHP binary resolved —
NON_PROD_PHP_BINresolved and persisted toCLAUDE.local.mdwith the selected environment key. - 🔀 Demo content handled — inventoried; observer + blade-
@elsetraps scanned; fake testimonials replaced (not deleted); replacement verified to survive an observer trigger. - 🤖 Non-production clean —
curlof the selected non-production landing page returns 0 demo markers; any remaining non-fake content blocks haveZaj-BACKLOG.mdrows targeting Phase 9 with owner, trigger, current placeholder, and acceptance criterion.