← Refringe
The Forge homepage, with the tagline Step into the Forge over a dimmed game backdrop and the SPT installer download below it.
The Forge homepage.

What it was

Single Player Tarkov (SPT) is a community project that turns Escape from Tarkov into an offline game you play on your own, keeping its full progression and adding support for mods.

The Forge was the official home for those mods. Modders published releases, declared what each one needed to run, and heard from the people using them. Players searched the catalogue, checked what worked with their version of SPT, and downloaded what they wanted. It replaced the community's older WoltLab-based Hub and brought across its users, mods, categories, comments, and reactions, with old Hub links redirecting to their new pages.

I created the Forge and led its development from the first commit in May 2024 until I retired from the project in August 2026. The code remains open source under the Mozilla Public License 2.0, in an archived repository.

By the numbers

Traffic over the Forge's last 295 days online.

1.6 billion requests served
Data served
125 TB
Page views
129M
Most visitors in one day
294,629
Threats stopped by Cloudflare
2.5M
Requests per day October 21, 2025 to August 11, 2026
Data served per month The busiest single day moved 1.44 TB, on November 16, 2025
10 TB
20 TB

* Partial month

Commits 2,062 of the project's 2,411 commits were mine

86%

Show the monthly figures as a table
Month Requests Data served Page views
October 2025 (11 days) 38,857,426 5.3 TB 4,835,962
November 2025 135,461,593 22.0 TB 17,020,581
December 2025 144,309,896 17.1 TB 15,233,666
January 2026 125,060,277 12.1 TB 12,416,093
February 2026 94,254,705 7.5 TB 9,315,293
March 2026 129,209,467 8.9 TB 10,153,354
April 2026 172,927,167 10.2 TB 10,547,473
May 2026 182,028,016 11.3 TB 11,325,046
June 2026 217,130,349 13.8 TB 12,993,052
July 2026 241,732,078 10.6 TB 16,048,634
August 2026 (11 days) 77,056,666 5.9 TB 8,947,993
Total 1,558,027,640 124.7 TB 128,837,147

Under the hood

The stack, and how production was laid out.

Production

Laravel Forge provisioned the servers and deployed every push to the main branch. Cloudflare sat in front of all public traffic. The application server handled the web work, while the database, cache, search, and file verification each ran on a machine of their own. Monitoring data went to a second PostgreSQL database, kept apart from the primary one.

Players and modders reached the Forge through Cloudflare, which proxied requests to the application server and served files from R2. The application server queried PostgreSQL, used Valkey for cache and queues, searched Meilisearch, and called outside services. The NightOwl agent on the application server wrote Nightwatch telemetry to a separate monitoring database. The verification worker pulled jobs from Valkey, downloaded files from mod file hosts to check them, and wrote results to PostgreSQL. Players downloaded mods from the file hosts after a redirect.

Players and modders Browsers and community tools Mod file hosts Where authors' files lived Downloads, via redirect HTTPS and WebSockets Cloudflare Proxy, cache, rate limits, and firewall R2 storage Assets and files Proxied requests Application server Laravel 13 on PHP 8.5 Octane on FrankenPHP Reverb WebSockets Horizon Queue workers Scheduler Recurring jobs NightOwl Nightwatch agent API calls Outside services Discord GitHub releases Cloudflare API, MaxMind Queries Cache, jobs Search Telemetry PostgreSQL Primary database Valkey Cache and queues Meilisearch Search indexes Monitoring database PostgreSQL for NightOwl Verification worker Throwaway Docker containers Results Pulls jobs Downloads files to check Players and modders Browsers and community tools HTTPS and WebSockets Cloudflare Proxy, cache, rate limits, firewall R2 storage Assets and files Proxied requests Application server Octane on FrankenPHP Reverb WebSockets Horizon Queue workers Scheduler Recurring jobs NightOwl Nightwatch agent Queries Cache Search PostgreSQL Database Valkey Cache, queues Meilisearch Search Telemetry Verification worker Docker containers Monitoring NightOwl data Results Jobs Outside services Discord, GitHub, Cloudflare, MaxMind Mod file hosts Where authors' files lived Downloads files Outside API calls Downloads, via redirect
Public traffic Server connections Outside the Forge Arrows point from the side that opens the connection.

Players and modders

Players browsed, searched, and downloaded through the site, modders published through it, and community tools read the public API. All of it arrived over HTTPS and WebSockets through Cloudflare.

Cloudflare

Every public request passed through Cloudflare first. It cached public API responses at the edge, enforced rate limits ahead of the server's own, blocked 2.5 million threats in the Forge's last 295 days, and served assets and uploads from R2 object storage.

Application server

Octane on FrankenPHP served every page and API request, keeping the booted application in memory between requests. The same machine ran Reverb for WebSockets, the task scheduler, the NightOwl monitoring agent, and Horizon's three worker pools: up to 20 general workers, 10 for download link checks, and 3 for alt detection and analytics.

PostgreSQL

The primary database: mods, releases, users, comments, the resolved version and dependency tables, and the activity and audit log. It moved from MySQL to PostgreSQL 18 in the Forge's final weeks, with PgBouncer pooling connections.

Valkey

A Redis-compatible store, reached over TLS and split into separate databases for general use, cache, queues, and API usage counters. Horizon's workers and the verification worker both pulled their jobs from it.

Meilisearch

Four search indexes (mods, addons, mod lists, and users), kept in sync by Laravel Scout and rebuilt every night. The command palette queried all four in a single multi-search call.

Monitoring with Nightwatch and NightOwl

Laravel Nightwatch recorded requests, queries, jobs, exceptions, outgoing HTTP calls, scheduled tasks, and more. Instead of sending that telemetry to Laravel's hosted service, the NightOwl agent on the application server received it over a local socket, buffered it in SQLite, and wrote it in batches to a second PostgreSQL database. A scheduled command pruned old records every day.

Verification worker

Docker and a queue worker for the verification queue only. It pulled jobs from Valkey, downloaded each file from its host, checked it in a throwaway container with no network, and wrote the results to PostgreSQL.

Outside services

Discord for sign-in and release announcements, GitHub releases for syncing SPT versions, Cloudflare's GraphQL Analytics API for the live visitor count, and MaxMind's GeoLite2 database for visitor locations, refreshed nightly.

Mod file hosts

The Forge never stored mod files. Authors linked to their own hosts, downloads were counted and then redirected there, and the verification worker fetched each file to check it, with a header check twice a day to spot changes.

Features

The features that set the Forge apart from a plain file host, from publishing and verifying mods to finding, collecting, and talking about them, and the tools that kept it all running. I'll go over the features and outline how they worked.

Publishing

Downloads and verification

Finding mods

Mod lists

Community

Moderation and administration

Public API

Lessons learned

