PHPackages                             linkrobins/support - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Utility &amp; Helpers](/categories/utility)
4. /
5. linkrobins/support

ActiveFlarum-extension[Utility &amp; Helpers](/categories/utility)

linkrobins/support
==================

Support ticket system for Flarum. Users open tickets, staff handle them in a private threaded view. Banned users can submit ban appeals with rate limits.

v1.4.0(1w ago)091↑229.7%MITTypeScriptPHP ^8.3

Since May 16Pushed 1w agoCompare

[ Source](https://github.com/linkrobins/support)[ Packagist](https://packagist.org/packages/linkrobins/support)[ Docs](https://linkrobins.com)[ RSS](/packages/linkrobins-support/feed)WikiDiscussions main Synced 1w ago

READMEChangelog (10)Dependencies (1)Versions (17)Used By (0)

Link Robins Support
===================

[](#link-robins-support)

A private support-desk extension for Flarum 2. Lets registered users open support tickets with staff, with an emphasis on workflows that keep forum-wide moderation actions (suspensions, bans) honest.

Features
--------

[](#features)

- **Private tickets.** Tickets are NOT Flarum discussions. A user's ticket is visible only to that user and to staff. Other users never see it, even if they know the URL.
- **Categories.** Admin-configurable, each with name, slug, color, icon, position, and an `is_appeal` flag.
- **Appeal flow.** Categories marked as appeals follow stricter rate limits and are filable by suspended users so they can plead their case. General categories are blocked for suspended users so a ban isn't trivially worked around.
- **Internal notes.** Staff can add replies marked as internal. These are filtered out at the database level for non-staff users -- the ticket owner doesn't see them in their list, can't fetch them directly, and the replyCount on the ticket reflects only what they can see. Visually, internal notes get a subtle background tint on the reply card for staff.
- **Reply moderation.** Staff can edit, soft-delete, restore, and permanently delete any reply via a `⋯` menu in the reply header. Edits stamp `edited_at` and `edited_by_user_id` so other staff can see the audit info; permanent deletion requires the reply to be soft-deleted first (no accidental single-click destruction).
- **Ticket moderation.** Staff can soft-delete tickets via a `⋯`menu in the ticket title row. Soft-deleted tickets are hidden from the index for both the owner and staff but remain reachable via direct URL for staff to restore. Permanent deletion is admin-only and cascades to all replies.
- **Rate limits.** Per-user, configurable. Defaults:
    - 3 appeals per 30 days
    - 1 concurrent open appeal at a time
    - 10 general tickets per 24 hours
- **Permanent appeal-ban.** A per-user flag (`support_appeal_banned`) that blocks appeals while leaving general tickets available. Toggled from the admin's "Appeal bans" tab.
- **Status workflow.** open → in\_progress → awaiting\_user → resolved → closed. Auto-advances based on who replies (staff to open ⇒ in\_progress; user to awaiting\_user ⇒ in\_progress). Closed tickets reject replies.
- **Assignment.** Staff can claim or unassign tickets. The assigned staff member shows in the staff control bar.
- **Notifications.** In-app and email. The ticket owner is notified when staff replies; staff are notified when a new ticket is opened or when the owner replies. Internal notes never produce notifications. Users can toggle these per driver in their notification preferences.
- **Decisions on appeals.** Resolved appeal tickets record a `decision` field (approved / rejected / null).
- **File attachments.** Optional integration with `fof/upload`. When installed, the compose and reply forms surface an "Attach files" button that uploads through `fof/upload`'s normal pipeline. No configuration here; the button respects whatever `fof/upload`permissions you've set.

Requirements
------------

[](#requirements)

- Flarum 2.0.0+
- PHP 8.2+

Installation
------------

[](#installation)

```
composer require linkrobins/support
php flarum migrate
php flarum cache:clear
```

Then enable the extension in admin → Extensions.

Permissions
-----------

[](#permissions)

The extension adds one permission:

- `linkrobins-support.handle_tickets` (default: moderate group) -- grants the ability to see all tickets, reply on any ticket, post internal notes, change ticket status, set decisions, and claim tickets.

Anyone in the admin group bypasses this check.

Filing tickets requires being authenticated; the policy doesn't add a separate permission for it.

Admin UI
--------

[](#admin-ui)

Settings live at admin → Extensions → Link Robins Support, with three tabs:

- **Categories.** CRUD for ticket categories.
- **Rate limits.** Configurable values for the appeal and general limits described above.
- **Appeal bans.** Search users and toggle their permanent appeal-ban flag.

Forum UI
--------

[](#forum-ui)

Users see:

- `/support` -- their tickets list, with filter chips for status.
- `/support/new` -- compose form. Banned-from-appeals users see only general categories; suspended users see only appeal categories.
- `/support/:id` -- the ticket page, with reply form and reply thread.

Staff additionally see:

- The "All" filter on the index, with status chips for cross-cutting views (open, in\_progress, awaiting\_user, resolved, closed).
- The staff control bar on each ticket: set status, claim/unassign, post internal notes via the reply form's "Internal note" toggle.

Data model
----------

[](#data-model)

Three tables:

- `linkrobins_support_categories` -- name, slug, description, color, icon, position, is\_appeal.
- `linkrobins_support_tickets` -- category\_id, user\_id, assigned\_staff\_id, subject, status, decision, last\_reply\_at, deleted\_at.
- `linkrobins_support_replies` -- ticket\_id, user\_id, content (parsed-source XML), is\_internal\_note, deleted\_at, edited\_at, edited\_by\_user\_id.

One column added to the existing `users` table:

- `support_appeal_banned` (boolean, default 0).

Replies use Flarum's content formatter via the `HasFormattedContent`trait. The rendered HTML is computed at serialize time via `formatContent()`, NOT cached in a `content_html` column -- this means formatter extensions like mentions and emoji apply to older replies the moment they're installed.

File attachments (fof/upload integration)
-----------------------------------------

[](#file-attachments-fofupload-integration)

If [fof/upload](https://packagist.org/packages/fof/upload) is installed and enabled, the compose form and reply form get an "Attach files" button. Uploaded files are stored, validated, and rendered by `fof/upload`; this extension only inserts the resulting BBCode marker into the message body. No additional configuration is needed -- if the user has permission to upload via `fof/upload`, the button appears.

### Privacy caveat

[](#privacy-caveat)

`fof/upload`'s download URLs act as capabilities: anyone who has the URL to a file can download it. CSRF protection limits direct hot-linking, but if a staff member copies a file URL out of a ticket and shares it elsewhere, that link works for anyone who clicks it.

This is identical to how `fof/upload` behaves on regular discussions, so it isn't unique to this extension. If you need a hard guarantee that ticket attachments can be read only by ticket-eligible users, `fof/upload` would need to be patched to gate downloads against per-resource policies. That's out of scope for v1.

In practice, for the support-desk use case, the risk is small: attachments tend to be screenshots and logs from the ticket-opener themselves, who is also the only non-staff party with the URL.

Security notes
--------------

[](#security-notes)

- The `creating()` hooks on both tickets and replies overwrite `user_id` with the authenticated actor's id. Even if the client sends `relationships.user`, JSON:API rejects it because the field isn't declared writable, AND the hook would overwrite it anyway.
- Visibility is enforced in two places that must stay in sync: the resource's `scope()` (for single-resource Show endpoints) and the Searcher's `getQuery()` (for list Index endpoints). Both use the same rules.
- Internal notes are filtered at the database level (a WHERE clause on `is_internal_note`), not at render time. A non-staff user hitting the API directly cannot bypass the filter.
- The "Update" endpoint is gated by `->can('update')`, which routes to `SupportTicketPolicy::update`. Field setters add a second layer of defense: even if the gate ever loosened, status / decision / assignment changes wouldn't take effect for a non-staff actor.
- Soft-deleted rows are hidden from non-staff at the DB query level (Eloquent's default SoftDeletes scope). Staff see them via `withTrashed()` in the resource scope, but Index/Searcher queries deliberately stay on the active set so the staff index isn't cluttered. Force-delete on a live (non-trashed) row is rejected by the `deleting()` hook -- a soft-delete must come first.

API summary
-----------

[](#api-summary)

EndpointMethodAuth`/api/linkrobins-support-categories`GETpublic`/api/linkrobins-support-categories`POSTadmin`/api/linkrobins-support-categories/:id`PATCH/DELETEadmin`/api/linkrobins-support-tickets`GETauthenticated`/api/linkrobins-support-tickets`POSTauthenticated`/api/linkrobins-support-tickets/:id`GETper-policy`/api/linkrobins-support-tickets/:id`PATCHstaff (handle\_tickets)`/api/linkrobins-support-tickets/:id`DELETEadmin, soft-deleted only`/api/linkrobins-support-replies`GETauthenticated`/api/linkrobins-support-replies`POSTauthenticated`/api/linkrobins-support-replies/:id`PATCHstaff (handle\_tickets)`/api/linkrobins-support-replies/:id`DELETEstaff, soft-deleted onlyModeration patterns:

- PATCH a ticket or reply with `{ attributes: { content: "..." } }` to edit (replies only). Stamps `edited_at` + `edited_by_user_id`.
- PATCH with `{ attributes: { isDeleted: true } }` to soft-delete. PATCH with `{ isDeleted: false }` to restore.
- DELETE permanently removes -- but only if the row is already soft-deleted; otherwise returns 400.

Supported filters (use `filter[name]=value` shape; Flarum 2 rejects unrecognized top-level params):

- On tickets: `filter[mine]=1`, `filter[status]=open`, `filter[categoryId]=N`
- On replies: `filter[ticketId]=N`

License
-------

[](#license)

MIT.

###  Health Score

47

—

FairBetter than 93% of packages

Maintenance98

Actively maintained with recent releases

Popularity14

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity57

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~1 days

Total

16

Last Release

9d ago

PHP version history (2 changes)v1.0.0PHP ^8.2

v1.1.1PHP ^8.3

### Community

Maintainers

![](https://www.gravatar.com/avatar/6ba4ada28098b217152b5ec67bff43409a54b808d7d038119af6f56715a1c3f9?d=identicon)[linkrobins](/maintainers/linkrobins)

---

Top Contributors

[![linkrobins](https://avatars.githubusercontent.com/u/213107296?v=4)](https://github.com/linkrobins "linkrobins (18 commits)")

---

Tags

supportflarumticketshelpdeskappeals

### Embed Badge

![Health badge](/badges/linkrobins-support/health.svg)

```
[![Health](https://phpackages.com/badges/linkrobins-support/health.svg)](https://phpackages.com/packages/linkrobins-support)
```

###  Alternatives

[flarum-lang/russian

Russian language pack for Flarum.

12127.5k](/packages/flarum-lang-russian)[fof/byobu

Well integrated, advanced private discussions.

61112.4k10](/packages/fof-byobu)[escalated-dev/escalated-laravel

An embeddable support ticket system for Laravel applications

262.8k2](/packages/escalated-dev-escalated-laravel)[fof/gamification

Upvotes and downvotes for your Flarum community

4162.0k6](/packages/fof-gamification)[fof/polls

 A Flarum extension that adds polls to your discussions

25130.2k9](/packages/fof-polls)[fof/user-bio

Add a user bio to user profiles

21102.0k9](/packages/fof-user-bio)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
