Unified SvelteKit and Node application #
The full website and AppView APIs run in one SvelteKit application and one
Node.js process. appview/src/ contains the SQLite database, migrations,
Jetstream worker, leaderboard queries, and Web Request/Response handlers;
SvelteKit +server.ts modules expose the APIs at /api/scenarios, /api/leaderboards,
/api/leaderboards/overview, /api/leaderboards/identity, /api/leaderboards/runs,
/api/leaderboards/promotions, /healthz, and /status.
The app uses Node 24.16's built-in node:sqlite driver and Umzug 3 with plain
SQL migrations. The container uses the same pinned Node release.
Runtime startup applies migrations, creates one ingestion worker, then serves
the generated SvelteKit handler. Shutdown stops new HTTP connections, waits for
active requests, stops the worker, and closes SQLite. Builds and prerendering do
not open the database or start ingestion. Ingestion is disabled unless
INGESTION_ENABLED=true.
Run locally #
From the repository root with Node 24.16.x:
npm ci
npm run dev
The full app opens at http://127.0.0.1:5173/; its APIs are served by the same
Vite process. Frontend changes use Vite HMR, and edits to imported Node runtime
modules restart the Node watch process. No second AppView process or API proxy
is needed. npm run build checks and builds the full app; npm start (or
npm run preview) runs the production Node handler at http://127.0.0.1:3000.
GET /healthz checks database availability and returns 503 if ingestion has
failed permanently; GET /status also reports ingestion phase, durable cursor,
last batch time, sanitized error state, and indexed table counts. Neither status
response includes the Jetstream API key. Public leaderboard reads need no
login. Anonymous result submissions use a scoped random cookie, separate from
AT Protocol account sessions.
To enable ingestion locally, add the following to the ignored root .env.local:
INGESTION_ENABLED=true
JETSTREAM_API_KEY=your-key
# Optional; defaults to https://jetstream.us-west.bsky.network
JETSTREAM_SERVICE=https://jetstream.us-west.bsky.network
The dev and start scripts load root .env.local. Vite may also read it for
build settings such as SITE_URL and HANDLE_RESOLVER, but builds never start
runtime services and only those explicit public settings enter client bundles;
server secrets remain server-side. Ordinary development honors
INGESTION_ENABLED, so set it to false if you do not want a local worker.
Browser tests set it to false explicitly. When ingestion is enabled, a
nonempty key and HTTPS Jetstream origin are required. The key stays in the
server process and is sent only to Jetstream HTTP/WebSocket requests.
The Compose service also reads root .env.local as optional runtime
configuration. It is ignored by Git and is not copied into the image. Existing
deployments should move any old appview/.env.local settings to the root
.env.local. The archive reader prefetches with an 8 MiB compressed-buffer
target and one concurrent block; decompressed block size also affects memory
use, so measure peak memory against actual archive data and deployment limits.
Configuration:
| Variable | Local default | Container default |
|---|---|---|
HOST |
127.0.0.1 |
0.0.0.0 |
PORT |
3000 |
3000 |
DATABASE_PATH |
appview/data/appview.sqlite |
/data/appview.sqlite |
INGESTION_ENABLED |
false |
false |
JETSTREAM_SERVICE |
https://jetstream.us-west.bsky.network |
same |
JETSTREAM_API_KEY |
unset | unset |
SITE_URL |
unset | build argument |
PUBLIC_ORIGIN |
unset | unset |
TRUSTED_PROXY_IPS |
unset | unset |
HEALTHCHECK_PATH |
/healthz |
derived from SITE_URL or overridden |
DATABASE_PATH relative values resolve from the process working directory; the
default path is independent of it. Parent directories are created automatically.
The container stores SQLite in /data/appview.sqlite on the existing named
volume. Changing Jetstream source or filters while keeping the database is
rejected because its cursor belongs to the original source and filter.
SITE_URL is a build-time public URL including the deployment path, such as
https://oscillatory.net/rad-cohesion/; it configures SvelteKit's base path and
public metadata. Its origin also sets SvelteKit's paths.origin at build time,
so adapter-node reconstructs request URLs correctly behind nginx and when
serving local HTTP. The runtime ORIGIN variable is not used by adapter-node 6.
Set PUBLIC_ORIGIN to the same exact origin (no path) so anonymous submissions
check browser origins and set a Secure cookie. Vite development and local
loopback Compose can omit PUBLIC_ORIGIN. Local Compose builds for
http://127.0.0.1:3000/; changing the browser-facing address or port requires
updating SITE_URL and rebuilding.
TRUSTED_PROXY_IPS is a comma-separated list of exact peer IP addresses
allowed to provide X-Real-IP. The reverse proxy must overwrite that header;
the app never reads X-Forwarded-For or X-Forwarded-Host. Unlisted peers use
their actual socket address for rate limiting. Configure the actual proxy peer
address seen by Node, not a network range or wildcard.
When deploying beneath /rad-cohesion/, the operational routes are
/rad-cohesion/healthz and /rad-cohesion/status.
HEALTHCHECK_PATH=/rad-cohesion/healthz may explicitly set the container probe
path; otherwise the image derives it from SITE_URL. Docker Compose defaults to
a local build at http://127.0.0.1:3000/. The production workflow
builds the root Dockerfile with the public SITE_URL, loads one image into the
server, and restarts the single Node container with its existing SQLite volume.
On Oscillatory, the container listens on 127.0.0.1:3010; port 3000 is used by
the PDS. nginx proxies the full /rad-cohesion/ prefix without stripping it. The
deployment playbook and proxy template live in the sibling my-servers
repository; this repository contains the application image and Compose setup.
Public scenario discovery #
GET /api/scenarios?q=<name>&limit=24&offset=0 lists scenarios from active
indexed authors, newest first. The optional q is a trimmed name substring
limited to 64 characters; limit is at most 100 and offset is at most
100,000. Each result includes its exact AT URI and CID, name, author DID and
current handle when available, publication time, occupied-cell count, and the
validated board layout used for thumbnails. The
response includes pagination totals. Reads are public and do not require
sign-in. The /scenarios/ page keeps search and pagination in the URL and opens
each result at its exact published revision. Visible cards are drawn once into
static images using one shared browser renderer. No PDS requests are needed for
these images, and the renderer is released when leaving the listing.
Public player pages #
/players/<DID>/ lists an active account's indexed published scenarios and
finished runs. The page uses tab=scenarios|runs and keeps each list's position
in scenarioOffset and runOffset. Author links use this stable DID address;
the current indexed handle is display metadata.
GET /api/players/<DID>?kind=scenarios|runs&limit=24&offset=0 returns the player
DID and handle in player, plus the selected kind, items, pagination total,
offset, and limit. Page sizes are at most 100 and offsets at most 1,000,000. Reads are
public and require no sign-in. Unknown and inactive accounts return 404. The
endpoint uses the existing indexed records, without per-item PDS or Bluesky
requests. Local browser items and Guest scores are excluded.
Public leaderboards and anonymous submissions #
The standalone /leaderboards/ page reads GET /api/leaderboards?challenge=<identity>&mode=best|all&limit=25&offset=0.
Pages contain at most 100 rows and offsets are capped at 100,000. The default
page is the empty-board challenge at the selected starting level (level 1 when
no level is specified). A pinned published-scenario leaderboard derives
the same identity from board contents, starting level, rules, timing policy,
and RNG version, so equivalent publications share standings. The attempt seed
and publication URI are not part of that identity. Best per player selects
each source identity's highest score before pagination; All runs keeps every
current result. Equal scores share a rank, and stable secondary ordering only
controls row display.
The query combines active indexed AT Protocol authors with the independent
anonymous_runs table. PDS updates replace the indexed row, deletes remove it,
and inactive authors are hidden according to the current indexed account state.
AT Protocol players display their verified current handle when available and otherwise
their DID. Account checks resolve the handle back to its DID through https://bsky.social
before storing it. Hourly checks refresh handle changes; service startup also queues
active accounts with missing handles. These lookups use the guarded HTTPS transport. Anonymous names belong to one result; a blank name appears as
“Guest.” A shared note marks all rows unverified.
GET /api/leaderboards/overview?limit=24&offset=0&challenge=<identity> lists
only layouts represented by eligible submitted runs. Pages contain at most 100
groups and offsets are capped at 100,000. The optional challenge selects its
layout group and returns that layout's other populated starting levels; it lets
the page rebuild an exact challenge view even after a pinned scenario changes
or disappears. Groups come from the submitted run snapshots and combine
equivalent layouts regardless of publication URI. A group includes its layout,
a representative pinned URI/CID when present, populated levels and challenge
identities, run counts, and most recent activity. Current scenario names are
used only when the indexed record still has that exact URI and CID and its
author is active; other custom layouts are labeled “Custom scenario.”
runCount counts all eligible submissions, while bestRunCount applies the
same per-player selection as the default best standings. The query uses active
PDS authors and unpromoted Guest submissions, with promoted Guest receipts
excluded as in the standings query. It aggregates snapshots in SQLite and does
not load current records from PDS.
For signed-out submissions, the UI calls GET /api/leaderboards/identity
before POST /api/leaderboards/runs with run set to the existing FinishedRun
JSON object and an optional displayName string. The identity endpoint issues a random one-year,
HttpOnly, SameSite=Lax cookie scoped to the API path. SQLite stores only the
SHA-256 hash of that cookie, never its bearer value. The POST accepts the
existing finished-run shape and validates it with the same shared game
validator and challenge identity function used by AT Protocol ingestion.
Submitting anonymously creates no AT Protocol record and needs no login. The
Finished runs panel offers one Submit score action: signed-in users publish the
existing local run record to their PDS, while signed-out users submit it here.
The optional public name is available only while signed out. A browser can
submit a name of at most 40 characters for each Guest run. Both the name and
run are public; neither is verified. Signing in does not publish local runs
automatically.
POST /api/leaderboards/promotions links prior Guest results to published
runs. It accepts at most 200 unique {runId, uri, cid} references and uses the
same-origin check, body/deadline bounds, peer-IP and cookie rate limits, and
HttpOnly cookie as Guest submissions. It never accepts a caller-supplied DID
as proof and never creates a Guest identity or row. A reference can update only
the row with that run ID owned by the cookie. Pending links are stored in
SQLite, one per owned Guest row, and remain pending until a valid finished-run
record is indexed for the exact URI and CID under an active account and its
full canonical payload matches the Guest run. Live commits, PDS author
snapshots, and account reactivation all reconcile pending links. Missing,
mismatched, invalid, or inactive publications leave the Guest row visible.
After confirmation the row remains as a hidden receipt, so deleting the PDS
record does not restore the Guest leaderboard entry and replaying the Guest
submission does not create a duplicate.
Concrete bounds are enforced by the AppView: 64 KiB request bodies, a 15-second
total body deadline, a 30-request-per-minute peer-IP limit, a 10-request-per-
minute cookie limit, at most 200 anonymous runs per browser, 100,000 total
anonymous runs, and 200,000 identity rows. Identity rows with no submitted runs
expire after 30 days; this cleanup runs at most once per hour after the identity
endpoint's peer-IP rate check. Repeating the same run ID and payload is
idempotent after rate checks; reusing that ID with another run payload returns
a conflict, while a retry preserves the first submission's display name. On a deployment where all visitors share the reverse
proxy's peer address, configure that exact peer IP and have the proxy overwrite
X-Real-IP so the backend rate-limits by visitor address.
The site pages, API endpoints, and operational routes share one origin and one
SvelteKit handler, so there is no separate API proxy location or CORS setup. A
reverse proxy should preserve the configured public prefix when forwarding to
Node so SvelteKit can route API calls and set the anonymous identity cookie at
the matching API path. For a public deployment, build with the public SITE_URL,
set PUBLIC_ORIGIN to its origin, configure the exact proxy peer in
TRUSTED_PROXY_IPS, and have nginx overwrite X-Real-IP.
The production deployment serves this whole app from one container on host
loopback. Its nginx configuration forwards the complete configured site prefix
to that container and overwrites X-Real-IP; the API does not have a separate
upstream.
Startup discovers candidate authors from a Jetstream archive scan, verifies
their current state from their PDS, then tails the live stream. Both archive and
live subscriptions explicitly request only kind=commit and the two Rad
Cohesion collections. /status reports disabled, starting, backfilling,
live, retrying, failed, stopping, or stopped, plus the bootstrap phase
and separate discovery/live cursors. A live OutdatedCursor starts a
commit-only archive catch-up from the latest saved live cursor. Discovery and
record updates are transactional with their durable cursor. Filtered/no-op
progress is checkpointed at most once every five seconds as batches arrive.
Quiet streams may retain unsaved no-op progress until another batch or shutdown;
replaying those ignored events after a crash is harmless.
The status timestamps have different meanings:
checkpointedAtMs: local wall-clock time when progress was written to SQLite.lastBatchAtMs: local wall-clock time when the worker last processed a batch.lastEventAtMs: timestamp carried by the last newly processed stream event. This date shows how far through history the worker has actually reached.eventLagMs: current time minuslastEventAtMs, clamped to zero.retryAtMs: when the scheduled retry becomes due, ornullwhen no retry is scheduled. The server's fullRetry-Afterdelay is respected for both planning requests and archive downloads; ordinary exponential backoff remains capped.
All times are Unix milliseconds. The last event date is saved with cursor
checkpoints and restored after restart. Existing databases initially report
null for that date and lag until a new event is processed. Empty batches retain
the last observed event date, so lag is an indication of stream progress rather
than a precise measure of missing Rad Cohesion records. The cursor is a sequence
number and cannot itself be converted to a date. retryCount is cumulative for
the current worker lifetime, including retries followed by successful progress.
Initial backfill cost #
The initial discovering phase starts from a durable scan cursor of zero and
requests a pinned archive tip. It downloads only commit events whose collection
is either the public scenario or finished-run lexicon. The explicit kinds
filter matters: Jetstream's collection filter narrows commit events, but does
not by itself exclude global account, identity, or sync markers. This scan never
requests those metadata kinds, so it avoids downloading unrelated global
account history.
After discovery reaches the pinned tip, the service saves that sequence as the
live cursor and enters reconciling. For each DID found in matching history, it
fetches the DID document and current PDS status, then lists both Rad Cohesion
collections from that PDS. Current records replace the author's scanned rows;
old/deleted publications are therefore removed even when their historical
deletion event is not in the scan window. Once every candidate DID has had an
initial current-state check attempted, bootstrap is marked complete and the
service enters live. Failed checks remain pending and retry with backoff;
new authors remain inactive until verified. Healthy authors receive their
regular status and record refreshes even if another author's PDS stays
unreachable. Bootstrap completion therefore does not mean every author was
successfully verified; /status reports pending checks and errors.
A commit received while an author is being checked advances that
author's sequence fence; the older PDS response is discarded and retried.
Completed record snapshots also retain their repository revision separately from the latest streamed revision. Events at or before a snapshot revision are already included in that snapshot. Multiple events sharing a newer streamed revision are all processed, including when they span batches.
Recovery after an expired live cursor #
The initial archive scan starts at zero. Once it finishes, normal live
processing advances cursor, while discoveryCursor remains at the end of that
scan. These describe different work in the current implementation; they are
not old and new storage formats.
If Jetstream can no longer resume that live cursor, recovery starts a new
archive scan at the latest saved cursor, with a fixed archive endpoint.
For example, if initial discovery finished at 100 and live processing reached
1,000, recovery scans only events after 1,000. If the archive has not advanced
beyond that point, the service waits with backoff and reports why in /status.
It never rewinds to download earlier history or skips unprocessed events.
Each archive batch saves its progress transactionally. Restarting during recovery keeps both that progress and the fixed endpoint. Completed author checks, status timestamps, and snapshot revisions remain intact. Matching commits advance their author's sequence, marking that author for a fresh PDS snapshot; an empty gap requires no new author snapshots. Regular scheduled checks still run when due after recovery.
Current author checks #
PDS lookups are public, unauthenticated GET requests. The worker resolves the
DID document without using the resolver's one-hour cache, follows its current
PDS endpoint, then checks com.atproto.sync.getRepoStatus and lists records
through com.atproto.repo.listRecords. Requests are serialized globally to
avoid bursts against authors' PDS providers. Each request has a 15-second
timeout, response bodies are limited to 2 MiB, and Retry-After is honored.
Record and status checks share a deduplicated FIFO queue. Completing a check
does not rebuild the remaining author list. Failed checks wait outside the ready
queue and join its tail when their backoff expires; new notifications cannot
shorten that delay. New work can wake an idle worker while other authors await
retry. A successful full record check also cancels any queued status check for
that author, since the full check already includes account status. Bootstrap
queues its initial candidates once and enters steady state after each has had
a first attempt, even if retries are already due. The periodic scheduler still
queries SQLite for newly due checks; /status uses SQL counts rather than
loading those author lists. Queues and retry timers are cleared on shutdown or
archive recovery; process restarts rebuild work from persisted author state.
Both DID-document and PDS requests use guarded HTTPS transport: DNS answers must all be public addresses, and each TLS connection is pinned to one validated address while certificate verification uses the original hostname. If an address cannot establish the connection, the worker tries the remaining addresses from that same DNS lookup; it never re-resolves during the attempt. All address attempts share one 15-second deadline and cancellation signal. Once an HTTP response arrives, its status or a later response-body failure does not trigger another address attempt. Redirects are not followed.
Account status is checked every hour; complete record listings are refreshed
once a day and immediately after an accepted commit for a known author. In a
healthy service/PDS path, a deletion or deactivation can remain visible for up
to one status-refresh interval (about one hour), plus queue delay. The status
check brackets two getRepoStatus calls and uses repo revisions when both are
present. The protocol makes rev optional, so without it this is a matching
best-effort status check, not an atomic snapshot. Full record listings likewise
use matching active/status reads and compare revisions when available. A PDS
that omits rev is still indexed; commit triggers and daily refreshes provide
eventual record convergence.
An inactive author is hidden from public results. A deactivated author keeps
its indexed rows hidden for possible reactivation; a deleted author has those
rows removed. RepoNotFound is treated as inactive with an unknown reason;
that response alone is not treated as proof of deletion. A confirmed PLC
tombstone from the registry's raw latest-operation log or explicit
status: "deleted" removes the rows. Reactivation triggers a full record
listing. A failed DID/PDS
lookup, invalid response, or rate limit never means “inactive” or “empty”: the
service keeps the last known rows/status, reports the failure and retry time in
/status, and retries fairly so one unavailable author does not block others.
If the PDS remains unavailable, its last known active state can remain visible;
the service reports that stale-check condition through lastError and retry
fields rather than making an unsupported deletion claim.
The steady-state minimum is about two status calls per active author per hour,
one fresh DID-document resolution per check, plus at least two paginated record
listing calls per author per day. /status shows the one-hour status interval,
24-hour record interval, and counts of authors currently due for each check.
Once bootstrap reaches live, these PDS checks continue while Jetstream is
disconnected and resume from persisted due state on process startup. Startup
reconciliation can also check its known authors without waiting for a live
Jetstream connection. The service reports retrying until the stream reconnects.
HTTP 429 from Jetstream means the hosted archive's download byte quota is
depleted; progress resumes as it replenishes. See the
Jetstream replay documentation.
Container #
docker compose up --build -d appview
docker compose logs -f appview
Compose publishes the service only on host loopback port 3000. The process runs
as the image's unprivileged node user. A Docker named volume mounts the entire
/data directory, including SQLite's WAL and shared-memory files. Rebuilding or
recreating the container preserves that volume; docker compose down --volumes
deletes it. Keep the volume on local storage, not NFS or another network filesystem.
Deploy one service instance. Stop the old instance before applying schema changes.
Container health checks derive their path from SITE_URL unless
HEALTHCHECK_PATH overrides it. For a subpath deployment, the image probes the
prefixed /healthz route automatically; inspect logs if the container becomes
unhealthy. The Docker restart policy restarts exited processes, not merely
unhealthy ones.
Backups and upgrades #
For this small deployment, a stopped-service backup is sufficient:
docker compose stop appview
docker compose run --rm --no-deps --entrypoint tar appview -C /data -cf - . > appview-backup.tar
docker compose up --build -d appview
The archive contains the entire data directory. Keep a copy outside this node.
To restore, stop the service and restore the archive into an empty data volume
with the original ownership; use the application image matching that backup.
Do not copy just the live .sqlite file: committed changes may still be in its
WAL. If uninterrupted backups become necessary, use SQLite's online backup API.
SQLite settings #
All service connections go through src/database.ts:
| Setting | Choice | Reason |
|---|---|---|
| Foreign keys | ON |
Enforce declared relationships on every connection. |
| Busy timeout | 5,000 ms | Allow brief lock contention before returning an error. |
| Journal mode | WAL |
Allow readers alongside the writer. Verified at startup. |
| Synchronous | FULL |
Sync each committed write transaction for power-loss durability. |
| Trusted schema | OFF |
Restrict functions/features usable from database schema objects. |
| Double-quoted string literals | Disabled | Catch mistaken quoting instead of accepting it as string data. |
| Extension loading | Disabled | This service needs no loadable SQLite extensions. |
| Defensive mode | Enabled | Disable SQL features that deliberately damage database structures. |
| Table typing | STRICT |
Required by convention for every migration, including bookkeeping. |
foreign_keys, timeout, and synchronous settings are connection settings; setting
them once in a migration would not configure later connections. WAL persists in
the file, but each connection still requests it explicitly.
The article's proposed settings are reasonable except that we choose FULL
over NORMAL initially. In WAL mode, NORMAL preserves consistency but may lose
recent commits after an OS crash or power failure. A rebuildable index could
accept that tradeoff if indexed data and its cursor always commit together.
For our anticipated write volume, retain durability until measurement justifies
relaxing it. Neither mode compensates for broken storage or missing backups.
Strict tables still allow lossless conversions such as text '123' to integer
123; they do not replace input validation. Use NOT NULL and CHECK constraints
for domain rules. Booleans should be integers checked to be 0 or 1.
Use explicit write transactions (BEGIN IMMEDIATE when applicable) and handle
lock errors: a busy timeout cannot prevent every SQLITE_BUSY condition.
Leave page size, cache size, memory mapping, temporary storage, and WAL automatic checkpointing at SQLite's defaults for now. In particular, do not copy large memory allocations from server-tuning recipes onto this constrained node.
Sources: Node 24.16 SQLite, SQLite pragmas, WAL, strict tables, Umzug.
Jetstream SDK fixes #
SDK 1.0.0 rejects async sync markers whose optional inner time is "".
The production archive event at sequence 24355929627 has that shape; its
outer timestamp and revision are valid. The install script in
appview/tools/patch-jetstream.mjs normalizes only that empty sync timestamp
to absence before the SDK's existing archive/live validation. The AppView's
commit-only filters no longer request sync markers, but the SDK patch remains
for its generic replay behavior and tests. Invalid nonempty timestamps,
revisions, commit fields, and game records are still rejected.
The same install script removes the SDK's 30-second cap on an explicit server
Retry-After delay. The cap still applies to exponential backoff when the server
does not provide a delay. Planning requests use the worker's retry loop, which
also respects Retry-After. Regression tests cover delays longer than 30 seconds,
both header formats, timestamp persistence, and status during quota waits.
The fix runs on local npm ci and explicitly during the Docker build. If using
npm ci --ignore-scripts locally, run
node appview/tools/patch-jetstream.mjs from the repository root afterward. It
is idempotent and refuses an SDK version/source it does
not recognize. Review/remove the fixes when a future SDK release resolves them.
Rebuild/recreate the service to pick it up; keep the existing data volume so it
resumes from the committed cursor without resetting or skipping the marker.
Migrations #
npm run typecheck:server
npm run test:server
The single Node startup invokes migrations before serving requests. See
migration conventions for filenames,
transaction behavior, and upgrade/rollback procedures. Migration
0001_ingestion_index.sql creates accounts, runs, scenarios, and the
singleton ingestion_state cursor table alongside schema_migrations; later
migrations add separate discovery progress, reconciliation markers, repository
revision fences, and status-refresh timestamps.
Umzug currently pulls in a moderate sprintf-js denial-of-service advisory
through its CLI's argument-parser dependencies. We use its programmatic API with
trusted migration names and SQL, and expose no migration CLI or formatting input
over HTTP. No dependency overrides or downgrade are applied. Track the upstream
fix when updating Umzug. See
GHSA-hp3w-g68c-fv3c.
Indexed data and event behavior #
The initial forward migration defines these tables:
| Table | Contents and purpose |
|---|---|
ingestion_state |
Source/filter identity, live cursor, independent archive-discovery cursor and tip, bootstrap phase, and completion/checkpoint times. |
runs |
AT URI primary key, current CID, author DID, validated record JSON, challenge identity, score, start kind, and finish time. Ranking and author/challenge indexes are present. |
scenarios |
AT URI primary key, current CID, author DID, validated record JSON, identity, display name, and creation time. |
accounts |
Candidate application DIDs, current active/status value, optional handle, last processed sequence, and latest known repository revision. Unrelated network accounts are not retained. |
Record events pass through the existing shared run/scenario validators and challenge identity code. Jetstream raw batches preserve invalid record payloads so an invalid replacement removes the former indexed version. Creates, updates, deletes, and the cursor are replay-safe. This service does not subscribe to account, identity, or sync event kinds. Current PDS snapshots provide account status and replace the author's current scenario/run rows; hourly checks handle status-only changes even when no new game record is published. Deactivation retains hidden rows for possible reactivation, deletion removes indexed runs and scenarios, and reactivation triggers a fresh listing. A run contains its starting layout, so deleting the source scenario does not delete the run.
Each validated full record is stored as JSON with selected query columns and SQL constraints. Runs are self-contained: their optional source-scenario reference does not require that publication to remain present. Shared challenge identity groups comparable results independently of the scenario publication URI.
Focused offline tests cover migrations, event lifecycle and replacement, current-author snapshot handling, private-address rejection, source-scenario deletion, no-op progress, transaction rollback, restart, and the installed SDK's HTTP planning, zstd/CBOR archive-block download, and live-stream seams. They use no Jetstream key or external network access.
Personal bests and ranks are computed from indexed SQL queries; there is no leaderboard cache table. Anonymous results live in tables separate from PDS-ingestion-owned records. Replay verification, exclusion/moderation, and published-save history remain separate work.
Jetstream protocol references: SDK, network replay.