A help centre and contact-support desk for Filament panels.
Knowble gives your customers somewhere to find answers and to ask when they can't, and gives your team one place to answer them. It runs in any Filament 5 panel, in a single-database app or a multi-tenant one, and almost everything about it can be configured.
Features
For your customers
- Help centre: sections and screens laid out the way your app's sidebar is, with tiles, Popular, Recently updated, Questions and Favourites.
- Articles, how-tos and questions: how-tos have numbered steps, articles have screenshots drawn at one consistent scale, and code shows in a labelled panel with a copy button.
- Search that understands "how do I…", with type and area filters, and treats
%literally. - On every article: previous and next, related articles, "Did this help?", a favourite star, Suggest an improvement, and Print to PDF.
- Contact support: suggests matching articles as somebody types, and takes file attachments.
- Your requests: each request's conversation, live, with a badge for unread replies and replies that can be changed for a few minutes.
- Help on any screen: a "Help" button that opens the article about it.
For your team
- A requests queue: priority, status, an internal note, help articles linked in a reply, and a waiting-time clock.
- A support report: what came in, what was resolved, how fast the first reply was, and who's waiting longest.
- Optional extras: support tiers per customer, an article and category editor, a suggestions queue, and engagement figures (reads, ratings and favourites per article).
- Email both ways, and events to hang in-app notifications on.
For you, the developer
- Content in your repository: a JSON file and an images folder, loaded on deploy with one command. No database seeding, and no help that differs between environments.
- Config over convention: every feature switches off, and every page's label, icon, group, position and placement (sidebar, user menu or hidden) can be changed.
- Single-database or multi-tenant: one host class tells Knowble who the tenant is.
Requirements
- PHP 8.3 or later
- Laravel 12 or 13
- Filament 5
barryvdh/laravel-dompdf, for Print to PDF (optional)
Installation
Install the package with Composer:
composer require polycode/knowble
Then either set it up by hand. Publish the config and migrations, and migrate:
php artisan vendor:publish --tag=knowble-config
php artisan vendor:publish --tag=knowble-migrations
php artisan migrate
Or let the installer do all of that, and choose what you want:
php artisan knowble:install
The installer can also run unattended, taking every choice as an option:
php artisan knowble:install --no-interaction --without-support --editor \
--staff-email=help@example.com --help-path=guides --migrate
It publishes the config and migrations and writes your choices into them. It also starts
database/knowble/content.json with an example article, and migrates if you ask. Run php artisan knowble:install --help for every option.
Knowble ships its own compiled stylesheet and serves it itself, from /knowble-assets/knowble.css
on each panel page's own domain, so there's nothing to publish or deploy, and an update reaches
your app with the package. It works on Filament's default theme and takes its colours from your
panel, dark mode included.
It also defines the two disks it needs if your app hasn't: knowble for help images (in
public/knowble) and support-attachments for files sent with requests (private, in
storage/app/support-attachments). Define either yourself to put it somewhere else, such as S3.
Setting up the panels
Register KnowblePlugin on the panel your customers use:
use Polycode\Knowble\KnowblePlugin;
public function panel(Panel $panel): Panel
{
return $panel
// ...
->plugin(KnowblePlugin::make());
}
That adds Help, Contact support and Your requests to a Support group in the sidebar.
Your team answers requests in a staff panel. Register KnowbleAdminPlugin on whichever one suits:
- An admin panel you already have: add the plugin to it and the Requests queue and Support report join its sidebar.
- A panel just for support: create one with
php artisan make:filament-panel supportand register the plugin there.
use Polycode\Knowble\KnowbleAdminPlugin;
->plugin(KnowbleAdminPlugin::make())
Requests still work without a staff panel: staff are emailed each one, but they can only answer in a panel.
Writing your help
Your help lives in your repository, in database/knowble/content.json with its images beside it in
database/knowble/images. Deploy it with two commands:
php artisan knowble:import # loads the file, matching on slug; --prune removes what is gone
php artisan knowble:sync-images # copies the images folder to the knowble disk
Both are safe to run on every deploy. knowble:import refuses a bad file before writing anything,
and --dry-run checks one.
The file holds two levels of categories (sections, and the screens under them), and articles filed under either:
{
"format": 1,
"categories": [
{ "slug": "invoices", "name": "Invoices", "order": 1, "parent_slug": null },
{ "slug": "invoices-sending", "name": "Sending invoices", "order": 1, "parent_slug": "invoices" }
],
"articles": [
{
"slug": "how-do-i-send-an-invoice",
"category_slug": "invoices-sending",
"type": "how_to",
"title": "How do I send an invoice?",
"body": "<p>From the invoice, in two steps.</p>",
"order": 1,
"published": true,
"steps": [
{ "title": "Open the invoice", "body": "<p>Go to Invoices and open it.</p>", "order": 1 },
{ "title": "Send it", "body": "<p>Press <strong>Send</strong>.</p><p><img data-id=\"send.png\" alt=\"The Send button\"></p>", "order": 2 }
]
}
]
}
typeisarticle,how_toorfaq. Questions appear on the Questions tab of help home.audience(optional) is a list of names, such as["nz"], for an article only some readers should see. Your host says which audiences each reader is in by implementingContracts\HasAudiences::audiences(); an article with no audience is for everyone. An app that installed Knowble before 1.3 adds the column first (see the changelog).- An image is named by
data-id, a file in the images folder. Knowble builds its address from the disk, so the same file works in every environment. - An image's width is a share of the article column, worked out from the file's own width (960px wide at a pixel ratio of 2 by default). Screenshots taken the same way all appear at one scale.
Prefer to write in the staff panel? Switch on features.editor, or call
KnowbleAdminPlugin::make()->editor(). Then php artisan knowble:export writes the panel's edits,
and any images uploaded there, back to the file for you to commit. Otherwise the next import
overwrites them.
Linking a screen to its help
Add a Help button to any page in the customer panel:
use Polycode\Knowble\Actions\HelpAction;
protected function getHeaderActions(): array
{
return [HelpAction::for('how-do-i-send-an-invoice')];
}
It stays hidden until that article is published, so a screen can name its article before it's written.
Anywhere else, these build links:
Knowble::articleUrl('slug');Knowble::categoryUrl('section', 'screen'), which links to a section, scrolled to one screen;Knowble::helpUrl('a search').
Configuration
Everything is in config/knowble.php.
Features
| Key | Default | What it switches |
|---|---|---|
features.support |
true |
Contact support, Your requests, the Requests queue and the Support report |
features.suggestions |
true |
Both kinds of suggestion together, and the suggestions queue |
features.suggest_improvement |
true |
Suggest an improvement, on an article |
features.request_article |
true |
Request an article, from search and the foot of help pages |
features.engagement |
true |
Favourites, feedback and Popular together, and the engagement page |
features.favourites |
true |
The favourite star, and the Favourites tab |
features.feedback |
true |
"Did this help?" on each article |
features.popular |
true |
Counting reads, and the Popular list |
features.pdf |
true |
Print to PDF (needs barryvdh/laravel-dompdf) |
features.editor |
false |
Writing articles and categories in the staff panel |
features.tiers |
false |
Support tiers, which the host assigns to each customer |
features.in_app |
false |
Notifications in the panels' bells as well as by email |
engagement and suggestions are master switches: off, everything under them is off. On, each
finer switch still turns its own part off.
With features.support off, paths.contact can name your own Contact support page instead, and
help pages link to it. Set it to null and they have no contact link.
Navigation
Each page can be given its own label, icon, group, sort order and placement (sidebar,
user-menu or hidden). A hidden page still opens from its link. Set them in config:
'navigation' => [
'group' => 'Help & support',
'help' => ['icon' => 'heroicon-o-question-mark-circle', 'sort' => 1],
'requests' => ['placement' => 'user-menu'],
],
Or in code, which takes precedence over config:
use Polycode\Knowble\Navigation\Placement;
KnowblePlugin::make()
->navigationGroup('Account')
->navigation('help', label: 'Guides', icon: Heroicon::OutlinedLifebuoy, sort: 5)
->navigation('requests', placement: Placement::UserMenu);
A placement of sidebar-and-user-menu (Placement::SidebarAndUserMenu) offers a page in both.
To put a page among a cluster's tabs instead of the sidebar, name the cluster. Its address then
starts with the cluster's, so paths.requests is the part after it, and it has no group heading
unless you give it one:
KnowblePlugin::make()
->navigation('requests', cluster: ProfileCluster::class, label: 'Support', sort: 4);
Help's icon is Knowble's fingerpost, knowble-fingerpost. Any page can use it by that name, in
config or in code.
The navigation items:
- Customer panel:
help,contactandrequests. - Staff panel:
support-requests,support-report,support-tiers,articles,categories,suggestionsandengagement.
Paths
Where the customer pages sit: help, support and support/requests by default, and section
for a section's page under help (help/section/invoices). Links in emails already sent depend on
them, so redirect from the old ones if you change them.
Content, attachments and email
content: the content file, its images folder, the disk images are served from, and the column width and pixel ratio images are scaled against.support: the attachments disk (SUPPORT_ATTACHMENTS_DISK), how many files a message can have and how big, and how many minutes a reply can still be changed.staff_emails: who's emailed about new requests, customer replies and suggestions (KNOWBLE_STAFF_EMAILS, comma separated). A multi-tenant host can supply its own.mail:reply_to_customersets Reply-To on staff's emails to the customer, so staff can answer from their inbox.reply_to_staff_member(off by default, which keeps staff addresses private) sets Reply-To on a reply to a customer to the staff member who wrote it.quote_to_customersquotes a staff reply in the email to the customer; off, the email only links to the conversation.dates: how dates and times are written, as PHP date formats (j M Yby default;M j, Yfor American readers). Month names follow the app's language, and times show in Filament's time zone, soFilamentTimezone::set($user->timezone)in a middleware shows each person their own. Reports count whole days where the reader is.storage_timezone: the zone Knowble's tables keep times in. By default it's the app's zone as the app starts, so an app that switchesapp.timezonefor each business (in a multi-tenant app, when tenancy starts) still stores every business's times in one zone, and each reader sees them in their own.connection: the database connection for Knowble's tables. In a multi-tenant app, use the central one.
Multi-tenant apps
Knowble doesn't depend on any tenancy package. Out of the box it assumes one database and no
tenants. For a multi-tenant app, extend the host and set knowble.host and knowble.connection.
This example uses stancl/tenancy:
use Polycode\Knowble\Hosts\SingleApp;
class AppKnowbleHost extends SingleApp
{
public function tenantId(): ?string
{
return tenancy()->initialized ? (string) tenant('id') : null;
}
public function tenantName(string|int $tenantId): string
{
return Tenant::find($tenantId)?->name ?? (string) $tenantId;
}
/** For the staff panel's filters and report. */
public function tenants(): array
{
return Tenant::query()->orderBy('name')->pluck('name', 'id')->all();
}
/** Links in emails, which are sent from the central domain. */
public function customerUrl(string|int|null $tenantId, string $path): string
{
return 'https://'.Tenant::find($tenantId)->domain.'/'.ltrim($path, '/');
}
/** Who may use help and contact support. */
public function mayUseHelp(Authenticatable $user): bool
{
return ! $user->isGuest();
}
public function staffEmails(): array
{
return Admin::query()->pluck('email')->all();
}
}
Everything a customer writes is stored against their tenant and their user. A request is only ever shown to the person who raised it, at the tenant that raised it.
When you delete a tenant, call Knowble::forgetTenant($tenantId). It deletes:
- the tenant's requests, their conversations and their files;
- its suggestions;
- its readers' reads, ratings and favourites.
It returns the attachment paths it deleted, in case you also keep backups to clear.
Notifications
Knowble tells staff about a new request, a customer's reply, a request the customer resolved and a suggestion, and tells a customer when the team replies. Each goes by email.
In the bell
Switch on features.in_app and each notification also goes in the panel's bell, through Filament's
database notifications. A panel without a bell gets one. Choose the channels for each notification
in config, so one can go to the bell instead of email:
'notifications' => [
'request_received' => ['mail', 'database'],
'suggestion_received' => ['database'],
],
The users need Laravel's notifications table and the Notifiable trait. php artisan knowble:install --in-app publishes the table's migration if the app has none, and warns about a
user model that isn't Notifiable.
Staff's notifications go to the staff panel's users whose addresses are in staff_emails. To
choose them another way, or to let people turn notifications off, override these on your host:
use Polycode\Knowble\Enums\NotificationType;
use Polycode\Knowble\Enums\Side;
public function staffUsers(): iterable
{
return Admin::query()->where('gets_support_notifications', true)->get();
}
public function wantsInApp(Side $side, string|int|null $tenantId, string|int $userId, NotificationType $type): bool
{
return ! Preference::off($userId, $type->value);
}
NotificationType::cases() lists every notification, with label() for a preferences screen.
In a multi-tenant app, a customer's user lives in the tenant's database. Override notifyInApp()
to write their notification there; staff's go to the central database as they are:
use Filament\Notifications\Notification;
public function notifyInApp(Side $side, string|int|null $tenantId, string|int $userId, NotificationType $type, Notification $notification): void
{
if ($side === Side::Staff) {
parent::notifyInApp($side, $tenantId, $userId, $type, $notification);
return;
}
Tenant::find($tenantId)->run(fn () => $notification->sendToDatabase(User::find($userId)));
}
Knowble asks wantsInApp() before every call to notifyInApp(), so a replaced delivery still
keeps each person's choice. The tenant database needs the notifications table too.
Each notification also carries its link in viewData['url'], for an app with its own bell view.
Turning email off
A customer who turned email off in your app shouldn't get Knowble's reply email. Override
wantsMail() on your host and Knowble asks it before every email to a customer. Contact support's
"Sent" message then says where the answer will be instead of promising an email:
public function wantsMail(Side $side, string|int|null $tenantId, string|int $userId, NotificationType $type): bool
{
return ! Preference::off($userId, 'email');
}
Events
For anything else, listen for these:
Polycode\Knowble\Events\RequestSubmitted, after a customer sends a request;Polycode\Knowble\Events\ReplySent, after either side replies.
Extending the staff screens
-
Your own actions on a request, before Delete and Restore, such as raising it in your bug tracker. Each action gets the request as
$record:KnowbleAdminPlugin::make()->requestActions(fn (): array => [ Action::make('raiseDefect')->action(fn (SupportRequest $record) => Defect::raiseFrom($record)), ]); -
Your own fields on a support tier, saved onto columns you add to
knowble_support_tiers:->tierFields([TextInput::make('uplift_percent')->numeric()]), and your own columns on the tier list:->tierColumns([TextColumn::make('uplift_percent')]). -
A line about each customer: a host that implements
Contracts\DescribesCustomersreturns a short line per tenant fromcustomerNote(), such as what their plan promises. Staff see it under the customer on the queue and on each request, whether or not tiers are on. -
Customers per tier: a host that implements
Contracts\CountsTierTenantsgives the tier screen a count, and a tier with customers on it can't be deleted. There's always one default tier, and it's always offered.
Using Knowble's data in your app
- Help usage:
HelpUsage::forTenant($tenantId, $from, $to)gives each article's reads and ratings over those days, with totals;HelpUsage::all()covers every tenant, andHelpUsage::monthlyViews()the last twelve months. Use it for a report of your own rather than querying Knowble's tables. - Raising a request for someone:
Requests::raise(tenantId: ..., name: ..., email: ..., userId: ..., subject: ..., body: ..., notify: false), from anywhere in your app. Withnotifyon, staff are told as if the customer had sent it. - Seed data: every model has a factory with no Faker, so demo data seeds in production:
SupportRequest::factory()->forTenant($id)->raisedBy($userId, 'Casey')->answered()->create(). - Backups:
SupportAttachment::disk()names the disk support files are on.
Customising
- Wording: every string goes through
__(), solang/en.jsonoverrides any of it. - Views: publish them with
php artisan vendor:publish --tag=knowble-views. Keep theknowbleclass on each view's root: Knowble's stylesheet only styles what's inside it, so it never touches your own screens and your theme's utilities never override Knowble's. - Emails: they all render through
knowble::mail.notice; publish and edit that view. - Article blocks: custom Filament rich editor blocks go in
content.blocks. The editor offers them and articles render them.
Security
- Requests: customer-side reads are scoped to this tenant and this person, including a request id typed into the address bar.
- Conversations: a conversation works out on the server which side it's on, from the panel it's in, and never takes that from the page. It checks access again on every action.
- Attachments: they sit on a private disk with no route of their own. A file streams only from
its own conversation, with
nosniff. A picture shows as a thumbnail through the same check: PNG, JPEG, GIF or WebP only, checked by its contents, up to 3MB, and limited per customer. It's sent as adata:image, so a Content Security Policy needsimg-src data:. - Search: wildcards in a search are matched literally.
- Rate limits: each customer can send five requests, and five suggestions, every ten minutes.
To report a security problem, email hello@polycode.co.nz. Please don't open a public issue for it.
Testing
composer install
vendor/bin/phpunit
The tests cover:
- a single-database app;
- a two-tenant host;
- an app with no staff panel;
- the article editor switched off;
- every navigation setting.
Every push runs them on PHP 8.3, 8.4 and 8.5, each with Laravel 12 and 13.
Changelog
Every release and what it needs from you is in CHANGELOG.md.
Licence
Knowble is commercial software: each project needs a licence. The terms are in LICENSE.md. © Polycode Limited.