NewWe open-sourced 50+ Laravel packages
Custom AI apps, agents and automation — Roundly ConsultingRoundly
All packages

The package works with zero host configuration. The published config/messages.php (the broadcasting block is covered in Broadcasting):

return [
    'models' => [
        'message' => RoundlyConsulting\Messages\Models\Message::class,
        'thread' => RoundlyConsulting\Messages\Models\Thread::class,
        'participant' => RoundlyConsulting\Messages\Models\Participant::class,
    ],

    'key_type' => env('MESSAGES_KEY_TYPE', 'bigint'),
    'primary_key_type' => env('MESSAGES_PRIMARY_KEY_TYPE', 'bigint'),

    'publicity' => [
        'public-by-default' => env('THREADS_PUBLIC', false),
        'everyone-can-join' => env('THREADS_EVERYONE_CAN_JOIN', false),
    ],

    'media' => [
        'attachments_bucket' => 'attachments',
        'disk' => env('MESSAGES_MEDIA_DISK', null),
        'private_disk' => env('MESSAGES_MEDIA_PRIVATE_DISK', 'local'),
        'visibility' => env('MESSAGES_MEDIA_VISIBILITY', 'private'),
        'accepted_mime_types' => [],
        'max_file_size' => null,
        'responsive_widths' => null,
        'warm_on_send' => true,
        'temporary_url_lifetime' => null,
        'cleanup_on_force_delete' => true,
    ],

    'system-messages' => [
        'enabled' => env('MESSAGES_SYSTEM_MESSAGES', false),
    ],

    'permissions' => [
        'enabled' => env('MESSAGES_PERMISSIONS', true),
    ],

    'notifications' => [
        'enabled' => env('MESSAGES_NOTIFICATIONS', false),
        'notification' => RoundlyConsulting\Messages\Notifications\NewMessageNotification::class,
        'channels' => ['database'],
    ],

    'preview' => [
        'length' => env('MESSAGES_PREVIEW_LENGTH', 120),
    ],

    'prune' => [
        'days' => env('MESSAGES_PRUNE_DAYS', 90),
    ],

    'broadcasting' => [
        'enabled' => env('REALTIME_MESSAGES', false),
        // channels + event names for threads, participants, typing and messages
    ],
];

Every key

KeyDefaultEnvPurpose
models.message / .thread / .participantpackage models—Swappable models — point at your own subclass. A class that isn’t the package model or a subclass of it throws InvalidConfigurationException.
primary_key_typebigintMESSAGES_PRIMARY_KEY_TYPEKey type of the package’s own tables and internal foreign keys (bigint, uuid, ulid — anything else throws). Fixed at first migrate.
key_typebigintMESSAGES_KEY_TYPEKey type of your sender and participant models — types the sender_id / participant_id morph columns and must match their primary key (bigint, uuid, ulid — anything else throws). Fixed at first migrate.
publicity.public-by-defaultfalseTHREADS_PUBLICNew group threads are public unless the caller says otherwise.
publicity.everyone-can-joinfalseTHREADS_EVERYONE_CAN_JOINWhether a new group thread lets anyone join it on their own.
media.attachments_bucketattachments—media-library bucket that attachments are stored in. A blank name is not set (the default applies); a non-string name throws.
media.disknullMESSAGES_MEDIA_DISKDisk for every attachment, whatever its visibility; null = chosen by visibility (private → media.private_disk, public → media-library’s default disk). A blank value (MESSAGES_MEDIA_DISK=) is not set, so the disk is chosen by visibility; a non-string name throws.
media.private_disklocalMESSAGES_MEDIA_PRIVATE_DISKNon-public disk for private attachments and their variants when media.disk is null — never the web-served public disk. A blank name is not set (local); a non-string name throws.
media.visibilityprivateMESSAGES_MEDIA_VISIBILITYprivate (signed URLs only) or public; anything else throws.
media.accepted_mime_types[]—Allowed mime types, a list of non-empty strings; [] accepts any file.
media.max_file_sizenull—Max attachment size in bytes, at least 1; null = media-library’s default.
media.responsive_widthsnull—Width ladder for image attachments, a list of positive integers; null = media-library’s default ladder.
media.warm_on_sendtrue—Queue variant generation for each image attachment on send.
media.temporary_url_lifetimenull—Lifetime of signed attachment URLs in minutes, at least 1; null = media-library’s default.
media.cleanup_on_force_deletetrue—Remove attachment files on hard delete and prune; unsent messages keep them.
system-messages.enabledfalseMESSAGES_SYSTEM_MESSAGESWrite translatable system messages on join, leave and rename.
permissions.enabledtrueMESSAGES_PERMISSIONSEnforce owner/admin/member roles on group threads — roles are kept either way.
notifications.enabledfalseMESSAGES_NOTIFICATIONSNotify the thread’s other participants on every send.
notifications.notificationNewMessageNotification—Notification class dispatched to recipients — a Notification subclass constructed with the message; anything else throws.
notifications.channels['database']—Channels the default notification uses (non-empty strings).
preview.length120MESSAGES_PREVIEW_LENGTHMax length of previews and quote excerpts, at least 1.
prune.days90MESSAGES_PRUNE_DAYSDefault retention window for messages:prune and Messages::prune(), at least 1.
broadcasting.enabledfalseREALTIME_MESSAGESBroadcast thread, participant, message and typing events.
broadcasting.*see Broadcasting—Channel names and event aliases — each a string; a blank value is not set (the shipped name applies), a non-string value throws.

