Imported from Laurin-Notemann/beerpong (
AGENTS.md). Install upstream withnpx skills add Laurin-Notemann/beerpong. Copyright stays with the author.
Versus
Versus (repo name beerpong) is a mobile app for tracking beer pong leagues with friends: groups, seasons, matches, rules, leaderboards and Elo. A Spring Boot API with a Postgres database serves an Expo / React Native app for iOS and Android.
What makes Versus special?
A group of friends uses Versus at the table, mid-game, often on bad Wi-Fi. It's important we keep the things that make that work. Here's a brief list of the things we can never compromise on.
1. Fast at the table
Entering a match has to be quicker than arguing about the score. Screens render from the persisted React Query cache first and refetch in the background. Never block a match entry on a network round trip that the user doesn't need to see.
2. Realtime, but offline tolerant
Every group member sees new matches, players and seasons live via the /update-socket websocket (see api/README-Socket-Updates.md). The app must keep working when the socket drops and must catch up on reconnect (useRefetchEverythingOnWifiReconnect).
3. Ship without the stores
Most fixes reach users as OTA updates, not store releases. Native changes (new native modules, config plugins, permissions) change the runtime fingerprint and force a new store build. Keep that in mind before adding a native dependency.
4. Observable
Errors, logs and traces from the app and the server go to Sentry (org versus-zr, projects mobile and server). The app's traces propagate into the API, so one trace spans both. If something can fail silently, make sure it shows up there.
A note from Laurin
Keep it simple. This is a small team side project, so the best change is usually the smallest one that makes the behavior obvious. Don't add machinery because it looks architecturally impressive. Fight scope creep, and honor the developer's intent in both a minimal and realistic fashion.
The rest of this document is meant to help you navigate the codebase and make changes effectively. Think of these instructions less as "hard rules", more as "good defaults". The developer's preferences should be able to override anything here.
A small glossary
We need to be on the same page with terminology. When communicating, use this language:
- you means the agent reading this file and changing Versus.
- we, us, and maintainers mean Laurin, Linus, Thies and the people building Versus.
- user means a person playing beer pong with the app.
- group means a set of players who compete together. Joined with a group code.
- season means a time-boxed competition inside a group. Leaderboards are scoped to a season (or all time).
- match means one game between two teams, with per-player moves (points, finishes) that follow the group's rules.
- profile / player means a person inside a group; a user account can have profiles in many groups.
- channel means an EAS Update channel.
productionis the iOS TestFlight/App Store build,previewis the internal Android APK. - runtime means the native fingerprint of a build. OTA updates only reach builds with the same runtime.
The three ways to hurt yourself
- Touching the live server by hand.
ssh privatenhosts the staging API and its Postgres (~/docker/beerpong-api). The database there is real user data. Never run destructive SQL,docker compose down -v, or volume prunes against it. Read logs freely; change things through the deploy workflow. - Breaking the runtime by accident. Adding or upgrading a native package, editing
app.jsonplugins, or changing permissions changes the fingerprint. The staging EAS workflow then builds and submits new native builds instead of publishing an update. Do it on purpose, not as a side effect. - Hand-editing generated API types.
mobile-app/api/generated/openapi.jsonandmobile-app/openapi/openapi.d.tsare produced from the backend. Change the Java DTOs/controllers and regenerate (seeOPENAPI_CODEGEN.md); never patch the generated files to make the app compile.
Hit every surface
The most common defect in this repo is a change that works on the path you tested and is missing everywhere else. Before calling work done, walk this list and say which entries applied:
- Both ends of the wire. A DTO change in
api/needs regenerated types and every consuming hook inmobile-app/api/callsandmobile-app/api/propHooksupdated. - Realtime. If a mutation changes data other group members see, the server must emit the socket event and the app must apply it (
mobile-app/api/realtime). - Cache. React Query is persisted to disk. A changed response shape must not crash on an old cached value.
- Platforms. iOS and Android. Permissions and native behavior differ.
- Reverse states. If you added a way in, add the way out. Create needs delete, join needs leave.
- Docs. Check whether the change makes existing guidance inaccurate. Apply the documentation rules before adding anything.
Dev servers
- Database:
cp .env.example .env, thenmake docker-db-up. The API readsPOSTGRES_HOST/PORT/DB_NAME/USER/PASSWORD,JWT_SECRET,BACKEND_SENTRY_DSNand theAWS_*S3 settings from the environment. - API:
cd api && ./mvnw spring-boot:run(Java 21), ormake docker-backend-upto run it in Docker. - App:
cd mobile-app && npm install && npm start. Use a development build (eas build --profile development); Expo Go doesn't have the native modules. EAS environmentdevelopmentpoints the app athttp://localhost:8080. - npm is the package manager for the app (
package-lock.json). Don't add a second lockfile. - Stop what you started. This machine runs other projects' servers too.
Test data
An empty database is a bad test. For realistic data, dump the staging database read-only (pg_dump through ssh privaten, container beerpong-db-staging) into your local docker database. Data flows one way: into your local copy, never back to the server.
Verifying
- Smallest proof that the change works. Run the tests and checks for the scope you touched:
- API:
cd api && mvn verify -Dspringdoc.skip=true(needs the local Postgres; tests use thetestprofile). - App:
cd mobile-app && npm run lint(eslint +tsc --noEmit),npm run ci:test(vitest),npm run ci:format.
- API:
- Test meaningful logic or observable behavior (Elo, leaderboard scoring, match validation). Don't add tests that mirror the implementation.
- Backend behavior changes ship with focused controller or service tests next to the existing ones in
api/src/test. - Don't verify with simulators, devices or browsers unless the developer asks.
Shipping
- API: push to
staging→Api Staging Deploybuilds the image and redeploysbeerpong-api-stagingon the server over SSH.maindeploys production (not currently running). - App: push to
staging→Mobile App EAS(GitHub Action) startsmobile-app/.eas/workflows/staging.ymlon EAS. It fingerprints the app. A matching build gets an OTA update on its channel. A new runtime gets a native build: iOS goes to TestFlight, Android to an internal preview APK. Build numbers are managed remotely by EAS. - The app checks for updates on foreground and applies a downloaded update when it goes to the background (
mobile-app/hooks/useOtaUpdates.ts).
Pull requests
- Never make a PR unless the developer explicitly asks you to do so.
- Conventional commit titles, plain language:
fix(mobile): leaderboard no longer shows stale season. - Body: the problem in a sentence or two, then how you fixed it. End with the model and harness that did the work.
- UI changes need before/after images. Motion or timing needs a short video.
- One concern per PR. If the description says "also", split it.
- The
Generate OpenApiaction may push achore: update openapi typescommit to your PR. Pull before pushing again.
Documentation
Most code changes do not need a documentation change. Agents can read the code.
- Keep a local explanation in a nearby code comment. Use a markdown doc only when the reasoning crosses the API/app boundary or needs context the code can't carry.
- Don't document every feature, enumerate fields, narrate control flow, or append PR summaries.
- When a documented decision changes, rewrite or remove the affected text. Don't append another account of the new behavior.
Plans and work artifacts
- Don't commit implementation plans, research notes, or agent scratch files. Keep temporary material outside the repo.
- A merged PR is the implementation record.
How it works
The app talks to the API over REST through a typed openapi-client-axios client generated from the backend's OpenAPI spec. Responses are wrapped in a ResponseEnvelope. Controllers delegate to services, which use Spring Data repositories and MapStruct mappers to turn DAOs into DTOs. After a write, the server publishes a socket event for the group, and connected apps update their React Query cache. Assets (avatars, match photos) are uploaded to S3-compatible storage via presigned URLs. Auth uses JWTs (api/.../auth).
Where code lives
api/- Spring Boot 3 API (Java 21, Maven).control(REST controllers),service,repository,model/dao+model/dto,mapping(MapStruct),sockets(realtime),auth(JWT). Config insrc/main/resources/application.yml.mobile-app/- Expo / React Native app with expo-router.app/holds only routes: the root layout (providers, group drawer, error boundaries),app/(main)/(the stack with every screen) andapp/(main)/(tabs)/(native tabs, one stack per tab). Non-route modules live inlib/,components/,api/(client, hooks, realtime),zustand/(local state),utils/(logging, Sentry),hooks/.mobile-app/.eas/workflows/- the EAS workflow that builds and updates the app..github/workflows/- API CI/CD, mobile CI, OpenAPI generation, and the trigger for the EAS workflow.docker/- local compose files for the database and backend.
Taste
- Complexity belongs at the boundaries (API mapping, client hooks). Screens stay dumb.
- Inferred types over annotations.
anyis the enemy. Imports use the@/alias; eslint forbids relative imports. - Never import
@react-navigation/*in the app. Expo Router bundles its own React Navigation; useexpo-router/react-navigation, theDrawer/Stack/NativeTabslayouts andStack.Toolbar. A second copy builds and type-checks fine but crashes at launch ("Couldn't register the navigator"). - Native UI over JS imitations: header buttons are
Stack.Toolbaritems, menus are native (Stack.Toolbar.Menu/@expo/uiMenuView), confirmations areAlert.alert. - Comments describe how a thing is used, and move when the code moves.
- No
console.*in app code outsideutils/logging.ts. Use aScopedLogger; its output also reaches Sentry Logs. - If a rule here fights the task in front of you, say so loudly and get a human sign-off before breaking it.
Additional tips
- Don't verify with browsers, simulators or computer use unless the developer explicitly agrees or requests it.
- Security is important, but shouldn't be over-indexed on for dev-only tooling.
