This runbook covers recurring deployment, monitoring, incident response, and rollback for the active Smart Portfolio Cloudflare Pages service. Complete the environment setup in Deployment before using these procedures.
| Surface | Address or path | Expected role |
|---|---|---|
| Public site | https://nicolasmgioanni.dev |
Primary visitor domain |
| Assigned production site | https://smart-portfolio-bds.pages.dev |
CI polling and production smoke target |
| Stable preview | https://develop.smart-portfolio-bds.pages.dev |
develop deployment and smoke target |
| Content metadata | /content-version.json |
Active normalized content hash and commit SHA |
| Artifact metadata | /artifact-integrity.json |
Published static artifact inventory |
| Verification Function | /api/contact/verify |
Turnstile verification and ticket issuance |
| Delivery Function | /api/contact |
Validated email delivery |
| Production quota database | smart-portfolio-contact-rate-limit-production |
Pseudonymous rolling 24-hour reservations |
| Preview quota database | smart-portfolio-contact-rate-limit-preview |
Isolated preview reservations |
HTTP checks on 2026-08-27 observed successful responses from the custom domain and both assigned-domain targets. This observation is point-in-time evidence only; current domain, DNS, certificate, and Pages state remains external and unverified by repository checks. CI uses only the assigned pages.dev hostnames, so custom-domain verification is a separate operator responsibility.
Use the narrowest authoritative source for each question:
| Question | Source of truth |
|---|---|
| What source revision is deployed? | commitSha in the live content-version.json |
| What canonical normalized content subset is active? | contentHash in the live content-version.json |
| What static files were uploaded? | Live artifact-integrity.json and the corresponding workflow artifact |
| What workflow logic ran? | .github/workflows/ci.yml at the candidate SHA |
| What non-secret Pages values were reviewed? | wrangler.jsonc at the candidate SHA |
| What D1 schema should be active? | Numbered SQL under migrations/ at the candidate SHA |
| Which D1 migrations are actually active? | The selected database's D1 migration ledger in Cloudflare |
| Which static requests invoke Functions? | public/_routes.json at the candidate SHA |
| Which static headers are intended? | public/_headers at the candidate SHA |
| Do required secrets, WAF, domains, DNS, and branch rules match the intended state? | Verify the external, repository-unverified state in the Cloudflare and GitHub dashboards |
Do not infer a successful source deployment from the content hash alone. Code-only revisions can share the same hash.
- Open or update a pull request targeting
mainordevelop. - Confirm the
verifyjob generated template content and passed documentation integrity, lint, typecheck, focused footer tests, the full suite, and the static build. - Merge or push the reviewed change to
develop. - Confirm the workflow selected
deploy_target=preview, validated the preview D1 binding, applied pending preview migrations, and deployed the currentdevelopSHA. - Review
https://develop.smart-portfolio-bds.pages.devmanually. Without selecting the final Send action, confirm the three-step name, contact-details, and review flow, prepared-security status, responsive behavior, focus and error states, and standalone completion layout through the verified component coverage. This no-delivery inspection consumes no Turnstile token, D1 quota slot, or email request. - Confirm the preview workflow and the pull-request verification workflow both pass for the exact candidate before merge. The automated smoke check covers only the scope listed in Deployment.
The preview build receives only NEXT_PUBLIC_TURNSTILE_PREVIEW_SITE_KEY. If that variable is blank, the contact form remains unavailable instead of using the production key. The key should identify a Managed Turnstile widget restricted to the preview hostname; final-submit execution and interaction-only appearance are controlled by the client.
- Merge the green change into
mainthrough the protected branch flow. - Confirm the push workflow resolves the current
mainSHA. - Confirm strict workbook generation succeeds before the quality gates.
- Confirm the deploy job downloads and revalidates
cloudflare-pages-buildinstead of rebuilding. - Confirm the final branch-tip check passes before D1 migration and Wrangler.
- Confirm the production D1 target check and pending migrations succeed before Pages upload.
- Confirm the assigned-domain smoke step passes.
- Verify the public custom domain, key static routes, security headers, and the contact page manually without selecting the final Send action.
- Record the active
contentHash,commitSha, workflow run, D1 migration result, and any manual checks in the release record.
The daily schedule at 13:17 UTC checks the current main source against one newly downloaded and validated workbook snapshot.
An unchanged check still performs checkout, dependency installation, target validation, strict workbook download, normalization, hashing, and a no-cache production manifest request. It stops before documentation validation, lint, typecheck, tests, build, artifact upload, and deployment.
When the hash of the canonical normalized content subset differs, the workflow runs the complete production path. Editing workbook formatting or data outside that subset does not intentionally trigger deployment.
Use the CI workflow dispatch input:
force_deploy=true, the default, runs full verification and production deployment even when the content hash matches.force_deploy=falseuses the same content-hash comparison as the schedule and stops early when content matches.
Use a forced run for a first deployment, an intentional rebuild, a retry after a code-only failure, or recovery after correcting external configuration. Force does not skip any quality or integrity gate.
On PowerShell:
$productionOrigin = 'https://smart-portfolio-bds.pages.dev'
Invoke-RestMethod "$productionOrigin/content-version.json"
Invoke-RestMethod "$productionOrigin/artifact-integrity.json"On a POSIX shell:
production_origin='https://smart-portfolio-bds.pages.dev'
curl --fail --silent --show-error "$production_origin/content-version.json"
curl --fail --silent --show-error "$production_origin/artifact-integrity.json"Confirm:
schemaVersionis1.contentHashis 64 lowercase hexadecimal characters.commitShais the intended 40-character source revision.generatedAtanddeployedAtare parseable timestamps.- The artifact manifest uses
sha256, carries the same commit SHA, and contains file records.
Both metadata endpoints should return explicit no-store and no-cache headers.
The automated deployment smoke requests only /. For a release, use the current route registry and check the exported portfolio, contact, and legal routes. At minimum, include:
//experience/research/projects/recommendations/contact/privacy/terms/security
Check both the assigned production domain and the primary custom domain. Confirm navigation, theme switching, responsive layout, reduced-motion behavior, and expected page content.
On representative static pages, confirm the Cloudflare response includes the intended policy from public/_headers:
Content-Security-PolicyPermissions-PolicyReferrer-PolicyStrict-Transport-SecurityX-Content-Type-OptionsX-Frame-Options
The repository does not automatically smoke-test these headers after deployment.
A safe method check does not consume Turnstile or send email:
curl --include 'https://smart-portfolio-bds.pages.dev/api/contact/verify'
curl --include 'https://smart-portfolio-bds.pages.dev/api/contact'Both GET requests should return HTTP 405, application/json, and this body:
{
"ok": false,
"error": "method_not_allowed"
}Repeat against the custom domain when validating its routing. An end-to-end contact check requires an authorized test destination, a valid Turnstile session, and review of downstream delivery. Do not run it as an unattended synthetic test.
Use only an owned test mailbox and non-sensitive fixture text. In preview first, then production:
- Inspect Resend logs and the suppression list for earlier visitor-confirmation attempts before assuming the old template failed to generate a second message.
- Complete the three-step wizard, select the final Send action once, and confirm the widget remains hidden unless Cloudflare requires interaction.
- Confirm the standalone green completion view includes the entered address and offers Send another message.
- Confirm Resend accepted the visitor confirmation before the owner notification and recorded the expected separate idempotency keys.
- Inspect both HTML and plain-text messages. Confirm the visitor copy, correction instructions, absolute Privacy and Terms links, owner reply-to, and compact owner summary.
- Select Send another message, then submit a second fresh request with the same normalized address. Confirm it receives a new submission UUID and Turnstile token and succeeds.
- Submit a third new message and confirm HTTP
429, JSON errorrate_limited, a validRetry-Afterheader, a red in-page limit notice, and no Resend request. - Check D1 only through bounded aggregate or redacted queries. Confirm no raw email, name, phone, or message columns exist; do not copy hashes or UUIDs into tickets or logs.
Resend API acceptance is not proof of mailbox delivery. Confirm receipt or provider delivery status, then check bounce and suppression state. Do not use an address belonging to another person to exercise the quota.
Repository checks cannot prove these controls. The live WAF state is unverified until an operator checks the active account and exercises the edge response. Confirm in the provider dashboards:
- Both contact endpoints are covered by the intended WAF rate limit.
- The active plan supports the required response behavior. Cloudflare custom rate-limit responses require Pro or higher, but plan eligibility alone does not satisfy the prerequisite.
- On Pro or higher, both endpoint actions are explicitly configured to return the required JSON rather than an interactive challenge, and a live edge check verifies the response status, content type, and body.
- On Free, record that the baseline rate limit cannot guarantee the JSON API contract, then upgrade or use another provider-compatible control before treating the prerequisite as satisfied.
- Production and preview secrets are present in the correct environments.
- Production and preview
CONTACT_RATE_LIMIT_DBbindings point to distinct non-placeholder IDs and report the expected migration as applied. - Turnstile hostname restrictions match each environment.
- The Resend sending identity remains verified, suppression and bounce state is understood, and SPF, DKIM, and DMARC remain correct for the sender domain.
- The custom-domain and
wwwbehavior, DNS, and certificates are correct. - Cloudflare Pages Git integration and legacy automatic deployment paths remain disabled.
The schedule starts two independent jobs:
verify, which performs the content comparison and deploys only when required.automation-heartbeat, which checks activity and may refresh its isolated branch.
The heartbeat does not depend on the verify result and does not deploy. It writes only after 30 days without newer activity on main or automation-heartbeat. Its contents: write permission is intentionally isolated from the verification and deployment jobs.
An unchanged content check is a successful no-op, not a skipped or failed schedule. Investigate a missing scheduled run through GitHub Actions state rather than assuming the workbook has not changed.
Use the first failing stage to decide whether production could have changed:
| Failure stage | Production impact | First action |
|---|---|---|
| Candidate marked stale | None | Review the newer branch run |
| Target or content validation | None | Correct configuration or workbook data |
| Documentation, lint, typecheck, or tests | None | Fix the reported repository failure |
| Static build or manifest creation | None | Reproduce locally with the documented command |
| Artifact upload, download, or verification | None | Inspect artifact transfer and candidate SHA |
| Branch recheck | None | Review the newer branch run |
| D1 target validation | None | Restore the reviewed database ID or correct environment isolation before retrying |
| D1 migration | The failing migration is rolled back; an earlier migration may already be active | Inspect the selected database and migration ledger before retrying |
| Wrangler | Usually prior deployment remains active | Check Cloudflare deployment history before retrying |
| Post-Wrangler smoke | Candidate deployment exists and may already serve through the stable alias; the failed job does not roll it back | Inspect live metadata and Cloudflare history immediately |
Detailed failure signatures and corrective commands are in Troubleshooting.
There is no automatic rollback in .github/workflows/ci.yml.
When the intended prior deployment is still present in Cloudflare Pages history:
- Confirm the target deployment's commit and content metadata before changing production.
- Use the authorized Cloudflare rollback or promotion control.
- Verify the assigned domain, custom domain, metadata files, static routes, headers, and contact method contracts.
- Open a repository or content fix so the next normal deployment does not restore the faulty state.
Provider rollback procedures and retention are external Cloudflare behavior and should be confirmed against the active account before an incident.
A Pages rollback does not revert D1 migrations. Before rolling back to older Function code, confirm that the active schema remains backward compatible. Database restoration or destructive corrective SQL is a separate operation that requires explicit review and should not be inferred from a Pages rollback.
When provider rollback is unavailable or a corrected release is preferable:
- Restore the intended repository source or workbook content.
- Run local verification.
- Merge the source correction to
mainwhen needed. - Dispatch the workflow with
force_deploy=true. - Complete all post-deployment checks.
The Actions artifact is retained for one day and is not a durable release archive. Production workbook snapshots are transient build inputs, so repository history alone may not reproduce an earlier content deployment.
When rotating a contact or Cloudflare credential:
- Create the replacement with the narrowest practical scope.
- Update only the correct GitHub or Cloudflare environment.
- Run a preview check when the provider supports isolated preview credentials.
- Perform a forced green production deployment if the browser build value changed.
- Validate the contact flow with an authorized test.
- Revoke the superseded credential after the new path is confirmed.
- Do not print values in logs, issues, screenshots, or documentation.
Changes to NEXT_PUBLIC_TURNSTILE_SITE_KEY or its preview counterpart require a new static build. Changes to server-only Pages secrets do not alter the static artifact, but still require runtime verification.
Rotating TURNSTILE_SECRET_KEY also changes both the verification-ticket signing key and the domain-separated quota HMAC key. Outstanding tickets become invalid, and reservations made with the prior derived key will not match new submissions even though they remain until expiry cleanup. Schedule the rotation with abuse monitoring and accept that the address quota may temporarily allow new slots during the overlap.
- CI validates only the assigned
pages.devaliases, not the custom domain. - Automated smoke checks do not exercise valid POST requests, D1 queries, DNS validation, or downstream delivery.
- Live D1 binding and migration state, WAF, Cloudflare plan, DNS, TLS, custom-domain, provider-secret, and branch-protection state are external and unverified by repository tests.
- The content comparison is hash-only and does not prove that the active commit matches
main. - A successful Wrangler upload creates the candidate deployment before smoke runs. A later smoke failure does not undo or automatically roll back that deployment.
- There is no uptime monitor, performance monitor, or synthetic browser test in the repository.
- GitHub's one-day artifact retention is not a long-term recovery mechanism.