A peer-to-peer sports wagering platform — bet against your friends instead of the sportsbook.
Users register, add friends, and send each other head-to-head wagers on upcoming NBA games. Once both sides accept, the bet locks in. A cron job polls live scores every minute, settles finished games automatically, and moves credits between the two bettors without anyone having to touch it.
Built over the summer of 2023 as a learning project, with the deliberate goal of being backend-heavy: UTC/timezone handling, multi-table settlement queries, scheduled jobs, and cache invalidation.
| Layer | Technology |
|---|---|
| Frontend | Angular 16, Angular Material, Bootstrap 5 |
| Backend | Node.js, Express 4 |
| Database | PostgreSQL (via pg) |
| Cache | Redis |
| Auth | JWT (jsonwebtoken) + bcrypt |
| Scheduling | node-cron |
| External data | API-Basketball via RapidAPI |
| Containers | Docker + Docker Compose |
BuddyBets/
├── backend/
│ ├── server.js # Express entrypoint (port 3000)
│ ├── db.js # Postgres connection pool
│ ├── routes/ # Route definitions per domain
│ │ ├── userrouting.js # → /auth
│ │ ├── betsrouting.js # → /bets
│ │ ├── baserouting.js # → /route (manual game/score refresh)
│ │ └── data-fromdb.js # → /database (cached reads)
│ ├── controllers/ # Request handlers + settlement logic
│ ├── middleware/
│ │ ├── authorization.js # JWT verification
│ │ └── validinfo.js # Registration/login input validation
│ ├── modules/
│ │ ├── automation.js # Cron schedules
│ │ └── datacaching.js # RapidAPI fetch + filtering
│ ├── queries/queriesfile.js # All parameterised SQL, in one place
│ ├── utils/ # Date helpers, JWT generator
│ ├── Dockerfile
│ └── compose.yaml
├── frontend/angular/ # Angular SPA
├── IAC/main.tf # Terraform placeholder (currently empty)
└── dump.rdb # Redis snapshot
- Node.js 18+
- PostgreSQL 14+ running locally on
5432 - Redis running locally on
6379 - A RapidAPI key subscribed to API-Basketball
The app expects a Postgres database with uuid-ossp enabled and tables for users, friends, games, bets, and betdetails. Table definitions live (commented) in backend/TABLES/TABLESTRUCTURE.js, and the full column set can be inferred from backend/queries/queriesfile.js.
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE TABLE users (
user_id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
name VARCHAR(255) NOT NULL,
username VARCHAR(255) NOT NULL UNIQUE,
email VARCHAR(255) NOT NULL UNIQUE,
password VARCHAR(255) NOT NULL,
credits INTEGER DEFAULT 0
);Create backend/.env:
dbname=wagering
password=your_postgres_password
JWTSECRET=your_jwt_signing_secret
APIKEY=your_rapidapi_keyThe Postgres user and host are currently hardcoded in
backend/db.js(logan@localhost) — change them there or move them to env vars.
cd backend
npm install
npm run dev # nodemon on :3000The cron jobs run as a separate process from the API server:
cd backend
npm run gameupdatescd frontend/angular
npm install
npm start # ng serve on :4200The Angular service layer points at http://localhost:3000 directly (see src/app/backendcalls.service.ts) — update this before deploying anywhere.
backend/compose.yaml brings up the API alongside a Postgres container. It reads the DB password from a Docker secret at backend/db/password.txt, which is gitignored — create it before running:
cd backend
mkdir -p db && echo "your_password" > db/password.txt
docker compose up --buildNote that Redis is not yet part of the Compose stack and must be running on the host.
- Request — User A picks a game and an opponent, chooses a side and a wager.
POST /bets/placebetwrites a row tobets(the two participants and the game) and a linked row tobetdetails(odds, wager, and which user took home vs away), joined by a foreign key onbetid. - Response — User B sees it under pending bets and either accepts (status →
accepted) or denies (status →denied). - Settlement — Every minute,
getScoresControllerrefreshes scores and status for in-progress games.DetermineWinnersthen runs a single SQL statement that joinsbetdetails → bets → games, compares the final score against each bettor's side, and stampswinneridon every accepted bet whose game has statusGame Finishedand no winner yet. TheRETURNINGclause hands back exactly the rows it just settled, which are used to pay out credits and flip the bet tofinished.
Doing the winner determination as one atomic UPDATE ... RETURNING avoids a read-then-write race and means the job is naturally idempotent — a bet that already has a winnerid is skipped on the next tick.
The RapidAPI basketball endpoint only accepts a single date per call, so GamesForNext7DaysCall fans out seven parallel requests via Promise.all, then filters the combined response down to league IDs 12 and 13. Filtering client-side across all leagues in one call per day uses fewer API credits than querying per-league.
Every day at 00:10 UTC, getGamesForDay pulls in the newly-visible seventh day, keeping a rolling 7-day forecast in Postgres, then writes the full week into Redis under the games key. The frontend reads that cache through GET /database/GetWeekGames rather than hitting Postgres on every page load.
All date arithmetic is done in UTC deliberately — the upstream API rolls over its schedule on UTC days, so working in local time produced off-by-one-day errors on games near midnight.
Base URL: http://localhost:3000. Endpoints marked 🔒 require a valid JWT in the token header; the token payload carries the user's UUID, so no user ID is sent in the request body.
| Method | Endpoint | Description |
|---|---|---|
POST |
/auth/register |
Creates a user with a bcrypt-hashed password. Requires name, email, username, password. Returns a JWT. |
POST |
/auth/login |
Validates credentials, returns a JWT or the relevant error. |
GET |
/auth/is-verify 🔒 |
Returns true if the supplied token is valid. |
GET |
/auth/dashboard 🔒 |
Returns the authenticated user's dashboard data. |
GET |
/auth/getuserid 🔒 |
Resolves the token to a user UUID. |
GET |
/auth/getfriends 🔒 |
Returns the user's friends, with usernames resolved from UUIDs. |
POST |
/auth/newfriendrequest 🔒 |
Takes a target username, stores both UUIDs in friends with pending status. Rejects duplicates in either direction. |
POST |
/auth/acceptfriendrequest |
Takes a request ID, sets status to accepted. |
POST |
/auth/denyfriendrequest |
Takes a request ID, deletes the row. |
| Method | Endpoint | Description |
|---|---|---|
POST |
/bets/placebet 🔒 |
Creates the bet and its details. Takes opponent ID, wager, odds, and game ID. |
GET |
/bets/getpendingbetsreceived 🔒 |
Incoming bet requests awaiting the user's response. |
GET |
/bets/getpendingbetssent 🔒 |
Outgoing requests awaiting the opponent. |
GET |
/bets/getongoingbets 🔒 |
Accepted bets on games that haven't settled yet. |
POST |
/bets/acceptbet 🔒 |
Sets bet status to accepted. |
POST |
/bets/denybet |
Sets bet status to denied. |
| Method | Endpoint | Description |
|---|---|---|
GET |
/database/GetWeekGames |
Returns the cached 7-day slate from Redis. |
GET |
/database/DockerTest |
Container health check. |
Normally driven by cron; exposed for debugging and backfill.
| Method | Endpoint | Description |
|---|---|---|
GET |
/route/updategames-database |
Full game refresh. |
GET |
/route/updategamestoday-database |
Rolls the 7-day window forward and re-caches. |
GET |
/route/updatescores-database |
Pulls current scores and game statuses. |
Defined in backend/modules/automation.js.
| Schedule | Job | Purpose |
|---|---|---|
10 0 * * * (UTC) |
getGamesForDay |
Extends the rolling 7-day forecast and refreshes the Redis cache. |
* * * * * |
getScoresController → DetermineWinners |
Updates live scores, then settles any newly-finished games and distributes credits. |
| Path | Component |
|---|---|
/ |
Register |
/login |
Login |
/dashboard |
Dashboard |
/gamesdisplay |
Upcoming games |
/betpage/:gameid |
Place a bet on a specific game |
/friends |
Add friends / manage requests |
/friendsdisplay |
Friends list |
/pendingbets |
Sent and received bet requests |
/ongoingbets |
Active and completed bets |
Routes are protected client-side by loginguard.guard.ts, which calls /auth/is-verify.
Honest list of what a second pass would address:
- Secrets and config — DB user/host and the frontend's API base URL are hardcoded; both should come from environment config.
- Committed artifacts —
dump.rdb(a Redis snapshot) is tracked in git and should be gitignored. - Redis in Compose — the Compose stack covers the API and Postgres only; Redis still has to run on the host.
- Missing auth on two endpoints —
/bets/denybet,/auth/acceptfriendrequest, and/auth/denyfriendrequestaccept a raw record ID without verifying the caller owns it. - Schema as code — table definitions live in comments rather than a migration tool.
- Terraform —
IAC/main.tfis an empty placeholder. - Tests — Angular spec files are CLI-generated stubs; there's no backend test suite.
Built as a personal learning project. Wagering is simulated with in-app credits — there is no real money, no payment processing, and no compliance or age-verification tooling. Not intended for production use.