Skip to content

Repository files navigation

cpp.social feedback

GitHub Discussions-backed feedback controls for cpp.social. The browser runtime is published to https://feedback.cpp.social; the API runs at https://feedback-api.cpp.social.

Deployment

GitHub Pages is built and deployed by .github/workflows/pages.yml. The Pages artifact contains the example at /example/, the versioned runtime under /v1/, and the OAuth callback at the exact registered URI:

https://feedback.cpp.social/v1/oauth/callback.html

The VPS checkout must be at /srv/feedback. Before enabling the NixOS configuration, place these non-empty, non-group/world-writable files in /srv/feedback/config/secrets/:

  • github-app-private-key.pem
  • github-client-secret
  • oauth-state-hmac-key

The secret files must be readable by UID 10001 in the container. Root-owned mode 0444 is suitable because Docker bind-mounts them read-only. The application configuration itself is committed as config/sites.toml.

The GitHub App must be public (installable by any account), have Discussions read/write permission, and register the exact callback URL https://feedback.cpp.social/v1/oauth/callback.html. A private GitHub App shows GitHub's 404 page to users who do not own it, before this service receives a callback. After adding or changing the Discussions permission, the repository owner must approve the installation's requested permission update. Existing browser sessions should then log out and sign in again so their user token is issued with the current installation permissions. A Resource not accessible by integration mutation error is not, by itself, proof that this permission or approval is missing. In particular, GitHub may reject addUpvote for a GitHub App user token even when comments and reactions work. This deployment currently uses GitHub App user tokens; native upvote writes remain unverified and must not be represented as working until tested with this token type.

Production SQLite files are persisted in the checkout's db/ directory. The deployment bind-mounts /srv/feedback/db to /data in the container, so the site databases are /srv/feedback/db/<site-id>.sqlite3. They are ignored by Git and survive container rebuilds and replacement.

The process is configured with explicit command-line arguments rather than environment variables. python -m feedback --help lists the config, secret-file, listener, proxy-trust, concurrency, and keep-alive options. Docker Compose passes those arguments directly and uses fixed read-only mounts.

The vps-8def0ca8 NixOS configuration imports and enables the feedback service. Rebuilding that host builds the image from /srv/feedback, starts it with Docker Compose, and provisions the feedback-api.cpp.social nginx virtual host and ACME certificate. No .env file is required.

Useful verification commands on the VPS:

systemctl status feedback-service
curl --fail --silent --show-error https://feedback-api.cpp.social/

Site mappings and metadata

Each site maps a browser resource to one GitHub Discussion. The configured mapping selects the value used as the discussion lookup term and title:

  • key: the stable application-provided resource.key (recommended when URLs or titles may change).
  • title: the resource title.
  • url: the complete canonical URL, including its origin.
  • pathname: only the canonical URL path, allowing equivalent pages on multiple origins to share a discussion.
  • custom: an arbitrary consumer-provided string for custom routing schemes.
  • number: the numeric number of an already existing GitHub Discussion. Unlike key, it is not a resource identifier and missing discussions are never created.

The browser package exports resourceFromDocument(). By default it reads the first non-empty title from meta[property="og:title"] and then <title>, reads link[rel="canonical"] when present, and otherwise uses the current location. Consumers can override titleSelectors and canonicalSelector, or construct a Resource directly and supply custom, pathname, or number. Metadata selection is intentionally client-side; the service only receives validated resource values and applies the site mapping.

const resource = resourceFromDocument({
  key: "articles/stable-id",
  titleSelectors: ['meta[name="feedback-title"]', 'meta[property="og:title"]', "title"],
  canonicalSelector: 'link[rel="canonical"]',
  custom: document.body.dataset.feedbackKey ?? "default-feedback-key",
});

The bundled example exercises multiple threads, batched native upvotes, main-post reactions, labels, polls, comments, minimized/deleted comments, replies, accepted answers, author associations, and GitHub links. Supply comma-separated resource keys with keys:

https://feedback.cpp.social/example/?site=feedback-cpp-social&keys=feedback%2Fexample%2Cfeedback%2Fexample-two&github=link

Add firstPost=hidden when the host page already represents the discussion's opening post. This frontend-only option keeps post reactions, native upvotes, poll controls, and comments visible. title=hidden also starts with the discussion title hidden. The example toolbar can toggle the title, root post, metadata, poll, and post actions without another request configuration.

API and intent configuration

Every site must list its required intents. Disabled intents return 404 and do not activate their refresh paths. The route contract, intent matrix, and audit of every server-side GitHub request path are in docs/API.md.

The browser runtime sends user-specific viewer queries, native upvotes, and comments directly to GitHub. The service handles OAuth, discussion discovery/creation, and shared anonymous reads where caching prevents every visitor consuming a GitHub request.

Use --verbose for privacy-safe route/status/timing and cache-decision logs. It does not log credentials, OAuth codes, bodies, origins, resource URLs, or client addresses.

Counter cache and discussion content

The service database keeps discussion identifiers and aggregate counters only. Discussion/comment bodies are never persisted or reused between requests, preventing deleted content from being served from a stale cache. Only concurrent reads for the same thread share an in-flight GitHub call.

cache_fresh_seconds is the age at which a requested tracked counter needs an authoritative GitHub refresh. The default is five seconds. A batched request refreshes only stale resources; recently refreshed resources in the same request are served from SQLite. For example, if A was last refreshed 30 seconds ago and B three seconds ago, requesting [A, B] sends only A's discussion ID to GitHub and returns B from SQLite in the same response.

refresh_cooldown_seconds is the minimum delay before retrying a refresh attempt for the same resource. It primarily prevents repeated GitHub calls after a failed or concurrent attempt. Concurrent requests also join the same in-flight site batch. The default is five seconds.

refresh_sweep_seconds controls the low-priority full maintenance cycle. The default is 86400 seconds (daily). The service walks only discussions whose last authoritative snapshot is that old, in batches of 50 with pacing between full batches. Targeted requests and successful upvotes continue independently.

Browser mutations do not write speculative server values. The next successful targeted or maintenance refresh replaces counters with GitHub's absolute counts. Counter responses use no-cache, so browsers revalidate with this service; that does not imply a GitHub request while the relevant snapshot remains fresh.

The browser runtime keeps the last counter snapshot and the last confirmed viewer native-upvote state in local storage for up to seven days. Consumers can render them synchronously while the API request is in flight, avoiding a flash of zero counters or unselected vote buttons. Snapshots contain only the site/resource key, discussion node ID, counts, viewer state, and save time; authentication tokens are not part of this cache. Storage is optional and failures fall back to the network normally.

Once authenticated, the runtime also resolves the current user's native-upvote state for all visible discussions in one batched GitHub query. That viewer-specific snapshot is reused for five minutes, including across reloads, and then refreshed on demand. This makes reactions created on GitHub or another device visible without turning every counter request into an authenticated GitHub request.

Operational logs are emitted at GitHub boundaries rather than for every HTTP request. Reaction-refresh lines include the site, trigger (requested or sweep), batch size, updated-row count, and duration. Failures include safe GitHub status/request IDs where available. Discussion discovery/creation, OAuth failures, rejected creation grants, one-off GitHub App bearer retries, vote failures, startup, and sweep summaries are also logged. Client IPs, origins, resource URLs, authorization codes, and tokens are not logged.

Local checks

python -m pytest -q
cd runtime && npm ci && npm run check && npm run lint && npm test && npm run build
docker compose -f compose.deploy.yaml config

About

bridges github discussion feedback into cpp.social

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages