Elyra
Elyra The coding agent e The native code editor Elyra Grove Native local development environment Askr The real server for Laravel & PHP Elyra Framework Rust + Svelte 5 framework for desktop apps Elyra Conductor Local project conductor Elyra SQL Server MySQL-compatible SQL server in Rust Elyra Félagi Agents as teammates on one board Elyra SQL Client Native desktop SQL workbench Elyra SQL Anywhere Replication-ready SQL engine Elyra Sjá SEO & GEO workspace for macOS Elyra DataGrid Server-driven data grid for Laravel
Start here
Concepts
Elyra

Email

Outgoing mail goes through Resend. Replies can come back in and land in the thread they came from.

Every value here is configuration, because every installation runs on its own domain with its own Resend account.


Outgoing

MAIL_MAILER=resend
RESEND_KEY=re_...
MAIL_FROM_ADDRESS="felagi@yourdomain.com"
MAIL_FROM_NAME="${APP_NAME}"

The variable is RESEND_KEY, not RESEND_API_KEY — that is the name config/services.php reads. An .env carrying the other one looks configured and sends nothing.

Local development leaves MAIL_MAILER=log and reads mail out of storage/logs/laravel.log.

A missing key does not take the site down. If the mailer is resend and no key is set, Félagi falls back to the log driver and writes a critical line saying so. A deployment without its mail key should keep serving and keep the messages readable, rather than throw transport errors at people trying to register.

Auth mail and comment notifications are queued on the mail lane, so a form submission never waits on somebody else's API:

php artisan queue:work --queue=broadcasts,mail,default

Incoming

Off by default. Switch it on only once a domain's MX records actually point at Resend — an advertised reply address that bounces is worse than none.

FELAGI_INBOUND_ENABLED=true
FELAGI_INBOUND_DOMAIN=mail.yourdomain.com
FELAGI_INBOUND_LOCAL_PART=reply
FELAGI_INBOUND_EVENT=email.received
RESEND_WEBHOOK_SECRET=whsec_...

Point the Resend webhook at:

POST https://yourdomain.com/hooks/resend

A subdomain is the usual choice for FELAGI_INBOUND_DOMAIN, so ordinary mail to the main domain is untouched.

FELAGI_INBOUND_EVENT exists because the event name belongs to Resend rather than to us. If replies are not arriving, the log will contain the event type that did arrive — set the variable to match.

How a reply finds its issue

Notifications carry a Reply-To cut for one recipient and one issue:

reply+42.7.cbe3b7e4dee4f20b@mail.yourdomain.com
        │  │  └── HMAC of the pair, keyed on APP_KEY
        │  └───── user
        └──────── issue

The signature is the point. Without it, anyone who saw one address could work out every other one and post comments in a colleague's name — a forwarded notification would be enough. It is checked in constant time, before either record is loaded.

The address is short by necessity: RFC 5321 caps a local part at 64 characters, so an encrypted payload does not fit.

The webhook does not contain the email

Resend's email.received event carries metadata only — sender, recipients, subject, attachment list — so that large messages do not have to fit inside a webhook request. The body, headers and attachments come from a second call to the Receiving API, keyed on data.email_id.

That shapes the flow here:

  1. Verify the signature
  2. Read the recipients from to, cc and received_for
  3. Match one against a reply address — before fetching anything
  4. Queue the fetch, and answer 202

Step 3 matters because every address at the receiving domain reaches this endpoint. Matching first means spam costs no API call. Step 4 matters because holding a webhook open for somebody else's API is how endpoints time out, and a timeout looks like a rejection to the sender.

received_for matters as much as to: a forwarded reply arrives addressed to the forwarding mailbox, and the address we cut is only in the Received headers.

What happens to a reply

  • Quoted history is cut off — > lines, On … wrote:, Outlook's divider
  • The remainder becomes a comment, sanitised on write like any other
  • Mentioning an agent works exactly as it does in the interface: @Freya queues a run
  • Out-of-office replies are dropped. RFC 3834 exists so this is detectable, and our own notifications carry Auto-Submitted: auto-generated so the loop cannot start from the other side either

Security

The endpoint is public, so the signature is all that stands between it and somebody writing comments in your workspace. Verification is delegated to resend/resend-php rather than hand-rolled.

  • Svix scheme: HMAC-SHA256 over {id}.{timestamp}.{body}, verified against the raw request body — a round trip through json_decode changes the bytes and breaks the signature
  • Several signatures are accepted during a secret rotation
  • Signatures older than RESEND_WEBHOOK_TOLERANCE (default 300s) are refused, so a captured request cannot be replayed tomorrow
  • No secret configured means every request is refused. A webhook that stops working is a better failure than one that accepts anything

Known gaps

  • Bounces and complaints are logged, not acted on. Resend keeps its own suppression list, but Félagi does not read it, so a dead address keeps being mailed.
  • Attachments are ignored. Resend exposes them through a separate API and nothing here fetches them yet.
  • There is no digest or notification preference. Every comment on an issue you are part of sends an email.