Buzz Sheet
Buzz Sheet is a Thai-first guide CMS for turning structured character, build, team, rotation, and comparison data into responsive game-guide pages.
Production target: https://guide.sudloh.com
This project is an independent community tool for Genshin Impact players. It is not affiliated with or endorsed by HoYoverse.
Included capabilities
- Searchable character guide directory.
- Structured editors for overview, weapons, artifacts, constellations, teams, and custom sections with autosave and conflict recovery.
- Better Auth email/password login with administrator-managed accounts.
- S3-compatible media uploads with reference-aware same-origin delivery for staged private objects.
- PostgreSQL transactions and a retryable outbox.
- Shared Redis cache invalidation and privacy-safe Server-Sent Events across replicas.
- Discord-triggered Lunaris catalog updates with exact-version synchronization.
The five reference workbooks are represented only by sanitized structural and formula fixtures. Their guide text and media are not imported or published.
Routes
| Route | Purpose |
|---|---|
/ |
Searchable public character directory |
/[character] |
Redirect to the first visible page |
/[character]/[page] |
Render a public guide page |
/login |
Public account sign-in |
/register |
Public account registration |
/admin/login |
Administrator email/password sign-in |
/admin/register |
Create and remove administrator accounts |
/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/health |
Liveness response |
/api/health?ready=1 |
PostgreSQL and Redis readiness response |
A page is public only when both its page and character are visible. A visible character without a visible page returns 404.
Local development
Requirements:
- Bun 1.3.14
- PostgreSQL
- Redis
- S3-compatible object storage
Install dependencies and create your local environment file:
bun install --frozen-lockfile
cp .env.example .env
.env.example contains placeholders only. Configure .env yourself; it is
ignored by Git and must never be committed. BETTER_AUTH_URL must exactly
match the application origin. Set BASE_URL to the public site origin for SEO
metadata, sitemap, and robots.txt. Existing administrators can create additional
credential accounts from /admin/register. Visitors can create public accounts at /register.
Prepare the database and start the application:
bun run db:migrate
bun run dev
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 |
Runtime design
Accepted edits use an expectedVersion and commit guide content, media reference
counts, and outbox records in one PostgreSQL transaction. The outbox worker then invalidates affected Redis tags
before publishing an opaque { type, id, version } event. Connected clients
refetch authoritative state; SSE never contains page content and is not the
source of truth.
The Discord catalog worker watches the private channel configured by
DISCORD_CHANNEL_ID. Every new message in that channel, including messages you send yourself,
triggers a check and synchronization of the latest version reported by Lunaris. Startup
only connects the listener; it does not replay channel history or start a sync.
Worker and sync results are sent to DISCORD_LOG_CHANNEL_ID; start notifications
include a clickable link to the message that triggered the check. Send
!lunaris stop in the configured channel to cancel the active fetch and clear
queued fetches; a later message starts a new check. Enable the Discord
Message Content intent and grant the bot View Channel and Send Messages in the
configured channels.
Public guides use a shared Redis-backed Next.js cache. Admin routes remain dynamic. SSE streams send a 90-second heartbeat and close after 30 minutes so clients reconnect. Traefik buffering is disabled by the response headers.
Presigned staged uploads remain private in S3. /media/[id] serves a staged
object publicly only while a published guide references it; otherwise
administrator authorization is required. The direct upload path writes public
CDN objects for guide images, so draft uploads on that path must not contain
sensitive material.
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
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
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
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.
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. Apply the commission database migration before enabling checkout.
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.
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.
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 these Gitea secrets later:
| 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 Sheet 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.