Ten things two years of building the Forge taught me, most of them the hard way.

  1. Pick the tools and keep them

    The Forge's admin panel had three lives: five weeks on Laravel Nova, nine months on Filament, and the rest of its life as custom Livewire pages. It wasn't the only thing I kept swapping out.

    Monitoring ran through Pulse, Telescope, 22 days of Sentry, and Flare, which I replaced with Nightwatch in June 2025, put back the very next day, and finally dropped in April 2026 for Nightwatch and the NightOwl agent. The moderation modals moved to Flux UI and back to Jetstream within a single day in March 2025. Octane went from FrankenPHP to Swoole and was back on FrankenPHP ten weeks later, and Blaze was installed, pulled out two days later, and reinstated a month after that.

    No single switch looked expensive on the day. Added up, each one cost a migration, a round of new configuration, and whatever history the old tool had collected, and I paid that far more often than the Forge's problems called for.

    Next time I'd choose carefully once and stay put until a tool genuinely stood in the way.

  2. Migrated data has a long tail

    Importing the old Hub looked like a one-off job. I started the WoltLab importer in the project's second week and didn't delete it until September 2025, sixteen months later, by which point its mod data object alone ran past 1,100 lines.

    Old data carries every shortcut the previous system ever allowed, and each one surfaced as its own bug: type mismatches, duplicate key violations, email addresses that differed only by case, non-Latin usernames that needed a fallback slug, and deadlocks while syncing follows. One day saw six import fixes land. Another fix, for registration dates that fell into daylight-saving gaps, was reverted the same day it shipped.

    Even after the importer was gone, the Hub kept reaching forward. Imported passwords stored with the older $2a$ bcrypt prefix had to be rewritten when Laravel 12 began rejecting them.

    A data migration is a project in its own right, and it deserves to be planned like one.

  3. Real data finds the slow paths

    The visitor analytics page worked fine until its table passed seven million rows and its queries started hitting a 30-second timeout. It got a capped date range, a separate COUNT DISTINCT query, and a covering index, and eventually its stats moved into a queued job altogether.

    That story repeated across the Forge. Pages that felt instant against seeded data started to drag once real traffic and real table sizes arrived. I made lazy loading throw an error in development in September 2024, yet I was still fixing N+1 queries two years later in the API, the RSS feed, chat, and update checks.

    Livewire components quietly carried a lot of state. One pass in July 2025 cut the mod action components' memory use by 42% and halved their response time, and the comment system was rebuilt so replies loaded only when asked for. Comment threads stopped walking up parent IDs in a loop and switched to recursive CTEs.

    Hardly any of it was clever: eager loading, indexes, and moving heavy work off the request. The hard part was knowing where each fix was needed, and only real data could show me that.

  4. Cache plain values, and clear only your own

    Caching rescued plenty of slow pages, then brought a family of bugs of its own. The cache handed counts back as strings, so navigation badges, unread notification counts, and two different pagination totals each needed an integer cast added after the fact. Caching Eloquent models, collections, and stdClass rows led to serialization errors until I switched analytics stats and computed properties to plain arrays and strings.

    The most dangerous bug was also the easiest to miss. The disposable email blocklist cleared its cache with Cache::flush, which empties the entire store rather than just its own keys, and it took two attempts a month apart, first versioned keys and then tags, to fix it properly. Even a Cache::has followed by a Cache::get in the download counter turned out to be a race.

    Whatever goes into a cache should be plain enough to come back out unchanged, and code should only ever clear what it put there itself.

  5. Make queued work safe to run twice

    In October 2025 the Forge posted its Discord announcements twice. It was the most visible sign of a problem that kept coming back: duplicate runs of the Discord job had to be fixed again in April 2026, and comment notifications repeated whenever a job was retried.

    The causes were in the queue settings and in when jobs were dispatched. retry_after was shorter than Horizon's worker timeouts, which let a second worker pick up a job the first was still running. Notifications were dispatched inside database transactions, so a worker could start before the data it needed had been committed.

    Fixing it took a notification log that deduplicated sends, explicit retry, timeout, and failure handling on every job, guards on every scheduled task, and dispatching only after commit.

    Anything that sends a message or writes a record from a queue will eventually run more than once, so it has to be harmless when it does.

  6. Keep bots out of the error log

    A public site receives a steady stream of junk requests, and left alone they show up as server errors that bury the problems that actually matter.

    Through 2026 I taught the Forge to turn that junk away at the door: URLs and form fields with malformed UTF-8, arrays where the API expected a single value, broadcast requests with invented socket IDs, and Livewire requests that tried to change locked properties. Responses that had been 500s became 400s, exceptions caused by bots and bad client input were filtered out of reporting, and Livewire's too-many-calls errors were suppressed.

    The noise had a cost beyond attention, too. In January 2026 I had to cut Flare's sample rate just to stay under its monthly limit.

    An error log is only useful when every entry in it is something worth fixing.

  7. Test against the database you run

    Two days in 2024, one in June and one in September, went to 21 workflow commits, about half of them just trying to get the test job to connect to MySQL. One of them is titled "Would you please just connect!".

    Reaching a database at all was only the start, because the differences between engines stayed hidden until something forced a second one into the picture. In January 2026 I fixed polymorphic type names whose backslashes MySQL had stripped.

    The move to PostgreSQL in July 2026 turned up a whole batch more: foreign keys with no index (MySQL adds one automatically, PostgreSQL does not), a notifications column that had to become jsonb, prepared statements emulated for PgBouncer, queries written for a single driver, and broken SQLite migrations. In the final weeks I added a CI matrix that ran the suite on both PostgreSQL and SQLite.

    Running the tests on the engine production actually used, and on more than one, would have caught them much sooner.

  8. Keep the safety nets on during big changes

    On 4 March 2026 I landed 68 commits in a single cleanup pass and reverted five of them before the day was over. Big changes were exactly when static analysis and browser tests were most useful, and exactly when they were most tempting to switch off or rush past.

    PHPStan climbed from level 5 in September 2024 to level 6 in January 2025, then was removed during the Livewire 4 upgrade in January 2026. It came back at its strictest level in April 2026, along with a batch of fixes.

    The browser tests moved from Dusk to Pest 4 in August 2025 and needed steady care after that: unique test data, explicit waits before assertions, and six tests skipped for 17 days over a Flux combobox bug. Smoke tests for every page and architecture tests arrived in April 2026. Sharding the browser tests across CI runners was added, removed five days later, and brought back with time-balanced shards in June 2026.

    Those checks never felt more inconvenient than in the middle of a big change, and they were never worth more.

  9. When a feature keeps fighting, simplify it

    The live visitor count went through six versions in eleven months: a Livewire counter, WebSocket broadcasts, a JavaScript and Echo client, a peak-only tracker, a count without WebSockets, and finally a five-minute query against Cloudflare's analytics. The last version was also the simplest.

    Other features followed the same path. Comment notifications started out subscribing people to every new comment on any page they owned or had commented on. Two rounds of cuts later, in January 2026, they only went out when someone replied directly to your comment. The API gained OAuth through Passport in May 2026 and lost it, along with Sanctum tokens, three weeks later in favour of an open, read-only API behind caching and rate limits. Caching rendered Markdown in database columns was reverted the day it shipped, and the light theme, which had needed its own contrast and flash fixes, was removed in July 2026.

    When a feature keeps fighting back, the answer is usually to make it do less.

Thank you

Building the Forge was extremely gratifying. It grew from a first commit in May 2024 into the place where the Single Player Tarkov community published, found, and talked about its mods.

Thank you to everyone who helped with its development and design. Thank you as well to everyone who used it, and to everyone who still does through its new fork (opens in new tab), maintained by its new owner (opens in new tab).

I wish the SPT community nothing but love.