High-level structure
OpenNotes::Client
OpenNotes::Client wraps Faraday and implements the headers and auth
contract:
X-API-Key— service account key, sent on every request.Authorization: Bearer <token>— GCP identity token, added automatically when running on Cloud Run (viaOpenNotes::GcpAuth).X-Adapter-Platform,X-Adapter-User-Id,X-Adapter-Username,X-Adapter-Trust-Level,X-Adapter-Admin,X-Adapter-Moderator,X-Adapter-Scope— per-request user identity headers added when auser:argument is passed. These map to the scopes concept in the public API contract.
PUBLIC_API_PREFIX = "/api/public/v1", defined in
lib/opennotes.rb. The client retries on 429, 500, 502, 503, and 504 with exponential
backoff (up to 3 retries).
Sidekiq jobs
- SyncPostToOpennotes (regular)
- SyncFlagToOpennotes (regular)
- SyncScoringStatus (scheduled, every 5 min)
Triggered by
DiscourseEvent.on(:post_created) and (:post_edited) in plugin.rb.Flow:- Maps the Discourse
Postto an Open Notes request payload viaOpenNotes::PostMapper. - POSTs to
/api/public/v1/requests. - Reads
moderation_actionfrom the response and branches:auto_hide/immediate_action→ hides the post, creates aReviewableOpennotesIteminretro_reviewstate (Tier 1).community_review→ creates aReviewableOpennotesIteminunder_reviewstate (Tier 2).
- Stores the
opennotes_request_idinpost.custom_fieldsfor later correlation.
ReviewableOpenNotesItem
ReviewableOpennotesItem subclasses Discourse’s Reviewable and adds an Open Notes-specific state
machine stored in reviewable.payload.
States:
| State | Meaning |
|---|---|
pending | Created, not yet picked up |
under_review | Tier 2: awaiting community votes |
auto_actioned | Intermediate before retro_review (rarely used) |
retro_review | Tier 1: post was auto-hidden; community can overturn |
consensus_helpful | Community agreed — staff confirmation pending |
consensus_not_helpful | Community disagreed — cleared |
action_confirmed | Staff confirmed the consensus action |
action_overturned | Community voted to reverse the auto-hide |
staff_overridden | Staff acted directly, bypassing consensus |
resolved | Terminal: action complete |
restored | Terminal: post was hidden and then restored |
dismissed | Terminal: item ignored |
build_actions method exposes Agree, Disagree, Ignore, and Escalate actions to
staff in the Discourse review queue, each calling the corresponding /api/public/v1/... endpoint
via OpenNotes::Client.
Ember frontend
The plugin injects UI into Discourse via outlet connectors and Glimmer components:connectors/topic-above-post-stream/opennotes-review-banner.gjs— renders the “Under Review” or “Auto-Hidden” banner above the post stream, driven byopennotes_statusserialized onto the topic view.connectors/post-menu/opennotes-post-badge.gjs— renders the consensus result badge (“Community Reviewed”, “Reviewed — No Action”) in the post action menu.components/community-review-panel.gjs— the full review panel shown at/community-reviews, including the vote widget.components/vote-widget.gjs— Helpful / Not Helpful voting UI, callsPOST /voteon the community reviews controller.components/opennotes-admin-dashboard.gjs— activity metrics panel in the admin area.
opennotes_status field is injected into the topic-view and post serializers in plugin.rb
via add_to_serializer, so the Ember layer never needs to make a separate API call to learn a
post’s review state.