Floxum code review of the referral extension. Verified every finding below against the current source — all nine hold. None block publishing; the migration one is the only thing with real downtime risk on a big forum. Fixes for each are in the comments.
Summary: backend is architecturally sound and idiomatic for Flarum 2.x (scoped container binding, concurrency-safe getOrCreateForUser fallback, denormalised referral_count to dodge N+1 on post-author lists, proper NULL handling in the unique index). Frontend is functional but plain ES5/ES6 rather than TypeScript. Two things to weigh up: the unbatched backfill UPDATE, and a DB INSERT happening on a GET serialization path.
Medium — production risk
Low — production risk
Low — robustness
Low — conventions
Low — dead code / technical debt
Floxum code review of the referral extension. Verified every finding below against the current source — all nine hold. None block publishing; the migration one is the only thing with real downtime risk on a big forum. Fixes for each are in the comments.
Summary: backend is architecturally sound and idiomatic for Flarum 2.x (scoped container binding, concurrency-safe
getOrCreateForUserfallback, denormalisedreferral_countto dodge N+1 on post-author lists, proper NULL handling in the unique index). Frontend is functional but plain ES5/ES6 rather than TypeScript. Two things to weigh up: the unbatched backfill UPDATE, and a DB INSERT happening on a GET serialization path.Medium — production risk
migrations/2026_05_27_000001_add_referral_count_to_users.php:32). The backfill runs a correlated(SELECT COUNT(*) FROM referral_invited_user WHERE …)for every users row in one statement. On a forum with tens of thousands of users this holds a write lock onusersfor the full count, queueing registrations/profile requests and risking timeouts during migration. Chunk it (chunk(500, …)over user IDs), or only update users that actually appear inreferral_invited_userto keep the working set small.Low — production risk
ListCampaignCodesControllerfetches all campaign codes with no pagination (src/Api/Controller/ListCampaignCodesController.php:18).whereNull('user_id')->orderBy('id','desc')->get()returns every code in one query/response. Ongoing campaigns could accumulate hundreds. Add a hard->limit(200)(plus atotalmeta field) or cursor/offset pagination.getOrCreateForUseron read path) (src/Api/UserResourceFields.php:43). ThereferralCodegetter may INSERT on every/api/users/mefor an eligible user who's never opened their profile — a write inside a GET breaks HTTP semantics and a caching proxy could serve a 200 that still triggered an upstream write. Split generation from serialization: an explicitPOST /api/referral/my-codethat generates on demand, with the attribute returningnulluntil a row exists; frontend calls it once when the referrals tab opens.Low — robustness
src/Http/CaptureReferralCookieMiddleware.php:28). The raw cookie is trimmed and stored with no length cap, then handed toInviteCode::where('code', strtoupper($code))->first(). Prepared statements stop injection, but an attacker can send an arbitrarily long cookie on every unauthenticatedPOST /api/userswith no early rejection. Valid codes are ≤8 chars — rejectstrlen($code) > 16in the middleware (set code to null).src/Api/Controller/CreateCampaignCodeController.php:28). Label is trimmed but unconstrained; the column isVARCHAR(255). A >255-char label throws a DB truncation/constraint error surfacing as an unhandled 500 instead of a clean 422. Add a length check throwingValidationException(['label' => [trans('linkrobins-referral.api.label_too_long')]]).Low — conventions
overrideonExtensionPagerather thanapp.extensionData(js/admin.js:16). Patching the sharedExtensionPage.prototype.contentand guarding onextension.idaffects all admin extension pages. Useapp.extensionData.for('linkrobins-referral').registerContent(() => m(ReferralAdminPage))with an extracted component and drop the prototype override.flarum-tsconfigdevDependency present but source is plain JS (js/package.json:9).admin.js/forum.jsare plain ES5/ES6, so the tsconfig dep is unused and misleading. Either migrate to TS (rename.ts, import from@flarum/core) or dropflarum-tsconfigfrom devDependencies.Low — dead code / technical debt
joinTimeattribute registered onUserModelbut never used (js/forum.js:26).UserModel.prototype.joinTime = Model.attribute('joinTime', Model.transformDate)is never read, and core exposesjoinedAt(notjoinTime), so it always resolves toundefined. Remove the line; useuser.joinedAt()if account age is ever needed.ReferralsPageinstead of LESS classes (js/forum.js:120).ReferralsPage.content()embeds layout/typography as inlinestyleattributes (~40 lines). Inline styles can't be themed without!importantand are invisible to the extension's own LESS. Extract into named classes (.ReferralProfile-code,.ReferralProfile-count) inless/forum.less, register via(new Extend\Frontend('forum'))->css(...), and swap toclassName.