New Client Template
Standard onboarding template for new client projects.
- Overview
- Active Tasks Template
- Key Decisions Template
- Project Overview Template
- Specifications Index Template
- SEO Automation v1 — Intake, Audit, Reporting Workflow
- SEO Readiness Portal MVP — Status and Evidence
- SEO Readiness Portal — Self-Serve Intake (Architecture + Operator Runbook)
Overview
New Client Template Overview
Standard onboarding template for new client projects.
Owner: John Last Verified: 2026-02-17
Contents
Template structure for client onboarding workflows
Active Tasks Template
Last Verified: [DATE] | Owner: [OWNER]
[CLIENT_NAME] — Active Tasks
Open Tasks
- #[ID] — [Task description]
Recently Completed
- [Task name] — [Completion summary, date]
Blocked Tasks
- #[ID] — [Task description]
- Reason: [Why blocked]
Key Decisions Template
Last Verified: [DATE] | Owner: [OWNER]
[CLIENT_NAME] — Key Decisions
Strategic Decisions
[Decision Title] ([DATE])
Decision: [What was decided] Rationale: [Why this decision was made] Implementation: [How it will be implemented]
Project Overview Template
Last Verified: [DATE] | Owner: [OWNER]
[CLIENT_NAME] — Project Overview
What is [CLIENT_NAME]?
[Brief description of what the client does, their industry, their core business]
Current Status
- Phase: [SDLC phase: Planning / Analysis / Design / Implementation / Testing / Deployment / Maintenance]
- Budget: [Amount and currency]
- Timeline: [Duration or deadline]
- Market: [Target market / geography]
Key Documents
- Architecture: [Path to architecture doc]
- Business Case: [Path to business case]
Specifications Index Template
Last Verified: [DATE] | Owner: [OWNER]
[CLIENT_NAME] — Specifications Index
Core System Specs
- [spec-name].md — [Brief description]
Architecture Documents
- [doc-name].md — [Description]
SEO Automation v1 — Intake, Audit, Reporting Workflow
SEO Automation v1 — Intake, Audit, Reporting Workflow
MC: #101968
Status: v1 review-ready
Owner: John / ALAI
Scope: reusable templates and credential-free public-page audit workflow for Asmir/partner SEO clients and ALAI-owned internal domains.
Operating rule
SEO/audit/optimization work must be template-first:
- Reuse the same intake, audit, report, and backlog templates.
- Improve the reusable templates/tooling whenever a one-off need appears.
- Do not claim ranking, traffic, or conversion impact without Search Console, Analytics, or equivalent verified evidence.
- Do not fabricate legal/Privacy/Terms content as part of SEO work.
Package location
Local package:
/Users/makinja/business/ALAI-Holding-AS/sales/seo-automation/
Key files:
README.mdclient-intake-email-bs.mdseo-intake-form.mdseo-audit-checklist.mdseo-client-report-template.mdseo-internal-backlog-template.mdseo-automation-roadmap.mdrun-basic-seo-audit.py
Audit runner
Credential-free runner:
run-basic-seo-audit.py
Checks include:
- HTTP status
- title
- meta description
- H1/H2
- canonical
- robots meta
robots.txtsitemap.xml- images and decorative-alt handling
- links and forms
Validation performed:
python3 -m py_compile run-basic-seo-audit.py
Internal run evidence
Run workspace:
/Users/makinja/business/ALAI-Holding-AS/sales/seo-automation/runs/20260524-internal-domains/
Important evidence files:
00-expanded-batch-report-v2.mdlocal-validation-v2.mdpr-ready-clean-worktrees.mdpublished-draft-prs.mdcode-review-report.mdpr-comments-evidence.mdfinal-pr-summary.md
PRs opened from the workflow
Product/site PRs:
- LumisCare canonical: https://github.com/johnatbasicas/vivacare/pull/3
- Drop SEO cleanup: https://github.com/johnatbasicas/drop/pull/57
- BasicFakta robots/sitemap: https://github.com/johnatbasicas/basicfakta/pull/1
Template package PR:
- SEO Automation v1 templates: https://github.com/johnatbasicas/alai-holding/pull/2
PR validation comments:
- LumisCare: https://github.com/johnatbasicas/vivacare/pull/3#issuecomment-4530170979
- Drop: https://github.com/johnatbasicas/drop/pull/57#issuecomment-4530171024
- BasicFakta: https://github.com/johnatbasicas/basicfakta/pull/1#issuecomment-4530171064
- SEO template: https://github.com/johnatbasicas/alai-holding/pull/2#issuecomment-4530171110
Current status
- PRs are opened and marked ready for review.
- Drop workflow concluded success, but PR merge state still reported
UNSTABLE; required checks must be reviewed before merge. - No PR was merged by automation.
- No deploy was performed by automation.
- Live post-deploy verification is still required after any approved merge/deploy.
SEO Readiness Portal MVP — Status and Evidence
SEO Readiness Portal MVP — status and evidence
Scope
SEO Readiness Portal is the productized continuation of SEO Automation v1. The MVP converts intake, readiness audit structure, backlog, and reporting workflow into a local-first SaaS scaffold.
Current implementation status
- Phase 1: scaffold, app shell, RBAC types, audit domain types, initial schema, spec validation.
- Phase 2: partner/client workspace, client detail page, structured intake UI, sample demo data, intake field definitions and guardrails.
- Phase 3: local/dev persistence for intake save/submit flow.
- Phase 4: local readiness audit runner with persisted audit results and audit detail page.
- Phase 5: local draft report and backlog generator from persisted audit findings.
- Phase 6: local Markdown report review/export workflow with persisted export metadata and checksum.
- Phase 7: local export review notes and internal approval status.
- Phase 8: local client handoff checklist for internally approved exports.
- Phase 9: local client handoff summary draft for approved checklist exports.
- Phase 10: local partner follow-up package draft from approved handoff summary drafts.
- Phase 11: internal SnowIT deploy readiness with Docker packaging, persistent volume, health endpoint, and Cloudflare Access trusted-header gate.
- Phase 12: live protected Cloudflare Access route completed and strictly authenticated on
seo-tools.alai.no; exactseo-tools.snowit.baroute remains blocked pendingsnowit.banameserver/Cloudflare zone activation. - Post-UAT UX fix: explicit Add new client flow added from
/partnersto/clients/new, creating a lead client, active site, and draft intake before redirecting into intake.
Add client UX delivered behavior
/partnersnow has a primary Add new client call-to-action and a simple 3-step workflow explanation./clients/newprovides a dedicated form for partner/owner, company, contact, website/domain, target markets, languages, priority services, competitors, CMS/hosting notes, access-status fields, and internal notes.- Creating a client persists a
leadclient, active site, and draft intake, then redirects to the intake page with a success notice. - Validation rejects missing required fields, invalid email, duplicate active domains, secret-like wording, and ranking/traffic/guarantee claims.
- Evidence:
UX-ADD-CLIENT-EVIDENCE.md,/tmp/evidence-102370/add-client-ux-validation.log,/tmp/evidence-102370/browser-add-client-flow.json,/tmp/evidence-102370/add-client-live-deploy-and-smoke.log,/tmp/evidence-102370-live-auth/verification.json, and screenshots under/tmp/evidence-102370/screenshots/plus/tmp/evidence-102370-live-auth/screenshots/.
Phase 3 delivered behavior
- File-backed local/dev workspace repository.
- Default app data path:
.data/workspace.jsonand gitignored. - Partner/client pages read through the persistence repository.
- Intake page server actions:
- save draft
- submit intake
- Repository updates client, site, and intake records.
- Submitted intake rejects missing required fields.
- Submitted intake rejects secret-like content such as password, API key, token, secret, or bearer wording.
Phase 4 delivered behavior
- Basic local readiness audit runner, not a live crawler.
- Client workspace action to run a local audit.
- Persisted audit records in local/dev file-backed workspace data.
- Audit detail page with readiness score, findings, and guardrails.
- Guardrail checks prevent ranking/traffic/guarantee wording in generated findings.
Phase 5 delivered behavior
- Generates a local draft readiness report from a persisted local audit.
- Generates client-safe backlog items from audit findings.
- Persists
reportsandbacklogItemsin local/dev workspace data. - Adds audit detail action to generate a draft report.
- Adds report detail page with executive summary, guardrails, and backlog draft.
- Guardrail checks prevent ranking/traffic/guarantee wording in generated report/backlog text.
Phase 6 delivered behavior
- Generates a local Markdown export from a persisted draft readiness report.
- Persists
reportExportsmetadata in local/dev workspace data, including filename, byte length, and SHA-256 checksum. - Stores the Markdown artifact under local
.data/exports/storage. - Adds report action to generate the Markdown export.
- Adds export detail route
/clients/[clientId]/reports/[reportId]/exports/[exportId]with metadata, guardrails, and Markdown preview. - Guardrail checks prevent positive ranking/traffic/guarantee wording in generated export text.
Phase 7 delivered behavior
- Persists
exportReviewNotesin local/dev workspace data. - Adds export detail action to save a local internal review note.
- Adds export detail UI for current local review status, reviewer, note, and review history count.
- Supports internal statuses: draft review, changes requested, approved internal.
- Guardrail checks reject secret-like wording and positive ranking/traffic/guarantee wording in review notes.
- Approval status is internal/local only and does not imply public deploy or client delivery.
Phase 8 delivered behavior
- Persists
handoffChecklistsin local/dev workspace data. - Adds export detail action to generate a local handoff checklist.
- Requires an
approved_internallocal export review note before checklist generation. - Adds export detail UI for latest local handoff checklist, linked review note, and pending checklist items.
- Checklist references local evidence such as export checksum and approved review note id.
- Guardrail checks reject secret-like wording and positive ranking/traffic/guarantee wording in checklist content.
- Checklist is internal/local only and does not imply deploy, publication, or client delivery.
Phase 9 delivered behavior
- Persists
handoffSummariesin local/dev workspace data. - Adds export detail action to generate a local handoff summary draft.
- Requires a local handoff checklist linked to an
approved_internallocal export review note before summary generation. - Adds export detail UI for latest local summary draft, recommended next steps, evidence references, and limitations.
- Summary references local evidence such as export filename/checksum, approved review note id, and checklist id.
- Guardrail checks reject secret-like wording and positive ranking/traffic/guarantee wording in summary content.
- Summary is local draft copy only and does not imply email sending, deploy, publication, or client delivery.
Phase 10 delivered behavior
- Persists
partnerFollowupPackagesin local/dev workspace data. - Adds export detail action to generate a local partner follow-up package draft.
- Requires a local handoff summary draft linked to a checklist and
approved_internallocal review note before package generation. - Adds export detail UI for latest local partner follow-up package draft, suggested message draft, client questions, internal preparation steps, evidence references, limitations, and guardrails.
- Package references local evidence such as export filename/checksum, approved review note id, checklist id, and handoff summary id.
- Guardrail checks reject secret-like wording and positive ranking/traffic/guarantee wording in package content.
- Package is local draft copy only and does not imply automated email sending, public deploy, publication, or client delivery.
Phase 11 delivered behavior
- Adds standalone Next.js production packaging for container runtime.
- Adds Dockerfile and internal Docker Compose profile.
- Binds the app origin to
127.0.0.1:3100only in the internal compose profile. - Uses file-backed Docker volume persistence at
/data/workspace.jsonfor internal v1. - Adds
SEO_PORTAL_ACCESS_MODE=cf-accessmode. - Adds app-level trusted-header access gate using
CF-Access-Authenticated-User-Emailafter Cloudflare Access authenticates upstream. - Adds
/api/healthendpoint for container/reverse-proxy checks. - Adds internal runbook for proposed
seo-tools.snowit.badeployment. - Local container verification confirms unauthenticated page access returns 401 and authenticated-header page access returns 200.
- Live DNS/Cloudflare Access for
seo-tools.snowit.bais not yet configured in this session.
Phase 12 live-route status
Completed live protected route:
https://seo-tools.alai.no
Observed behavior:
- unauthenticated
/partnersrequest returns HTTP302to Cloudflare Access login; - spoofed
CF-Access-Authenticated-User-Emailrequest without Access session still returns HTTP302to Cloudflare Access login; - local origin remains bound to
127.0.0.1:3100and blocks unauthenticated page access with HTTP401; - browser verification confirms the live unauthenticated page lands on
Sign in ・ Cloudflare Access, not the origin app; - strict authenticated UAT for
info@snowit.bapassed: OTP was received/submitted and the browser loadedhttps://seo-tools.alai.nowith titleSEO Readiness Portal.
Exact SnowIT URL status:
https://seo-tools.snowit.bais confirmed as the desired SnowIT-branded URL, but is not live yet.snowit.bais currently hosted in AWS Route53 and is not an active Cloudflare zone in the available Cloudflare account.- Cloudflare Access rejected
seo-tools.snowit.bawithdomain does not belong to zone. - A pending Cloudflare zone for
snowit.baexists with nameserversaspen.ns.cloudflare.comandwells.ns.cloudflare.com. - Existing Route53 records have been mirrored into the pending Cloudflare zone as DNS-only records, and
seo-tools.snowit.bahas been prepared there as a proxied CNAME to the Cloudflare Tunnel target. - Nameserver migration/delegation was approved as the correct direction, but is blocked by missing registrar/NIC access path. AWS Route53Domains reports
.baas unsupported, and no Bitwarden item for NIC.ba/UTIC/SnowIT domain registrar credentials was found.
Evidence
Product worktree:
/Users/makinja/business/ALAI-Holding-AS/products/SEO-Readiness-Portal
Evidence files:
PHASE-1-EVIDENCE.mdPHASE-2-EVIDENCE.mdPHASE-3-EVIDENCE.mdPHASE-4-EVIDENCE.mdPHASE-5-EVIDENCE.mdPHASE-6-EVIDENCE.mdPHASE-7-EVIDENCE.mdPHASE-8-EVIDENCE.mdPHASE-9-EVIDENCE.mdPHASE-10-EVIDENCE.mdPHASE-11-EVIDENCE.mdPHASE-12-EVIDENCE.mdUX-ADD-CLIENT-EVIDENCE.md/tmp/evidence-102370/add-client-ux-validation.log/tmp/evidence-102370/browser-add-client-flow.json/tmp/evidence-102370/browser-add-client-flow-summary.txt/tmp/evidence-102370/add-client-live-deploy-and-smoke.log/tmp/evidence-102370-live-auth/verification.json/tmp/evidence-102370-live-auth/SUMMARY.md/tmp/evidence-102370/screenshots/01-partners-add-client-cta.png/tmp/evidence-102370/screenshots/02-new-client-form.png/tmp/evidence-102370/screenshots/03-created-client-intake.png/tmp/alai/seo-readiness-phase3-http-smoke.txt/tmp/alai/redzo-102064-review.md/tmp/alai/company-mesh-responder/2026-05-26T11-20-45-604Z-mesh-msg-1c229051-b7e6-4bdc-af43-c442d3dd8fe2.json/tmp/alai/seo-readiness-phase4-http-smoke.txt/tmp/alai/redzo-102070-review.md/tmp/alai/seo-readiness-phase5-http-smoke.txt/tmp/alai/seo-readiness-phase6-hard-evidence-102091/command-log.txt/tmp/alai/seo-readiness-phase6-http-smoke.txt/tmp/alai/seo-readiness-phase7-hard-evidence-102112/command-log.txt/tmp/alai/seo-readiness-phase7-http-smoke.txt/tmp/alai/seo-readiness-phase8-hard-evidence-102220/command-log.txt/tmp/alai/seo-readiness-phase8-http-smoke.txt/tmp/alai/seo-readiness-phase9-hard-evidence-102231/command-log.txt/tmp/alai/seo-readiness-phase9-http-smoke.txt/tmp/alai/seo-readiness-phase10-hard-evidence-102323/command-log.txt/tmp/alai/seo-readiness-phase10-http-smoke.txt/tmp/alai/seo-readiness-phase11-evidence-102338/command-log.txt/tmp/alai/seo-readiness-phase11-evidence-102338/docker-build.txt/tmp/alai/seo-readiness-phase11-evidence-102338/docker-compose-ps.txt/tmp/alai/seo-readiness-phase11-evidence-102338/container-health.json/tmp/alai/seo-readiness-phase11-evidence-102338/container-unauth-partners.txt/tmp/alai/seo-readiness-phase11-evidence-102338/container-auth-partners.txt/tmp/alai/seo-readiness-phase11-evidence-102338/browser/phase11-browser-verify.json/tmp/alai/gemini-102338-phase11-review-v2.md/tmp/alai/seo-readiness-phase12-evidence-102350/seo-tools-alai-unauth-curl.txt/tmp/alai/seo-readiness-phase12-evidence-102350/seo-tools-alai-spoof-and-local.txt/tmp/alai/seo-readiness-phase12-evidence-102350/browser/live-unauth-browser-verify.json/tmp/alai/seo-readiness-phase12-evidence-102350/browser/seo-tools-alai-access-redirect.png/tmp/evidence-102350/cloudflare-tunnel-remote-config-add-seo-tools.txt/tmp/evidence-102350/cloudflare-tunnel-remote-config-seo-tools-127001.txt/tmp/evidence-102350-info-snowit-strict/verification.json/tmp/evidence-102350-info-snowit-strict/SUMMARY.md/tmp/evidence-102350-info-snowit-strict/screenshots/S04-after-login.png/tmp/evidence-102350/live-edge-and-origin-check-after-auth-uat.txt/tmp/alai/seo-readiness-phase12-evidence-102350/cloudflare-access-seo-tools-alai.txt/tmp/alai/seo-readiness-phase12-evidence-102350/cloudflare-access-seo-tools-snowit.txt/tmp/alai/seo-readiness-phase12-evidence-102350/route53-snowit-ba-records-summary.txt/tmp/alai/seo-readiness-phase12-evidence-102350/cloudflare-snowit-pending-zone-record-sync.txt/tmp/alai/seo-readiness-phase12-evidence-102350/cloudflare-access-seo-tools-snowit-retry-after-zone-sync.txt/tmp/alai/seo-readiness-phase12-evidence-102350/snowit-ba-nameserver-change-request-bs.md/tmp/alai/seo-readiness-phase12-evidence-102350/aws-registrar-snowit.txt- Company Mesh/P2P PASS:
mesh-thr-ede2b968-6abc-4354-a24b-be13f4f0262d/mesh-msg-1827f723-6ae5-4f22-b868-afd2da5d1dc2 /tmp/alai/redzo-102231-review-v2-with-evidence.md- Company Mesh/P2P PASS:
mesh-thr-83994892-4a0c-4dc7-96c3-c553511f9a8f/mesh-msg-32536a8d-8826-4a74-a665-a52c07805f33
Validation commands observed through Phase 11:
npm run type-check
npm run validate:spec
npm run validate:phase2
npm run validate:phase3
npm run validate:phase4
npm run validate:phase5
npm run validate:phase6
npm run validate:phase7
npm run validate:phase8
npm run validate:phase9
npm run validate:phase10
npm run validate:phase11
npm run build
npm audit --audit-level=high
python3 /tmp/alai/seo-readiness-phase3-smoke.py
python3 /tmp/alai/seo-readiness-phase4-smoke.py
python3 /tmp/alai/seo-readiness-phase5-smoke.py
python3 /tmp/alai/seo-readiness-phase6-smoke.py
python3 /tmp/alai/seo-readiness-phase7-smoke.py
python3 /tmp/alai/seo-readiness-phase8-smoke.py
# Phase 9 smoke was curl-based; see /tmp/alai/seo-readiness-phase9-http-smoke.txt
Observed results:
- type-check: PASS
- validate:spec: PASS
- validate:phase2: PASS
- validate:phase3: PASS
- validate:phase4: PASS
- validate:phase5: PASS
- validate:phase6: PASS
- validate:phase7: PASS
- validate:phase8: PASS
- validate:phase9: PASS
- build: PASS
- npm audit high threshold: PASS; moderate postcss/next advisory remains and was not force-fixed
- local HTTP smoke: PASS for
/,/partners,/clients/client-demo-nordic-clinic,/clients/client-demo-nordic-clinic/intake - Phase 4 local HTTP smoke: PASS for audit detail route
/clients/client-demo-nordic-clinic/audits/<auditId> - Phase 5 local HTTP smoke: PASS for report detail route
/clients/client-demo-nordic-clinic/reports/<reportId> - Phase 6 local HTTP smoke: PASS for export detail route
/clients/client-demo-nordic-clinic/reports/<reportId>/exports/<exportId> - Phase 7 local HTTP smoke: PASS for export review status markers on
/clients/client-demo-nordic-clinic/reports/<reportId>/exports/<exportId> - Phase 8 local HTTP smoke: PASS for local handoff checklist markers on
/clients/client-demo-nordic-clinic/reports/<reportId>/exports/<exportId> - Phase 9 local HTTP smoke: PASS for local handoff summary draft markers on
/clients/client-demo-nordic-clinic/reports/<reportId>/exports/<exportId> - Phase 9 independent review: Redzo APPROVE and Company Mesh/P2P PASS
- Phase 10 local HTTP smoke: PASS for local partner follow-up package markers on
/clients/client-demo-nordic-clinic/reports/<reportId>/exports/<exportId> - Phase 11 packaging validation: PASS for standalone config, Docker packaging, localhost-only origin,
/datavolume, access mode, allowlist configuration, and runbook checks - Phase 11 Docker build: PASS for
alai/seo-readiness-portal:internal - Phase 11 container health: PASS for
http://127.0.0.1:3100/api/health - Phase 11 unauthenticated page access: PASS/blocked with HTTP 401 and
x-seo-portal-access: blocked - Phase 11 authenticated-header page access: PASS/allowed with HTTP 200 and
x-seo-portal-access: cf-access - Phase 11 browser verification: PASS for blocked unauthenticated
/partnersand allowed authenticated-header/partners - Phase 11 independent read-only Gemini CLI review: PASS for deploy-readiness packaging and access-gate behavior, with live DNS/Cloudflare hookup explicitly not claimed
- Phase 11 Company Mesh/P2P pre-verifier: PASS for ready_for_review scope on deploy-readiness packaging and access-gate behavior
- Phase 12
seo-tools.alai.nolive protected route: PASS for unauthenticated Cloudflare Access redirect and spoofed trusted-header redirect - Phase 12
seo-tools.snowit.ba: BLOCKED by Cloudflare Access zone ownership requirement untilsnowit.bais active/delegated to Cloudflare or the internal URL decision changes
Non-goals preserved
- No public unauthenticated deploy.
- No live DNS/Cloudflare Access configuration completed for
seo-tools.snowit.bain this session. - No nameserver migration for
snowit.bawas executed automatically. - No production database connection or secrets.
- No Google Search Console or Analytics integration.
- No paid keyword/SERP API integration.
- No ranking or traffic claims.
Suggested next phase
Phase 13 candidate: obtain registrar/NIC access path for snowit.ba, execute the approved nameserver migration to Cloudflare, then create the exact seo-tools.snowit.ba Access application and run authenticated browser verification through the SnowIT URL. Pending Cloudflare zone is already pre-populated.
2026-06-22 — LIVE self-serve autonomous loop verified (MC #102915)
- Status: LIVE in production, autonomous self-serve loop confirmed end-to-end WITHOUT human involvement.
- Azure: app
seo-readiness-alai(rg-seo-readiness-prod), state Running, image20260618-ga4-selfheal(real audit engine). - Public self-serve path:
https://seo-tools.snowit.ba/intake/<token>(CF Access bypass for/intake/*;/partnersstays gated). - Independent Proveo live E2E (real browser/Playwright) — PASS:
- Magic-link intake form renders (HTTP 200, screenshot).
- AI chatbot live (
/api/intake-chat) returns real Bosnian response — tier: Groq. - Form submit → IntakeSubmission v1 persisted (screenshot).
- Auto-audit pipeline fired post-submit:
live_crawlOK +report_draftOK, audit (3 findings), report with 9-step Bosnian action plan — all in ~10s.
- Evidence:
/tmp/alai/seo-portal-e2e-104192/(screenshots step3/step5, step4-uat-chat.json, workspace-live-after-submit.json); til-done receipt/tmp/til-done/102915-20260622T133626Z.json; verdict/tmp/evidence-102915/verdict.json. - Open follow-ups (non-blocking):
- Production chatbot single-tier Groq only — no Anthropic fallback key on live app → 503 risk if Groq down.
- Minor:
pipeline_record/notifystages not persisted to workspace snapshot (intake-pipeline.ts~L208). - Google GSC/GA restricted-scope still pending Google OAuth verification (#103428) — affects live ranking/traffic data only, NOT the crawl-based audit which works now.
Conclusion: ALAI/SnowIT has a working autonomous SEO tool that serves clients (audit + Bosnian action plan) without Asmir or CEO at every step except the designed partner-mediated magic-link entry.
See Also — Agent Runbook
The canonical end-to-end workflow runbook (intake to John deep-report, anti-pitfalls, trigger checklist) lives here:
SEO Readiness Portal — Self-Serve Intake (Architecture + Operator Runbook)
SEO Readiness Portal — Self-Serve Intake (Architecture + Operator Runbook)
Shipped: 2026-06-04
Image: alairegistry.azurecr.io/seo-readiness-portal:20260604-selfserve-intake
Validation: MC #102923 Proveo PASS — /tmp/alai/seo-wsv-evidence/VALIDATION-REPORT.md
Deploy Evidence: /tmp/alai/seo-deploy-102929/FLOWFORGE-DEPLOY-REPORT.md
Overview
The self-serve intake feature enables Asmir (SnowIT sales) to send zero-effort referrals to potential SEO clients. Asmir creates a client record in the SEO Portal /partners workspace, the system automatically sends a branded magic-link email, and the client fills out a web-based intake form that triggers an automated SEO audit pipeline — all without Asmir lifting a finger after the initial client creation.
Asmir Zero-Effort Workflow
- Create client: Asmir visits
https://seo-tools.snowit.ba/partners/clients/new, enters client name + email. - System auto-sends: Intake magic-link email sent immediately (SnowIT-branded, subject "SEO Readiness Assessment for {clientName}").
- Client completes intake: Client clicks link → fills web form (or chats with AI assistant) → submits.
- Auto-pipeline runs: Live SEO audit + findings + draft report generated automatically.
- Asmir reviews: Asmir sees client status change from "link_sent" → "intake_submitted" → "report_ready" in
/partners/referralsdashboard.
Total Asmir effort: Type name + email. Done.
Feature Scope (WS-A/B/C)
WS-A: Magic-Link Token + Email Delivery
Deliverable: HMAC email-bound re-usable intake token + public /intake/[token] route + SnowIT-branded link email on client create.
- Token model:
src/lib/auth/intake-token.ts- HMAC-SHA256 signed with
INTAKE_TOKEN_SECRET(64-char hex, Azure env var). - Payload:
{clientId, email, expiresAt}. Default expiry: 30 days. - Token hash stored on
Client.intakeTokenHash; plaintext NEVER persisted. - Re-usable: Same token works for multiple submissions (versioned append model).
- HMAC-SHA256 signed with
- Public route:
src/app/intake/[token]/page.tsx(Next.js App Router)- Token verification:
verifyIntakeToken(token)→ clientId + email. - Invalid/expired/forged token → 200 error page ("Link not valid / expired"), no data leak.
- Valid token → loads single-client intake form pre-filled with client name/email.
- Cross-client isolation: Token A cannot access Client B's data (clientId derived from token, NEVER from URL/body).
- Token verification:
- Email trigger:
src/app/clients/new/actions.tscreateClient()- Calls
~/system/tools/mail-native.jswith SnowIT-branded template. - Subject: "SEO Readiness Assessment for {clientName}".
- Body: SnowIT logo + personalized intro + magic link to
https://seo-tools.snowit.ba/intake/{token}. - KNOWN GAP: Email currently sent via
--account alaisender (snowit.ba mailbox not yet provisioned). Body is SnowIT-branded; sender is alai.no. Follow-on MC to provision Asmir@snowit.ba sender mailbox.
- Calls
- Persistence:
src/lib/workspace/persistence.ts- Versioned
IntakeSubmission[]appended toClient.intakeSubmissionsarray. - Each submission timestamped; re-submit creates new version, does NOT overwrite.
- Versioned
Evidence: /tmp/alai/seo-ws-a-evidence/
Validation: Playwright 10/10 PASS, validate-magic-link.ts 33/33 PASS.
WS-B: SnowIT White-Label AI Chatbot
Deliverable: SnowIT-branded AI assistant on intake page (Ollama-first tier-router, rate-limited, secret-guarded, forbidden-claim-guarded).
- UI Widget:
src/components/chatbot/IntakeChatWrapper.tsx- Floating Action Button (FAB) in bottom-right corner of intake page.
- Slide-out chat panel: "SEO Assistant" header + "Powered by Snowit" footer.
- Collects same fields as intake form (
IntakeFormFields). - Two-way sync: chat updates form, form updates chat context.
- API Route:
src/app/api/intake-chat/route.ts(POST)- Token-gated: Requires
tokenin body; callsverifyIntakeToken(). - Invalid/expired token → 401
IntakeChatErrorResponse {code: 'token_invalid'}. - ClientId derived from token, NEVER from body (single-client scope enforced).
- Rate-limited: 10 messages / 60 seconds per token (in-process sliding window). 11th message → 429
{code: 'rate_limited'}. - LLM tier-router:
src/lib/chat/tier-router.ts(Ollama FORGE → Groq → Anthropic Haiku waterfall). - Secret-scan guard: User input + assistant output scanned for
/\b(password|api[_ -]?key|secret|token|bearer)\b/i. Match → pre-canned refusal, no credential in response. - Forbidden-claim guard: User input + assistant output scanned for
/\b(rank\s*#?1|first page|traffic lift|guaranteed results)\b/i. Match → pre-canned refusal. - Field extraction:
src/lib/chat/field-extractor.tsparses conversation →Partial<IntakeFormFields>returned asfieldUpdates(form ↔ chat sync).
- Token-gated: Requires
- System prompt:
src/lib/chat/system-prompt.ts- Hard rules:
- Ask ONE question at a time.
- NEVER ask for passwords, API keys, tokens, or credentials.
- NEVER promise rankings, traffic, or guaranteed results.
- Friendly, adaptive to client's language.
- Role: SEO intake assistant for SnowIT SEO readiness service.
- Hard rules:
- Cost discipline:
- Ollama FORGE (10.0.0.2:11434): PRIMARY path, zero cost, ~50 tok/s (qwen2.5:7b-instruct-q8_0).
- Groq: Fallback #1, ~$0.10/1M input tokens.
- Anthropic Haiku: Fallback #2, ~$0.80/1M input tokens.
- Cloud only on local outage:
tier-router.tshealth-checks/api/tagsbefore calling Ollama (avoid cloud cost on idle).
- DEFERRED: WS-B4 (pre-seed chat from live crawl) NOT implemented. Chat starts fresh without crawl data. Follow-on MC can add: "if website provided, call
runLiveCrawlAudit()and seed assistant context with detected title/description/services."
Evidence: /tmp/alai/seo-ws-b-agentforge-evidence/implementation-summary.md
Validation: validate-intake-chat.ts 5/5 PASS, Ollama live turn JSON verified.
WS-C: Auto-Pipeline on Intake Submit
Deliverable: Intake submit triggers audit → findings → draft report (GBP fallback for no-website clients).
- Pipeline trigger:
src/lib/pipeline/run-intake-pipeline.ts- Called on intake form submit (
src/app/intake/[token]/actions.ts). - Runs:
runLiveCrawlAudit()→generateFindings()→generateReport(). - GBP/no-website mode: If
intake.hasWebsite === falseORcanonicalUrlempty → audit runslocal_readiness_gbpstage (12 GBP-specific checks: claim/manager/NAP/category/hours/photos/reviews/posts), skips website-dependent checks, still drafts report (labeled as readiness assessment, not ranking prediction). - Security:
rawTokenNOT passed into pipeline (no token in audit/findings/report records).
- Called on intake form submit (
- Stage derivation:
src/lib/workspace/referral-stages.ts- Client record status derived from data state (NOT manual flag):
created→ client created, no token yet.link_sent→intakeTokenHashpresent.link_opened→ future (requires link-click tracking, not implemented).intake_submitted→intakeSubmissions.length > 0.audit_ready→ audit record exists.report_ready→ report draft exists.
- Monotonic progression: stage never regresses.
- Client record status derived from data state (NOT manual flag):
- Email notification:
src/lib/email/notify.ts- Sends "New intake submission for {clientName}" to Asmir (partner email).
- Security:
secretGuard()scans notification body for credentials/tokens before send. Token redacted as[REDACTED]if accidentally included. forbiddenClaimWordsenforced in audit runner + report generator + exporter (aligned withsrc/lib/workspace/persistence.ts).
- Asmir dashboard:
src/app/partners/referrals/page.tsx- Shows all clients with derived stage (created → link_sent → intake_submitted → report_ready).
- Real-time status derived from data, no manual updates needed.
- Re-submit behavior: New intake submission appends new version, pipeline records both runs (versioned history, not overwrite).
Evidence: /tmp/alai/seo-ws-c-evidence/validate-referral-pipeline.txt
Validation: validate-referral-pipeline.ts 32/32 PASS.
Cloudflare Access Carve-Out (Defense-in-Depth)
Problem: SEO Portal is protected by Cloudflare Access (trusted-header SSO). Public intake links (/intake/[token]) must be accessible to unauthenticated clients, but the rest of the app (/partners, /api/health) must remain gated.
Solution: Created a SEPARATE Cloudflare Access application with a BYPASS policy for /intake/* paths ONLY. The edge lets /intake/* through, and the app's own token gate (verifyIntakeToken()) protects the intake pages (defense-in-depth: CF bypass + app token gate).
CF Access App
- App ID:
06f98e51-1f07-4660-b93b-c426ccff21ce - Name: "SEO Portal - Public Intake Path"
- Domains:
seo-tools.snowit.ba/intakeseo-tools.alai.no/intakeseo-tools.snowit.ba/api/intake-chatseo-tools.alai.no/api/intake-chat
Bypass Policy
- Policy ID:
d9e51759-c151-4ee7-8094-e5191e5f9163 - Name: "Public Intake Bypass"
- Decision:
bypass - Include:
{ "everyone": {} } - Precedence: 1
Verification (Live Prod)
| Path | Expected | Result |
|---|---|---|
/intake/bogus-token |
200 "Link not valid" (app served, no CF redirect) | ✅ PASS |
/api/intake-chat |
401 from app (origin reached, not CF 302) | ✅ PASS |
/partners |
302 to CF Access login (still gated) | ✅ PASS |
/api/health |
302 to CF Access login (not in bypass) | ✅ PASS |
Key signal: Header x-seo-portal-access: blocked proved request hit the app, confirming CF Access carve-out working.
CRITICAL DEPLOY STEP: This CF Access carve-out is MANDATORY for the self-serve intake feature. Without it, all /intake/[token] requests are redirected to CF Access login instead of reaching the app's token-gated intake page. The first cutover attempt FAILED because this step was missing. Deploy runbook now documents this as a hard gate.
Evidence: /tmp/alai/seo-deploy-102929/FLOWFORGE-DEPLOY-REPORT.md (Part 1: CF Access carve-out creation + edge verification).
Azure App Service Deploy
Deploy Target
- Resource group:
rg-seo-readiness-prod - App Service:
seo-readiness-alai - Region: Sweden Central
- Registry:
alairegistry.azurecr.io - Image:
alairegistry.azurecr.io/seo-readiness-portal:20260604-selfserve-intake - Public URLs:
https://seo-tools.snowit.ba(Cloudflare custom hostname)https://seo-tools.alai.no(Cloudflare custom hostname)
Required Environment Variables
INTAKE_TOKEN_SECRET: 64-char hex (HMAC signing key for intake tokens). Generated on deploy, stored in Azure App Settings (NOT in image).INTAKE_BASE_URL:https://seo-tools.snowit.ba(used in magic-link email).
Post-Deploy Verification
| Check | Expected | Result |
|---|---|---|
| Image tag | 20260604-selfserve-intake |
✅ PASS |
| App state | Running |
✅ PASS |
/intake/bogus → 200 |
App served (no CF redirect) | ✅ PASS |
/partners → 302 |
CF Access login | ✅ PASS |
Evidence: /tmp/alai/seo-deploy-102929/FLOWFORGE-DEPLOY-REPORT.md (Part 2: deploy + ZAKON PI2 post-deploy verification).
Known Gaps
- SnowIT sender mailbox not provisioned: Magic-link emails currently sent via
--account alaisender (alai.no). Body is SnowIT-branded; sender is alai.no. Follow-on MC to provisionAsmir@snowit.basender mailbox. - WS-B4 chat pre-seed from live crawl: Deferred. Chat starts fresh without crawl data. Follow-on MC can add: "if website provided, call
runLiveCrawlAudit()and seed assistant context with detected title/description/services." - Rate-limiter resets on container restart: Rate limiter state is in-process Map; resets on Azure container restart. Acceptable for MVP; Postgres migration follow-on.
- CF token rotation: MC #102790 paused. The global CF API key was used for this deploy (scoped token was invalid). Rotation task should be resumed.
Operator Runbook
Create Client + Send Magic Link
- Visit
https://seo-tools.snowit.ba/partners/clients/new(Asmir's Cloudflare Access account). - Enter client name + email.
- Submit → magic-link email sent automatically.
- Client receives: "SEO Readiness Assessment for {clientName}" with link to
https://seo-tools.snowit.ba/intake/{token}.
Monitor Referral Status
- Visit
https://seo-tools.snowit.ba/partners/referrals. - Client status derived from data:
link_sent→ email sent, awaiting client action.intake_submitted→ client submitted form/chat.report_ready→ audit + report draft generated.
Troubleshoot "Link not valid"
Possible causes:
- Forged token: Token signature invalid (client manually edited URL). Expected behavior: 200 error page, no data leak.
- Expired token: Token
expiresAtpast (default 30 days). Expected behavior: 200 "Link expired" error page. - INTAKE_TOKEN_SECRET mismatch: Azure env var changed after token was minted. Fix: Re-send magic link from
/partners/clients/{clientId}(generates new token with current secret).
Debug command:
az webapp config appsettings list -g rg-seo-readiness-prod -n seo-readiness-alai --query "[?name=='INTAKE_TOKEN_SECRET'].value" -o tsv
If secret changed → re-send magic link from portal.
Troubleshoot Rate Limit (429)
Symptom: Client sees "Too many requests" after 10 chat messages in 60 seconds.
Expected behavior: Rate limiter prevents abuse on public /api/intake-chat route.
Workaround: Wait 60 seconds for sliding window to reset.
Long-term fix: Postgres-backed rate limiter (survives container restarts). Follow-on MC.
Re-Deploy Self-Serve Intake
Pre-flight:
cd /Users/makinja/business/ALAI-Holding-AS/products/SEO-Readiness-Portal
npm run type-check && npm run build && npm run validate:spec
Build image:
az acr build -r alairegistry -t seo-readiness-portal:YYYYMMDD-tag-name .
Deploy:
az webapp config container set \
--resource-group rg-seo-readiness-prod \
--name seo-readiness-alai \
--container-image-name alairegistry.azurecr.io/seo-readiness-portal:YYYYMMDD-tag-name \
--container-registry-url https://alairegistry.azurecr.io
az webapp restart --resource-group rg-seo-readiness-prod --name seo-readiness-alai
Post-deploy verify (ZAKON PI2):
curl -sI https://seo-tools.snowit.ba/intake/bogus | grep -E "HTTP|content-type"
# Expect: HTTP/2 200, content-type: text/html; charset=utf-8
curl -sI https://seo-tools.snowit.ba/partners | grep -E "HTTP|location"
# Expect: HTTP/2 302, location: https://<CF-Access-login-url>
Rollback:
az webapp config container set \
--resource-group rg-seo-readiness-prod \
--name seo-readiness-alai \
--container-image-name alairegistry.azurecr.io/seo-readiness-portal:20260602-real-audit \
--container-registry-url https://alairegistry.azurecr.io
az webapp restart --resource-group rg-seo-readiness-prod --name seo-readiness-alai
Previous known-good: 20260602-real-audit. Do NOT open origin IP-lock. Do NOT touch any Bilko domain.
Evidence Ledger
| Artifact | Location | SHA-256 |
|---|---|---|
| Validation report (Proveo PASS) | /tmp/alai/seo-wsv-evidence/VALIDATION-REPORT.md |
72e79a31... |
| Playwright results (10/10 PASS) | /tmp/alai/seo-wsv-evidence/playwright-results.json |
6d35586e... |
| WS-A evidence | /tmp/alai/seo-ws-a-evidence/ |
— |
| WS-B evidence | /tmp/alai/seo-ws-b-agentforge-evidence/ |
— |
| WS-C evidence | /tmp/alai/seo-ws-c-evidence/ |
— |
| Deploy report (FlowForge) | /tmp/alai/seo-deploy-102929/FLOWFORGE-DEPLOY-REPORT.md |
— |
| Live probes | /tmp/alai/seo-wsv-evidence/live-probes.txt |
4e03b886... |
| Intake page HTML (SSR) | /tmp/alai/seo-wsv-evidence/intake-page-content.html |
033ad52f... |
| Screenshots (AC1-AC10) | /tmp/alai/seo-wsv-evidence/screenshots/ |
— |
Status: LIVE in production as of 2026-06-04.
Next: Provision Asmir@snowit.ba sender mailbox (WS-A follow-on). Resume CF token rotation MC #102790.