Files
WebForm-mw/PRODUCTION_CUTOVER.md
T
Joel Brock e4085aa0f3 Lightbox: pin and size to visible viewport when framed
The staff-report iframe is expanded to full content height (no nested
scrollbar), so a modal <dialog> centered in its viewport landed in the
middle of the whole document and sat off-screen unless the parent page
was scrolled there. Cross-origin means the iframe can't read the
parent's scroll position itself.

Tab.tpl now broadcasts the iframe's visible slice (webform-mw-viewport,
rAF-throttled on scroll/resize/load and after each height change), and
AttachmentLightbox pins to that slice and fills its height. Falls back
to a fixed box when no viewport message arrives (older extension), and
standalone mode keeps native centering at a taller 85vh.

Extension bumped to 0.3.1 (template change -> re-copy + cv flush on
prod). PRODUCTION_CUTOVER.md change log updated.
2026-06-19 08:03:40 -07:00

12 KiB

Production cutover guide

Step-by-step CiviCRM-side checklist to recreate the Co-op Survey environment on a new CiviCRM (e.g. when moving from client.crm.fci.coop to production crm.fci.coop). The Next.js app deploy is covered separately in DEPLOYMENT.md (Render) and AMPLIFY_DEPLOY.md (AWS Amplify). This document captures everything that must exist inside CiviCRM for those deployments to work.

Keep this file current — every time we discover a manual Civi-side prerequisite, add it here so the next environment cutover is a single read-through.


0. Prerequisites on the target CiviCRM

  • CiviCRM 5.50+ on PHP 7.4 or 8.x.
  • A service-user contact with an API key generated (Contact view → edit → API Key field). The same user's permissions are what the app runs as.
  • The site's CIVICRM_SITE_KEY from civicrm.settings.php.
  • If CiviCRM sits behind webserver-level Basic Auth, credentials for it.

These three (CIVI_BASE_URL, CIVI_API_KEY, CIVI_SITE_KEY) are the core env vars the app needs.


1. Activity type and custom field groups

The form writes one activity per submission, of type Check-in (organizing) (machine name, exact spelling).

That activity type must exist with the six custom field groups attached, all extending Activity filtered to this single activity type:

Custom group machine name Stage
Check_in_data__organizing_ 0 (always-on survey data + the Stage snapshot field)
Stage_1 1 — Convene & Prepare
Stage_2 2 — Grow & Plan
Stage_3 3 — Connect & Gather
Stage_4 4 — Excite & Build
Stage_5 5 — Fulfill & Stabilize

Field names within those groups are referenced explicitly by config/form.ts (search for civiField:). If you renamed any field on the source CRM, mirror the change here or update the config.

Sanity check after import: Activity → Search for any existing Check-in (organizing) row and confirm every custom field renders.


2. The Stage custom field default — must be cleared

Custom field id 242 (label "Stage", inside the Check_in_data__organizing_ group) must have an empty default value. The form is intentionally not the authority on stage; staff set Stage on their own stage-change check-ins, and /api/data derives the org's current stage from the most recent stage-bearing activity.

If a default is set (the original config had "Unknown" as the default), every form-submitted activity gets stamped with that value and contaminates the stage-derivation logic.

The Civi admin UI's dropdown does not allow you to select "blank", so clear it via API or SQL:

Option A — API (preferred):

cv api4 CustomField.update \
  where='[["id","=",242]]' \
  values='{"default_value":null}'

Or in the API Explorer v4 (Support → Developer → API Explorer v4):

Entity: CustomField
Action: update
where:  [["id","=",242]]
values: {"default_value": null}

Option B — direct SQL (only if the API isn't handy):

UPDATE civicrm_custom_field SET default_value = NULL WHERE id = 242;

Then cv flush or Administer → System Settings → Cleanup Caches.

Cleanup of historical bad data (optional, only if production was running with the bad default for a while):

cv api4 Activity.update \
  where='[["activity_type_id:name","=","Check-in (organizing)"],
          ["Check_in_data__organizing_.Stage","=","Unknown"]]' \
  values='{"Check_in_data__organizing_.Stage": null}'

