CYOACatalog

command module
v0.0.0-...-0bd001b Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 3, 2026 License: Apache-2.0 Imports: 52 Imported by: 0

README

cyoa.cafe

Catalog and hosting platform for CYOA games — interactive "Choose Your Own Adventure" projects that live as self-contained web pages. The site indexes them, hosts them, and lets people find, play, rate and discuss them.

Runs in production at cyoa.cafe.

This repository holds the whole serving side of the product:

Directory What it is
src/ Frontend — React 18 + TypeScript + MUI, built with Vite
*.go (repo root) Backend — Go, built on PocketBase as a framework
pb_hooks/ PocketBase JavaScript hooks, loaded from disk at runtime
semantic-search/ Semantic search service — Python, FastAPI, Gemini embeddings
docs/ Architecture, repository map, CDN configuration
e2e/ Playwright end-to-end tests

The pipeline that fills the catalog — crawling, downloading, optimizing and publishing games — is a separate project with its own repository.

How it fits together

One Go binary is the whole server. It embeds PocketBase (SQLite database, REST API, authentication, admin UI), adds the site's own HTTP routes on top, and serves the compiled frontend through go:embed. There is no separate API process and no Node runtime in production.

Two things run beside it:

  • Semantic search — a small FastAPI process bound to 127.0.0.1:8100. nginx routes /api/semantic-search and /api/similar-games/{id} to it. The Go backend is not involved in those requests at all.
  • Cloudflare — CDN and cache in front of everything. See docs/cdn-cache.md; the cache rules are load-bearing and easy to get subtly wrong.

Game files live in Cloudflare R2 and are served from <slug>.cyoa.cafe.

Deeper detail: docs/ARCHITECTURE.md. File-by-file map: docs/STRUCTURE.md.

Requirements

  • Go 1.25+
  • Node 20+ or Bun — build only, not needed at runtime
  • Python 3.11+ — only if you are working on semantic-search/

Getting started

cp .env.example .env      # fill in what you need — see Configuration below
make install              # install frontend dependencies
make build                # tsc + vite build + go build -> ./dist/serve
make run                  # serve on http://127.0.0.1:8090

main.go embeds the built frontend with //go:embed dist, and dist/ is generated, not committed. A bare go build on a fresh clone therefore fails — always go through make build, which runs the frontend build first.

On first start, open http://127.0.0.1:8090/_/ and create a superuser. The database is created empty in pb_data/; see Database schema to get the collections in place.

Development loop
make dev                  # Vite on :8091 + `go run` on :8090

make dev proxies API calls to the production instance. It exists for frontend work against real data. Any change to Go code or to pb_hooks/ must be tested with make build && make run on :8090 instead — make dev never exercises your local backend, and the failure mode is that your change appears to do nothing.

Tests
go test ./...             # Go unit tests
make test                 # Playwright end-to-end (builds first)

Configuration

Every setting comes from environment variables read from .env at startup. .env.example has the complete annotated list. The ones that cause quiet misbehaviour when missing:

Variable Needed for
PB_APP_URL Absolute links in emails, push notifications, SEO tags
R2_* Game hosting storage — uploads fail without it
CLOUDFLARE_API_TOKEN, CLOUDFLARE_ZONE_ID Purging the CDN when a game is re-uploaded
TURNSTILE_* Bot protection on the game-claim flow
VAPID_* Web push. Rotating these invalidates every existing subscription
SHOUTBOX_ENABLED, SHOUTBOX_ANON_SALT Site chat. Disabled → endpoints 404 and the UI hides itself

Email delivery (verification, password reset) is configured in the PocketBase admin UI under Settings → Mail, not through environment variables.

Database schema

The database is PocketBase's own SQLite file under pb_data/. This repository ships no database — it would contain real user accounts.

To bring an instance up with the right collections, import the schema snapshot from the admin UI: Settings → Import collections. See docs/pocketbase-collections.md for the collection list and the procedure.

The schema has no automated migration story: it is managed by hand through the admin UI, and the snapshot is a point-in-time export. Read it as documentation of intent, not as a guaranteed mirror of what production runs today.

Deployment

make ship SSH_HOST=root@your-server

make ship copies only the compiled binary dist/serve and restarts the service. pb_hooks/ is read from disk on the server and is not embedded in the binary — changing a hook means shipping that file separately and restarting.

The semantic search service deploys on its own; see semantic-search/README_DEPLOY.md.

Status and expectations

This is a working production system for one specific site, not a product you can deploy for your own catalog without effort. Parts of it are wired to cyoa.cafe by name: the Cloudflare zone, the R2 buckets, the game subdomain scheme. There is no multi-tenant story and no upgrade path between schema versions.

It is published so the code can be read, learned from and borrowed — not as a supported release. Issues and pull requests are welcome; there is no roadmap commitment behind them.

License

Apache License 2.0. You may use, modify and redistribute this code, including in closed-source and commercial work, as long as you keep the license and attribution and state what you changed. Nothing here obliges you to publish code you build on top of it.

The license covers this source code only. It grants no rights to the "cyoa.cafe" name or branding, and it says nothing about the game content hosted on the site — that belongs to its respective authors.

Documentation

Overview

usernames.go — вменяемые username при OAuth-регистрации + правило «сменить один раз».

Зачем:

  1. Discord/OIDC часто отдают ник, который не проходит паттерн поля username (точки, юникод, слишком короткий) или уже занят — тогда PocketBase-ядро генерит безликое `usersNNNNNN`. Хук перехватывает создание и кладёт в CreateData осмысленный уникальный username (из ника → имени → email), попутно заполняя пустой name.
  2. updateRule коллекции = `id = @request.auth.id` разрешает юзеру править свою запись, а PB-правила не умеют ограничивать ОТДЕЛЬНОЕ поле. Поэтому «username можно сменить лишь однажды» держим серверным гардом: первая смена разрешается и выставляет флаг username_locked, повторная — 403.

Требует в коллекции users булево поле `username_locked` (default false).

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL