Files
buzz-sheet/README.md
T
2026-10-05 18:47:41 +07:00

16 KiB
Raw Blame History

Buzz Guide

A Thai-first Genshin Impact guide CMS for publishing character builds, teams, rotations, and comparisons from structured data.

Live site: guide.sudloh.com

Buzz Guide is an independent community project and is not affiliated with or endorsed by HoYoverse. The repository and deployment resources retain the buzz-sheet name.

Features

  • Public guides: a searchable character directory and responsive guide pages.
  • Visual editing: structured editors for overviews, weapons, artifacts, constellations, teams, and custom sections, with autosave and conflict recovery.
  • Accounts: Sudloh Account OIDC sign-in with local Buzz sessions, IDs, and roles. Legacy email/password and registration OTP remain available only before SUDLOH_OIDC_ONLY=true cutover.
  • Media: S3-compatible uploads and publication-aware delivery for staged files.
  • Catalog updates: Discord-triggered synchronization with Lunaris.
  • Commissions: PromptPay checkout, Slip2Go verification, ticket attachments, and opt-in browser push notifications.
  • Shared state: PostgreSQL transactions, a retryable outbox, Redis caching, and Server-Sent Events across application replicas.

The five reference workbooks are included only as sanitized structural and formula fixtures. Their guide text and media are not imported or published.

Contents

Local development

Requirements

  • Bun 1.3.14
  • PostgreSQL
  • Redis
  • S3-compatible object storage

Setup

git clone https://git.astrxl.dev/astral/buzz-sheet.git
cd buzz-sheet
bun install --frozen-lockfile
cp .env.example .env

Fill in .env using the placeholders in .env.example. Keep credentials out of Git; .env is ignored by the repository.

Setting Purpose
BETTER_AUTH_URL Must exactly match the application's origin.
BASE_URL Public origin used for SEO metadata, the sitemap, and robots.txt.
Database, Redis, and S3 settings Connection details for the required services.

Apply the committed database migrations, then start the development server:

bun run db:migrate
bun run dev

With the Sudloh client configured, /auth/login starts the Account sign-in redirect automatically. Before OIDC cutover, visitors can register at /auth/register. For background processing, run the outbox worker in a separate terminal. Run the Discord worker when using catalog updates; its configuration is described below.

bun run worker:outbox
bun run worker:discord

Email, commission payments, and push notifications need their corresponding service credentials. See External prerequisites for the production configuration details.

Commands

Command Purpose
bun run dev Start the Next.js development server
bun run test Run the Vitest unit and integration suite
bun run test:coverage Produce V8 coverage output
bun run typecheck Run TypeScript without emitting files
bun run lint Run ESLint
bun run security:audit Check dependency advisories and time-limited exceptions
bun run build Compile the production application
bun run db:generate Generate a Drizzle migration from schema changes
bun run db:migrate Apply committed Drizzle migrations
bun run import:guides Preview the one-time Google Sheets guide migration (-- --apply to write)
bun run worker:discord Listen for Lunaris update announcements
bun run worker:outbox Process cache and event outbox records

Routes

Route Purpose
/ Searchable public character directory
/[character] Redirect to the first visible page
/[character]/[page] Render a public guide page
/auth/login Public account sign-in
/auth/register Public account registration
/admin Character and page overview
/admin/[character]/[page] Visual page editor
/admin/create Create a structured guide from the synced character catalog
/media/[id] Same-origin media response with publication checks
/api/healthz PostgreSQL, Redis, Slip2Go, Discord bot, and Lunaris status

The comprehensive check returns 503 if any dependency fails. Slip2Go uses a HEAD request to check reachability without submitting a payment slip; it cannot validate the API secret. The Discord check requires a heartbeat from the connected catalog worker within 45 seconds.

A page is public only when both its page and character are visible. A visible character without a visible page returns 404.

How updates and uploads work

Editing and live updates

Edits carry an expectedVersion. Accepted changes save guide content, media reference counts, and outbox records in a single PostgreSQL transaction.

The outbox worker invalidates the affected Redis cache tags before publishing an event containing only { type, id, version }. Clients then fetch the current state. SSE carries change notifications, never guide content; PostgreSQL remains the source of truth.

Public guides use a shared Redis-backed Next.js cache. Admin routes stay dynamic. SSE streams send a heartbeat every 90 seconds and close after 30 minutes so clients reconnect. Response headers disable Traefik buffering.

Discord catalog synchronization

