Community group telemetry backend
Scope
This backend implements the aggregate-only slice of epic #176. A VRDex-owned VRChat account observes a connected group's member count and visible group instances. It never accepts a customer's VRChat credentials and does not store instance user lists, usernames, or user IDs.
Control plane
communityVrchatIntegrations is the community-owned lifecycle record. Connect, disconnect, private reads, publication settings, and event association require manage_integrations. Connect allocates one healthy account only while assignedGroupCount < capacity - reservedHeadroom; Convex mutation serialization prevents concurrent over-allocation. The same VRChat group cannot be active on two community profiles. Disconnect immediately disables collection and public fields, then a fenced disconnecting assignment makes the service account leave the group before releasing account capacity. Reconnect resets freshness and opens a new telemetryEpochStartedAt; private and public projections filter observations, sessions, coverage, associations, and rollups to that epoch so a previous group's retained history cannot appear under the new connection. Ingestion also refuses to reuse an open session from an older epoch.
collectorAccounts stores an opaque alias, VRChat service-account ID, capacity, health, request budget, credential generation, and an external secret reference. It never stores the credential. Local proof authentication reuses an alias-scoped operating-system vault session only after validating the authenticated immutable account ID; invalid sessions are removed and there is no plaintext fallback. The production worker resolves its provider session from the external secret. That gate was cleared by BASIC's 2026-07-27 risk acceptance of durable VRChat service-account sessions for VRDex-owned proof accounts — a product-owner decision, not a VRChat grant, and the stop condition in docs/planning/community-group-telemetry.md still stands. The secret must record the vrchatUserId it belongs to, and the worker sends it on every control-plane call so a collector paired with another account's secret cannot do work. collectorAccountLeases grants a bounded work claim. communityVrchatIntegrations.leaseGeneration makes fencing tokens monotonic across release and reassignment, so an old worker cannot resume writes with a reused token.
Reassignment is an internal operator action and is allowed only after the source account is quarantined, retiring, or retired. The mutation serially checks target headroom, releases the old lease, moves the allocation, and opens an unknown coverage window before the next fenced claim. Registering new credentials does not automatically reactivate a quarantined or retired account; an operator must reconcile its external group memberships before explicitly returning it to ready.
The account-specific /telemetry/worker HTTP action authenticates a SHA-256 worker key using a constant-time comparison. Every non-claim operation also binds the active lease to that authenticated collector account, worker ID, and fencing token. Lease expiry is checked against trusted server time; collector observation timestamps are validated separately within a bounded clock-skew window. It carries two protocols. Telemetry: claim, membership result, aggregate ingest, failure, and release. Ownership proofs: proof_claim, proof_budget, proof_result, proof_release, proof_auth_failure, and proof_rate_limit. Proof operations are not lease-scoped — they are bounded by the account's request budget instead, of which proofs may take min(floor(limit / 2), limit - 2) per minute so an atomic telemetry poll always fits, enforced by shared proof:account:<id> and proof:global counters rather than per worker. The worker key is re-checked after the request body is read, and again inside the mutations that grant ownership or change account state, so a rotation cannot be outrun by a slow request. Responses and fleet-health queries redact the secret reference and worker-key hash.
Observation model
communityPopulationObservations: one immutable total population, instance count, and world distribution per successful poll; the poll ID is the idempotency key.instanceSessions: first seen, last seen, and confirmed close for an immutable provider location. The canonical location combines world and instance identifiers, so the same instance suffix can exist in different worlds. Only successful complete instance enumerations increment the miss counter. Two consecutive misses close a session; seeing the same provider location later opens a new session.lastObservedAtrecords actual visibility, whileclosedAtrecords the later confirmation poll.instancePopulationObservations: aggregate population per visible instance per successful poll. These support exact confirmed-event recaps without collecting people.communityMemberCountObservations: a row on count change or after a six-hour heartbeat.collectionCoverageWindows: observed, estimated, stale, unknown, or degraded intervals. Missing time never produces a zero observation.
Every observation carries source, collector version, observed time, coverage, and fencing token. The v1 source is first_party; vrcpop and vrcx are reserved adapter values, not active integrations. The adapter replaces subjects embedded in hidden(...) or private(...) instance-locator markers, including legacy user IDs without a usr_ prefix, before ingestion. Defense-in-depth validation rejects unredacted subject markers, remaining usr_ identifiers, foreign group markers, inconsistent world/location pairs, negative/non-integer counts, duplicate provider locations, malformed world IDs, oversized values, and control characters.
Rollups and retention
community-telemetry-v1 rollups use UTC hour/day/event windows and trapezoidal integration between observations no more than five minutes apart. They include current population, active instances, peak concurrency, player-minutes, coverage ratio, member count/growth, and world distribution. Re-running the same window updates the existing versioned row, so late or corrected raw observations deterministically replace the rollup.
The hourly Convex cron schedules the previous hour, previous UTC day, and paged recent-event work. Recent-event work starts private time/world suggestion scans and recomputes every in-window event that has a confirmed session association. Suggestion scans are paged and bounded to the event window plus a six-hour setup lead. Manual confirmation and suggestion approval schedule the event rollup immediately; removing the final confirmed session deletes the event recap instead of retaining an empty public artifact. The daily compaction cron removes raw group and instance observations older than 90 days only when the corresponding hourly rollup exists. Each compaction page looks up only the hours and event-linked sessions present in that page. Instance data linked to a confirmed event is retained until that event has a rollup. Session boundaries, coverage windows, member changes, and rollups remain.
Public projection
getPublicCommunityTelemetry is the only public projection. Each of current population, hourly history, member count, member growth, and event recaps defaults off and is included independently. Hourly history is one deliberate bundle containing its documented rollup fields. Current population disappears after six minutes without a successful poll, which covers the five-minute healthy quiet cadence plus scheduling tolerance; the projection remains stale if another enabled historical surface is still present. Disconnect returns no public telemetry.
profiles.getPublicBySlug attaches this same projection to the community profile. The normal web route, /api/v0/communities/{slug}, /api/v0/profiles/{slug}, hosted MCP, and stdio MCP therefore share one visibility boundary and one PublicCommunityTelemetrySchema. Existence-only callers opt out of the telemetry fanout. Internal observation IDs, integration/account IDs, raw coverage reasons, group IDs, and service-account metadata are excluded.
Verification
Backend tests cover authorization, public-off defaults, concurrent capacity allocation, quarantine/reassignment, fleet stops, monotonic fencing, idempotency, concurrent sessions, close/reopen behavior, malformed input, 401 account isolation, redaction, stale public behavior, deterministic recomputation, and rollup-gated compaction. Worker tests cover the provider adapter, aggregate-only projection, account-scoped operating-system vault records, immutable identity validation, expired-session removal, transient validation failures, slow-metadata caching, request budgets, cadence jitter, 429 backoff, gap-aware player-hours, and diagnostic redaction.