Files
WebForm-mw/PRODUCTION_CUTOVER.md
T
Joel Brock 5124010b8a Lightbox: fix proxy 404, cap modal size, allow multi-Civi embed
Three quick fixes off first-deploy testing:

1. /api/staff/file 404'd for valid files. Refactor fileBelongsToOrg
   to SELECT the org's and activities' file columns and JS-compare
   instead of WHERE ... OR with custom field refs (APIv4 fragility
   around nested OR + dotted custom fields). Same ownership probe,
   same shape /api/staff/report itself uses to read file values.

2. Lightbox ballooned to full report height because the staff iframe
   auto-grows to fit content (often 3000+ px). Cap to a fixed
   640px x min(92vw, 900px) box so it stays a reasonable preview
   regardless of iframe document size.

3. Production frame-ancestors blocked crm.fci.coop from iframing
   survey.fci.coop -- the CSP only included the dev Civi origin
   derived from CIVI_BASE_URL. Add CIVI_FRAME_ALLOWED_ORIGINS
   (comma-separated) so one app deploy can be embedded by both
   dev and prod Civi. Falls back to CIVI_BASE_URL for single-Civi
   compatibility.

PRODUCTION_CUTOVER.md updated inline and in the change log.
2026-06-16 16:15:28 -07:00

10 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 adds an Engagement Report tab to Organization contact pages that embeds the staff report in an iframe.

  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).

  2. Administer → System Settings → Extensions → Add new → Refresh, then Install next to "WebForm-mw".

  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.


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-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.