Search by

fewchurs / laravel

fewchurs

Drop a Fewchurs feature-request board into any Laravel app.

v1.9.1 2026-11-09 07:51 UTC

This package is auto-updated.

Last update: 2026-11-09 07:51:41 UTC


README

A feature-request board in any Laravel app: install, add one env var, drop one Blade tag. Server-rendered by default, reactive with Livewire, and a plain PHP client for fully custom UIs.

composer require fewchurs/laravel
php artisan fewchurs:install
FEWCHURS_API_KEY=fr_live_…
<x-fewchurs::board />

That's it — the board renders, votes and comments work with JavaScript disabled, and your API key never reaches the browser.

Add your domain to your app in the Fewchurs dashboard. The API key only works there. The package sends the domain each page was opened on (the request's host) as the app ID, so APP_URL doesn't matter. Artisan commands have no visitor, so they use the APP_URL host. To always send one fixed ID instead, set FEWCHURS_APP_ID.

localhost always works, so you can test with just the key. While you open the board on localhost with APP_DEBUG=true, it shows a small reminder to add your domain before going live. Visitors never see it.

What you get

<x-fewchurs::board /> The board, anywhere in your own layout.
<x-fewchurs::sheet /> A floating button that slides the board in from the side or bottom.
/feedback A ready-made page, rendered inside your app's layout.
<livewire:fewchurs-board /> The same board, reactive, if you use Livewire.
Fewchurs::features() The PHP client, for a UI you build yourself.
php artisan fewchurs:check Answers "is my key right?" in one command.

Requirements

PHP 8.2+, Laravel 10, 11, 12 or 13. No dependencies beyond Laravel and Guzzle (which Laravel already ships with), and no build step — the board ships its own scoped CSS, so it does not assume Tailwind.

As a sheet

Put the board behind a floating button instead of on the page. Add it once, for example to your main layout, and it shows on every page:

<x-fewchurs::sheet />                    {{-- slides in from the right --}}
<x-fewchurs::sheet side="left" />
<x-fewchurs::sheet side="bottom" />
<x-fewchurs::sheet label="Suggest a feature" />
<x-fewchurs::sheet>💡 Ideas?</x-fewchurs::sheet>

Use your own button instead of the floating one: hide it with :trigger="false" and add data-fewchurs-open to any element.

<x-fewchurs::sheet :trigger="false" />

<button data-fewchurs-open>Feedback</button>

The board (in the sheet and on the page) opens on the list of requests. A "New request" button next to the Top and New tabs switches it to the form, and "Cancel" returns to the list. If submissions are off, the button doesn't show. Comments and Notify me open in a popup that closes with the × button, Escape or a click outside.

The sheet loads the board from /feedback?embed=1 in a frame, and only the first time it opens, so pages where nobody opens it make no API calls. It needs the /feedback route (so route.prefix must not be null), takes app and tab like the board, and uses the colours from config('fewchurs.theme'). Without JavaScript the button is a link to the full page. On phones it fills the screen. While the board loads, the sheet shows a shimmer shaped like the board, and if the page inside fails it shows a message with "Try again" and a link to the full page. If your app sends X-Frame-Options: DENY or a CSP frame-ancestors 'none', allow your own site to frame /feedback.

Loading and errors

When the API can't be reached (or rejects the key) and there is no recent copy to show, the board shows a title, a message and a "Try again" button inside the board, under the tabs and the New request button, and the rest of your page keeps working. The failure itself is still logged.

Failure Title APP_DEBUG=false APP_DEBUG=true
Wrong key Invalid API key Copy the key from your Fewchurs dashboard (Apps → API keys) and set FEWCHURS_API_KEY in your .env file. The same
Wrong app ID Invalid API key This API key is not set up for other.com. Add it to your app in the Fewchurs dashboard. The same
No connection Couldn't load requests Couldn't reach the feedback service. Could not reach the Fewchurs API. Check this server's internet connection and FEWCHURS_BASE_URL. [connection]
Anything else Couldn't load requests Something went wrong. Try again. The server's message, its code and the HTTP status

Except for the wrong app ID, which comes from the server and names the ID, the APP_DEBUG=false messages and the titles come from the errors and error_titles groups in the lang file, so you can reword or translate them.

Keep APP_DEBUG off in production, as Laravel already asks.

The plain board arrives with your page, already loaded, so it has no loading state. For a shimmer, use the sheet, or Livewire with lazy, which also shimmers while switching tabs:

<livewire:fewchurs-board lazy />

Using the client

use Fewchurs\Laravel\Facades\Fewchurs;

$page = Fewchurs::features()->top(limit: 20);
$page = Fewchurs::features()->latest(limit: 20, cursor: $page->nextCursor);

$feature = Fewchurs::features()->submit(
    title: 'Add dark mode',
    description: 'Would love a dark theme.',
    email: auth()->user()?->email,   // the server follows this email for you
);

Fewchurs::features()->vote($feature->id);
Fewchurs::features()->unvote($feature->id);
Fewchurs::features()->follow($feature->id, email: 'a@b.com');

Fewchurs::comments()->list($feature->id, limit: 50);
Fewchurs::comments()->create($feature->id, text: '+1', authorName: 'Sam');

Fewchurs::config()->showBranding;

Every call throws a typed exception on failure — RateLimitedException, NotFoundException, InvalidApiKeyException, ConnectionException and friends, each with a userMessage() that is safe to show a visitor while APP_DEBUG is off.

A key used from an app ID it is not set up for gets a wrong_app error from the server. This throws InvalidApiKeyException, is never retried, and getMessage() names the ID to add.

Identity

The server needs an X-Device-Id per visitor. The package resolves one for you:

'identity' => ['mode' => env('FEWCHURS_IDENTITY', 'hybrid')], // cookie | user | hybrid
  • cookie — a random value in a signed, httpOnly, SameSite=Lax cookie.
  • user / hybrid — the signed-in user's id, so votes follow them across browsers and devices; guests fall back to the cookie.

Everything is HMAC-hashed with your APP_KEY before it leaves your server — the raw cookie, the user id and the email never reach Fewchurs.

Multi-tenant or team-level identity:

Fewchurs::resolveDeviceIdUsing(fn (Request $request) => hash('sha256', $request->user()->team_id));

Always hash. Passing an email or a raw id throws.

Privacy policy note: the cookie is functional — it records which requests this visitor voted on — so most consent setups class it as necessary rather than tracking. It is still a first-party identifier and belongs in your privacy policy. Sample wording:

We set a fewchurs_did cookie so we can remember which feature requests you have voted on or followed. It contains a random identifier, not your name or email, and expires after a year.

Customising

1. Config — config/fewchurs.php covers the route prefix, tabs, per-page count, whether submissions, comments and follows are open, whether the email field is hidden, optional or required, and whether writes require a signed-in user.

2. Theme tokens

<x-fewchurs::board :theme="['primary' => '#0F766E', 'radius' => '4px']" />

Height. The board fills the height of the element it sits in and never grows taller than the screen. The heading, the tabs and New request stay in place, and only the list scrolls. Give its parent a height to size it, for example <div style="height: 600px">. Change the limit with max_height, or use none to let the board grow with its list as before:

<x-fewchurs::board :theme="['max_height' => '80vh']" />

3. Publish the views and the strings

php artisan vendor:publish --tag=fewchurs-views
php artisan vendor:publish --tag=fewchurs-lang

The views are small, single-purpose Blade files meant to be edited. Translations are ordinary Laravel lang files, so lang/vendor/fewchurs/fr/messages.php just works.

4. Slots

<x-fewchurs::board>
    <x-slot:header>
        <h1>Help shape {{ config('app.name') }}</h1>
    </x-slot:header>

    <x-slot:empty>
        <x-my-empty-illustration />
    </x-slot:empty>
</x-fewchurs::board>

5. Headless — set route.prefix to null, use the client, and build whatever you like.

More than one board

'apps' => ['docs' => ['api_key' => env('FEWCHURS_DOCS_KEY')]],

A board sends the main app_id unless you give it its own app_id in this array.

<x-fewchurs::board app="docs" />
Fewchurs::app('docs')->features()->top();

Events

Event::listen(FeatureSubmitted::class, fn ($e) => Slack::send("New request: {$e->feature->title}"));

FeatureSubmitted, Voted, Unvoted, Commented, Followed, Unfollowed, RequestFailed.

Testing your app

$fake = Fewchurs::fake([['id' => 'feat1', 'title' => 'Add dark mode']]);

// … exercise your app …

$fake->assertSubmitted(fn ($feature) => $feature->title === 'Add dark mode');
$fake->assertVoted('feat1');
$fake->assertNothingSubmitted();

The fake sits behind the real client, so your tests exercise the same headers, validation and error mapping as production.

Security

The key is a real secret here, and the package keeps it that way.

  • The key never reaches the browser. All calls are server-to-server; the browser talks only to your app's own routes. A test asserts the rendered HTML and JSON never contain it, and CI fails if a key-shaped string is committed anywhere in the repo.
  • It never reaches your logs either. Every log line and exception message is scrubbed of bearer tokens and fr_… keys before it is written.
  • CSRF on every write route (they run in the web group).
  • Throttling per visitor, not per IP — a whole site shares one server IP, so writes are limited by the resolved device id (route.throttle, 30/min by default).
  • Honeypot plus a signed minimum-time check on the submit and comment forms, so basic bots are dropped without a CAPTCHA. The timestamp is encrypted, so it cannot be forged or replayed.
  • Output is escaped. Titles, descriptions and comments are written by strangers. The views use {{ }} throughout and say so in a comment — if you publish them, keep it that way.
  • Ids are validated before they are interpolated into an API path, so a crafted id cannot reach a different endpoint.
  • Redirects are built from the package's own route, never from the Referer header.
  • Device ids are stripped from everything handed back to the page; features and comments carry isMine instead.
  • Theme values are allowlisted before being inlined into a style attribute.
  • HTTPS is required for the API base URL outside local development, with certificate verification on.

Set board.require_auth to true if the board should only accept writes from signed-in users.

Commands

php artisan fewchurs:install   # publish config, print next steps
php artisan fewchurs:check     # verify the key, connectivity and plan
php artisan fewchurs:warm      # pre-warm the config cache after a deploy

Caching

/config is cached app-wide for 5 minutes (cache.config_ttl), so plan changes and the "Hide vote counts" switch show up quickly. Feature pages are cached per visitor for 15 seconds, because hasVoted and isFollowing are per visitor and a shared entry would hand one visitor's vote state to everyone. A longer-lived stale copy is kept so an unreachable API degrades into "showing recent data" instead of an error page.

License

Proprietary. You may use the package in your own apps to connect them to Fewchurs, and edit the config, views and translations you publish. The source is public for transparency, not open source. See LICENSE.