Skip to content

ENGAGE EAGLE Integration

Daniel Truong edited this page Oct 8, 2026 · 6 revisions

ENGAGE → EAGLE Integration

EPIC runs two products in separate OpenShift namespaces:

Product Namespaces Purpose
ENGAGE (epic-engage) c72cba-{dev,test,prod} Staff create and manage public engagement sessions
EAGLE (eagle-api and frontends) 6cdc9e-{dev,test,prod} Public EA platform; citizens browse projects and comment

Engagements flow ENGAGE to DEMI to EAGLE. ENGAGE pushes each engagement to DEMI with PUT /engage/engagements/:engagementId through APIM (subscription engage). DEMI owns those rows. DEMI's sync-out (Storage queue sync-out, a worker, and an Eagle consumer) writes them into EAGLE as comment periods marked isMet: true, through eagle-api's protected /api/commentperiod endpoints. The consumer signs in with Keycloak client demi-sync-out (client credentials, realm role staff). Nothing writes EAGLE's MongoDB directly. eagle-api mirrors every comment period write back to DEMI, and for an ENGAGE-owned row DEMI accepts only the Eagle-owned fields. The Eagle consumer is removed when eagle-public's Angular line retires.

Traffic from ENGAGE is one-way: EAGLE never calls ENGAGE. For the sync-out worker see the DEMI wiki pages Sync-Architecture and ADR-014-Sync-Out.

Sync target

EPIC_SYNC_TARGET in met-api and met-cron selects where ENGAGE pushes. The sections below describe the payload and calls for the eagle path, a direct push to eagle-api.

Value ENGAGE pushes to
eagle eagle-api only. Default; unknown values fall back to it
both eagle-api and DEMI
demi DEMI only

Every value needs EPIC_URL, because ENGAGE asks eagle-api whether a project id is a project or a project notification. DEMI is reached with DEMI_API_URL and DEMI_API_KEY. met-cron loads the same EPIC config as met-api, so scheduled publish and close-out push too.

The manual job ENGAGEMENT_DEMI_BACKFILL (python3 invoke_jobs.py ENGAGEMENT_DEMI_BACKFILL in the met-cron pod) loads existing engagements into DEMI. It refuses to run unless IS_EAO_ENVIRONMENT is true and EPIC_SYNC_TARGET is both or demi.

The push is gated on IS_EAO_ENVIRONMENT and on the engagement's metadata carrying a project_id. Without either, ENGAGE skips the call.

Direct push flow (eagle and both)

sequenceDiagram
    participant Staff as EAO staff
    participant Engage as ENGAGE met-api
    participant KC as Keycloak
    participant API as eagle-api
    participant DB as MongoDB

    Staff->>Engage: Create, edit, publish or delete engagement
    Engage->>KC: Service account grant
    KC-->>Engage: Bearer token

    alt No tracking id stored
        Engage->>API: POST /api/commentperiod
        API->>DB: Insert CommentPeriod (isMet true)
        API-->>Engage: 200 { _id }
        Engage->>Engage: Store _id as project_tracking_id
    else Tracking id stored
        Engage->>API: PUT /api/commentperiod/{tracking_id}
        API->>DB: Update CommentPeriod
    else Engagement deleted
        Engage->>API: DELETE /api/commentperiod/{tracking_id}
        API->>DB: Remove CommentPeriod
    end
Loading

project_tracking_id is stored on EngagementMetadataModel after the first successful POST, and every later call uses it to build the PUT or DELETE URL. If it is absent, the next sync creates a new comment period with a POST.

Sync triggers

Every one of these calls ProjectService.update_project_info(), except the last:

Event ENGAGE code eagle-api call
Engagement created engagement_service.create_engagement() POST
Engagement edited, published or unpublished engagement_service.edit_engagement() PUT
Scheduled auto-publish engagement_service.publish_scheduled_engagements() PUT
Engagement closed when due engagement_service.close_engagements_due() PUT
Engagement slug changed engagement_slug_service.update_engagement_slug() PUT
Engagement metadata created or updated engagement_metadata_service.create_metadata(), update_metadata() POST or PUT
Engagement deleted engagement_service.delete() → delete_from_epic() DELETE

