Repository navigation
EN Bridge Guarantees
Français · Type: explanation + reference · Audience: author, admin · Context: Reality Bridge · Status: ready · Applies from: 4.2.5
The Reality Bridge,
localprofile (SQLite) · contract: stable · verified by CI (support matrix of 4.4.0, 2026-10-23): Node 22.13+ with node:sqlite, on Ubuntu; Chromium and WebKit end to end · automated checks: jobsreality,reality-xcheck; CI planreality; 2 test files · people's passes: none · known limits:bridge-dedupe-retention,bridge-no-queue-for-connectors,bridge-tenant-isolation-scope,bridge-proof-scopeThe Reality Bridge,
distributedprofile (Postgres) · contract: experimental · verified by CI (support matrix of 4.4.0, 2026-10-23): Postgres 16 in CI; The qualification profile: HTTPS, three instances (ops/qualify/) · automated checks: jobsbridge-postgres,deployment-smoke; CI planreality; 1 test file · people's passes:bridge-postgres-httpsnot run (#77) · known limits:bridge-dedupe-retention,bridge-no-queue-for-connectors,bridge-tenant-isolation-scope,bridge-proof-scopeThe connectors (email, Telnet, SSH, Open Badges) · contract: experimental · verified by CI (support matrix of 4.4.0, 2026-10-23): Node 22 on Ubuntu (Telnet and SSH not on Windows) · automated checks: jobs
connectors,connector-archive-e2e; CI planreality; 3 test files · people's passes:connectors-real-securitynot run (#79) · known limits:bridge-dedupe-retention,bridge-no-queue-for-connectors,bridge-tenant-isolation-scope,bridge-proof-scope
What the Reality Bridge and the player promise about a signal: a duplicate, the order, a delivery repeated or replayed, a Bridge that is away or busy, two tenants side by side, and what a signal does to a game's progress and to its proof. Each answer is read from the current code and, where a test shows it, tied to that test. Each says where it stops.
| Question | Answer | Where it stops |
|---|---|---|
| A connector sends the same signal twice | Accepted once: 202 the first time, 200 with "duplicate": true after |
The key is remembered as long as the store holds the signal |
| In what order does a player get signals? | Numbered 1, 2, 3… per player, in the order the Bridge accepted them, applied in that order | No order between players, and the time something happened outside does not reorder |
| The answer was lost, the page crashed | Delivered again from the last acknowledged sequence, recognised, applied once | Delivery is at least once, never "exactly once" |
| The Bridge is busy or away | 503 busy with Retry-After: 1 (nothing written), 429 over a quota; the game plays on |
The connector must send again; the Bridge does not queue for it |
| Two tenants on one deployment | Each request resolved to one tenant before anything is read; tokens and signals of one are refused by the other | No row-level security in Postgres; one process holds every tenant's keys |
| Can the outside block the game? | No: a required signal has a fallback, and the game is proved closed, per scenario and adversarially |
The proof covers the declared signals, not the connectors' behaviour |
A connector gives each fact a deduplication key (dedupeKey): the email connector sha256('email:' + Message-ID),
Telnet sha256('telnet:' + session + ':' + line), the demonstration webhook <source>:<id>. On POST /v1/signals
the Bridge answers 202 the first time a key is seen for a player, and 200 with the stored signal and
"duplicate": true for a key it already accepted: the same id, the same sequence, nothing written.
The lookup, the sequence and the write are one step. In one process, proposals for a player are taken one at a time
(a lock per player); across processes, one database transaction does it, backed by unique constraints on
(tenant, player, dedupe_key) and (tenant, player, sequence) (bridge/migrations/0001.up.sql). Two connectors, or
two instances, proposing the same key at the same instant: one acceptance.
Guarantee ·
bridge-duplicate-200· referenced · scope self-checked by the author (AI), not reviewed A signal proposed again with adedupeKeythe Bridge already accepted for that player is answered 200, with the first signal's sequence and"duplicate": true; the player receives it once.
Guarantee ·
bridge-dedupe-concurrent· referenced · scope self-checked by the author (AI), not reviewed Fifty proposals of one key at once give one acceptance and one id; a hundred distinct keys at once give the sequences 1 to 100, none shared.
Limit ·
bridge-dedupe-retentionThe Bridge recognises a key while it holds the signal. On the JSON-lines journal,compactdrops acknowledged signals older than its retention (90 days by default; a player's last signal always stays): a key sent again after that is a new signal. A SQL store keeps every signal (it has no retention yet). The key is per player: the same key for two players makes two signals.
Each player has its own sequence, contiguous from 1, drawn when the Bridge accepts a signal. The player reads signals by sequence (a stream or a fetch by cursor, both say each signal's sequence) and the engine applies them in that order: a signal that arrives ahead of a gap waits until the gap is filled, then the cursor moves over the contiguous run.
- The order is the Bridge's order of acceptance. A connector's
occurredAttravels in the signal for information; it never reorders anything. - There is no order between two players, nor between two tenants.
- With several instances, a signal accepted by one reaches a stream another holds, in order, once each.
Guarantee ·
bridge-sequences-contiguous· referenced · scope self-checked by the author (AI), not reviewed Whatever the interleaving of concurrent proposals and their repeats, a player's sequences are 1..n with no gap and no duplicate, and adedupeKeyis accepted once.
Delivery is at least once, applied once. The player's transport keeps three cursors: received (the last sequence
the Bridge sent), delivered (the last handed to the game) and durable (the last acknowledged, after the save). The
client verifies a signal, hands it to the engine, waits for the save to be durable, and only then sends
POST /v1/ack { through }. Every request to the Bridge, a poll or a reconnection, asks from the durable cursor: a
signal handed over and not acknowledged (a refusal that may pass later, a save that failed, a crash before the
acknowledgement) is delivered again, with a wait that grows up to a minute while nothing settles. Before 4.1.8 the
transport asked from the delivered cursor.
The save keeps the last sequence received without a gap and the ids applied above it (GameState.reality, at most
1 024 above the cursor), so a signal delivered again is recognised and not applied twice. A signal is also an entry
of the session, { external: { id, sequence, signal, source, receivedAt, playerId } }: npm run replay applies it
offline, with no Bridge.
Guarantee ·
bridge-ack-after-durable-save· referenced · scope self-checked by the author (AI), not reviewed The client acknowledges a signal only after the save that holds it is durable, and only through the contiguous cursor.
Guarantee ·
bridge-resume-from-durable· referenced · scope self-checked by the author (AI), not reviewed In polling, a signal handed to the game and not acknowledged is asked for again from the acknowledged cursor.
Guarantee ·
bridge-session-replays-offline· referenced · scope self-checked by the author (AI), not reviewed A session with signals replays offline, with no Bridge, to the same state.
| What happens | What the Bridge answers | What to do |
|---|---|---|
| Another process holds SQLite's write lock longer than 5 s | 503 {"error": "busy"}, Retry-After: 1; nothing written |
Send it again (the connectors do, with the same key) |
| The store does not answer |
/readyz and /healthz 503 {"status": "unavailable"}; /livez still 200 |
Look at the database |
A serve lost its maintenance lease (4.1.20) |
Its writes answer 503 and /readyz fails until it takes a lease again |
Read its maintenance.lease.lost log line |
| Over a connector's quota (120 a minute) or a player's backlog (1 000 unacknowledged) | 429 quota or pending
|
The connector sees it; nothing is dropped in silence |
| An address over 60 requests a minute on the routes anyone may call, or 60 failed authentications | 429 rate, Retry-After: 60
|
Wait |
A connector sends a proposal whose answer was lost again with the same key: three tries in all by default, then a
refusal it counts (timeout, unreachable). The email connector leaves a message unseen when the Bridge refused it
for a reason that may pass, and reads it again at the next poll. On the player's side, a game offline or without a
signal plays on, and what arrives later is applied when it arrives. An instance killed or crashed loses nothing: a
transaction is committed or not there (tests/bridge-fanout.test.ts kills one instance of three during 1 000
proposals).
Guarantee ·
bridge-busy-503· referenced · scope self-checked by the author (AI), not reviewed A SQLite store busy longer than its wait answers 503 withRetry-After: 1within two seconds. Once the lock is free, the next proposal is accepted with sequence 1.
Limit ·
bridge-no-queue-for-connectorsThe Bridge does not hold a refused proposal for later: a 503 or a 429 is the connector's to send again. A connector's quota is counted per instance: with N instances, a connector may propose N times its quota.
A tenant is one operator's game in one environment, with its own Biscuit root, event key, operator token, quotas and revocations (Several tenants). On a server holding several:
- A request is resolved to its tenant by its
Host(each tenant'shosts), or byX-Web-Scumm-Tenantwhen--tenant-headerallows it, before anything is read; no tenant matches, a 404tenant. - Every row of the store carries its tenant, and every query filters on it.
- A connector's token is bound to its tenant (
grantadds it), so a token of tenant A is refused by tenant B even when both were (wrongly) given one Biscuit root. - A tenant of a shared deployment signs
WorldSignalV2, which names its tenant: a signal of A presented to a player of B is refusedaudience-mismatch, even under one shared event key.
Guarantee ·
bridge-tenant-token-bound· referenced · scope self-checked by the author (AI), not reviewed A connector token bound to its tenant is refused by another tenant (403), even when both tenants share one Biscuit root.
Guarantee ·
bridge-tenant-routing· referenced · scope self-checked by the author (AI), not reviewed One server holding several tenants routes each request byHost, by the tenant header only when allowed, and answers 404 to a request that names no tenant it holds.
Limit ·
bridge-tenant-isolation-scopeThe isolation is in the Bridge's code, not in the database: there is no row-level security per tenant in Postgres, and a process serving several tenants holds every one's private keys (an instance compromised is every tenant's compromise: Security).
A signal reaches the game as the event of the same name, like a player's input:
-
once(the default,true): its effect runs once per game, however many times it arrives. Withonce: false, each distinct signal from the Bridge runs it again; a repeated delivery of the same one never does. - A signal that arrives while the game is busy (a dialogue, a cutscene, a minigame) waits for it.
- The first signal binds the save to its pseudonymous player (
GameState.reality.playerId, thep-…the Bridge gave at pairing; never the capability). A save that reaches a device linked as someone else (imported from another device, or saved before an unlink and a new pairing) is a mismatch: nothing of it is applied or acknowledged, the game plays on, and the pause menu's World link offers to use this device's link with that save; its link state then starts over for this player (cursor 0), and the signals whose effect the save already has are not applied again. A delivery without a player binds nothing (4.4.0). - The save and the session hold the player id, the cursor and the ids applied: never a token, an email, a message or a payload.
Guarantee ·
bridge-applied-once· referenced · scope self-checked by the author (AI), not reviewed The engine applies a declared signal once, records it in the session, and saves it with the state.
Guarantee ·
bridge-save-bound-to-player· referenced · scope self-checked by the author (AI), not reviewed The first delivery binds the save to its player; a delivery for another player is a mismatch and changes nothing.
A proof never assumes the outside cooperates. npm run solve:reality (part of npm run prove:game and of
quest-n-click release) proves the game in three kinds of world:
- closed: no signal at all; the game must be finishable through its fallbacks;
-
each scenario of
games/<id>/reality/scenarios/*.json({ "signals": ["mail.answer.wrong", "mail.answer.correct"] }), then each replay the connectors recorded (games/<id>/replays/*.json): those signals in that order, each able to arrive at any point after the one before; when the game hasrequiredsignals, at least one scenario sends every one of them; - adversarial: any declared signal, at any point, again and again.
Each must be solved with no softlock and never truncated. The solver hands the engine the signal itself: no service
is contacted. In the Studio's MCP, solve takes reality to try one world. npm run validate checks that a
required signal's fallback names a rule; the content lint (npm run lint, in verify:game) checks that the closed
witness plays it, and warns fallback-unplayed when the game finishes without the outside by another route (the
fallback is declared on a rule nobody needs: declare the action the witness plays, or make the declared one the way
through). How to read a solver's result in general: What the solver proves.
Guarantee ·
bridge-proof-closed· referenced · scope self-checked by the author (AI), not reviewed In the closed world the solver finishes the game through the fallback, never through a signal.
Guarantee ·
bridge-proof-adversarial· referenced · scope self-checked by the author (AI), not reviewed In the adversarial world, duplicates and every order of the declared signals never lose the game.
A speedrun's category says what it accepts from outside: forbidden (a signal in the run is reality-forbidden),
recorded (each signal applied is kept with its signed JWS, its key id and its hash, and the verifier checks the
signature with the Bridge's public keys, --keys or GET /v1/keys; no proof is missing-reality-proof) or live
(the same proof, in a category of its own, since a live signal's latency and availability cannot be reproduced;
without the keys the verifier cannot conclude, no-keyring). Details: Speedrun for authors.
Guarantee ·
bridge-speedrun-reality-proof· referenced · scope self-checked by the author (AI), not reviewed A run whose Reality proof is missing gets the codesignal-missing, an altered onesignal-signature, and one signed by another key the verdictmissing-reality-proof. Without a keyring the verdict isinconclusive(no-keyring).
Limit ·
bridge-proof-scopeThe proof covers the signals the game declares, in any order and number. It says nothing of whether a connector will ever send one, nor of what a connector does with its input.
Per player: the playerId, the SHA-256 of its capability, its expiry, the session id, the origin it paired from.
Per signal: its id, sequence, name, source, the connector's deduplication key, the hash of what the connector saw
(evidenceHash), the times, the signed envelope and, since 4.1.2, the payload as accepted (to sign it again after a
rotation): never a token, an email, a message or a badge. Pairing codes wait ten minutes: in memory on the journal (a
restart forgets them), in the pairings table of a SQL store. The log (standard output) names events and pseudonymous
ids, never a secret. Retention, export and deletion:
Install and run the Bridge; everything a game, a Bridge and its connectors keep about a player:
Privacy.
- "Exactly once": never promised. Every hop may repeat; the guarantees above make a repeat harmless.
-
The connectors and the
distributedprofile are experimental: no person has yet run a connector against a real email provider, a real badge or a terminal exposed on a network, nor thedistributedprofile behind a real domain for 24 to 48 hours (Stability and support). - No row-level security per tenant in Postgres, no retention on a SQL store, no connector quota shared between instances.
- The reference Bridge is for a game, a developer, an event or a small studio's games; the protocol is what stays.
Generated publication · publication générée — current documentation · documentation courante: quest'n'click 4.4.0 r1, documentation revision · révision de la documentation 8dc717c69acb62c4742b69c54a7e7dfb620f75d8.
Earlier versions and permalinks · versions précédentes et liens permanents: https://wanoo.github.io/quest-n-click/docs/
An edit made in this wiki is overwritten at the next publication · une modification faite dans ce wiki est écrasée à la publication suivante.
Edit the source · Modifier la source · Report an error · Signaler une erreur
Index (6)
Start (6)
Play (9)
Install (17)
- Requirements
- Install from the release archive
- Install from npm, once published
- Install from the repository to work on the engine
- doctor: checking your machine
- Image toolchain
- Audio toolchain: ffmpeg, fluidsynth, Furnace, uv
- Upgrade
- Upgrade notes: 4.4
- Upgrade notes: 4.3
- Upgrade notes: 4.2
- Upgrade notes: 4.1
- Upgrade notes: v3
- Upgrade notes: v2 to v3
- Coming from web-scumm
- Package reference
- Install troubleshooting
Create a game (55)
- Your first game
- Layout of a created project and of the engine repository
- The working loop
- Content principles and the identity contract
- Rooms
- Geometry: placement, walk zones, masks, layers, camera
- Something is visible but unreachable
- Reactions
- Game commands
- Conditions
- Flags, variables and events: which to use
- Custom code
- Characters
- Items and inventory
- World map
- Dialogues
- Scripts and events
- Moving characters: patrols and walks
- Exits
- Chapters and objectives
- State and saves: persistent or temporary, renaming an id, migrations
- Staging: wide rooms and animations
- The stage: lights, particles, floors
- Skin and fonts
- Title and credits
- Heroes: several characters and hero choice
- Built-in minigames
- A custom minigame
- Translations
- Music in stems
- Sound effects
- Voices: per language and per hero
- Images: prompts, cutting sheets, transparency, scale
- Asset provenance
- The storyboard, source of truth
- Puzzle design
- Classic mechanics
- Review pages
- DSL reference
- Checking your game
- What the solver proves
- Solver options
- Playtests
- Remix
- Speedrun for authors
- The sealed ending
- Production template
- Example games
- Working with an AI
- Build and release a game
- Publish on GitHub Pages
- The static game on Clever Cloud
- Other static hosts
- Sub-path, cache and service worker, incomplete deploys, going back
- Testing on a phone over the LAN
Local Studio (24)
- The local Studio: overview and launch
- Edit a room
- Placement and stage
- Preview, apply, undo
- Storyboard
- Assets and cutting
- Check: validator, solver, critical path, heat, health, coverage
- Before and after captures
- Semantic diff
- Play and replay a session
- Rule explainer
- Language and objectives
- Voices and the heroes × languages matrix
- Map
- Notes
- Remix in the Studio
- Music
- The Assistant: bring your own key
- Demo mode and studio-apply
- Local and LAN security
- Studio API
- Local MCP: setting up each client
- MCP tools
- Studio troubleshooting
Team Studio (21)
- What runs where
- Concepts: revision, transaction, span, lease, preview, build
- Starting as an invited dev
- Working together day to day
- Roles and actions: the full matrix
- Invitations
- Managing members
- Write refused on the hosted Studio: stale base revision (409)
- Write refused on the hosted Studio: span locked (423)
- Recovering after a lost lease, a disconnection or a stale preview
- Comments
- Map and semantic diff
- Publishing builds
- Remote MCP
- The game in Git
- Upgrading the engine of a hosted game
- Hosted assets
- The server's command line
- Health and metrics routes
- Trying it locally with compose.yml
- Known limits
Clever Cloud (23)
- Clever Cloud: overview, cost and prerequisites
- Apps and add-ons
- Domains and TLS
- Keycloak
- Generic OIDC and GitLab
- Configuration
- First deployment
- Backups
- RPO and RTO
- Restore the Studio on a fresh installation
- Redeploys
- Upgrading the server and checking state after a migration
- Limits of going back
- Monitoring
- Security
- Rotating keys
- Recovering admin access
- Changing domain or OIDC provider
- Worker, PostgreSQL or Cellar down
- Exporting the game and leaving the installation
- Retention
- Clever Cloud troubleshooting
- Costs and teardown
Reality Bridge and connectors (16)
- The Reality Bridge: what it is
- Quick start
- Guarantees
- Install and run the Bridge
- Backup and restore
- Several tenants
- Signals and the manifest
- The Bridge's command line
- Connectors overview
- Email connector
- Telnet connector
- SSH connector
- Open Badges connector
- The connector SDK
- Security: Biscuit and signatures
- Bridge and connector troubleshooting
Contribute (25)
- Welcome
- Developer setup
- The dev loop
- Before a pull request
- Conventions
- Writing documentation
- Architecture
- Code tour
- Import boundaries
- Which tests run when
- Mutation testing
- The coverage ratchet
- The quality baseline
- CI explained
- Field passes
- Benchmarks
- Releasing
- npm packages
- Decisions and ADRs
- Governance
- Bot pull requests
- Contributing with AI agents
- Security policy
- Code of conduct
- Licences and credits