Add PRODUCTION_CUTOVER.md and link from DEPLOYMENT.md

Cross-references the Civi-side cutover checklist from the deploy doc
so anyone deploying knows the CiviCRM prerequisites are tracked in a
single place.
This commit is contained in:
Joel Brock
2026-06-09 12:09:00 -07:00
parent 4ca3c194d7
commit 9341f67231
2 changed files with 284 additions and 0 deletions
+6
View File
@@ -3,6 +3,12 @@
This app is built to deploy to **[Render](https://render.com)** as a Web
Service via the included `render.yaml` blueprint.
For the **CiviCRM-side** setup that must happen before this app can
work (extension install, custom-field defaults, relationship types,
option groups, post-deploy verification), see
[`PRODUCTION_CUTOVER.md`](./PRODUCTION_CUTOVER.md). Keep that doc
current — every Civi prerequisite we discover belongs there.
## Pre-flight checklist
Before deploying, confirm:
+278
View File
@@ -0,0 +1,278 @@
# 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
`<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`:
```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` route must allow the CiviCRM origin
in its `frame-ancestors` CSP, or the iframe will refuse to render.
The build reads `CIVI_BASE_URL` and adds its origin to the CSP
automatically — so make sure `CIVI_BASE_URL` on the app deploy points
at the **production** CRM origin (`https://crm.fci.coop`), not
`client.crm.fci.coop`.
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` (or
whatever your production CRM origin is).
---
## 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=<id>&cs=<checksum>` — 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.