Payload

Built by ProjectService._construct_epic_payload().

Field Type Notes
isMet bool Always true. Marks the comment period as ENGAGE-managed
isPublished bool false for Draft and Unpublished, true for every other status
metURL string Public ENGAGE engagement URL, shown to citizens
metURLAdmin string Staff ENGAGE admin URL, shown in eagle-admin
metBannerImageUrl string Banner image from ENGAGE object storage
dateStarted ISO date Engagement start, UTC
dateCompleted ISO date Engagement end, UTC
instructions string Engagement description
informationLabel string Engagement name
commentTip string Always empty
milestone ObjectId From the EPIC_MILESTONE setting
openHouse string Always empty
relatedDocuments string Always empty
project ObjectId eagle-api Project _id

ENGAGE sends openHouse; eagle-api reads openHouses. The value is always empty, so nothing is lost, but the two names do not match.

Visibility

eagle-api stores isPublished on the CommentPeriod document and derives the read array from it on both POST and PUT:

  • isPublished: true → read is ['public', 'staff', 'sysadmin'], so citizens see the comment period in eagle-public.
  • isPublished: false → read is ['staff', 'sysadmin'], staff only.

If a caller omits isPublished, eagle-api falls back to whether the incoming read array contains 'public'.

ENGAGE derives the flag from engagement status:

ENGAGE status isPublished sent Publicly visible
Draft false No
Scheduled true Yes, as pending
Published true Yes, as open
Closed true Yes, as closed
Unpublished false No

Project notifications

A project notification has no separate CommentPeriod document. ENGAGE writes the commenting fields onto the ProjectNotification document instead.

ENGAGE first probes GET /api/project/{id}; a non-200 means the id is a project notification and GET /api/projectNotification/{id} is used instead. The update is PUT /api/projectNotification/{id}?publish={true|false} carrying dateStarted, dateCompleted, isMet, metURL and a pcp state derived from the current time against the engagement dates:

Condition pcp
Engagement not publicly visible none
Before the start date pending
After the end date closed
Otherwise open

Deleting the engagement clears the same fields — pcp: 'none', isMet: false, empty metURL, null dates, publish=false — without deleting the notification document.

Authentication

ENGAGE authenticates to eagle-api with a Keycloak service account, obtaining a token by password grant. Credentials live in the epic-keycloak-details Secret in the c72cba namespace, under EPIC_KC_CLIENT_ID, EPIC_KEYCLOAK_SERVICE_ACCOUNT_ID, EPIC_KEYCLOAK_SERVICE_ACCOUNT_SECRET and EPIC_JWT_OIDC_ISSUER.

EPIC_URL in the met-api ConfigMap holds the target environment's /api/commentperiod URL and is what points a given ENGAGE namespace at the matching EAGLE namespace. The project-notification calls derive their base URL by stripping the last path segment from it.

eagle-admin behaviour

Clicking an isMet comment period that has a metURLAdmin — in the comment period table or in the project banner — opens that ENGAGE admin URL in a new tab instead of navigating to the eagle-admin detail page. That redirect is what keeps ENGAGE-managed periods out of eagle-admin's editor; eagle-admin's own create, edit and delete calls are generic and carry no isMet check.

Publish and unpublish stay available through PUT /api/commentperiod/{id}/publish and PUT /api/commentperiod/{id}/unpublish.

eagle-public behaviour

eagle-public (Angular line) reads comment period data from DEMI and makes no ENGAGE API call at runtime. The site is read-only: it has no comment form, file upload or CAC sign-up. Comments are taken in ENGAGE only. A comment period with isMet and an http or https metURL shows a link to ENGAGE that opens in a new tab. Legacy comments are stored in DEMI and read through /demi-search/search?dataset=Comment.

Related

Clone this wiki locally