The project
GameSentry tracks League of Legends and Teamfight Tactics matches, then posts a summary to a Discord channel[1]. Each message combines match statistics with a short, playful comment in French generated from that data.
I built this personal project around a simple idea: bringing players’ results into the place where they already talk to one another. Behind that straightforward experience, most of the work lies in coordinating several external services and managing background processing.
What the application does
From the web interface, users can connect a Discord server[1], choose a destination channel, and add players using their Riot ID[7]. Each player can also be linked to a Discord user[1] so they can be mentioned in notifications.
Summaries adapt to each game: result and individual statistics for League of Legends, placement and team composition for Teamfight Tactics. The dashboard displays recent notifications and account usage.
The project also includes Free and Pro plans with limits on servers, players, and daily generations. Subscriptions are handled through Stripe[2] and Laravel Cashier[3]. An administration area exposes operational states and lets administrators retry failed processing.
Why Laravel?
The backend uses Laravel 13[4] with PHP 8.4[6]. GameSentry’s central requirement is to run recurring processes that depend on several external services. Laravel[4] brings the necessary building blocks into one environment: a scheduler, queues, an HTTP client, cache, locks, and database access through Eloquent[5].
The scheduler selects players whose tracking interval has elapsed and dispatches the work. Queues run Riot API[7] calls, comment generation, and Discord[1] delivery in the background, with retries, backoff delays, and timeouts. The dashboard remains independent from this work, although some configuration operations, such as resolving a Riot ID[7], remain synchronous.
This structure keeps the domain rules in a single PHP[6] application while separating the web, worker, and scheduler processes in production. It avoids introducing a standalone service for every integration.
Fortify for authentication
Laravel Fortify[8] provides the authentication mechanisms used by the application: registration, login, password recovery, email verification, two-factor authentication, and passkeys. The screens are built with React[9] and Inertia[10], while Fortify[8] handles the server-side flows.
Using this library reduces the amount of custom authentication code to maintain. GameSentry’s domain authorization rules remain application-specific, however: Fortify[8] does not decide which Discord servers[1] or players a user may manage.
Cashier for subscriptions
Laravel Cashier[3] connects users and their Stripe[2] subscriptions to the Laravel[4] model. It is used to open a Checkout session, access the billing portal, and track subscription status. Stripe[2] webhooks synchronize a user’s plan: the browser returning after payment is not sufficient on its own to establish access rights.
Application limits remain centralized in a separate PlanLimitService. Cashier[3] manages the relationship with Stripe[2], while GameSentry determines what the Free and Pro plans allow.
Why Inertia and React?
The interface uses React 19[9], TypeScript[11], and Tailwind CSS 4[12], connected to Laravel[4] through Inertia 3[10]. Laravel[4] controllers return pages and their data, Inertia[10] handles client-side navigation, and React[9] renders the interactive components.
For this dashboard, the architecture keeps routes, sessions, and authorization in Laravel[4] without building a dedicated REST API for a separate frontend. It preserves an SPA-like experience while the backend remains responsible for navigation and the data passed to each page.
A decoupled React[9] SPA would have required an API contract and more coordination around forms, errors, and authentication. Next.js[13] would have been relevant for an application centered on its ecosystem or with specific rendering requirements. Combined with the existing Laravel[4] backend, it would have introduced a second application framework to operate. GameSentry does not need that separation for its primarily authenticated interface.
The trade-off is deliberate: the React[9] pages are coupled to Laravel[4] controllers. If a mobile client or public API became necessary, that additional interface would have to be designed. Inertia[10] does not provide it automatically.
Application architecture
The browser communicates with Laravel[4] through Caddy[14]. The scheduler and worker share PostgreSQL[15] with the web process, and the job queue itself is stored in the database. The diagram shows the main application flows without detailing the Docker[16] networks.
External integrations
Discord: installing the bot and publishing messages
Connecting a server uses Discord’s OAuth2 authorization flow[1], with the scopes required to install the bot. A state parameter stored in the session is checked on return. The backend then verifies that the bot is present and retrieves the channels available to the user.
This flow installs and associates the bot, while access to the GameSentry account remains managed by Laravel[4] and Fortify[8]. Requests to Discord[1] use the bot token kept on the server. Messages combine a player mention, the generated comment, and a statistics embed. Allowed mentions are restricted to the targeted Discord[1] user.
Riot: identifying players and detecting new matches
The Account API resolves a Riot ID[7] to a PUUID, the identifier used to retrieve match history. GameSentry’s services then handle the endpoints and routing specific to League of Legends and Teamfight Tactics, along with the required profile metadata.
The application polls match histories periodically; it does not receive a push event whenever a match ends. Riot’s APIs[7] enforce several levels of rate limits, including application, method, and service limits. The job budget configured in GameSentry is therefore an internal setting, not an exact reproduction of Riot’s quota[7].
429 responses trigger a per-game cooldown calculated using Retry-After when available. The number of players also affects the polling interval. These safeguards reduce pressure on the APIs without promising instant tracking or the complete absence of rate limiting.
Data Dragon: enriching visual summaries
Data Dragon[17] provides Riot’s[7] static assets for profile icons and League of Legends champion artwork. GameSentry builds URLs for the relevant assets and version, while match data itself comes from the match APIs.
Visual asset configuration is separated from the main processing flow. An unavailable asset must not prevent the summary from being built: images can be omitted.
Groq: commenting on already-computed statistics
The Groq service[18] receives a normalized match summary. Statistics are calculated and selected in application code; the model is only responsible for producing a short, playful comment in French.
The prompt asks for a complete, concise sentence grounded in the supplied data. This instruction cannot absolutely guarantee a model’s response. Calls have timeouts and a daily quota. The resulting comment is stored so it can be reused if Discord[1] delivery must be retried.
In the current implementation, a generation error causes the job to be retried. If the daily quota is exhausted, the notification fails: there is not yet a degraded mode that sends only the statistics.
Why offer a Pro plan?
Every tracked player adds periodic Riot[7] requests, potentially followed by a match fetch, a generation request, and a Discord[1] delivery. Per-account quotas make this usage more predictable and avoid offering unlimited use on infrastructure and APIs with finite capacity.
| Per-account limit | Free | Pro |
|---|---|---|
| Connected Discord[1] servers | 1 | 3 |
| Total tracked players | 3 | 15 |
| Groq[18] calls per day | 50 | 250 |
The Pro plan provides greater capacity while retaining explicit limits. It also makes it possible to implement a complete subscription, billing, and entitlement synchronization flow.
A subscription does not increase the quotas assigned by Riot[7]. Both plans share the same regulation mechanisms. Limiting accounts helps control load, while polling budgets, cooldowns, and error handling remain necessary regardless of the plan.
Separating match discovery from notification delivery
Processing is split into two stages. A first job queries Riot’s match history[7], compares the returned identifiers with the last known match, and creates notifications for newly discovered games. A second retrieves match details, prepares the summary, requests a comment from Groq[18], and sends the message to Discord[1].
This separation makes it possible to retry notification delivery without repeating the entire discovery process. When a player is added, the application initializes a starting point in their history so previous matches are not announced as new ones.
Polling accounts for the number of players and configured request budgets. Rate-limited responses from Riot[7] trigger a per-game cooldown. Notifications are therefore periodic, with a delay that depends on the scheduler, the queue, and the external APIs.
Preparing for retries and limiting duplicates
An external request can succeed while the worker stops before recording its result. Retrying the job blindly could then publish the same message twice.
GameSentry combines several safeguards: a database uniqueness constraint for each player and match, unique jobs, execution locks, and a persisted nonce for Discord[1] delivery. The summary, generated comment, and message data are stored before delivery so they can be reused during a retry.
These mechanisms reduce duplicates in the expected recovery scenarios, but they are not a promise of exactly-once delivery under every possible failure. Errors and failed notifications remain visible for diagnosis.
From development to deployment
Local development uses SQLite[19]. In production, PostgreSQL 17[15] stores application data as well as jobs, cache entries, and sessions, removing the need for an additional Redis[20] service.
Docker Compose[16] separates the web, worker, and scheduler processes while reusing the same application image. GitHub Actions[21] runs the validation suite before publishing that image to GHCR[22]. Deployment selects the image for the relevant commit, resolves it by digest, applies migrations, and checks both the services and the public health endpoint. A shared lock on the VPS serializes deployments and backup operations so they cannot overlap.
The repository includes PHPUnit tests[23], static PHP analysis[6], and TypeScript checks[11]. CI also runs the test suite against PostgreSQL[15] in addition to SQLite[19].
Takeaways and next steps
GameSentry is about more than generating a witty comment. It gave me the opportunity to build an asynchronous pipeline across several APIs, with quotas, persisted states, and recovery after failures.
The next improvements include more direct tests for the polling and delivery pipeline, operational metrics, and notification preferences. The current scope covers League of Legends and Teamfight Tactics. Valorant remains a possible extension, subject to the required API access. It would also be useful to provide a degraded mode when the Groq[18] quota is exhausted by sending only the statistics.
Technical references
The numbers in the text refer to the official documentation below.
- Discord: OAuth2 installation and bot API.
- Stripe Billing: Subscriptions, payments, and billing.
- Laravel Cashier: Stripe integration for Laravel.
- Laravel: Queues, retries, and background processing. See also the scheduler.
- Eloquent: ORM and data models.
- PHP: Language manual.
- Riot Games Developer Portal: Application, method, and service API limits.
- Laravel Fortify: Server-side authentication.
- React: Components and interactive interfaces.
- Inertia: Navigation and data sharing between the backend and pages.
- TypeScript: Frontend type safety.
- Tailwind CSS: Utility-first styles.
- Next.js: Framework considered as an alternative, but not used in the project.
- Caddy: HTTP and HTTPS server.
- PostgreSQL 17: Production database.
- Docker Compose: Running containerized services.
- Data Dragon: League of Legends static assets.
- Groq: Text generation API.
- SQLite: Local database.
- Redis: Service considered as an alternative, but not required here.
- GitHub Actions: Validation and deployment automation.
- GitHub Container Registry: Publishing and retrieving container images.
- PHPUnit: Automated PHP tests.