Skip to main content

Club connection backend contract

clubConnection.get is restricted to the club owner or manage_integrations. It returns independent enabled features, current own-member evidence, baseline readiness and role assignment allowlists. Legacy integrations default to analytics alone. Feature selection does not grant staff or bot permissions.

setFeature has the same human gate. It applies one explicit feature value against the current integration, preserving changes made from other tabs. setProviderRoleAllowlist is owner-only, accepts existing active VRDex roles in that club and bounded VRChat role IDs. It lowercases provider role IDs before deduplication and storage. The caller must submit the role's displayed updatedAt; a stale full-list replacement is rejected. Successful saves return the new updatedAt for the next edit. Provider reads and queued actions may use different letter casing for the same role ID. Staff pickers and dispatch authorization compare IDs without case sensitivity, including both assignment and removal. Before a provider write, the worker resolves role IDs against the current provider role list and submits the provider's current spelling. It still rejects IDs outside that list. These IDs constrain later role assignments; saving them performs no provider write and does not prove the provider role exists. Dispatch must resolve the current provider role and reject changes to the bot's own roles.

Authenticated worker HTTP ingestion calls internal recordAuthority with collectorAccountId, hashed worker credential, workerId, fencingToken, integrationId, epochStartedAt, and authority containing groupId, userId, optional ownerUserId, membershipStatus, permissions, and observedAt. The mutation checks current assigned account, credential, active lease, epoch, kill switches, bot identity, group identity and a 60-second freshness window. Snapshots are invalid after credential rotation, reassignment or a new epoch. The permission catalog is never a substitute for own-member grants.

Readiness is a baseline only: membership requires member-data access, posts require announcement management, instances require member-instance creation. Each actual operation must additionally apply its specific permission checks and obtain fresh authority immediately before execution. A stale snapshot is unknown, never ready. The polling cadence may exceed the freshness window.

New and reused pending provider reads advance the integration's next poll hint to the current time. Existing provider backoff and kill switches still apply. After a budget-delayed data read, the worker refreshes authority if its evidence is older than 30 seconds and rechecks membership and grants. If the data itself ages beyond that margin during refresh, it records an explicit read failure instead of sending stale evidence that leaves the request running.

clubProviderReads.get returns server-authoritative fresh and remainingFreshMs for the stored observation. The duration is zero for failed, absent, future-dated or expired evidence. The 60-second boundary is expired. The query still rechecks the requester, feature, epoch and collector access.

The browser supplies a unique freshnessNonce per mount or refresh attempt, including when request returns a previously used request ID. Convex caches queries by their arguments and elapsed wall time does not invalidate them. The nonce forces a new server evaluation; it grants no authority or lifetime. It is optional for existing query callers, which must not treat a cached duration as new evidence. The shared hook measures elapsed time with performance.now(), starting before the query, so client clock offsets and transport delay cannot prolong readiness. The hook accepts the first deadline for each attempt nonce, request ID and observation timestamp. Reactive updates for that same observation retain the deadline even when the server returns a smaller remainder. Replayed results, including larger durations, cannot extend or revive it. observedAt identifies the observation; it is never compared to the device clock. A changed observation or a pending/running read that succeeds gets a new nonce evaluation, with actions disabled until it arrives. Null, failed, stale and denied results immediately disable readiness. Operation execution retains its independent server authorization and freshness checks.

The analytics population and connection authority displays use separate 360,000 ms and 60,000 ms windows. These two windows include the exact expiry boundary. clubAnalytics.getContext and clubConnection.get accept an optional freshnessNonce; connection results also return now, captured once for both authority validation and the response. Existing callers may omit the nonce.

Each mounted query owner creates a nonce and records performance.now() before subscribing. Its first non-null server now fixes the calibration for that attempt. The display deadline is query start plus the signed difference between observedAt + window and that initial server time. Rerenders, child remounts and reactive results with a newer now retain the calibration. New observations use their own timestamps against that same calibration; their signed offsets must not be clamped. Timers and renders check the same monotonic deadline, so device wall-clock offsets or changes cannot extend either window. Initial query delay is charged conservatively and can shorten the displayed lifetime.

A query-owner remount, scope change or return from a skipped connection query uses a new nonce to avoid treating a cached result as a fresh calibration. Before calibration, freshness is false. Analytics can calibrate an empty result from its server now. An initially null connection has no server time; the first later integration triggers one new nonce-backed query before authority can be fresh. A later null or missing observation retains an established calibration, but immediately suppresses the positive display. Invalid or future-dated observation timestamps are rejected. Current category access, server absence, authority rejection, enabled features and readiness still control what appears. Display freshness does not replace any backend operation gate.

Backend tests cover unauthorized reads, feature defaults and updates, fenced snapshot rejection, freshness, identity mismatch, credential rotation, expired leases, role ID validation and cross-club role rejection. Provider writes and the completed onboarding UI remain separate integration work.