This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Pixl is a Bun/Turborepo monorepo (bun workspaces: apps/*, packages/*) for a Hack Club YSWS ("You Ship, We Ship") pixel-art multiplayer game.
| App | Stack | Purpose |
|---|---|---|
apps/server |
Bun, Express, ws, Drizzle ORM, Supabase (Postgres) |
Game server - auth, player state, WebSocket game/lobby logic, projects, shop, economy |
apps/game |
Godot 4 | The 2D multiplayer game client (not TypeScript - GDScript/Godot project) |
apps/landing |
Next.js 16, React 19, Tailwind 4 | Marketing site (pixl.rsvp) |
apps/dashboard |
Next.js 16, React 19, Tailwind 4, shadcn/radix, Supabase | Admin/review dashboard - moderation, tickets, review queue, stats |
apps/web-shell |
Next.js 16, React 19 | React migration of the player-facing web shell - docs (/docs) so far, /shop//dashboard/etc. still on the old static site. See below. |
apps/pixorpheus |
Bun, TypeScript, Slack Bolt v4, Express, Supabase | Slack bot - tickets, AI chat, moderation DMs, slash commands |
apps/pixo-dm |
Node (CommonJS), Express | Standalone Railway service that relays dashboard-initiated player DMs through Slack as Pixo - plain node index.js, not Bun-native; don't convert it unprompted |
packages/config |
JSON + plain ESM | Single source of truth for the program's facts - name, launch date, Hackatime cutoff, canonical URLs, economy rates. See below. |
packages/theme |
JSON + plain ESM | Single source of truth for the LEDGER color palette (dark/light, web + Godot). See below. |
packages/docs-engine |
Bun/TypeScript | Builds docs/*.md into static per-page HTML + OG preview cards under apps/game/web/docs/. See below. |
Each app has its own package.json/scripts and is largely independent; they share only Supabase as a common data layer (each app talks to Supabase directly rather than through a shared internal API), plus Hack Club Auth/Slack OAuth for identity.
Run from the repo root unless noted. This repo uses Bun - see the Bun-specific guidance below.
bun install # install all workspaces
# Turborepo shortcuts (root package.json)
bun run dev # run all apps' dev servers concurrently
bun run landing # turbo dev --filter=@pixl/landing
bun run dashboard # turbo dev --filter=@pixl/dashboard
bun run web-shell # turbo dev --filter=@pixl/web-shell
bun run build # turbo build (all apps)
bun run config:sync # regenerate committed config copies (packages/config)
bun run theme:sync # regenerate committed theme/palette copies (packages/theme)
bun run docs:build # regenerate apps/web-shell/public/<slug>/og.png from docs/*.md (packages/docs-engine)
bun run previews:build # regenerate OG preview cards for hand-authored web-shell pages (/shop, /ideas, ...)
# Per-app (cd into the app, or use --cwd)
bun run --cwd apps/server dev # game server, tsx watch on src/index.ts
bun run --cwd apps/server build # tsc build to dist/
bun run --cwd apps/server db:generate # drizzle-kit generate (schema -> migration)
bun run --cwd apps/server db:migrate # drizzle-kit migrate
bun run --cwd apps/server db:studio # drizzle-kit studio
bun run --cwd apps/landing dev # next dev
bun run --cwd apps/dashboard dev # next dev -p 4900
bun run --cwd apps/dashboard typecheck # tsc --noEmit
bun run --cwd apps/pixorpheus start # Slack bot (src/index.ts), Bun-native, no build step
bun run --cwd apps/pixorpheus dev # same, with --watch
bun run --cwd apps/pixorpheus typecheck # tsc --noEmitThere is no root-level test suite; apps/pixorpheus's test script is a placeholder. Check an individual app's package.json before assuming a script (lint/typecheck/test) exists there.
Each app has its own .env (see .env.example where present, e.g. apps/server/.env.example). Bun auto-loads .env files - don't add dotenv to Bun-run apps (note apps/server still imports dotenv/config itself in some files; follow existing conventions in that file rather than changing it unprompted - apps/pixorpheus relies on Bun's auto-load and has no dotenv dependency). Common vars: SUPABASE_URL / SUPABASE_SERVICE_KEY (shared across apps), JWT_SECRET, Hack Club Auth (HCA_CLIENT_ID/SECRET/REDIRECT_URI), Slack tokens for pixorpheus/dashboard.
Never hardcode the launch date, the Hackatime cutoff, a pixl.rsvp URL or the
economy rates in an app - they all live in packages/config/pixl.json.
- Nothing imports
@pixl/configat runtime, not even the TS apps - Railway/Vercel build each app from its own/apps/<app>root, so a workspace package outside that root doesn't resolve there. Every consumer instead reads a generated, git-committed copy produced bybun run config:syncafter you editpixl.json- never hand-edit the generated files, the next sync overwrites them. - TS apps:
server/pixorpheusimport../config.generated.js(the.jsextension is required, they run as ESM);landing/dashboardimport../_generated/config(or the@/app/_generated/configalias) - all exposeconfig,launchDate,hackatimeCutoffUnix,hasLaunched,launchDateLabel, etc. - Godot (
apps/game/scripts/pixl_config.gd) readsapps/game/pixl.json, and the game's web pages readPixl.configinapps/game/web/pixl.js- both are also generated copies from the same sync. - Dates are ISO-8601 UTC. Format them with
timeZone: "UTC"; a naivenew Date("2026-08-18T00:00:00")means midnight in the reader's timezone and is the exact drift this package exists to stop.
Launch-state copy (Pixo's persona/FAQ, the Slack welcome messages) switches itself
via hasLaunched() - there is no string to flip on launch day.
palette.json is the one place the LEDGER palette (dark/light) lives as data, named
color tokens the game, the web shell, and the docs previews all read.
- Nothing imports
@pixl/themeat runtime, for the same build-isolation reason as@pixl/config. Runbun run theme:syncafter editingpalette.json; it rewritesapps/game/theme.json(read byapps/game/scripts/pixl_theme.gd) and the token lines insideapps/game/web/pixl.css's:root{}blocks, between/* <pixl-theme:...> */markers - both are committed but generated, never hand-edit them. godot.darkonly carries the token subset Godot actually consumes;godot.lightdoesn't exist yet (no light-mode design for the game) -PixlThemefalls back togodot.darkregardless of the player's web-shell choice.packages/docs-engine/src/og.tsreadsweb.darkdirectly at build time for preview cards, so it never needs its own synced copy.
Doc pages now render in apps/web-shell (see below) - this package only still generates OG preview cards. bun run docs:build writes one og.png per doc into apps/web-shell/public/<slug>/ (no docs/ nesting there on purpose - apps/web-shell sets basePath: "/docs", which already prefixes everything under public/). Source files are named <order>-<slug>.md (order sets nav position, slug sets the URL). {{token}} placeholders pull from packages/config/pixl.json at build time via packages/docs-engine/src/tokens.ts's buildTokens(), re-run after editing pixl.json; unknown tokens fail the build. packages/docs-engine/src/markdown.ts's render() (the actual markdown-to-HTML parser) is imported directly by apps/web-shell/lib/docs.ts as a workspace package - this is the one place in the repo that happens, since apps/web-shell's Dockerfile uses a repo-root build context specifically to make that resolve (see below).
- Entry point
src/index.ts; Express HTTP routes live undersrc/routes/*(auth, profile, projects, shop, sidequests, story, friends, explore, admin, reports, hackatime, vault, notifications, uploads, events). - Real-time game state is handled separately in
src/ws/gameServer.ts(the authoritative multiplayer/WebSocket loop) andsrc/ws/lobbies.ts(private village / lobby grouping). src/auth/session.tsissues/validates JWT sessions signed withJWT_SECRET; Hack Club Auth (HCA) is the identity provider.src/db/client.ts+src/db/schema.ts(Drizzle) define the Postgres schema (via Supabase). Rundb:generateafter schema changes, thendb:migrateto apply.- Cross-cutting concerns:
src/xp.ts(leveling/XP),src/moderation.ts/src/imageModeration.ts(content moderation, ties intodashboard's review queue),src/rateLimit.ts,src/hackatime/api.ts(coding-time tracking integration),src/shipsArchive.ts(project submission history).
- Not a Node/Bun project - it's a Godot 4 project (
project.godot,scenes/,scripts/,addons/,shaders/). GDScript, not TypeScript. Don't try tobun install/run it like the other apps. web/holds the web export target;exports/andbuild/hold build artifacts (gitignored).- The minimap and world map draw a baked top-down PNG of each world (
assets/map/*.png+bounds.json), produced byscripts/tools/world_map_baker.gd. Re-bake after moving tilemaps: openscripts/tools/bake_world_map.gdin the editor and hit File > Run, orgodot --script scripts/tools/bake_world_map_cli.gd(needs a real display,--headlessbakes black). If the PNGs are missing both maps fall back to a flat placeholder rect.
- Both run Next.js 16 with React 19 and Tailwind 4 (very recent, training-data knowledge of Next.js APIs/conventions is likely stale).
apps/landinghas anAGENTS.mdflagging this explicitly: read the relevant guide innode_modules/next/dist/docs/before writing Next.js code in either app, and heed deprecation notices. dashboardruns on port 4900 (next dev -p 4900/next start -p 4900) since multiple apps run concurrently in dev. It uses shadcn/radix-ui components and talks to Supabase directly plus Slack OAuth (app/api/auth/*) for admin login.- Page routes follow Next App Router conventions (
app/<route>/page.tsx); shared page-local components live inapp/_components/.
- Next.js 16 App Router, deployed on Orchard (
pixl-web-shell, no public hostname of its own - reached only throughapps/landing/next.config.ts'srewrites(), over the Orchard cluster's internal service DNS). Each page family lives at its own literal route segment (app/docs/[slug],app/(shell)/dashboard, and so on as later slices land) - there is no app-widebasePath(removed 2026-08-24 once a second family joined docs; seedocs/superpowers/specs/2026-08-23-react-migration-design.md's addendum for why one was ever needed and why it stopped working). - Auth: an httpOnly
pixl_sessioncookie holds the same JWTapps/serverissues.proxy.tscatches?token=on any request (Hack Club Auth redirects back to whatever page login started from, not one fixed callback route) and moves it into the cookie.lib/session.tsverifies it locally (JWT_SECRETmust matchapps/server's exactly). Server Components callapps/serverdirectly vialib/server-api.ts; Client Components that need to mutate go throughapp/api/proxy/[...path]/route.tsinstead, since the cookie is unreadable by client JS by design. This only applies toapps/web-shell- the Godot client and any page still on the old static site
(
apps/game/web/) keep using the token-in-localStorage flow unchanged.
- the Godot client and any page still on the old static site
(
- The
(shell)route group (app/(shell)/layout.tsx) renders the sidebar/topbar chrome, ported fromapps/game/web/pixl.js'smountTopbar(), around every signed-in page.app/(shell)/nav-data.tsholds the nav structure;shell-nav.tsxis the interactive Client Component (mobile sheet, theme picker); the layout itself is a Server Component that renders the full-page signed-out gate or fetches the wallet/Restoration numbers before the page ever reaches the browser. - The RE/payout economy formulas (
rePerHour,projectPayoutUsd, etc. inpixl.js) aren't ported here yet -/dashboardonly ever displays valuesapps/serveralready computed (wallet pixels/RE/level), it never runs the formulas itself. Port them as their own tested module (matchingpackages/docs-engine/src/tokens.ts's pattern: byte-for-byte identical toapps/server's own math, this must never drift) when a slice first needs to compute a payout number client-side, likelycalcorprojects. - Reads
docs/*.mddirectly viapackages/docs-engine'srender()/buildTokens()(a workspace-package import - this app's Dockerfile uses a repo-root build context specifically so that resolves, unlikeapps/dashboard's isolated per-app context). - Docker image uses Next's
output: "standalone"- required in this monorepo, not just an optimization: without it, the runtime stage would need to drag along the whole workspace's hoistednode_modules(Bun installs into a shared/repo/node_modules/.bun/...store for any workspace-package-dependent app, since the install runs against the rootpackage.json) to keepapps/web-shell/node_modules's symlinks from dangling.output: "standalone"traces the real dependency graph into concrete files instead. - See
docs/superpowers/specs/2026-08-23-react-migration-design.mdfor the full migration design (its 2026-08-24 addendum has the slice order for everything after docs) anddocs/superpowers/plans/for the per-slice implementation plans.
- Bun-native TypeScript, no build step (
bun run src/index.ts) - a single Bolt v4 process, organized by feature undersrc/(tickets/,chat/,ai/,memory/,commands/,pixelate/,github/,external/,slack/). There is no separate dashboard process anymore - helper/admin ticket moderation lives inapps/dashboard(the Next.js app), which resolves tickets throughsrc/external/ticketApi.tson this bot. models.jsonlists OpenRouter models available to the AI chat/roast/fact features.- See
apps/pixorpheus/README.mdfor the full slash-command reference and architecture table before modifying bot behavior.
- Small standalone Express service (
index.js, CommonJS, plainnode/require, not Bun-native, deliberately not ported): exposesPOST /api/external/dm, called byapps/dashboardto deliver a player DM through Slack as Pixo, authenticated withEXTERNAL_API_KEY. - Runs on its own Railway deployment separate from
apps/pixorpheus; enforces a per-user, global, and daily rate limit in-process (in-memory, so state resets on redeploy) to bound blast radius if the API key leaks.
Default to Bun over Node.js/npm/yarn/pnpm across this repo (this applies to apps/server too, even though its package.json scripts currently invoke tsx/node/drizzle-kit directly - don't rewrite those scripts unprompted, but use bun for anything new).
bun <file>instead ofnode <file>orts-node <file>bun testinstead ofjest/vitestbun build <file>instead ofwebpack/esbuildbun installinstead ofnpm/yarn/pnpm installbun run <script>instead ofnpm run/yarn run/pnpm runbunx <package>instead ofnpx <package>- Bun auto-loads
.env- don't adddotenvto new Bun code. Bun.serve()for HTTP/WebSocket servers (supports routes, WS, HTTPS) instead ofexpressin new Bun code.bun:sqliteinstead ofbetter-sqlite3;Bun.redisinstead ofioredis;Bun.sqlinstead ofpg/postgres.js; built-inWebSocketinstead ofws.Bun.fileovernode:fsreadFile/writeFile.Bun.$\cmd`instead ofexeca`.
For HTML-import-based frontends (not used by the Next.js apps here, but the default for any new Bun frontend): Bun.serve() serving an index.html that <script type="module" src="./frontend.tsx">s a React entrypoint, no Vite. See node_modules/bun-types/docs/**.mdx for the full Bun API reference.
You are still an elite software engineer first. This section only governs tone in chat, not code.
Personality
- Talk naturally and casually, like a smart Gen Z programmer. Slang (
yo,fr,ngl,lowkey,cooked,W,L,based, 💀, 🙏, 🔥) is fine but never forced into every sentence, natural over cringe. - React honestly: bug fixed →
W; nasty bug →bro we were COOKED; clever fix →that's actually kinda fire; bad code → say it's bad straight up, no corporate hedging. - Match the user's energy - if they go serious, drop the slang.
Coding behavior stays unchanged
- Inspect the existing codebase before assuming. Follow existing architecture/conventions. Don't rewrite working code unprompted. Verify with tests/typecheck/build before claiming something works.
Scope
- Casual register + emojis are for chat prose only. Code, commit messages, UI copy, and docs stay clean and emoji-free - see the no-emojis and casual-commits conventions already in play for this repo.
Final response format (when it's a real task, not chit-chat)
What I changed
- short bullet
Why
- brief explanation
Checks
- tests/build/typecheck results
Core rule: speak like Gen Z, think like a senior engineer. For destructive commands or risky changes, drop the goofiness and warn clearly.