Storage / CDN (S3-compatible)

Route all file uploads — avatars, logos, email attachments, generated PDFs, knowledge-base documents — to an external S3-compatible bucket. Configure once in admin, no controller changes needed, no .env editing required.

Where to find it

Admin → Settings → left rail → click Storage / CDN (under the Infrastructure group).

Why move uploads off-server

  • Shared hosting disk quota. Email attachments and KB documents fill up your storage quota fast.
  • Faster page loads. Pair with a CDN to serve avatars/logos from edge servers worldwide.
  • Survives migrations. Switch hosts without losing user uploads.
  • Cheaper at scale. S3 storage runs <$0.005/GB-month vs. typical hosting at ~$0.10/GB-month.

Supported providers

Any S3-compatible API. Picking a provider just sets a label — all credentials are entered manually:

  • AWS S3 — the original. Best deliverability, most expensive.
  • Cloudflare R2 — zero egress fees. Pair with Cloudflare CDN.
  • Wasabi — cheapest at scale, no egress fees.
  • Backblaze B2 — very cheap, S3-compatible API.
  • DigitalOcean Spaces — bundled with most DO setups.
  • Linode Object Storage — same as DO.
  • MinIO — self-hosted S3 on your own server.
  • Custom S3-compatible — anything else that speaks S3 protocol.

Configuration fields

Enable External Storage

Master toggle. When off, MailTrixy uses the local storage/app/public/ folder (default Laravel behaviour). When on, all new uploads go to your configured bucket.

Existing files don't auto-migrate. When you flip this on for the first time, files already on local disk stay there. New uploads go to S3. To migrate old files, use a one-off script or accept the split — old avatars will 404 once the local files are deleted unless you copy them up.

Provider preset / Region

  • Provider Preset — just a label for organisation. All providers use the same field set below.
  • Region — the bucket's region. AWS uses us-east-1, eu-west-1, etc. Cloudflare R2 uses auto. Wasabi uses us-east-1 by default.

Access Key ID + Secret Access Key

  • The credentials generated in your provider's dashboard. Encrypted at rest in system_settings.
  • Permissions needed: PutObject, GetObject, DeleteObject, ListBucket on the target bucket only.
  • For AWS, create a dedicated IAM user with an inline policy scoped to your bucket — never use your root account keys.
  • Leave the Secret field blank when editing later to keep the existing value.

Bucket + Endpoint URL

  • Bucket — bucket name. Just the name, e.g. mailtrixy-prod.
  • Custom Endpoint URL — required for non-AWS providers. Leave blank for AWS S3. Examples:
    • Cloudflare R2: https://<account-id>.r2.cloudflarestorage.com
    • Wasabi: https://s3.us-east-1.wasabisys.com
    • Backblaze B2: https://s3.us-west-002.backblazeb2.com
    • DigitalOcean Spaces: https://nyc3.digitaloceanspaces.com
    • MinIO self-hosted: https://minio.your-server.com

Public URL prefix (CDN) + Path-style URLs

  • Public URL Prefix — optional. If you've put a CDN in front of your bucket (CloudFront, Cloudflare, BunnyCDN), enter the CDN URL here, e.g. https://cdn.yourdomain.com. Asset URLs will then point to the CDN instead of directly to the bucket.
  • Use path-style URLs — tick for MinIO and some R2 setups that don't support virtual-hosted-style URLs. Leave unticked for AWS S3, Cloudflare R2 default, Wasabi, B2, Spaces.

Test Connection button

Before you click Save, click Test Connection. MailTrixy:

  1. Connects to your provider with the credentials you just typed (not the saved ones — tests what's in the form).
  2. Uploads a tiny probe file (.mailtrixy-probe-{random}.txt) to verify write permission.
  3. Deletes the probe file to verify delete permission.
  4. Shows a green “Connection OK” banner with the bucket name and region OR a red error explaining what went wrong (wrong key, wrong region, bucket doesn't exist, etc.).

This means you find out about typos immediately, before saving breaks all future uploads.

How it works under the hood

On every boot, StorageHelper::applyRuntimeConfig() (in AppServiceProvider::boot()) reads the saved settings and overrides Laravel's filesystems.disks.s3 configuration at runtime. Then config('filesystems.cloud') is set to s3.

From that point, every call to Storage::disk('s3')->put(...) in the codebase routes to your bucket. No controller or service knows about the provider details — the abstraction is at the filesystem layer.

The public and local disks continue to use local storage for files that should stay on the server (private temp files, queue payloads, etc.).

CORS & bucket policy

For the bucket to serve files publicly (avatars, logos shown on the marketing page), set the bucket's CORS policy to allow GET from your domain. Example CORS rule for an AWS S3 bucket:

[
    {
        "AllowedHeaders": ["*"],
        "AllowedMethods": ["GET", "HEAD"],
        "AllowedOrigins": ["https://yourdomain.com"],
        "ExposeHeaders": [],
        "MaxAgeSeconds": 3000
    }
]

And make sure the bucket policy or ACL allows public read on the relevant prefix (e.g. settings/branding/*, avatars/*).

Last updated 28/05/2026