AI Decision System
Replayable, offline-verifiable records for AI-assisted go/no-go decisions: experiment promotions, typed model answers and human-approved actions.
QR·PIN·초음파로 휴대폰·화면 연결, Firebase·WebRTC 등으로 실시간 스트리밍, 7 UX 프리셋·CLI 제공
Pair phones and screens with QR, PIN, broadcast, or experimental ultrasonic sound. Stream realtime input over Firebase, PartyKit, WebRTC or BroadcastChannel. 7 UX presets and a CLI.
개발 도구TypeScript · HTMLGitHub @takaoumehara온라인운영자 미확인
써 보셨어요? 한 번만 눌러 주세요. 로그인은 필요 없어요.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기메이커가 쓴 소개는 위의 한 줄이 전부입니다. 더 자세한 내용은 저장소 README에서 읽을 수 있습니다. 저장소 README ↗
README 전체를 불러오는 중입니다.
README 전문 보기 ↗snap-pair is a DevTool for multi-screen interactive web experiences. A phone becomes the controller and a big screen becomes the host. People pair by scanning a QR code, typing a 6-digit PIN, or (experimentally) hearing an ultrasonic chirp in their normal browser, with nothing to install, and every device in the room shares live input and state.
You bring the product (a quiz, a drawing wall, a game, a light show, a showroom) and build it on top. snap-pair handles the pairing, the realtime transport and the phone-side details that are easy to get wrong.
Transport API.npx snap-pair init) that scaffolds a working app.The npm package is snap-pair-core (MIT).
# 1. Scaffold a new app with the interactive wizard (en/ja)
npx snap-pair init
# …or non-interactively (CI, AI agents)
npx snap-pair init --yes --preset room-quiz-poll --out my-quiz
# 2. Or add the library to an existing React app
npm i snap-pair-core
pnpm add snap-pair-core
yarn add snap-pair-core
bun add snap-pair-core
Peer dependencies: react (18.2+). Optional: qrcode (QR rendering in
HostHUD), partysocket (a sturdier PartyKit socket) and react-dom (only
the generated templates use it).
The smallest possible multi-screen app needs no server at all. It pairs two tabs in the same browser with a PIN:
import { BroadcastChannelTransport } from 'snap-pair-core';
// Tab 1: the host (big screen)
const host = new BroadcastChannelTransport({ pairing: 'pin' });
await host.connect();
const { pairing } = await host.createRoom({ initialState: { strokes: [] } });
console.log('PIN', pairing.pin); // e.g. '042917'
host.onMessage((m) => draw(m.payload)); // m.type === 'stroke'
// Tab 2: the controller
const ctrl = new BroadcastChannelTransport({ pairing: 'pin' });
await ctrl.connect();
await ctrl.joinRoom('042917');
await ctrl.broadcast('stroke', { x: 0.42, y: 0.17 });
Swap BroadcastChannelTransport for PartyKitTransport (or
WebRTCTransport) and the same code works across the internet.
FirebaseTransport shares room state but has no ephemeral messaging, so
broadcast is not available there (see Transports).
Try it live in two tabs →
| Method | What the guest does | Works with | Helpers |
|---|---|---|---|
| QR code | Scans with the camera; the URL carries ?room= or ?pin= | All transports | buildPairingJoinUrl, parseJoinUrl, useQrRenderer, HostHUD |
| 6-digit PIN | Types 042 917 (full-width digits and dashes are normalized) | PartyKit, WebRTC, BroadcastChannel | generatePin, normalizePin, isValidPin, verifyPin |
| Ultrasonic sound (Proximity) | Host emits ~18–20 kHz FSK; phone listens on mic | PartyKit, WebRTC, BroadcastChannel (experimental) | encodeSoundToken, decodeSoundToken, startSoundEmitter, startSoundListener, useSoundPairing |
| Room code | Types a 6-character code like ABC 234 (no look-alike characters) | All transports (Firebase's default) | normalizeRoomCode, generateRoomCode |
| Broadcast | Opens another tab/window on the same machine | BroadcastChannel | BroadcastChannelTransport |
The host shows everything with one component:
import { HostHUD, useQrRenderer } from 'snap-pair-core';
const renderQr = useQrRenderer(); // undefined if `qrcode` isn't available, so the HUD shows the code only
<HostHUD pairing={pairing} renderQr={renderQr} peerCount={peers.length} status={status} />;
Sound pairing limitations: experimental Proximity mode. Needs microphone permission on the guest, a speaker that can produce ~18–20 kHz, and a relatively quiet room. Chrome/Edge work best; Safari is uneven. The tone is not a secret — anyone nearby can recover the token; use
admitfor gated rooms. Prefer QR or PIN in production.
| If you need… | Use | Why |
|---|---|---|
| Shared-state apps (turn-based games, checklists, lobbies) with server-checked joins (up to 300), auth and persistence | Firebase (useSnapPair default) | Cloud Functions admit every guest; RTDB rules limit what members can write. Shared state only: no ephemeral messaging |
| Input from phones over the internet, rooms up to hundreds, simple deploy | PartyKit | A tiny WebSocket relay (examples/partykit/); the host's browser owns the room |
| The lowest latency (drawing, games, motion) | WebRTC | Peer-to-peer DataChannels; signaling rides on PartyKit (or any transport with messaging) |
| Several windows or displays on one machine, offline | BroadcastChannel | No network, no server, no account |
All four implement the same Transport interface (connect, createRoom,
joinRoom, setState, send, broadcast, onMessage, onPeers,
onState, onStatus…), so switching is a one-line change. Check
transport.capabilities (messaging, presence, serverAuthoritativeJoin)
when your UI needs to degrade gracefully.
Firebase has no ephemeral messaging.
FirebaseTransportreportscapabilities.messaging === falseand rejectssend/broadcast, andsetStatereplaces the whole state object, so concurrent writers would clobber each other. All seven presets stream input, so none of them runs on Firebase. When you pick Firebase,npx snap-pair initoffers two options: a config-only Firebase app (preset: null, shared state throughuseSnapPair), or Firebase for your app plus PartyKit for the preset's realtime messages.
import { PartyKitTransport, WebRTCTransport, FirebaseTransport } from 'snap-pair-core';
const party = new PartyKitTransport({ host: 'my-relay.me.partykit.dev', pairing: 'pin' });
const p2p = new WebRTCTransport({ signaling: party }); // DataChannel star, host in the middle
const fb = new FirebaseTransport({ db, auth, functions }); // server-authoritative rooms, shared state only
WebRTC reconnects on its own (ICE restart on the same connection, then
re-offers with exponential backoff) and splits frames over 16 KiB into chunks
(up to 1 MiB per message). Pass reconnect: false to turn recovery off.
Seven ready-made UX patterns. Each one has a template you can scaffold with
npx snap-pair init, a recommended transport, message shapes and a rate
limit the template stays under (PRESETS / getPreset(id) expose all of it).
| Preset | id | Host shows | Phone sends (rate limit) | Transports (recommended first) | Pairing (default first) |
|---|---|---|---|---|---|
| Stroke Stream | stroke-stream | Shared canvas | stroke batches (≤30/s) | WebRTC · PartyKit · BroadcastChannel | QR · code · PIN |
| Particle Blast | particle-blast | Particle field | blast taps / swipes (≤10/s) | PartyKit · WebRTC · BroadcastChannel | QR · PIN · code |
| Type Throw | type-throw | Word wall | throw short text (≤2/s) | PartyKit · WebRTC · BroadcastChannel | QR · PIN · code |
| Room Quiz / Poll | room-quiz-poll | Question + live tally | vote (once per question) | PartyKit · BroadcastChannel · WebRTC | PIN · QR · code |
| Virtual Controller | virtual-controller | The game | input d-pad / buttons (≤60/s, on change) | WebRTC · PartyKit · BroadcastChannel | QR · code · PIN |
| Motion / Sensor | motion-sensor | Scene driven by tilt | motion orientation (≤30/s) | WebRTC · PartyKit · BroadcastChannel | QR · code · PIN |
| Local Multi-Display | local-multi-display | One scene across windows | tick, hello (≤60/s) | BroadcastChannel | broadcast |
Firebase is not listed for any preset; see the note under
Transports. Templates send controller
input to the host only (send({ to: hostId })), keep the host as the only
state writer (allowGuestState: false) and clamp every payload on the host.
npx snap-pair init # interactive wizard (default command)
npx snap-pair init --yes --preset room-quiz-poll --out my-quiz
npx snap-pair init --yes --preset virtual-controller --transport webrtc --json
npx snap-pair init --yes --architecture managed --no-scaffold # Firebase config only
npx snap-pair presets # list the 7 presets (--json for the registry)
npx snap-pair recommend "a tilt racing game for 4 friends"
The wizard speaks English or Japanese (from LANG / LC_ALL, or --lang en|ja) and offers four ways in (--path skips the menu):
Path (--path) | You answer | You get |
|---|---|---|
1. By experience (ux) | Which of the 7 presets feels right | That preset, then a transport it supports (recommended first) |
2. By architecture (architecture) | Same device, realtime, P2P, or managed | A transport, then the presets that fit it |
3. By stack (stack) | What you already have: Firebase, Cloudflare/PartyKit, or no backend | A transport built around your stack |
4. Describe it (consult) | A sentence in English or Japanese, e.g. "audience votes on a stage screen" | A rule-based recommendation you can accept or adjust |
Each transport option prints a summary, pros, cons and a cost line with
free-tier notes. Picking Firebase offers a config-only Firebase app or
Firebase plus PartyKit for the preset's realtime messages. Non-interactively:
--yes --architecture managed --no-scaffold writes the Firebase-only config,
and --yes --stack firebase --preset <id> keeps your Firebase app and puts
the preset on PartyKit (--transport firebase --preset <id> is rejected).
Every path writes snap-pair.config.json and scaffolds a Vite + React
template (host and controller in one app, picked by URL), plus a PartyKit
relay (party/server.ts) when the transport needs one:
{
"$schema": "./node_modules/snap-pair-core/dist/config.schema.json",
"version": 1,
"preset": "room-quiz-poll",
"transport": "partykit",
"pairing": "pin",
"locale": "en",
"maxPlayers": 300,
"partykit": { "host": "", "party": "main" }
}
| Flag | Meaning |
|---|---|
--preset <id|none> | One of the 7 preset ids, or none |
--transport <t> | broadcast | partykit | webrtc | firebase |
--pairing <m> | qr | code | pin | broadcast |
--architecture <a> | same-device | realtime | p2p | managed |
--stack <s> | firebase | cloudflare | none |
--describe <text> | Free-text idea for the consult path |
--out <dir> | Target folder (default: ./snap-pair-<preset>) |
--partykit-host <h>, --max-players <n> | Written into the config (maxPlayers: 2–300) |
--no-scaffold, --force | Config only; overwrite existing files |
-y, --yes | Accept every default (non-interactive) |
--json | JSON result on stdout (config, files, nextSteps); messages go to stderr |
--lang <en|ja> | Language |
Re-run the command any time to change your choices.
Everything is exported from snap-pair-core (ESM and CJS, with types).
Subpath entries share the same classes as the root:
| Import path | Contents |
|---|---|
snap-pair-core | Everything below, plus presets, i18n and the room server |
snap-pair-core/hooks/useSnapPair | useSnapPair only (the old snap-pair-core/src/hooks/useSnapPair path still works) |
snap-pair-core/transports/{base,firebase,partykit,webrtc,broadcast} | One transport each |
snap-pair-core/config.schema.json | JSON Schema for snap-pair.config.json |
snap-pair (bin) | The CLI: npx snap-pair |
useSnapPairimport { useState } from 'react';
import { PartyKitTransport, useSnapPair } from 'snap-pair-core';
// Firebase (default): server-assisted rooms
const sp = useSnapPair({ db, auth, functions, guest: { id: '', name: 'Ada' }, maxPlayers: 8 });
// Any other transport: pass an instance (you own it) or a factory (the hook owns it)
const [party] = useState(() => new PartyKitTransport({ host: 'my-relay.me.partykit.dev', pairing: 'pin' }));
const sp = useSnapPair({ transport: party, guest: { id: '', name: 'Ada' } });
const { room, authReady, createRoom, joinRoom, updateState, updateOwnPlayer, updateRoomStatus, leaveRoom } = sp;
Wait for authReady before calling createRoom(initialState) or
joinRoom(code). Both modes return the same shape. With a transport,
authReady follows the connection, localGuest.id becomes the transport's
peer id, and you use the instance's send / broadcast / onMessage for
ephemeral input. Keep an instance stable (useState(() => new X())); a
factory (transport: () => new X()) is created once and disconnected on
unmount. The mode is fixed for a component's lifetime. With Firebase, members
may update shared state, their own name, connected and lastSeenAt, and
only the host may change the room status.
| Export | Notes |
|---|---|
Transport | Abstract base: connect / disconnect, createRoom / joinRoom / leaveRoom, setState, send / broadcast, on* subscriptions that return an idempotent unsubscribe, and capabilities |
FirebaseTransport, FirebaseRoomStore | RTDB + callable Cloud Functions; serverAuthoritativeJoin: true |
PartyKitTransport | Options: host, party, pairing, socketFactory (inject partysocket in bundled apps), connectTimeoutMs. Falls back to WebSocket with backoff |
WebRTCTransport | Options: signaling (any transport with messaging), iceServers (default: one public STUN), connectTimeoutMs, reconnect (false or { maxAttempts, baseDelayMs, maxDelayMs, iceRestartGraceMs }), maxMessageBytes (16 KiB), maxReassembledBytes (1 MiB) |
BroadcastChannelTransport | Same-origin tabs/windows; isBroadcastChannelSupported() |
RelayTransport | Shared host-authoritative engine behind the last three. Common options: pairing: 'code' | 'pin', maxPlayers, admit(peer), namespace, joinBaseUrl, heartbeatMs, peerTimeoutMs, allowGuestState |
| Export | Notes |
|---|---|
generatePin(), normalizePin(), isValidPin(), formatPin(), verifyPin() | Uniform 6-digit PINs (crypto.getRandomValues), constant-time verify |
deriveRoomId(), deriveChannelName() | Hash a code or PIN into a namespaced room key; works on plain-HTTP LAN origins too |
buildPairingJoinUrl(), parseJoinUrl() | ?pin= / ?room= join links |
useQrRenderer(), createQrRenderer(), toQrDataUrl(), loadQrLib() | QR rendering via the optional qrcode package |
generateRoomCode(), normalizeRoomCode(), ROOM_CODE_ALPHABET | 6-character, look-alike-free room codes |
| Export | Notes |
|---|---|
useWakeLock(enabled), createWakeLock(), isWakeLockSupported() | Keeps a controller screen on; re-acquires when the tab becomes visible |
needsPermission(), requestOrientationPermission() | iOS 13+ permission prompt; call from a tap handler. Resolves 'granted' | 'denied' | 'unsupported' |
subscribeOrientation(), subscribeMotion() | alpha/beta/gamma and acceleration readings |
lockScreenOrientation(), unlockScreenOrientation(), getScreenOrientation() | Screen orientation lock where supported |
button.onclick = async () => {
if (needsPermission() && (await requestOrientationPermission()) !== 'granted') return;
const stop = subscribeOrientation(({ beta, gamma }) => transport.broadcast('tilt', { beta, gamma }));
};
Every client utility feature-detects at call time and degrades to a no-op, so it is safe to import during SSR.
| Export | Notes |
|---|---|
HostHUD | Host pairing panel: QR (via renderQr), room code, PIN, join link with copy button, peer count, status. locale="en" | "ja" | "auto"; every string can be overridden with labels |
ControllerWrapper | Phone-side shell: status bar, reconnect banner, wake lock toggle, iOS motion permission button, fullscreen + orientation lock. Each control hides itself where the browser lacks the API |
import { ControllerWrapper } from 'snap-pair-core';
<ControllerWrapper
transport={party} // or status={status}
roomCode={room?.code}
onReconnect={() => joinRoom(code)}
motion // shows the iOS permission button until granted
orientation="portrait" // fullscreen button that also locks orientation
locale="auto" // 'en' | 'ja' | 'auto'; override strings with labels
>
{({ status, motionPermission }) => <Pad disabled={status !== 'connected' || motionPermission !== 'granted'} />}
</ControllerWrapper>;
wakeLock defaults to true; onMotionPermission, className and style
are also accepted.
PRESETS, getPreset(id), supportedTransports(preset), pairingFor(preset, transport) and presetsForTransport(transport) expose the preset registry
the CLI uses. createTranslator(locale), t(), detectLocale() and
SUPPORTED_LOCALES (en, ja) are the bundled dictionaries behind the CLI,
HostHUD and ControllerWrapper.
pairingCodes/{code} # Admin SDK only
roomMembers/{roomId}/{uid}: true # Admin SDK only; client cannot read/write
roomCreationLimits/{uid} # Admin SDK only; fixed-hour create quota
rooms/{roomId}/meta
rooms/{roomId}/players/{uid}
rooms/{roomId}/state
rooms/{roomId}/joinState # Admin SDK only
functions/src/): createSnapRoom,
joinSnapRoom, and a scheduled cleanupExpiredData.database.rules.json): membership-gated reads and narrow
client writes, with no broad room-level write.state is intentionally generic. It does not validate a product schema or
limit payload size and write rate. Every product must add validation,
payload limits and throttling before going to production.Design notes: docs/plan-phase1.md, docs/plan-phase2.md, docs/plan-phase3.md.
What is this? A way to make many people's phones join one shared screen instantly. Everyone scans a QR code (or types a short code) in their normal browser, with no app to download, and their phones become part of one live, synchronized experience.
Who is it for? People who run events, venues, classes, streams, showrooms or exhibitions and want the audience to take part with their own phones, and the engineers and AI builders who make those experiences.
What can you build? Live votes, polls and quizzes on a big screen; group checklists and "everyone's ready" checks; audience reactions, prediction games and collaborative drawing; a synchronized phone light show; any "one shared screen + many phones + instant result" moment.
These are three separate paths, not steps of one process. Pick the one that matches what you want right now.
| A. Just see it work | B. Have an AI build my app | C. Work with the source | |
|---|---|---|---|
| For | "Show me in 2 minutes" | "I want a custom app without writing code" | "I'm an engineer and want to read, modify or contribute" |
| Install | Nothing | One AI coding tool | An AI coding tool and Git/Node.js |
| Download | Nothing (or one HTML file) | Nothing: the AI reads the instructions from the web | The repository |
| Credit card? | No | Depends on the backend (see Firebase setup) | Depends on the backend |
| Share a link? | Same browser / same network | Yes, once deployed | Yes, once deployed |
| Go to | Path A | Path B | Path C |
examples/snap-pair-lite.html, open it,
and scan the QR code with a second device. It's a fixed tic-tac-toe demo
running on the free Firebase Spark plan with no credit card (see
examples/README.md).You do not need to download or clone this repository. Connecting two small tools (MCP servers) is enough, plus telling the AI to read this project's build instructions from the web.
You need one AI coding tool that can run commands and fetch web pages: Claude Code, Cursor, Codex, Gemini CLI or similar. Don't have one? Pick one: Cursor (an editor with AI built in), Claude Code (VS Code extension or the CLI from claude.com/code), or Codex / Gemini CLI.
Open a chat in your AI tool in any folder (an empty folder is fine).
Paste this, as-is:
Fetch https://raw.githubusercontent.com/takaoumehara/snap-pair-skill/main/SKILL.md
and use it as your build instructions.
Connect these two MCP servers if they aren't connected yet:
- firebase: npx -y firebase-tools@latest mcp
- snap-pair-provisioner: npx -y snap-pair-provisioner
Then help me build: [describe what you want, e.g. "a live quiz game where
guests join by QR code and answer on their phones"].
Answer the AI's questions as they come up. It will ask which Google account to use for Firebase and, at one point, show a one-time sign-in link. That click is the only manual step, by design, for your account's safety.
If your tool cannot fetch web pages, download just
SKILL.md and paste its contents into the chat instead.
git clone https://github.com/takaoumehara/snap-pair-skill.git
cd snap-pair-skill
npm install
npm test
Then read the API overview and the design notes in
docs/.
Only needed for the Firebase transport. PartyKit, WebRTC and BroadcastChannel don't use Firebase at all.
| Your goal | Use | Credit card? | Others can join by URL? |
|---|---|---|---|
| Learn, experiment, let a child build and test | Firebase Emulator (on your computer) | No | No, local only |
| Free public demo, open rules, small groups | Spark plan + Lite mode (examples/) | No | Yes (up to 100 simultaneous connections) |
| Real guests with server-checked rooms | Blaze plan | Yes | Yes |
npm install
npm --prefix functions install
npm --prefix functions run build
npx firebase-tools emulators:start --only auth,database,functions
Spark needs no card, serves a public site with Firebase Hosting and allows Realtime Database with a hard cap of 100 simultaneous connections. Spark cannot deploy Cloud Functions, and snap-pair's secure mode relies on them for room creation and joining. Use it for Lite mode, or pick another transport.
Blaze keeps the free quotas (about 2,000,000 function invocations/month; RTDB up to 200,000 simultaneous connections) and only bills beyond them, so a small event often costs nothing. The card must be on file to enable it.
npm --prefix functions run build
firebase use YOUR_EXISTING_PROJECT
firebase deploy --only functions
firebase deploy --only database
Both callables enforce Firebase App Check. Configure a debug token for local development against a real project, and never disable enforcement in the deployed callable. Set a budget alert in Firebase console → Usage and billing (alerts notify; they do not hard-cap charges).
The MCP servers from Path B create the project, enable services and write
your .env for you. See
SKILL.md for what is
automatic and what stays a one-time manual step.
joinState stay server-only.capabilities.serverAuthoritativeJoin === false).
Gate entry with admit(peer) and maxPlayers. Room keys are hashed and
namespaced, so raw PINs never appear in URLs or relay logs.examples/partykit/) and don't use PINs alone
for anything sensitive.SKILL.md teaches an AI coding agent to generate a correct
snap-pair integration and to choose a transport and preset. Point your agent
at the raw URL:
https://raw.githubusercontent.com/takaoumehara/snap-pair-skill/main/SKILL.md
or copy it into your agent's skills directory. For a private-input,
aggregate-reveal, commitment-threshold pattern, see
references/one-room-one-decision.md.
No. Guests open a URL in the browser they already have, by scanning a QR code or typing a code.
Prototyping on one machine: BroadcastChannel. Phones over the internet:
PartyKit (WebRTC for the lowest latency in small rooms, ≈2–16 people).
Shared-state apps where joins must be checked on a server: Firebase (it has
no ephemeral messaging, so pair it with PartyKit for streamed input). Or run
npx snap-pair init (or npx snap-pair recommend "<your idea>") and let the
CLI decide.
Not for BroadcastChannel, the Firebase Emulator, or Firebase Spark Lite mode. Firebase's secure mode needs the Blaze plan, which needs a card, though small events usually stay within the free quota. PartyKit has its own free tier.
Firebase secure rooms are designed for up to 300 participants. Relay
transports are limited by maxPlayers and by how many connections the host
browser can handle. WebRTC uses a star around the host, which suits small
groups best.
iOS 13+ requires a permission prompt triggered by a tap. Call
requestOrientationPermission() from a click handler (or use
ControllerWrapper, which adds the button for you). The page must be served
over HTTPS.
Yes. Transports, pairing helpers and client utilities are plain TypeScript.
Only useSnapPair, useWakeLock, useQrRenderer, HostHUD and
ControllerWrapper need React. The live demo
is plain JavaScript.
Yes. No module touches window, document or navigator at import time.
Create transports inside effects or client components.
Phase 1: Transport abstraction, FirebaseTransport, HostHUD
Phase 2: PartyKit, WebRTC and BroadcastChannel transports; PIN and QR helpers; wake lock and orientation utilities
Phase 3: npx snap-pair init wizard (4 paths, en/ja) with
presets and recommend commands, 7 preset templates,
ControllerWrapper, useSnapPair({ transport }), i18n, ESM/CJS build
with an exports map, WebRTC auto-reconnect and message chunking
Proximity pairing: Web Audio ultrasonic FSK (sound) as an experimental pairing layer
Still to do:
bufferedAmount), mesh topology, host
migration, TURN guidance, real-browser end-to-end testsuseSnapPair imports firebase/auth
statically, so non-Firebase apps still bundle it; a Firebase-free hook
entry or lazy loadingrooms/$roomId/messages), so presets can run on itnpm test
npm run typecheck # library + templates
npm run build # dist/ (ESM + CJS + d.ts), dist/cli, templates/
node dist/cli/index.js --help
npm --prefix functions test
npm --prefix functions run typecheck
npm run test:rules-emulator
.github/dependabot.yml opens weekly update PRs
for npm in /, /functions and /tools/snap-pair-provisioner, plus GitHub
Actions. Minor and patch updates are grouped into one PR per directory; major
updates get their own PR. All of them are labeled dependencies..github/workflows/ai-update-helper.yml
runs scripts/ai-update-helper.mjs:
dependabot[bot]), it diffs package.json / package-lock.json between
base and head (falling back to the PR body), fetches GitHub release notes
or CHANGELOG.md for each package, finds import / require usages with
git grep, and asks Gemini for a per-package summary, an impact verdict
(Not affected / Possibly affected / Affected) and proposed diffs. The
result is one PR comment that is updated in place on every push.package-lock.json with the npm registry's latest versions
(no npm install needed) and updates a single open issue titled
"AI dependency update report".Create a Gemini API key in Google AI Studio.
Add it as GEMINI_API_KEY in both places under
Settings → Secrets and variables:
With the GitHub CLI:
gh secret set GEMINI_API_KEY
gh secret set GEMINI_API_KEY --app dependabot
Optional: set a repository variable GEMINI_MODEL (Settings → Secrets
and variables → Actions → Variables) to choose the model. The default is
gemini-3.5-flash. Gemini 1.5 Flash is retired, so don't use it.
Without GEMINI_API_KEY the script logs a notice and exits successfully. The
workflow never fails just because the key is missing.
Permissions. The workflow sets permissions: {} at the top level and grants
each job only what it needs: contents: read + pull-requests: write for the
PR job, and contents: read + issues: write for the weekly job. The API key
goes in the x-goog-api-key header, never in the URL or the logs.
Run locally. This prints the Markdown instead of writing to GitHub:
# Weekly report (queries the npm registry; without a key it prints only the table)
node scripts/ai-update-helper.mjs --dry-run
# PR analysis for a local commit range
node scripts/ai-update-helper.mjs --dry-run --mode pr --base origin/main --head HEAD
Review AI suggestions before merging. The verdicts and diffs are generated by an LLM from release notes and grep results. They can be incomplete or wrong. Treat them as a starting point, and rely on CI and your own review.
MIT. See LICENSE.