The catalog worker watches the private channel set by DISCORD_CHANNEL_ID. Every new message, including one sent by you, triggers a check and synchronization with the latest version reported by Lunaris.

  • Startup connects the listener without replaying history or starting a sync.
  • Results go to DISCORD_LOG_CHANNEL_ID; start notifications link to the triggering message.
  • Send !lunaris stop in the watched channel to cancel the current fetch and clear queued fetches. A later message starts another check.
  • Enable the Discord Message Content intent and grant the bot View Channel and Send Messages in the configured channels.

Media visibility

Upload path Visibility
Presigned staged upload Private in S3. /media/[id] allows public access only while a published guide references the object; otherwise administrator authorization is required.
Direct guide image upload Public CDN object, including uploads made for drafts.
Commission attachment Served from S3_PUBLIC_URL; anyone with the URL can view it.

Keep sensitive material out of the direct upload path and commission attachments.

Verification

Run the complete host-safe verification set with:

bun install --frozen-lockfile
bun run security:audit
bun run test
bun run typecheck
bun run lint
kubectl kustomize k8s/ >/dev/null

The formula suite validates the exact sanitized corpus of 379 extracted formula cells plus arithmetic edge cases. An optional Redis integration test uses independent connections for two conceptual replicas:

REDIS_INTEGRATION_URL=redis://127.0.0.1:6379 \
  bun run test -- tests/redis-replication.integration.test.ts

It uses unique key and channel prefixes and removes its cache state afterward. Without REDIS_INTEGRATION_URL, that external-service test is skipped.

Containers

The application image compiles Next.js in a Node builder stage, then runs the standalone server and bundled workers on Bun as the unprivileged numeric UID/GID 1000:1000. The migration image runs the committed Drizzle migrations with the same identity.

docker build \
  --target app \
  --build-arg BASE_URL=https://guide.sudloh.com \
  --build-arg NEXT_DEPLOYMENT_ID=<immutable-revision> \
  --build-arg VCS_REF=<immutable-revision> \
  --tag buzz-sheet:<immutable-revision> \
  .

docker build \
  --target migration \
  --build-arg VCS_REF=<immutable-revision> \
  --tag buzz-sheet:migrate-<immutable-revision> \
  .

The runtime images do not contain local .env files. BASE_URL is required during the Next.js build and at runtime. CI supplies the build argument, and the Kubernetes ConfigMap supplies the runtime value. Keep both values aligned when changing the public site origin.

Kubernetes

Kustomize resources live in k8s/ and define:

  • Namespace buzz-sheet.
  • Two rolling web replicas, one outbox worker, and one Discord catalog worker.
  • ClusterIP port 3000 and Traefik ingress for guide.sudloh.com.
  • Cloudflare edge TLS with the cluster's standard HTTP Traefik origin route.
  • Startup, liveness, and dependency-aware readiness probes.
  • Web requests of 500m CPU/1 GiB and limits of 1 CPU/2 GiB.
  • HPA from 2 to 6 web replicas at 70% CPU or 75% memory and a PDB with one available.
  • Non-root, read-only containers with dropped capabilities and seccomp.
  • Ingress NetworkPolicy allowing the web pods only from Traefik in kube-system.
  • ARM64 scheduling constraints matching the production hosts and images.
  • A versioned migration Job and namespace-scoped CI deployer permissions.

PostgreSQL, Redis, and S3 are external services. The manifests deliberately do not create in-cluster data stores or credentials.

External prerequisites

Application secrets

Before the first rollout, a cluster administrator must provision a buzz-sheet-env Secret in the buzz-sheet namespace containing:

DATABASE_URL
BETTER_AUTH_SECRET
RESEND_API_KEY
REDIS_URL
S3_ENDPOINT
S3_PUBLIC_URL
S3_BUCKET
S3_ACCESS_KEY_ID
S3_SECRET_ACCESS_KEY
NEXT_SERVER_ACTIONS_ENCRYPTION_KEY
TURNSTILE_SITE_KEY
TURNSTILE_SECRET_KEY
COMMISSION_PROMPTPAY_TYPE
COMMISSION_PROMPTPAY_VALUE
COMMISSION_RECEIVER_ACCOUNT_NUMBER
SLIP2GO_VERIFY_URL
SLIP2GO_API_SECRET
WEB_PUSH_PUBLIC_KEY
WEB_PUSH_PRIVATE_KEY
WEB_PUSH_SUBJECT
DISCORD_BOT_TOKEN
DISCORD_CHANNEL_ID

NEXT_SERVER_ACTIONS_ENCRYPTION_KEY must be generated once and remain stable across replicas and rolling deployments. Do not place secret values in the ConfigMap or commit them to this repository.

Email verification

Before deploying required email verification, add sudloh.com to Resend and publish the DNS records shown in its domain setup. Verify the domain, create a sending-only API key restricted to it, and set RESEND_API_KEY in the external Secret. Account verification and password reset emails are sent from Buzz Guide <[email protected]>; this address does not need an inbox. Existing sessions continue until they expire or the user signs out, after which an unverified address must be verified before signing in again.

Commission payments and attachments

The commission page accepts PromptPay mobile, national ID, or e-wallet identifiers. For COMMISSION_PROMPTPAY_TYPE=mobile, use a real 10-digit Thai mobile number registered with PromptPay for receiving transfers in COMMISSION_PROMPTPAY_VALUE. For nationalId use the registered 13-digit ID; for ewallet use its 15-digit ID. An ordinary bank account number cannot be used as the mobile identifier. Set COMMISSION_RECEIVER_ACCOUNT_NUMBER to the recipient value in the format Slip2Go expects for its receiver check. Set SLIP2GO_VERIFY_URL to the full image-verification endpoint from your Slip2Go API Connect account. Set SLIP2GO_API_SECRET to the raw secret; the server adds the Bearer authorization prefix when calling Slip2Go. Payment slips and ticket images use the configured S3 bucket and are served from S3_PUBLIC_URL. Anyone with an attachment URL can view it. Each checkout accepts payment for one hour after creation. The outbox worker removes expired unpaid checkouts; paid tickets remain available. Apply the commission database migration before enabling checkout.

Browser push notifications

Commission message notifications require a stable VAPID key pair and subject (bunx web-push generate-vapid-keys --json). Set the three WEB_PUSH_* values in the web deployment secret and apply the push-subscription migration before enabling notifications. Users opt in from a prompt when entering the commission area. Browser push can arrive after tabs close; on iPhone and iPad, the site must be added to the Home Screen before the browser offers push permission.

Ingress and client IP handling

Traefik must overwrite X-Real-Ip for every request, and the web pods must be reachable only through Traefik before enabling the configured client IP based rate limits. The NetworkPolicy assumes Traefik runs in kube-system with the app.kubernetes.io/name=traefik pod label; verify those labels in the target cluster before applying it.

Deployment notifications

Every page also subscribes to /api/active. When a serving pod reports a newer deployment ID than the browser's current build, a persistent Thai notification offers a full reload into the new release.

Initial bootstrap

The base manifests contain deliberate replace-me image and deployment values. For initial bootstrap, the platform operator applies k8s/base through an environment overlay that supplies an existing immutable image revision. The aggregate k8s/ kustomization includes the migration resource for rendering and validation; normal releases create their own revision-named migration Job.

Use this command for local manifest validation only:

kubectl kustomize k8s/ >/dev/null

Gitea Actions release

.gitea/workflows/ci.yml verifies pushes to main and scheduled runs, including a dependency audit and a Git history secret scan. A time-limited advisory exception is recorded in security/audit-exceptions.json. After the repository variable DEPLOY_ENABLED is explicitly set to true, a successful push to main builds and scans ARM64 images at:

registry.neko-piranha.ts.net/astral/buzz-sheet:<git-sha>
registry.neko-piranha.ts.net/astral/buzz-sheet:migrate-<git-sha>

Configure the following Gitea secret before enabling deployment:

Secret Purpose
KUBE_CONFIG_B64 Base64 kubeconfig for the ci-deployer identity

The kubeconfig must contain a certificate-valid API server address and its CA. The workflow rejects insecure-skip-tls-verify and does not rewrite the cluster server address.

The deploy job creates a revision-named migration Job, waits for completion, updates NEXT_DEPLOYMENT_ID, and only then rolls out the immutable application image to the web and both worker deployments. Schema changes must follow an expand/contract sequence so the previous application revision stays compatible during a rolling update.

Acknowledgements

This project would not be possible without the open-source software and community services it builds on:

  • Lunaris — character, talent, material, and game data used by catalog synchronization and guide tools. Buzz Guide links back to Lunaris for material references where appropriate.
  • Next.js — application framework.
  • React — user interface library.
  • Bun — JavaScript runtime and package manager.
  • PostgreSQL — primary database.
  • Redis — cache, event transport, and presence state.
  • Discord.js — Discord catalog update worker.
  • Drizzle ORM — database schema and migrations.
  • shadcn/ui, Tailwind CSS, Lucide, and dnd-kit — interface components, styling, icons, and drag-and-drop interactions.

Special thanks are also available on the site’s Special Thanks page.

Genshin Impact and its characters, names, and assets are property of their respective rights holders. Please review the applicable terms and licenses for each third-party dependency before redistributing or deploying this project.