Verify: pull one activity back and confirm Stage is null:

cv api4 Activity.get \
  where='[["activity_type_id:name","=","Check-in (organizing)"]]' \
  select='["id","Check_in_data__organizing_.Stage"]' \
  limit=5

3. Framework Stage option group on Organizations

Each Organization contact has a custom field that holds its current Framework Stage. The option group backing it (referenced by STAGE_OPTION_GROUP_ID in config/form.ts, currently 75) must contain these six values, spelled exactly:

  • Inquiry
  • Organizing
  • Feasibility
  • Business feasibility
  • Store Implementation
  • Stabilize newly opened co-op

If the option group id differs in the target CRM, update STAGE_OPTION_GROUP_ID in config/form.ts and redeploy.


4. Relationship type: form-filler → organization

The form resolves "what org is this submission about?" by looking up the form-filler's Primary Contact relationship to an Organization. The relationship type must exist with:

  • name_a_b = Primary Contact
  • side A = Individual
  • side B = Organization

This is the value of FORM_CONTACT_RELATIONSHIP in config/form.ts. If you use a different relationship type in the target CRM, update that constant and redeploy.

Per-org setup (this is the recurring operational step): for each Organization that should receive a survey, create exactly one active Primary Contact relationship from the staff/board contact who will fill out the form to that Organization.

  • Zero active relationships → the form returns No active "Primary Contact" relationship found for your contact.
  • More than one active relationship → the form refuses with multiple active … relationships; staff must resolve before this link will work. (Deactivate the older ones.)

5. Install the webform-mw Civi extension

