A Takuzu game native to the AT Protocol. atkuzu.tomplanche.com
binairo atproto game takuzu
Svelte 50%
TypeScript 35%
Rust 10%
JavaScript 2%
SCSS 2%
Dockerfile <1%
Shell <1%
HTML <1%
<1%

README.md

atkuzu #

A game built on the AT Protocol, inspired by AT Mot.

atkuzu = AT Protocol + Takuzu (aka Binairo). Players solve a daily puzzle; results are written to their own PDS as atproto records.

This is a SvelteKit app with browser OAuth already wired up: users log in with their own AT Protocol handle (Bluesky or any other PDS), and actions are taken on their behalf against their own repo. There is no game logic here yet, this is the OAuth/session/DB scaffolding the game will be built on.

Forked from the atproto-sveltekit-template, see NOTICE.md for attribution.

Dev Setup #

Should work with any package manager if you prefer to use another. But these are directions for pnpm

  1. Copy .env.example to .env, .env.example is the default dev settings
  2. pnpm install
  3. May need to run pnpm approve-builds for the build scripts for sqlite
  4. pnpm run dev or pnpm run dev:logging with pino-pretty for pretty logging

If you are running locally on a different port than 5173 or something else odd can set OAUTH_DOMAIN in .env to the domain and port. Just make sure to use either 127.0.0.1 or [::1] (ipv6) for oauth to work for local development.

Implementation details #

How OAuth is configured #

OAuth is configured entirely from environment variables, no code changes needed.

All atproto actions taken for the user happen server side, so we can take advantage of the confidential client and longer session lifetimes.

Types of OAuth clients #

  • Development local: Set DEV=true in .env to use a local development client. This is a special client just for development, it does not require a public url. As outlined here. This is the default from copying .env.example
  • Production Public: Remove DEV=true, set OAUTH_DOMAIN to your publicly accessible domain in .env to use a production public client. These have a lower atproto oauth session lifetime, limited to 2 weeks.
  • Production Confidential: Follow Production Public and set OAUTH_JWK to the value from node ./bin/gen-jwk.js. These are cryptographic signing keys and should be kept secret and private. These have the longest session lifetime: 180 days for refresh tokens, or indefinitely if refreshed, pending revocation or jwk rotation.

Production most likely wants the confidential client for the longest lifetime. The cookie session lifetime is shorter and could expire before the atproto session lifetime; that's configured in ./src/lib/server/session.ts, default 30 days, reset on every logged-in web action. More on client types and session lifetime.

OAuth scopes #

OAuth scopes are permissions to the user's repo you are requesting. Read more at atproto.com/specs/permission.

Set requested scopes via the OAUTH_SCOPES environment variable, tailored to whatever lexicons the game ends up writing to. The default in client.ts requests write access to atkuzu's own collections (see Lexicons below). For development or full access to a user's repo, atproto transition:generic.

You may find that the OAuth consent screen doesn't immediately reflect a scope change. The PDS can cache those. Docs say this can be anywhere from 15-30mins

OAuth branding #

A couple of other odds and ends you can set for OAuth to customize the branding. This mostly shows up on yourpds.com/account for now, but it is a standard and more may adopt it. Docs:

  • OAUTH_CLIENT_NAME (string, optional): human-readable name of the client
  • OAUTH_LOGO_URI (string, optional): URL to client logo. Only https: URIs are allowed.
  • OAUTH_TOS_URI (string, optional): URL to human-readable terms of service (ToS) for the client. Only https: URIs are allowed.
  • OAUTH_POLICY_URI (string, optional): URL to human-readable privacy policy for the client. Only https: URIs are allowed.

Database #

This project uses drizzle ORM with the sqlite adapter to make it easy to run locally and get started. It's used for the server-side and atproto session stores. This will work in production as well, but if you want to change out the database layer it shouldn't be too bad since there aren't many db queries right now.

Handle input #

HandleInput.svelte is a typeahead component for AT Protocol handles, used on the login screen. It queries the public Bluesky appview (public.api.bsky.app) and remembers recently picked handles in localStorage.

Lexicons #

atkuzu's record types are defined as lexicons under the com.tomplanche.atkuzu.* NSID authority (domain authority atkuzu.tomplanche.com, reversed):

  • lexicons/com/tomplanche/atkuzu/result.json: com.tomplanche.atkuzu.result, one immutable write-once record per completed daily puzzle (puzzle number, whether it was solved, time taken, and optionally how many cells were toggled)
  • lexicons/com/tomplanche/atkuzu/stats.json: com.tomplanche.atkuzu.stats, a single mutable per-account record (currentStreak, maxStreak, gamesPlayed, gamesWon, lastPuzzleNumber), also doubling as the "this account plays atkuzu" declaration record
  • lexicons/com/tomplanche/atkuzu/preferences.json: com.tomplanche.atkuzu.preferences, a single mutable per-account record of board display preferences (theme: digits, sunmoon, or colors), read back on load and synced across devices

Validate the schemas against the Lexicon Style Guide:

pnpm run lexicons:validate

Publish them as com.atproto.lexicon.schema records (run by the account that controls the namespace authority) to make them network-resolvable:

ATKUZU_PUBLISH_IDENTIFIER=you.example ATKUZU_PUBLISH_PASSWORD=<app-password> pnpm run lexicons:publish

Publishing also requires a _lexicon DNS TXT record on _lexicon.atkuzu.tomplanche.com with value did=<publishing did>. Lexicon resolution is not hierarchical, each distinct authority needs its own exact TXT record, resolvers never walk up the DNS tree. See Lexicon Publication and Resolution.

Writes still pass validate: false (see records.ts): not every PDS resolves third-party lexicons over the network for validation yet, confirmed failing on the account's own PDS with Unknown lexicon type even after publishing and DNS were both in place.

Other production considerations #

There isn't a great way to run a "sidecar process" with SvelteKit: a Jetstream listener that runs alongside the SvelteKit application. If the game ends up needing to listen to the firehose or jetstream for real-time record creation, swap the database layer for something not embedded, then run a separate container (or process) with a script in a loop to consume that data. @atcute/jetstream is a good way to do this in TypeScript. For an overview of that pattern, see the Statusphere quick start guide.

Production #

Local server like a VPS #

  1. Install docker
  2. Copy .env.example to .env and fill in the variables. Make sure to remove DEV=true
  3. docker-compose up

The docker compose comes with Caddy, if you have another reverse proxy you can remove it from the docker compose and just reverse proxy to port 3000. You may also have to adjust the Caddyfile depending on your setup.

Daily puzzle scheduling #

Generating and publishing each day's puzzle isn't handled by the SvelteKit process itself: it's a separate cron job on the VPS (Europe/Paris midnight, DST included), not something kept alive alongside the site. See src/routes/daily/README.md for what that job actually runs.