tomatophp/filament-pos
A complete point-of-sale system for FilamentPHP: manage products, orders, payments, and receipts through a fast, intuitive interface. Supports barcode scanning, multiple payment methods, and printable invoices—ready to drop into any Laravel admin panel.
Confirm the real prerequisite before you type composer require. This is a Filament panel plugin, not a standalone app. It is designed to sit on top of TomatoPHP's Ecommerce Builder (tomatophp/filament-ecommerce), which owns products, prices, and orders. You need Laravel 10/11, PHP 8.2+, Filament v3+, and Ecommerce Builder installed and working first. If Ecommerce Builder is not present, "adding the POS" actually means adopting a second commerce schema — treat that as a deliberate architectural decision, not an install step.
Minimal path to a running register:
composer require tomatophp/filament-pos
php artisan vendor:publish --tag=filament-pos-config # verify the real tag in the repo
php artisan migrate
Register the plugin on a panel provider:
use TomatoPHP\FilamentPos\FilamentPosPlugin;
public function panel(Panel $panel): Panel
{
return $panel
->id('pos')
->path('pos')
->plugin(FilamentPosPlugin::make());
}
Confirm the namespace, plugin class, and publish tags against src/ and the README before pasting — TomatoPHP classes move between majors, and there is no long tail of blog posts or Stack Overflow answers to catch you.
Read these four things in this order, before writing any code:
src/FilamentPosPlugin.php — what it registers (resources, pages, widgets) and which panel hooks it assumes.database/migrations/ — the single highest-value read. Do POS sales write into ecommerce_orders, or into their own pos_* tables? That one fact decides whether you can ever cleanly exit the package.config/filament-pos.php — payment methods, receipt options, register settings, feature toggles.src/Resources/ and src/Pages/ — where the cashier screen lives and which Filament conventions it leans on.First use case: one register, one cashier, a small real catalog, one complete sale through tender and receipt. Pop-up retail or an internal staff store is the right scope to start with. The register is a Filament page, so every cart action is a Livewire round trip — you want to feel that latency at the actual counter before anyone commits to a retail floor.
Day-one checklist for a developer:
Give the register its own panel. The strongest day-to-day pattern is a separate panel (->id('pos')->path('pos')) with its own navigation, color, and permissions. Sharing a panel between back-office administration and cashier work is where you will constantly fight navigation clutter and accidentally expose admin resources to the counter.
Treat the plugin as a consumer of your catalog, not its owner. Product creation, pricing, tax classes, and stock adjustments belong in Ecommerce Builder (or your own resources). The POS reads them. Any write path from POS back into catalog tables should be explicit, tested, and documented — otherwise a cashier edit silently mutates your online storefront.
Wrap the payment and inventory edges in your own adapters. The package's payment story is almost certainly "record the tender type," not "process a card." Build so a real PSP can drop in later without rewriting the register:
// app/Pos/TenderRecorder.php
final class TenderRecorder
{
public function record(Order $order, string $method, int $amountMinor): void
{
DB::transaction(function () use ($order, $method, $amountMinor) {
// wrap whatever the package does so you can swap in a PSP later
});
}
}
Own the receipt Blade view from the start. Layout, tax lines, register/cashier identifiers, and legal footers are the most reliably customised part of any POS. Budget a day for it and keep it in your app's views, not in vendor files.
Export, don't query. Accounting, BI, and dashboards should read from an export surface or domain events you control — never directly from POS tables. This keeps the register layer swappable and is your only real insurance if you fork or abandon the package.
Seed roles and permissions explicitly. Read src/Policies/ and write a seeder that grants register access, view_any_order, refunds, and cash-management abilities. Cashier and manager should not be the same role in a real store.
Verify Filament tenancy support before inventing one. If the package does not wire into Filament's tenancy primitives, do not bolt on a parallel store_id scoping scheme — you will end up with two authorization models that disagree. Check first, then design.
Test the cart, not the pixels. A Livewire/Pest test that builds a cart, applies tender, and asserts the resulting order and stock movement is worth more than a screenshot test of the register page:
it('decrements stock when a sale is completed', function () {
$product = Product::factory()->create(['stock' => 5]);
// build cart via the page component the package registers
// assert order created, stock === 4
});
Iterate against the demo loop. Keep a local seeder with barcodes you can scan or type, and a second browser tab open as a "second terminal" — it is the cheapest way to surface concurrency issues before the pilot does.
Pin the trio as one upgrade unit. Constrain Filament, Ecommerce Builder, and this package to exact versions together in composer.json. Filament ships breaking majors roughly annually and plugins lag unpredictably; independent minor drift gives you a broken admin with a confusing stack trace. Upgrade all three in one PR, on a branch, with a real sale performed by hand afterwards.
Inventory concurrency is the bug that will bite you. A server-rendered Livewire register doing read-then-write stock decrements can oversell with two terminals — or two browser tabs. Read the checkout path and confirm there's a transaction with lockForUpdate or an atomic decrement. If not, add it at your adapter boundary before the pilot, not after.
Assume there is no offline mode until you prove otherwise. Every cart action is a synchronous HTTP request. On flaky store Wi-Fi staff will feel it immediately. Test on the real network, at the real counter, with the real tablet.
Hardware is the most likely pilot killer. Receipt printing, cash-drawer kicks, and barcode scanning almost certainly need a local bridge agent or a browser peripheral API, and nothing in the package metadata suggests that's solved. Prove printing and scanning on target hardware in week one. If you can't, scope the register as "order entry with emailed receipts" and price it accordingly.
Watch for panel and navigation collisions. The plugin registers resources and pages against a panel; if it assumes a specific panel ID or navigation group, it can collide with your structure. Symptoms: duplicate nav entries, missing resources, or RouteNotFoundException on panel boot.
Debugging tip: when a cashier action fails silently or the cart won't advance, check storage/logs/laravel.log first. Filament frequently swallows the real error behind a toast; the stack trace will tell you whether the failure is in the package, Ecommerce Builder, or your own policy layer.
Verify the release metadata yourself. The reported last release date is in the future, and 71 stars / 0 dependents mean no downstream project is surfacing bugs before you do. Check commit cadence, CI matrix, and open-issue responsiveness before treating any of this as production-ready. The absence of dependents is a real validation gap, not a rounding error.
Keep your fork escape hatch cheap. MIT licensing makes "fork and own" a credible third option. Price it in from day one: keep your integration thin, document the schema assumptions (especially whether sales land in ecommerce_orders or pos_*), and avoid deep edits to vendor files so a fork stays a day of work rather than a quarter.
How can I help you explore Laravel packages today?