# 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`](./DEPLOYMENT.md) (Render) and [`AMPLIFY_DEPLOY.md`](./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):** ```bash 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): ```sql 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): ```bash 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: ```bash 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 `/sites/default/ext/`). The directory must be named exactly `webform-mw` (matches `` in `info.xml`). 2. `Administer → System Settings → Extensions → Add new → Refresh`, then **Install** next to "WebForm-mw". 3. Configure — add to `civicrm.settings.php`: ```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`](./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: ```bash 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`](./DEPLOYMENT.md#first-time-render-setup). 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=&cs=` — the form should load with prefill. 4. Submit a stub answer. Then in Civi, pull the new activity: ```bash 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.