ManaFest
Launching soonManaFest is an MTG companion platform for game tracking, deck and playgroup management, iOS life counting, and personal play-history analytics. I build the backend and web app for a three-person team.
The problem
Magic players play many games without an effective way to examine patterns across game history. Which decks saw the most play last month? Which decks win most often? How does a new commander compare with an older commander? How has a player’s winrate changed over time? How does a player’s position affect their winrate?
Manual tracking is tedious, so few players maintain a complete game history. ManaFest combines game tracking, social features, and analytics in a single platform.
Product features
ManaFest combines three product areas:
- Social features. Players create decks, connect with other players, manage playgroups, record games, send invitations, and comment on play history.
- Live life counter. The iOS app provides a life counter for active Magic games. Players can join a live game, and game completion automatically records participants, decks, and winner.
- Analytics. Current analytics use templates for commander, player, and playgroup leaderboards, plus recent wins. A planned query builder will let players create and save shareable analysis templates.
Responsibilities
I build the backend service in Node.js and TypeScript, plus the web app in TypeScript and React. The backend supports the web app and iOS client. Browser users can join live games and appear in tracked sessions, although the browser does not include the live life counter. A teammate builds iOS. A second teammate collaborates on AWS infrastructure and owns data engineering.
Data model
ManaFest focuses on Commander, Magic’s most popular format. The domain model includes several connected records:
- Users own decks. Decks contain versions. Each version designates a commander, and each commander is a card with multiple printings. The schema supports selecting a commander printing for a deck version, although the feature is not yet available in the interface.
- Playgroups contain user members.
- Games connect players to decks, recording the specific deck each player brought to a specific game.
- Live game sessions maintain separate active state before becoming recorded games.
The relationships require carefully scoped queries and response types. Loading hundreds of games with complete relationships can exceed appropriate server memory limits, so the repository layer uses targeted queries and purpose-specific DTOs.
The initial DTO design fragmented as product features expanded, requiring a redesign of several response types.
I later migrated the backend from TypeORM to Drizzle. The migration affected substantial backend code, but Drizzle better matches the query style and provides stronger type safety.
Privacy enforcement
Users and playgroups can be private. Client-side rendering cannot protect private data because browser developer tools expose every field sent by the server. Privacy enforcement belongs in the server response layer, where each object is filtered according to the requesting viewer’s permissions.
Repeatedly loading privacy settings and checking visibility across every relationship would add unnecessary work to each endpoint.
The server resolves visibility once for each request, scopes queries to accessible data, and builds privacy-aware response DTOs before serialization.
Live game sessions
The iOS life counter manages active game state, including turn order, life totals, player tracking, and mid-game joins. Live state introduces reliability requirements absent from most Magic applications.
Host failover is a central requirement. The host device holds live game state, so a disconnected host must not end the session. EventBridge has a 60-second minimum scheduling interval, which is too slow for responsive failover during a game.
A Step Function invokes a Lambda every 15 seconds to scan DynamoDB for live sessions with stale hosts. The server then notifies remaining guests, allowing a guest to claim the host role and resume play.
Several guests can attempt to claim the host role simultaneously. The server serializes the race, selects a single successor, and preserves game state. After a successor assumes the host role, play resumes. Game completion promotes the live session into a recorded game with participant, deck, and winner data.
The backend also routes game events to notification and analytics services. Live-session coordination and event routing form the most complex server responsibilities.
Infrastructure
The backend runs on AWS ECS behind an ALB, with Postgres on RDS and DynamoDB for live-session state. SNS, SQS, and EventBridge route game events to notification and analytics services. S3 and CloudFront serve static assets. Glue imports Scryfall’s bulk card export on a schedule, keeping the card catalog current without runtime API calls.
Auth0 provides authentication. Patreon supports supporter-tier features. In-app bug reports and feature requests automatically create Jira tickets for triage.
Stack
Backend: Node.js, TypeScript, Drizzle ORM, Postgres, and DynamoDB. Web: TypeScript and React. Infrastructure: AWS ECS, ALB, RDS, DynamoDB, Step Functions, Lambda, SNS, SQS, EventBridge, Glue, S3, and CloudFront. Auth0 provides authentication. A three-person team builds ManaFest: I own the backend and web app, a teammate owns iOS, and a second teammate owns data engineering and collaborates on AWS infrastructure.
Status
ManaFest has a soft launch with a small group. Public launch follows current cleanup. manafest.gg will host the public product.