Environment

The switches you are most likely to flip are env-backed, so you rarely publish the config at all:

MESSAGES_KEY_TYPE=bigint
MESSAGES_PRIMARY_KEY_TYPE=bigint
THREADS_PUBLIC=false
THREADS_EVERYONE_CAN_JOIN=false
MESSAGES_MEDIA_PRIVATE_DISK=local
MESSAGES_MEDIA_VISIBILITY=private
MESSAGES_SYSTEM_MESSAGES=true
MESSAGES_PERMISSIONS=true
MESSAGES_NOTIFICATIONS=true
MESSAGES_PREVIEW_LENGTH=120
MESSAGES_PRUNE_DAYS=90
REALTIME_MESSAGES=true

Set key_type and primary_key_type before you publish and run the migrations — both are read when the migrations run.

Boolean switches

Every bool switch is parsed as a boolean, so .env values mean what they say — true, 1, on and yes turn it on; false, 0, off and no turn it off:

MESSAGES_PERMISSIONS=1    # true / 1 / on / yes — enforce roles
THREADS_PUBLIC=off        # false / 0 / off / no — keep new threads private

A blank value (THREADS_PUBLIC=) is not set, so the default applies. Anything else throws RoundlyConsulting\PackageToolkit\Exceptions\InvalidConfigurationException instead of quietly reading as the default.

Strict values

Every other setting is just as strict. A setting that is not set — absent, null, or blank like a host’s MESSAGES_PRUNE_DAYS= — takes its default; an optional one (media.disk, media.max_file_size, media.temporary_url_lifetime) stays unset. Integers accept an int or a plain integer string (every env value is a string): MESSAGES_PRUNE_DAYS=ninety or 12.5 throws rather than becoming 0 — which for prune.days would have meant pruning every message. A broadcast channel or event name, a bucket or a disk name must be a string; media.visibility must be private or public; the notification class must be a Notification subclass; a model must be the package model or a subclass of it.

Inspecting the live configuration

php artisan about includes a Messages section that reports the configured models, key type, thread publicity, role enforcement, system messages, notifications, broadcasting, preview length, prune retention and the attachment settings. It reports only the shape of the configuration — never message content or participants. A broken preview, retention, attachment or notification setting shows as INVALID instead of failing.

Show your open-source love

This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.

More ways to support, including crypto

By donating, you agree to our donation terms.

Want this built into your product?

We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.