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

Configuration

Every environment variable that matters, with the traps marked.


Application

APP_NAME="Félagi"
APP_URL=http://felagi.test
APP_TIMEZONE=UTC

Store in UTC and let the interface do the converting. APP_TIMEZONE is the application's own clock, not a user preference — autopilot schedules carry their own timezone precisely so a shared default does not have to be anyone's.

APP_URL appears in password reset links, verification links, signed URLs and the setup command shown when connecting a machine. Pointing it at the wrong host breaks all of them silently.

The session cookie trap

SESSION_COOKIE=felagi_session

Laravel derives the session cookie name from APP_NAME with Str::snake(), which does not transliterate. Félagi yields félagi_session, and RFC 6265 requires cookie names to be ASCII tokens. Browsers drop the Set-Cookie header entirely, the session never persists, and every POST fails CSRF verification with 419 Page Expired — pointing at CSRF, while the cause is the name of the application.

config/session.php now derives an ASCII-safe name with Str::slug() and ignores an unusable SESSION_COOKIE, so the variable is belt-and-braces rather than load-bearing.

curl accepts the invalid name happily. Only a real browser reveals the bug.


Database

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=felagi_db

MySQL 8, because task claiming depends on SELECT … FOR UPDATE SKIP LOCKED. SQLite has no such clause; concurrency tests skip themselves there rather than pretend.

MySQL also refuses literal defaults on JSON columns, which SQLite accepts. Model-level $attributes carry those defaults instead.

Sessions, cache and queues

SESSION_DRIVER=redis
CACHE_STORE=redis
QUEUE_CONNECTION=redis

Redis for sessions means migrate:fresh no longer signs everyone out — which matters when the schema changes several times a day.

Redis for the queue rather than database, because Redis is already required here and a database queue polls a table that the board is also reading. Redis blocks on a pop instead, which is the difference between a live update arriving now and arriving on the next poll.

Give the queue a Redis database with maxmemory-policy noeviction. Under allkeys-lru — a perfectly ordinary cache setting — Redis is free to evict queued jobs when memory runs short, and those jobs are work the application believes it has already handed off.

The worker is not optional

Broadcasts ride their own queue so a slow job cannot delay the interface:

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

composer dev runs one. In production it needs a supervisor.

With sync, forgetting the worker cost nothing. With a real queue it means the interface stops updating and nothing says why — so the dashboard checks the backlog and says so itself.

Broadcasting

See Broadcasting.

Daemon protocol

FELAGI_MIN_DAEMON_VERSION=0.2.0
FELAGI_DAEMON_DOWNLOAD_URL=https://github.com/kwhorne/Felagi/releases

Raising this is a breaking change for whoever runs the daemons. They are refused at the door with a 426 and the download URL, which is the right failure — a clear message before any work starts, rather than a refusal halfway through a run. But they will do nothing until they are updated, so it belongs in a release note rather than a patch.

Raising the minimum version locks out older daemons with a 426 that tells them where to get a newer one. Both are read from config/felagi.php, along with the heartbeat and poll intervals the server advertises.


Knowledge base

Variable Default What it does
FELAGI_KNOWLEDGEBASE_PATH storage/knowledgebase Where article documents are written, one HTML file per article

The database row is metadata — title, parent, position — and the document is a file. Point this at a shared volume if more than one machine serves the application, or every article will be readable on exactly one of them.

The directory must be writable by the web user and is never served directly: articles go through the application, which knows who is asking and which workspace they are in.

Full behaviour in Knowledge base.


Outgoing webhooks

Variable Default What it does
FELAGI_WEBHOOK_ALLOW_PRIVATE false Permit endpoints inside the network Félagi runs on. Link-local is refused regardless.
FELAGI_WEBHOOK_TIMEOUT 10 Seconds before a delivery is abandoned
FELAGI_WEBHOOK_FAILURES 12 Consecutive failures before a webhook is stopped
FELAGI_WEBHOOK_KEEP_DAYS 7 How long delivery logs are kept

A queue worker must handle the webhooks lane or nothing is ever delivered.

Full behaviour in Webhooks.


The API

Variable Default What it does
FELAGI_API_THROTTLE 300 Requests a minute, per token

Full contract in The API.


The sandbox

Not configured here — it lives in ~/.felagi/config.json on each runtime, because only the machine knows what its kernel can enforce.

{ "sandbox": "confined" }

confined (default), limits, or off. The effective policy is reported at registration and shown in Administration → Runtimes. Full behaviour in the daemon's README.


Deliveries

Variable Default What it does
FELAGI_DELIVERY_SECRET unset The token in the return-path webhook URL. Unset means the endpoint is closed, and answers 404 to everything.

It moves issues, so it fails closed. See Delivering work.


Daemon rate limits

Variable Default Real traffic
FELAGI_DAEMON_THROTTLE_POLL 120 20/min — a claim every 3 seconds
FELAGI_DAEMON_THROTTLE_BEAT 30 4/min — a heartbeat every 15 seconds
FELAGI_DAEMON_THROTTLE_WORK 600 ~80/min per running task
FELAGI_DAEMON_THROTTLE_REGISTER 20 1, at startup

Per token, per minute. Raising one for a busy installation does not open the door for anybody else. See the protocol.


Two-factor authentication

Variable Default What it does
FELAGI_2FA_REQUIRED unset Unset means required in production, nowhere else. Set it to override that in either direction.
FELAGI_2FA_GRACE_DAYS 5 Days from when an account begins before it is demanded. 0 demands it immediately.
TRUSTED_PROXIES unset Required before using the IP allowlist behind a proxy. Without it the address Félagi sees is the proxy's, and allowlisting that exempts the internet.

The nullable default is the point. (bool) env('FELAGI_2FA_REQUIRED', true) would make an unset variable mean on, and a developer who then switched it off locally would be one .env copy away from carrying that into production. Absent means "decide from the environment", which is the safe thing to forget about.

Full behaviour in Two-factor authentication.


Security headers

App\Http\Middleware\SecurityHeaders sets the policy on every response. Two parts need attention when things change:

connect-src includes the Reverb origin, derived from configuration. The websocket runs on a different port and is therefore a different origin — 'self' alone blocks it. Run output arrives over that socket, so blocking it leaves the pane silent rather than broken.

Cache-Control: no-store is set on every HTML response. Laravel's default of no-cache, private still permits the browser to store the page, so a restored tab can resurrect a form whose CSRF token belongs to a session that no longer exists. no-store is the only value that rules that out.

HSTS is only sent in production, so local development over plain HTTP is unaffected.


Local environment (Grove)

The project is developed against Grove, which serves *.test domains and bundles MySQL and Redis.

grove status        # daemon, runtimes, services, and the site for this directory
grove env           # a .env snippet wired to Grove's services

One thing to know:

public/hot outlives npm run dev. If the interface suddenly renders unstyled, that file is pointing at a Vite server that is no longer running. Delete it.