Certainly! Here’s a comprehensive, step-by-step guide to building a modular, scalable messaging/chat system in a Laravel + Filament v3 application, covering all your requirements.
1. Database Schema
Migration Overview
You’ll need the following tables:
threads: Conversation containers, linked to records (tenancy, property, etc.)messages: Individual messages in a thread.message_recipients: Pivot for message delivery/read status per user.message_templates: Saved reusable templates.attachments: Files linked to messages.sms_credits: Tracks SMS usage/purchase (if needed).
Example Migrations
threads Table
php artisan make:migration create_threads_table
Schema::create('threads', function (Blueprint $table) {
$table->id();
$table->string('subject');
$table->foreignId('created_by')->constrained('users');
$table->nullableMorphs('linked_record'); // e.g., tenancy, property, etc.
$table->timestamps();
$table->softDeletes();
});
messages Table
Schema::create('messages', function (Blueprint $table) {
$table->id();
$table->foreignId('thread_id')->constrained();
$table->foreignId('sender_id')->constrained('users');
$table->enum('channel', ['in_app', 'email', 'sms']);
$table->text('body');
$table->timestamps();
});
message_recipients Table
Schema::create('message_recipients', function (Blueprint $table) {
$table->id();
$table->foreignId('message_id')->constrained();
$table->foreignId('recipient_id')->constrained('users');
$table->enum('status', ['sent', 'delivered', 'failed', 'read'])->default('sent');
$table->timestamp('read_at')->nullable();
$table->timestamps();
});
message_templates Table
Schema::create('message_templates', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('subject');
$table->text('body');
$table->foreignId('created_by')->constrained('users');
$table->timestamps();
});
attachments Table
Schema::create('attachments', function (Blueprint $table) {
$table->id();
$table->foreignId('message_id')->constrained();
$table->string('file_path');
$table->string('file_name');
$table->string('mime_type');
$table->timestamps();
});
sms_credits Table (Optional)
Schema::create('sms_credits', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained();
$table->integer('credits')->default(0);
$table->timestamps();
});
2. Models & Relationships
Thread
class Thread extends Model
{
use SoftDeletes;
protected $fillable = ['subject', 'created_by', 'linked_record_type', 'linked_record_id'];
public function messages() {
return $this->hasMany(Message::class);
}
public function linkedRecord() {
return $this->morphTo();
}
public function creator() {
return $this->belongsTo(User::class, 'created_by');
}
}
Message
class Message extends Model
{
protected $fillable = ['thread_id', 'sender_id', 'channel', 'body'];
public function thread() {
return $this->belongsTo(Thread::class);
}
public function sender() {
return $this->belongsTo(User::class, 'sender_id');
}
public function recipients() {
return $this->hasMany(MessageRecipient::class);
}
public function attachments() {
return $this->hasMany(Attachment::class);
}
}
MessageRecipient
class MessageRecipient extends Model
{
protected $fillable = ['message_id', 'recipient_id', 'status', 'read_at'];
public function message() {
return $this->belongsTo(Message::class);
}
public function recipient() {
return $this->belongsTo(User::class, 'recipient_id');
}
}
MessageTemplate
class MessageTemplate extends Model
{
protected $fillable = ['name', 'subject', 'body', 'created_by'];
public function creator() {
return $this->belongsTo(User::class, 'created_by');
}
}
Attachment
class Attachment extends Model
{
protected $fillable = ['message_id', 'file_path', 'file_name', 'mime_type'];
public function message() {
return $this->belongsTo(Message::class);
}
}
3. Filament Resources
ThreadResource
- List threads (inbox)
- Show thread details (messages, participants)
- Modal to compose new message (individual/group)
php artisan make:filament-resource Thread
ThreadResource Table
public static function table(Table $table): Table
{
return $table
->columns([
TextColumn::make('subject')->searchable(),
TextColumn::make('linked_record_type'),
TextColumn::make('created_by')->label('Started By'),
TextColumn::make('messages_count')->counts('messages')->label('Messages'),
TextColumn::make('created_at')->dateTime(),
])
->filters([
// Add filters for record type, participants, etc.
])
->actions([
Tables\Actions\ViewAction::make(),
Tables\Actions\Action::make('compose')
->label('Compose New Message')
->action(fn () => $this->openComposeModal()),
]);
}
ThreadResource Form (Compose Modal)
public static function form(Form $form): Form
{
return $form
->schema([
Select::make('recipients')
->label('To')
->multiple()
->options(User::all()->pluck('name', 'id'))
->required(),
Select::make('linked_record')
->label('Relation')
->options([
// Populate with tenancy, property, etc.
])
->required(),
TextInput::make('subject')->required(),
RichEditor::make('body')->required(),
FileUpload::make('attachments')
->multiple()
->directory('message-attachments'),
Toggle::make('send_email')->label('Send as Email'),
Toggle::make('send_sms')->label('Send as SMS'),
]);
}
MessageTemplateResource
- CRUD for templates (admin only)
php artisan make:filament-resource MessageTemplate
4. UI: Individual & Group Message Composition
- Use Filament modals/forms for both.
- For group: select group, auto-fill recipients, show SMS credit info.
Group Message Form Example
public static function form(Form $form): Form
{
return $form
->schema([
Select::make('group')
->label('Select Group')
->options([
'property_x' => 'All tenants in Property X',
// ...other groupings
])
->reactive()
->afterStateUpdated(fn ($state, callable $set) => {
// Auto-fill recipients based on group
$set('recipients', User::where('property_id', $state)->pluck('id'));
}),
Select::make('recipients')
->multiple()
->disabled()
->options(User::all()->pluck('name', 'id')),
TextInput::make('subject')->required(),
Textarea::make('body')->required(),
FileUpload::make('attachments')->multiple(),
Toggle::make('send_email'),
Toggle::make('send_sms')
->afterStateUpdated(fn ($state, callable $get, callable $set) => {
if ($state) {
// Calculate and display SMS credits
$body = $get('body');
$credits = ceil(strlen($body) / 140);
$set('sms_credits_needed', $credits);
}
}),
TextInput::make('sms_credits_needed')->disabled(),
]);
}
5. Email & SMS Integration
Email (Laravel Mailables)
Mail::to($recipient->email)->send(new MessageMailable($message));
- Create a
MessageMailableclass for formatting. - Log email delivery status in
message_recipients.
SMS (Twilio Example)
use Twilio\Rest\Client;
$twilio = new Client($sid, $token);
$message = $twilio->messages->create(
$recipient->phone,
[
'from' => $fromNumber,
'body' => $body,
]
);
// Log status
MessageRecipient::create([
'message_id' => $messageId,
'recipient_id' => $recipient->id,
'status' => $message->status,
]);
- Handle incoming SMS via webhook and route to the correct thread/message.
6. Key Logic: SMS Credit Calculation
function calculateSmsCredits($body)
{
$length = strlen($body);
return min(ceil($length / 140), 3);
}
- Deduct credits from
sms_creditstable before sending.
7. Notification Sync
- Use Laravel Notifications for in-app and push.
$user->notify(new NewMessageNotification($message));
- In
NewMessageNotification, implementtoDatabase,toMail, and (optionally)toBroadcastfor push.
8. Message Threads Tied to Records
- Use
morphToonThreadforlinked_record. - When composing from a tenancy/property/task, auto-populate
linked_record_typeandlinked_record_id.
9. File Attachments
- Use Filament’s
FileUploadfield. - Store files in a secure directory (
storage/app/message-attachments). - In
Attachmentmodel, generate secure download URLs.
10. Search Functionality
- In
ThreadResource, add a search bar:
TextInput::make('search')
->placeholder('Search messages...')
->reactive()
->afterStateUpdated(fn ($state, callable $set) => {
// Filter threads/messages by subject, body, recipient
});
- Or use Filament’s built-in searchable columns.
11. Read/Seen Status
- Update
message_recipients.statustoreadand setread_atwhen a user views a message. - Show unread counts/badges in the UI.
12. Mobile Responsiveness & Push Notifications
- Filament v3 is responsive by default.
- For push: use Laravel Echo + Pusher or Firebase Cloud Messaging.
- In
NewMessageNotification, implementtoBroadcastand listen on the client.
13. Best Practices & Scalability
- Use policies for authorization (who can view/send messages).
- Use queues for sending emails/SMS.
- Archive old threads via scheduled jobs.
- Use eager loading to reduce N+1 queries.
14. Integration with Existing Resources
- Add a “Messages” relation manager to
TenancyResource,PropertyResource, etc., showing related threads. - Allow composing messages from these pages with context auto-filled.
15. Summary
This modular approach ensures:
- Scalability: Separate tables for threads, messages, recipients, attachments.
- Flexibility: Supports multiple channels, group/individual messaging, templates.
- Integration: Works with Filament’s resource system and Laravel’s notification/mail/queue features.
- Security: Attachments stored securely, access controlled by policies.