The extension does two jobs:

  • adds an Engagement Report tab to Organization contact pages that embeds the staff report in an iframe; and
  • exposes the file-upload proxy route civicrm/webform-mw/upload (handled by CRM_WebformMw_Page_Upload) that the app's /api/upload POSTs binary files to. This route was added in v0.3.0 — a CRM running an older version has the report tab but every form file upload returns 502 Bad Gateway (the app can't reach the route, so /api/upload fails closed). Production must be on v0.3.0 or later.
  1. Copy WebForm-mw/civi-extension/webform-mw/ to the CRM's [civicrm.extensionsDir] (usually <civi-root>/sites/default/ext/). The directory must be named exactly webform-mw (matches <key> in info.xml). When upgrading an already-installed extension, overwrite the existing directory in place.

  2. Administer → System Settings → Extensions → Add new → Refresh, then Install next to "WebForm-mw" (first install) or run the Upgrade action if one is offered. If neither, Disable then Enable the extension.

    Then flush cachescv flush, or Administer → System Settings → Cleanup Caches. New menu routes (like civicrm/webform-mw/upload) are only registered after the router is rebuilt; copying files without a flush leaves the upload route 404ing and uploads 502ing.

    Confirm the upload route resolves (a 400 means the route is live; a 404/login redirect means the flush didn't take):

    curl -s -X POST https://crm.fci.coop/civicrm/webform-mw/upload
    # → {"error":"Missing file part"}   ✓ route exists
    
  3. Configure — add to civicrm.settings.php:

    define('WEBFORM_MW_APP_URL',   'https://survey.fci.coop');
    define('WEBFORM_MW_STAFF_KEY', '...same value as STAFF_REPORT_KEY in the app env...');
    

    WEBFORM_MW_STAFF_KEY must equal the STAFF_REPORT_KEY env var on the Next.js app. Rotate them together.

  4. Confirm: open any Organization contact in CiviCRM — there should be an Engagement Report tab. Click it; the staff report should load with no nested scrollbar (the extension auto-sizes the iframe via postMessage).

Full extension docs: civi-extension/webform-mw/README.md.


6. CSP / frame-ancestors — app side

The Next.js app's /staff/report and /api/staff/file routes must allow every CiviCRM origin that will iframe them, or the browser will refuse to render.

Set CIVI_FRAME_ALLOWED_ORIGINS (comma-separated) in the app deploy env. Each origin needs the scheme:

CIVI_FRAME_ALLOWED_ORIGINS=https://crm.fci.coop,https://client.crm.fci.coop

Include both prod and any staging Civi origins you want to keep embedding. If unset, the build falls back to the origin of CIVI_BASE_URL (single-Civi compatibility).

Confirm after deploy:

curl -sI https://survey.fci.coop/staff/report | grep -i content-security-policy

Should include frame-ancestors 'self' https://crm.fci.coop https://client.crm.fci.coop (or whatever list you configured). If you see only one origin and the other Civi is failing to embed, the env var is missing or stale — trigger a new build, not just a restart.


7. App env vars (cross-reference)

The full list lives in DEPLOYMENT.md. Production-specific reminders:

  • CIVI_BASE_URL — production CRM, not the client/staging one.
  • STAFF_REPORT_KEY — long random secret, identical to the WEBFORM_MW_STAFF_KEY in civicrm.settings.php (step 5).
  • HEALTH_TOKEN — set this in production, or /api/health returns 404. Use openssl rand -hex 32.
  • Confirm none of the three core CIVI_* vars are missing — the app refuses to start in NODE_ENV=production if any are unset.

8. Post-deploy verification

Run these against the production deploy:

  1. curl https://survey.fci.coop/healthz{"ok":true,"service":"coop-checkin"}

  2. curl 'https://survey.fci.coop/api/health?token=$HEALTH_TOKEN' → all checks green. This validates Civi connectivity, the Primary Contact relationship type, the Check-in (organizing) activity type, all six custom groups, the Stage option group, and the option-group ID match.

  3. Pick a test Individual contact that has a Primary Contact relationship to a test Organization. Generate a Civi checksum for that Individual and load: https://survey.fci.coop/?cid=<id>&cs=<checksum> — the form should load with prefill.

  4. Submit a stub answer. Then in Civi, pull the new activity:

    cv api4 Activity.get \
      where='[["activity_type_id:name","=","Check-in (organizing)"]]' \
      orderBy='{"id":"DESC"}' limit=1 \
      select='["id","subject","Check_in_data__organizing_.Stage"]'
    
    • Stage must be null (not "Unknown"). If it's "Unknown", step 2 above wasn't completed on this CRM.
    • subject should be Co-op Survey (form submission).
  5. Open the target Organization in CiviCRM → Engagement Report tab. The staff report should render the submission you just made.

  6. On the form, attach a file to any file field. It should upload without error. A 502 here means the webform-mw extension on this CRM is older than v0.3.0 (or the post-upgrade cache flush was skipped) — see step 5.


Change log

When you discover a new manual prerequisite, append a short note here so the rationale survives.

  • 2026-06-08 — Documented the field-242 "Unknown" default issue after a production submission was stamped Stage = "Unknown". Cleared via CustomField.update; see step 2.
  • 2026-06-18 — Extension bumped to v0.3.1 (lightbox fix). The staff-report iframe is expanded to full content height, so the attachment lightbox (a modal inside the iframe) centered in the whole document and sat off-screen unless the parent page was scrolled to the middle. Tab.tpl now broadcasts the iframe's visible slice (webform-mw-viewport) and the app pins/sizes the lightbox to it. Re-copy the extension to prod and cv flush (template change) — same procedure as step 5; no settings change.
  • 2026-06-16 — Step 5 now states the file-upload feature requires extension v0.3.0+ (the civicrm/webform-mw/upload proxy route) and documents the upgrade-vs-first-install path plus the mandatory cache flush. Surfaced when production uploads returned 502: prod Civi still had v0.1.0, which has the Engagement Report tab but not the upload route, so /api/upload couldn't reach it and failed closed. Added a post-deploy upload check as step 6 of section 8.
  • 2026-06-16 — Step 6 split off CIVI_FRAME_ALLOWED_ORIGINS as a separate env from CIVI_BASE_URL. Surfaced after the production cutover hit a frame-ancestors block: the app's CSP only listed the staging Civi origin (derived from CIVI_BASE_URL), so prod (crm.fci.coop) couldn't iframe survey.fci.coop. The new var takes a comma-separated list so one app deploy can be embedded by both dev and prod Civi.