arseno25 / filament-privacy-blur
Visual privacy layer for Filament — blur and mask sensitive data in tables, forms, and infolists
Fund package maintenance!
Requires
- php: ^8.2
- filament/filament: ^4.0|^5.0
- spatie/laravel-package-tools: ^1.15.0
Requires (Dev)
- larastan/larastan: ^3.0
- laravel/pint: ^1.0
- nunomaduro/collision: ^8.0
- orchestra/testbench: ^9.0|^10.0
- pestphp/pest: ^3.7|^4.0
- pestphp/pest-plugin-arch: ^3.0|^4.0
- pestphp/pest-plugin-laravel: ^3.0|^4.0
- rector/rector: ^2.0
- spatie/laravel-ray: ^1.26
This package is auto-updated.
Last update: 2026-04-01 20:55:38 UTC
README
About
Filament Privacy Blur provides visual privacy protection for sensitive data in Filament admin panels. It helps prevent accidental exposure during screen sharing, shoulder surfing, or working in public spaces.
What It Does
- Visual Blur — CSS-based blur that requires user interaction to reveal
- Data Masking — Server-side redaction using configurable strategies (email, phone, NIK, etc.)
- Interactive Reveal — Click-to-reveal or hover-to-reveal with automatic re-blur
- Global Reveal Toggle — Eye icon in topbar to reveal all authorized fields instantly
- Authorization-First — Full integration with Laravel Gates, Policies, and permissions
- Audit Logging — Optional tracking of reveal actions with user, IP, and context
- Export Safety — Automatic masking during Filament exports
What It Does NOT Do
This is a visual privacy layer only. It does NOT provide:
- Data encryption at rest or in transit
- Backend access control or API-level data redaction
- Database-level security
- Protection against determined attackers with developer tools
Blur modes keep original data in the DOM. For highly sensitive fields, use mask mode (server-side redaction) or implement data masking at your model/API layer.
Why This Package Exists
When building Filament admin panels, you often need to display sensitive data (emails, salaries, phone numbers) while reducing the risk of accidental exposure during:
- Screen sharing in meetings or presentations
- Working in public spaces (cafes, coworking spaces)
- Pair programming sessions
- Client demonstrations
This package provides a convenient, authorization-aware way to add visual privacy without manually implementing blur logic for every field.
Features
- 🔒 7 Privacy Modes —
blur,blur_click,blur_hover,blur_auth,mask,hybrid,disabled - 👆 Interactive Reveal — Click-to-reveal (auto re-blurs after 5s) or hover-to-reveal
- 👁️ Global Reveal Toggle — Topbar button to reveal all authorized fields instantly
- 🎭 8 Mask Strategies — email, phone, NIK, full name, API key, address, currency, generic
- 🛡️ Authorization-First — Secure-by-default using Laravel Gates, Policies, and abilities
- 📊 Audit Logging — Track reveal actions with user, IP address, user agent, and resource context
- 📤 Export Safety — Automatic masking during Filament exports
- 🎛️ Per-Panel Config — Exclude specific panels or customize settings per panel
Requirements
- PHP: 8.2 or higher
- Laravel: 11 or higher
- Filament: v4.x or v5.x
Installation
composer require arseno25/filament-privacy-blur
Publish the configuration file:
php artisan vendor:publish --tag="filament-privacy-blur-config"
Publish and run the migration (for audit logging):
php artisan vendor:publish --tag="filament-privacy-blur-migrations"
php artisan migrate
Setup
Register the plugin in your Filament panel provider:
use Arseno25\FilamentPrivacyBlur\FilamentPrivacyBlurPlugin; public function panel(Panel $panel): Panel { return $panel ->plugin( FilamentPrivacyBlurPlugin::make() ->defaultMode('blur_click') ->blurAmount(4) ->exceptColumns(['id', 'created_at', 'updated_at']) ->exceptPanels(['public']) ->enableAudit() ); }
Plugin Configuration Options
| Method | Description | Default |
|---|---|---|
defaultMode(string $mode) |
Default privacy mode | blur_click |
blurAmount(int $amount) |
CSS blur intensity (1-10) | 4 |
exceptColumns(array $columns) |
Columns to exclude from privacy | [] |
exceptResources(array $resources) |
Resource classes to exclude | [] |
exceptPanels(array $panels) |
Panel IDs to exclude | [] |
enableAudit() |
Enable audit logging | disabled |
showGlobalRevealToggle() |
Show global reveal toggle | enabled |
hideGlobalRevealToggle() |
Hide global reveal toggle | - |
Quick Start
Blur a Table Column
use Filament\Tables\Columns\TextColumn; TextColumn::make('email')->private(),
Mask a Table Column
TextColumn::make('email') ->private() ->privacyMode('mask') ->maskUsing('email'),
Reveal with Authorization
TextColumn::make('salary') ->private() ->revealIfCan('view_sensitive_data'),
Protect Form Inputs
use Filament\Forms\Components\TextInput; TextInput::make('email')->private(),
Protect Infolist Entries
use Filament\Infolists\Components\TextEntry; TextEntry::make('phone') ->private() ->maskUsing('phone'),
Never Reveal (Maximum Security)
TextEntry::make('api_key') ->private() ->revealNever() ->privacyMode('mask') ->maskUsing('api_key'),
How It Works
The package makes privacy decisions server-side and renders HTML data attributes that guide frontend behavior:
- Server-side decisions — Authorization, mode, and reveal capability are determined server-side
- Client-side execution — Alpine.js respects server-rendered attributes and cannot override them
- Secure-by-default — Fields with
->private()but no explicit authorization will blur for everyone and allow NO reveal - Blur vs Mask — Blur modes keep original data in DOM (hidden by CSS), mask modes replace data server-side
Authorization
This package is ability-first — it prioritizes Laravel's built-in authorization system.
Recommended: Ability-Based Authorization
Use revealIfCan() with Laravel Gates or Policies:
// Using a Gate ability Gate::define('view_ssn', function ($user, $record) { return $user->isAdmin() || $user->id === $record->user_id; }); TextColumn::make('ssn') ->private() ->revealIfCan('view_ssn', $record),
Custom Closure Authorization
TextColumn::make('salary') ->private() ->authorizeRevealUsing(function ($user, $record) { return $user?->is_admin || $user?->department_id === $record?->department_id; }),
Permission-Based Authorization
// Single permission TextColumn::make('data')->private()->permission('view_data'), // Multiple permissions (any match) TextColumn::make('admin_field') ->private() ->visibleToPermissions(['view_admin', 'edit_admin']),
Role-Based Authorization (Optional)
// Single role TextColumn::make('secret')->private()->visibleToRoles(['admin']), // Multiple roles TextColumn::make('internal') ->private() ->visibleToRoles(['admin', 'manager']),
Force Blur for Specific Roles
TextColumn::make('internal_notes') ->private() ->revealIfCan('view_internal') ->hiddenFromRoles(['guest', 'contractor']),
Users in these roles will always see blur, even if authorized via other means.
Secure-by-Default in Practice
// ❌ This will blur for EVERYONE, NO reveal allowed TextColumn::make('email')->private() // ✅ Only admins can reveal TextColumn::make('email') ->private() ->visibleToRoles(['admin']) // ✅ Users with 'view_email' permission can reveal TextColumn::make('email') ->private() ->permission('view_email')
Privacy Modes
blur — Always Blurred
- Authorized: Plain text
- Unauthorized: 🔒 Blurred, no reveal
- Raw data in DOM: Yes
TextColumn::make('notes')->private()->privacyMode('blur'),
blur_click — Click to Reveal (Default)
- Authorized: 🔒 Blurred, click to reveal (auto re-blurs after 5s)
- Unauthorized: 🔒 Blurred, no click
- Raw data in DOM: Yes
TextColumn::make('email')->private()->privacyMode('blur_click'),
blur_hover — Hover to Reveal
- Authorized: 🔒 Blurred, hover to reveal
- Unauthorized: 🔒 Blurred, no hover
- Raw data in DOM: Yes
TextColumn::make('address')->private()->revealOnHover(),
blur_auth — Blur for Unauthorized Only
- Authorized: Plain text
- Unauthorized: 🔒 Blurred, no reveal
- Raw data in DOM: Yes
TextColumn::make('internal_notes') ->private() ->privacyMode('blur_auth') ->revealIfCan('view_internal'),
mask — Server-Side Masking
- Authorized: Plain text
- Unauthorized: 🎭 Masked text (e.g.,
j***e@example.com) - Raw data in DOM: No
TextColumn::make('email') ->private() ->privacyMode('mask') ->maskUsing('email'),
hybrid — Maximum Protection
- Authorized: 🎭 Masked text
- Unauthorized: 🎭 Masked + 🔒 blurred
- Raw data in DOM: Masked only
TextColumn::make('ssn') ->private() ->privacyMode('hybrid') ->maskUsing('nik'),
disabled — No Privacy
- All users: Plain text
- Privacy effect: None
TextColumn::make('public_field')->private()->privacyMode('disabled'),
Mode Behavior Table
| Mode | Authorized | Unauthorized | Interactive Reveal | Global Reveal | Raw Data in DOM |
|---|---|---|---|---|---|
disabled |
Plain | Plain | N/A | N/A | Yes |
blur |
Plain | 🔒 Blur | No | No | Yes |
blur_click |
🔒 Blur → Click | 🔒 Blur | Yes (auth only) | Yes (auth only) | Yes |
blur_hover |
🔒 Blur → Hover | 🔒 Blur | Yes (auth only) | Yes (auth only) | Yes |
blur_auth |
Plain | 🔒 Blur | No | No | Yes |
mask |
Plain | 🎭 Masked | No | No | No |
hybrid |
🎭 Masked | 🎭 Masked + 🔒 Blur | No | No | Masked only |
Global Reveal Toggle
The global reveal toggle appears as an eye icon button in the Filament topbar.
When Does the Toggle Appear?
- Shows: When at least one field on the page can be globally revealed
- Hides: When no globally revealable fields exist on the current page
This happens automatically via Alpine.js — no configuration needed.
What Can Be Globally Revealed?
The toggle can only reveal fields that:
- The current user is authorized to view (via
revealIfCan(),permission(), etc.) - The field is NOT marked as
revealNever() - The field is NOT in
hiddenFromRoles()for the current user - The privacy mode supports global reveal (
blur_click,blur_hover)
What Cannot Be Globally Revealed?
The toggle will NEVER reveal:
- ❌ Unauthorized fields (user lacks ability/permission)
- ❌
revealNever()fields - ❌
maskmode fields (masked server-side, no blur to remove) - ❌
hybridmode fields - ❌ Fields where user is in
hiddenFromRoles()
Security Guarantee
The global reveal toggle cannot bypass authorization. Only fields where the server explicitly sets data-privacy-can-globally-reveal="true" will be revealed. The frontend Alpine.js code only respects server decisions.
Mask Strategies
| Strategy | Example Output | Description |
|---|---|---|
email |
j***e@example.com |
Shows first char, masks middle, shows domain |
phone |
0812****7890 |
Shows prefix, masks middle digits, shows suffix |
nik |
3173********9012 |
Shows first 4 and last 4 digits |
full_name |
Jo** Do* |
Shows first 2 and last 2 chars of each word |
api_key |
sk_***_key |
Shows first 3 and last 3 chars |
address |
Jl. Sudirma*** |
Shows first 12 characters |
currency |
*** |
Masks currency values entirely |
generic |
J***h |
Shows first and last character |
Using Mask Strategies
// Built-in strategy TextColumn::make('email')->private()->maskUsing('email'), // Custom closure TextColumn::make('account_number') ->private() ->maskUsing(fn ($state) => substr($state, 0, 4) . ' ****'),
Audit Logging
When enabled, the package logs reveal actions to the privacy_reveal_logs table.
When Does Audit Logging Happen?
Audit logging occurs when:
- Audit is enabled at plugin level (
->enableAudit()) - Audit is not disabled for the specific field (
->withoutAuditReveal()) - A reveal action is performed (click, hover, or global toggle)
- The user is authorized to perform the reveal
Stored Audit Fields
| Field | Description |
|---|---|
user_id |
ID of the user who revealed the data |
tenant_id |
Tenant ID for multi-tenant apps |
panel_id |
Filament panel ID |
resource |
Resource identifier (e.g., App\Filament\Resources\UserResource) |
page |
Full URL of the page where reveal occurred |
column_name |
Name of the column that was revealed |
record_key |
Primary key of the record |
reveal_mode |
Privacy mode used (e.g., blur_click) |
ip_address |
IP address of the user |
user_agent |
Browser/user agent string |
created_at |
Timestamp of the reveal action |
Enabling Audit Logging
FilamentPrivacyBlurPlugin::make()->enableAudit(),
Per-Field Audit Control
// Disable audit for specific field TextColumn::make('public_field') ->private() ->withoutAuditReveal(),
Export Safety
Why Visual Blur is Not Safe for Exports
CSS blur only affects visual rendering in the browser. When exporting data to CSV/Excel, the original data is included unless explicitly masked.
How Export Context is Handled
The package automatically detects Filament export contexts and:
- Replaces blur with masking —
blur_clickfields are masked using the configured strategy - Preserves mask strategies — Custom masking strategies are applied during export
- Protects sensitive data — Original values are never included in exports
Export Behavior Examples
// In browser: Shows blurred with click-to-reveal // In export: Shows as "j***e@example.com" TextColumn::make('email')->private()->maskUsing('email'), // In browser: Shows blurred with click-to-reveal // In export: Shows as "0812****7890" TextColumn::make('phone')->private()->maskUsing('phone'),
Security Limitations
Important: What This Package Does NOT Provide
- Not Encryption — Blur modes keep original data in the DOM
- Not Backend Access Control — Does not replace API authentication
- Not API-Level Redaction — Does not protect JSON API endpoints
- Not Database Security — Does not encrypt data at rest
Recommended Usage by Data Sensitivity
| Data Type | Recommended Mode | Reason |
|---|---|---|
mask or blur_click |
Mask for high sensitivity, blur for medium | |
| Phone | mask or blur_click |
Mask for high sensitivity, blur for medium |
| Salary | blur_click with auth |
Blur with strict authorization |
| SSN/Tax ID | mask + revealNever() |
Always mask, never reveal |
| API Keys | mask + revealNever() |
Maximum security |
| Internal Notes | blur_auth with auth |
Blur for unauthorized, clear for authorized |
For Highly Sensitive Data
Consider implementing backend data redaction:
- Mask data in model accessors
- Use API resources with conditional field inclusion
- Implement field-level encryption in your database
- Use Laravel's
Hiddenattribute on Eloquent models
API Reference
Column/Entry/Field Macros
private()
Enable privacy with default settings (equivalent to privacyMode('blur_click')):
TextColumn::make('email')->private()
privacyMode(string $mode)
Set the privacy mode:
TextColumn::make('salary') ->private() ->privacyMode('blur_click'),
Available modes: 'blur', 'blur_click', 'blur_hover', 'blur_auth', 'mask', 'hybrid', 'disabled'
revealOnClick() / revealOnHover()
Convenience methods for common modes:
TextColumn::make('email')->revealOnClick(), TextColumn::make('address')->revealOnHover(),
revealNever()
Prevent all reveal methods:
TextColumn::make('api_key')->private()->revealNever(),
blurAmount(int $amount)
Set CSS blur intensity (1-10):
TextColumn::make('salary')->private()->blurAmount(8),
maskUsing(string|Closure $strategy)
Set masking strategy:
TextColumn::make('email')->private()->maskUsing('email'), TextColumn::make('custom')->private()->maskUsing(fn ($s) => $s[0] . '***'),
Authorization Methods
revealIfCan(string $ability, Model $record = null)
Authorize via Laravel Gate or Policy (recommended):
TextColumn::make('ssn')->private()->revealIfCan('view_ssn', $record),
authorizeRevealWith(string $ability, Model $record = null)
Alias for revealIfCan() with explicit semantics:
TextColumn::make('notes')->private()->authorizeRevealWith('view_notes', $record),
authorizeRevealUsing(Closure $callback)
Custom authorization closure:
TextColumn::make('salary') ->private() ->authorizeRevealUsing(fn ($user, $record) => $user?->id === $record->manager_id),
permission(string $permission)
Require specific permission:
TextColumn::make('data')->private()->permission('view_data'),
visibleToPermissions(array $permissions)
Require any of the specified permissions:
TextColumn::make('admin_field') ->private() ->visibleToPermissions(['view_admin', 'edit_admin']),
visibleToRoles(array $roles)
Require any of the specified roles:
TextColumn::make('secret')->private()->visibleToRoles(['admin']),
hiddenFromRoles(array $roles)
Force blur for specific roles:
TextColumn::make('internal')->private()->hiddenFromRoles(['guest']),
Audit Methods
auditReveal(bool $enabled = true)
Enable audit for this field:
TextColumn::make('salary')->private()->auditReveal(true),
withoutAuditReveal()
Disable audit for this field:
TextColumn::make('public_field')->private()->withoutAuditReveal(),
Plugin Configuration Methods
defaultMode(string $mode)
Set default privacy mode:
FilamentPrivacyBlurPlugin::make()->defaultMode('blur_click'),
blurAmount(int $amount)
Set default blur intensity:
FilamentPrivacyBlurPlugin::make()->blurAmount(6),
exceptColumns(array $columns)
Exclude specific columns:
FilamentPrivacyBlurPlugin::make()->exceptColumns(['id', 'created_at']),
exceptResources(array $resources)
Exclude specific resources:
FilamentPrivacyBlurPlugin::make()->exceptResources([App\Filament\Resources\PublicResource::class]),
exceptPanels(array $panels)
Exclude specific panels:
FilamentPrivacyBlurPlugin::make()->exceptPanels(['public', 'reports']),
enableAudit()
Enable audit logging globally:
FilamentPrivacyBlurPlugin::make()->enableAudit(),
showGlobalRevealToggle() / hideGlobalRevealToggle()
Control toggle visibility:
FilamentPrivacyBlurPlugin::make()->showGlobalRevealToggle(), FilamentPrivacyBlurPlugin::make()->hideGlobalRevealToggle(),
Configuration
After publishing, edit config/filament-privacy-blur.php:
return [ 'default_mode' => 'blur_click', 'default_blur_amount' => 4, 'default_mask_strategy' => 'generic', 'except_columns' => ['id', 'created_at', 'updated_at'], 'except_resources' => [], 'except_panels' => [], 'audit_enabled' => false, 'icon_trigger_enabled' => true, ];
Compatibility
| Component | Supported Versions |
|---|---|
| PHP | 8.2, 8.3, 8.4 |
| Laravel | 11, 12 |
| Filament | v4.x, v5.x |
| Alpine.js | Bundled with Filament |
Package Integration
| Package | Status | Notes |
|---|---|---|
| Filament Shield | ✅ Compatible | Works via Laravel Gates |
| Spatie Laravel Permission | ✅ Compatible | Works via can() method |
| Spatie Tenancy | ✅ Compatible | Tenant ID captured in audit logs |
Contributing
-
Install dependencies:
composer install
-
Run tests:
composer test -
Run static analysis:
composer analyse
-
Run code style checks:
composer test:lint
-
Fix code style:
composer lint
Development Standards
- PHPStan — Static analysis at Level 4
- Laravel Pint — Laravel code style fixer
- Pest — Testing framework
Changelog
Please see CHANGELOG for recent changes.
License
The MIT License (MIT). Please see License File for more information.
Made with ❤️ for the Filament community