A team-finding site for gamers, live at https://www.teamtavern.net.
Players, groups and communities post what they are looking for in a game, by rank, role, region, language and schedule, and message those who fit. The feed puts the posts that fit yours first, and the site emails you when a new one does. It covers Valorant, League of Legends, Counter-Strike 2, Dota 2, Apex Legends, Overwatch, Rainbow Six Siege, Marvel Rivals, Team Fortress 2, Heroes of the Storm and Valheim.
The browser app and the API server are both PureScript, compiled by a single
spago build, and they share their HTTP contract as types.
A route is a type. This one signs a player in:
type StartSession =
PostJson_ (Literal "sessions") RequestContent
==> (NoContent ! BadRequestJson BadContent ! Internal_)
type BadContent = Variant
( unknownPlayer :: {}
, wrongPassword :: {}
, unknownDiscord :: { nickname :: String }
)The server's handler for it has to answer with one of those three responses,
and the client's call gets the same Variant back and matches on it:
response <- fetchBody (Proxy :: _ StartSession)
(inj (Proxy :: _ "password") { emailOrNickname, password })
response # onMatch
{ noContent: const $ navigateReplace_ "/"
, badRequest: onMatch
{ wrongPassword: const $ showError "Wrong password." }
(const $ showError "Something went wrong.")
}
(const $ showError "Something went wrong.")Rename a field or drop a response, and whichever side still uses it stops compiling.
| Part | Built with |
|---|---|
| Client | Halogen with Halogen Hooks, Sass, bundled by esbuild |
| Server | Node, with routing by Jarilo from purescript-bklaric |
| Database | PostgreSQL, queried with inline SQL |
| Serving | Caddy, which sends crawlers to a prerenderer and everyone else the single-page app |
| Amazon SES | |
| Tests | Playwright, driving the site through a browser against a seeded stack |
| Running it | Docker Compose |
src/TeamTavern/
Routes/ the HTTP contract, shared by client and server
Server/ the Node API
Client/ the Halogen app, its styles and static assets
Shared/ static data both sides need
Database/ SQL schema and seed data
stacks/ docker compose files, Caddyfiles and env files
test-playwright/ the Playwright suite
CLAUDE.md describes the conventions in each of these in detail.
You need:
- Volta, which picks the Node and npm versions pinned in
package.json. The compiler, spago and the rest of the toolchain aredevDependencies. - Docker.
- bash. On Windows that is Git Bash; the scripts do not run under
cmd.exeor PowerShell.
Three packages are resolved from checkouts next to this one, so clone all four side by side:
git clone https://github.com/bklaric/team-tavern
git clone https://github.com/bklaric/purescript-bklaric
git clone -b recursive-castable https://github.com/bklaric/purescript-untagged-union
git clone -b fix-variant-decoding https://github.com/bklaric/purescript-yoga-jsonThen build and bring up the test stack, which seeds its own database:
cd team-tavern
npm install
./build.sh
docker compose --env-file stacks/test.env -f stacks/docker-compose.test.yml up -dThe site is at http://localhost:8080. The first up builds the prerenderer
image, which takes a while.
Every seeded game has a player to sign in as, such as [email protected]
with the password tester-password. The stack sends no real email: whatever
the site sends to an address shows at
http://localhost:8080/mail?to=<address>.
To throw the database away and start over:
docker compose --env-file stacks/test.env -f stacks/docker-compose.test.yml down -v./node_modules/.bin/playwright install chromium # once
npm test # reseeds the test stack, then runs every spec
npm test -- feed.spec.ts # only the specs you name
npm run typecheck # typechecks the suitenpm test takes the test stack down and brings it back up itself, so it
starts from a fresh database each time.