Project Status - atprotogo #
Last Updated: September 22, 2026
Current State #
The application builds (npm run build) and runs on Cloudflare Pages with a D1 database. Gameplay, social and image features are implemented end to end, and game records are really published to the AT Protocol network.
Implemented #
Platform #
- SvelteKit app built for Cloudflare Pages (
@sveltejs/adapter-cloudflare), deployed withnpm run deploy/wrangler pages deploy - D1 database (
atprotogo-db, bound asDB) accessed through Kysely (kysely-d1) - OAuth sessions and state in KV when the
SESSIONS_KV/STATES_KVbindings are configured, otherwise an in-memory store - Bluesky OAuth via
@atcute/oauth-node-client, in two configurations: a public loopback client for local development (the atproto localhost exception — the authorization server synthesises the metadata from theclient_id, so nothing is published and no signing key is used) and a confidential client that publishes<PUBLIC_BASE_URL>/oauth/v2/client-metadata.jsonand signs its requests withPRIVATE_KEY_JWK - Image generation (board, OG, reaction, nudge) with
@cf-wasm/resvg
Features #
- Game Creation: Create Go games with various board sizes (5x5, 7x7, 9x9, 13x13, 19x19)
- Game Joining: Join waiting games or play with specific opponents
- Gameplay: Place stones, pass, resign
- Handicap: Support for handicap stones (up to 9 on 19x19, 5 on 13x13)
- Scoring: tenuki territory scoring with dead-stone marking and winner determination
- Reactions: Comment on specific moves with text, emoji, and star ratings
- Profiles: Player profiles with game history and ELO ratings
- Image Generation: Board SVGs/PNGs, OG images, reaction images, nudge images
- Real-time: Client-side Jetstream subscriber (
src/lib/firehose.ts) streams moves, passes and resignations into the game page
Architecture Notes #
Real-time updates #
The game page subscribes to Bluesky's public Jetstream over a WebSocket and applies boo.sky.go.move / pass / resign records as they are published, so players see opponent moves without refreshing.
D1 discovery index #
Route handlers resolve a game by reading the D1 games table first and only sampling the network when it is missing. That index is maintained by a Durable Object Jetstream consumer in the standalone firehose-worker/ Worker (its own wrangler.toml, deployed separately from the Pages app): the Durable Object holds a WebSocket to Jetstream filtered to boo.sky.go.{game,move,pass,resign,action}, applies records to the same D1 database through the shared helpers in src/lib/server/firehose-records.ts, resumes from a stored Jetstream cursor after reconnects (~36h of retention), and uses a Durable Object alarm as keepalive. A Cron Trigger bootstraps and recovers the consumer. It is a separate Worker because @sveltejs/adapter-cloudflare emits a fixed _worker.js and offers no supported way to export extra Worker modules from the Pages app.
File Structure #
src/
├── routes/
│ ├── game/[id]/+page.svelte # Main game UI with reactions
│ ├── profile/[did]/ # Player profiles
│ ├── og-image/[id]/+server.ts # Social image generation
│ └── api/games/
│ ├── +server.ts # Create game
│ └── [id]/
│ ├── join/+server.ts # Join game
│ ├── move/+server.ts # Record move
│ ├── pass/+server.ts # Record pass
│ ├── cancel/+server.ts # Resign
│ ├── reaction/+server.ts # Create reaction
│ └── score/+server.ts # Scoring
├── lib/
│ ├── server/
│ │ ├── db.ts # D1 + Kysely, ensureGameInDb()
│ │ ├── auth.ts # OAuth client
│ │ ├── scoring.ts # tenuki scoring
│ │ ├── elo.ts # Ratings
│ │ └── firehose-records.ts # Shared record → D1 helpers (used by firehose-worker)
│ ├── atproto-client.ts # AT Protocol helpers
│ ├── firehose.ts # Client-side Jetstream subscriber
│ └── components/
│ └── Board.svelte # jgoboard board component
lexicons/
├── boo.sky.go.game.json
├── boo.sky.go.move.json
├── boo.sky.go.pass.json
├── boo.sky.go.resign.json
├── boo.sky.go.reaction.json
└── boo.sky.go.profile.json
migrations/
└── 0001_initial_schema.sql # D1 schema
firehose-worker/ # Durable Object Jetstream consumer
Development #
Local Development #
# Install dependencies
npm install
# Build, then serve the worker with its D1 and KV bindings
npm run build
npx wrangler pages dev --port 8788
Logging in needs neither a tunnel nor a signing key on a loopback origin, so
npm run setup:key is only required to exercise the deployed (confidential)
client configuration. Open the app at http://127.0.0.1:8788, not
localhost — the spec only allows 127.0.0.1/[::1] redirect URIs. See
LOCAL_DEVELOPMENT.md.
Production (Cloudflare Pages) #
# Build for production
npm run build
# Deploy to Cloudflare
npx wrangler pages deploy .svelte-kit/cloudflare --project-name=atprotogo
The discovery-index consumer deploys separately:
npm run deploy:firehose
Production Stack:
- Cloudflare D1 database (
atprotogo-db) - Cloudflare KV for sessions/state (optional bindings)
- Environment secrets from the dashboard
- Global edge deployment
Known Limitations #
- Scoring is a territory estimate that relies on players marking dead stones
- The D1 discovery index only contains what the Jetstream consumer has seen (plus manual backfill via
npm run backfill), so older games may be missing until backfilled - No time controls or undo functionality
Documentation #
README.md- Project overview and setupCLOUDFLARE_DEPLOYMENT.md- Deployment guideMIGRATION_SUMMARY.md- Migration detailsLOCAL_DEVELOPMENT.md- Local OAuth setup