Local development,
nothing to install around it
.
Grove serves *.test domains with automatic routing, local HTTPS, bundled multi-version PHP and Node, bundled databases and a mail-catcher — from a single Rust core. No Homebrew, no Composer, no dnsmasq.
Responses and uploads stream, in both directions: a Server-Sent Events endpoint or an AI token stream arrives token by token instead of landing all at once when PHP closes the request, and a large upload no longer sits in memory while it is forwarded.
Coming from Herd? Migrate in a few minutes →
The free core stays free. Pro adds shared, synced team infrastructure on top of your local environment.
- End-to-end encrypted team secret sync
- Reproducible team environments — clone to running in one command
- License activation, priority support & commercial license
Free & open source for solo use — no account needed.
Download Grove
Native installers for Apple Silicon macOS and Linux (beta), or
build from source.
Intel Macs are no longer shipped as a build — notarization on the Intel runner hangs too often to release from — but
cargo build --release still works there.
macOS · Apple Silicon
Grove_1.12.4_aarch64.dmg
13.0 MB
macOS · Apple Silicon
grove-v1.12.4-aarch64-apple-darwin.tar.gz
7.7 MB
Linux · x86_64
grove-v1.12.4-x86_64-unknown-linux-gnu.tar.gz
8.7 MB
Linux · x86_64
Grove_1.12.4_amd64.deb
14.1 MB
Linux · x86_64
Grove-1.12.4-1.x86_64.rpm
14.1 MB
Linux · x86_64
Grove_1.12.4_amd64.AppImage
88.4 MB
One Rust core,
everything bundled.
DNS, reverse proxy, FastCGI and TLS are built in. PHP, Node, databases and mail are downloaded and supervised by Grove itself — no Homebrew, Composer, dnsmasq or OpenSSL required.
Automatic *.test routing
An embedded DNS resolver serves your *.test domains — no manual hosts-file editing, no dnsmasq.
Local HTTPS
A private root CA with on-demand per-site leaf certificates, so every site gets trusted TLS locally. Through 1.3.2 that CA was re-minted every time it was read: loading it from disk parsed the PEM and called self_signed again, so cert_pem() reported a certificate with a new serial and a new validity window — neither the file on disk nor the one the OS trust store had been told to trust. Leaf certificates still chained, because the name and key matched, which is why it survived as long as it did. From 1.4.0 the CA on disk is used as it is. The other claim this card rests on — that a leaked Grove CA cannot mint a certificate for anything outside your TLD — lived in two doc comments until 1.8.0. It is a regression test now, through rustls and webpki, the same verifier and rules a browser applies: a Grove-signed leaf for google.com is refused, the same leaf from a CA without the constraint is accepted, so the refusal is the extension's doing rather than an expired date or a broken chain, and the certificates Grove actually issues are accepted, wildcards included. 1.8.0 also moves the CA private key from root to you, because the daemon that signs leaves is no longer root and a key it cannot read is HTTPS that does not work. That cost is stated rather than glossed: code running as you can read the key now, bounded by the name constraint and by those names resolving to loopback — while the root-privileged IPC surface the same code could already reach disappears entirely. Since 1.12.2 rustls is 0.23.46, for four published advisories, with the fixed certificate verifier and root certificates.
Bundled PHP
Install multiple self-contained PHP versions (grove php install 8.5 | 8.4 | 8.3), with per-site isolate and lazy FPM pools. Three faults in how Grove installs and checks PHP were found the hard way and are fixed in 1.11.1. GD in Grove's PHP could not read or write a JPEG, and nothing said so: static-php-cli builds only an extension's required libraries, which for GD is zlib and libpng, so GD loaded, php -m listed it, phpinfo() said GD Support => enabled, and imagejpeg() did not exist, which showed up as failing tests in a Laravel project on a machine where nothing had changed. The rebuilt 8.5.11 has imagejpeg, imagewebp and imagettftext, so grove php install 8.5 replaces the build you have; 8.4 and 8.3 have not been rebuilt yet, and grove php ext now says so for any build you have installed. That command asks each module whether it works and not only whether it is loaded, with seven probes each run inside the build being audited: GD for JPEG, WebP and FreeType, curl for TLS, PDO for the SQLite and MySQL drivers and intl for ICU data, and a module that is present but cannot do its job counts as unhealthy. Grove read the build listing from GitHub without a token, so when its 60 requests an hour ran out, grove php install 8.5 quietly installed upstream common 8.5.8, without mysqli and intl, and said only no grove build for 8.5 yet, which was not true; a listing that cannot be fetched is an error now, naming the variant, the cause and the likely rate limit. And a reinstalled php-fpm used to be written over the existing file, which macOS does not accept, because a binary's code signature is tied to the file it was checked on: the install reported success, php-fpm -v exited 137 with no output, every site answered 502 and codesign -v said the signature was valid. It is renamed into place now. Since 1.12.0 the builds come from elyracode.com, listed in php-runtimes.json, and each download is still verified against its sha256 digest.
Bundled Node.js
Download node, npm and npx with per-site Node versions — no nvm or Homebrew.
Bundled services
Grove downloads and supervises PostgreSQL, MySQL, Redis — and since 1.7.0 ElyraSQL — itself; no separate database or cache to install. grove service install elyrasql fetches the one static binary, verifies its published SHA-256 and runs it on 127.0.0.1:3307 beside MySQL; your app keeps the MySQL driver, and grove env prints the block. Adding it surfaced something: sqlx, the driver every Rust client and Grove's own convert tool sit on, could not connect to ElyraSQL 1.11.1 at all, so the fourth database required 1.11.2 — and 1.11.3, pinned by Grove 1.8.0, brought two more a Grove site can hit: SET @var inside a stored procedure reaching the session instead of vanishing when CALL returns, and an expression-depth limit that follows the calling thread's stack rather than overflowing a small one. Grove 1.11.0 pinned it to 1.12.0, which closes two ways around column grants, and 1.11.1 pinned 1.12.3, which brings it closer to MySQL in ways a project can see: DDL commits an open transaction first, so a ROLLBACK after it no longer undoes earlier writes, START TRANSACTION READ ONLY refuses writes, SELECT ... INTO @var sets the variable, also for locals in stored procedures, CALL returns each SELECT in a procedure as a result set, LIKE 'a\_b' matches a literal underscore, and SHOW TABLES LIKE filters and lists views; 1.10.1 had moved it to 1.11.4 for a rustls advisory. Grove 1.12.1 pinned 1.12.5: a long-running database no longer grows in memory under a steady write load, and a subquery inside a CASE arm or a function argument now runs; it used to be refused. Grove 1.12.3 pins 1.12.6, which acts on the SIGTERM Grove sends when it stops a database, every time an on-demand one goes idle: it flushes what it has committed and exits cleanly. It used to be ended by the signal at once. Grove 1.12.4 pins 1.12.9: USER(), CURRENT_USER() and CONNECTION_ID() now answer for the connection that asks, a dump from mysqldump now restores with the mysql client, and mysqldump can dump a database, with its views, triggers, procedures and partitioning. The archive unpacks to a versioned directory beside the data directory, so the new build leaves the existing database alone, and the daemon moves an installed ElyraSQL to the new pin by itself when it starts: the build is fetched in the background and started the way it ran before, with autostart and on-demand kept. ElyraSQL may move across a minor release, because one file opens in any release of a major; MySQL and PostgreSQL take only patch releases by themselves, because their data directories do not always carry across a minor, and Redis does too, because it is built from source. Verified by installing 1.11.4 with the released 1.10.1, writing three rows, letting the new daemon fetch 1.12.0 and start it on the same file, and reading the three back and adding a fourth. macOS on Apple silicon and Linux; upstream publishes no Intel macOS build. Since 1.9.0 a database can run only while something is connected: grove service on-demand mysql on hands the port to the daemon, the server starts behind the first connection and stops cleanly after ten idle minutes by default. An idle MySQL at 517 MB went to nothing, and the first query after it had stopped was answered in 0.35 s including the start. Since 1.10.0 it starts earlier than that: a browser resolves myapp.test before it connects, often while the address is still being typed, and Grove answers that DNS lookup by starting the site's idle databases in the background. A lookup 0.4 s ahead made the first request after MySQL went idle take 43 ms instead of 354 — the same as with MySQL already running. And grove service start no longer says started for a server that has already died — it waits until the port answers and otherwise shows the server's own error lines.
Built-in mail-catcher
An SMTP server captures outgoing mail, with a Mailpit-style viewer to inspect it.
Reproducible environments
Commit a grove.toml and grove up brings the whole project environment — PHP/Node versions, services, dev processes — to a known-good state on any machine.
The app declares, Grove supervises
grove dev used to guess your dev processes — a hardcoded Vite server and queue worker, blind to Reverb, Horizon or stripe listen, and assuming npm in a bun project. On Laravel 13.16+ it now reads artisan dev:list and supervises exactly what the application declares, each with its own log file, autostarted at boot with no terminal left open. It skips the processes Grove already is (it serves the site over FPM and tails the log itself) and excludes vendor-registered ones, so a Composer package cannot start processes inside the daemon. Vite still gets trusted local HTTPS automatically.
Database client
A built-in database browser auto-connects from your .env — browse tables and run queries free; the Pro schema inspector adds row editing, with a guard that blocks edits against production.
The site you are working on, among a hundred
With 140 sites the Sites list is a long scroll. Since 1.11.0 ⌘K, anywhere in the app, opens a palette that lists the ten projects you worked on last, each by the newest of three signals and saying which one it was — git 20m ago, request 3m ago, opened just now. The git signal reads the times of the index, HEAD and its log, following a worktree's .git file, and leaves out FETCH_HEAD because a background fetch is not you working. Typing fuzzy-searches every site by hostname, then by path; ↑↓ choose, ↵ focuses the Sites list on that project with a chip to clear it, and ⌘↵ opens it in the browser. Above the list, / or ⌘F opens a filter where every word must appear in a site's name, path, driver or PHP version, and it shows N of M. Driven by keyboard in Chrome against 144 sites and the git repos of a scratch Grove: two sites with requests, a worktree and three repos worked on 23 minutes to six days ago came out in the right order, abn found abonnementsoversikt.test first, and the palette kept what was typed through the four-second background refresh.
Does each project get the PHP it asks for?
Composer checks a project's php and ext-* requirements against the PHP that ran composer install, at install time, on whatever machine that was. Grove knows which PHP build serves each site, so grove php check checks the same requirements against that build, from composer.json and from every package in composer.lock, and checks what Composer cannot: whether a module that is loaded can do the job. A GD without JPEG satisfies ext-gd, and that is how 1.11.1's GD problem went unnoticed. It reports a PHP version outside the constraint, a missing extension, an extension that is loaded but not working, and a site set to a PHP that is not installed, each naming who asked, with packages that ask the same folded into one line and dev-only requirements marked. It shows in grove php check, as a warning from grove link, as a line in grove doctor, and as a warning sign in the app's Sites list and palette. Since 1.12.0.
A palette that does things
Since 1.12.0 the arrow key or Tab on a site in the palette lists its actions, ordered by how often they are reached for: focus it, open it in the browser, show its requests, its database, its logs, start or stop dev, share it publicly, turn HTTPS on or off, use another installed PHP, check what it asks of PHP, copy the URL, reveal the folder. Labels show the state they change from, read when the list opens, so it says Stop dev while dev runs, and Esc goes back to the sites with the search kept. A greater-than sign lists commands: any tab, a new site, park a folder, settings, restart the daemon, check for updates.
Database time-travel
grove db takes point-in-time snapshots of Grove’s bundled databases and restores them instantly — undo a bad migration or seed in seconds. Until 1.9.0 a MySQL restore was not quite that: a dump recreates the tables it holds and says nothing about others, so a table the migration had created survived the restore. Each database is dropped and recreated on the way in now, so it comes back exactly as it was — and two snapshots in the same second no longer overwrite each other. Since 1.9.0 a project can also have a database per git branch: grove db branches on, and checking out a branch swaps in that branch's data, so a feature branch's migrations stop landing in main's tables. The database keeps its name and only its contents move, so the app, artisan, a test run and a database client all follow the checkout without being told; on MySQL the swap is one atomic RENAME TABLE, measured at 43–46 ms for a 70 MB schema.
Another branch, running beside yours
Reviewing a colleague's branch used to mean stashing, checking out and migrating your own database into their schema. Since 1.10.0 grove try feature/invoices runs it beside yours at myapp--feature-invoices.test, in a worktree, on its own copy of your database, with your .env copied and the hostname moved everywhere it appears. vendor, node_modules and public/build are cloned copy-on-write, so it takes seconds and no disk; composer install only catches up with their lock file, and their migrations run on their copy. Your checkout and your database are never touched. A try whose setup fails part-way is undone completely, and --done refuses while the worktree holds uncommitted work, naming the files. The name uses a double hyphen because every subdomain of a site routes to that site.
Request timeline & replay
Grove records a live timeline of requests across all your *.test sites — expand any request to see headers and body, and replay it with one click (or grove replay) while you fix the code. A request that writes finds what it wrote last time when you replay it — the order exists, the email is taken — so since 1.10.0 grove replay <id> --same-data snapshots the site's database on the first replay and puts it back before every later one, and the same request can be tried again from the same starting point as often as it takes. grove routes shows which route got slower: typical and recent are both medians, so one slow request moves nothing, and a flagged route's baseline stops taking samples so a regression cannot quietly become the new normal. It catches the jump — an N+1, a dropped index — and says plainly that a slowdown creeping in a few percent at a time is absorbed and never flagged. And grove bisect --good <commit> --request <id> finds the commit that broke a recorded request: each step runs beside your checkout at its own .test address with a fresh copy of your database migrated to that commit, replays the request, and your checkout never moves. It first checks the request really fails at the bad end, and waits three seconds per step because at two OPcache still answered with the previous commit's code.
Local webhook hub
Any request to /__grove/hooks/<bucket> is captured and acknowledged — a local webhook.site. Expose it with grove share, point Stripe/GitHub at it, inspect each delivery and re-deliver it to your app.
Turn a request into a test
From any captured request or webhook, copy it as a curl command, a .http file, or a Pest feature test — turn a failing request into a regression test in one click. The timeline keeps at most 1 MiB of a body, and every export now says so when it applies: a partial body used to be exported as if it were complete, so the app's puzzled response looked like a bug in your code rather than Grove's capture limit.
Environment bundles
grove bundle export packages a project's grove.toml, .env and database into one shareable file; grove bundle import brings it up and loads the database — reproducible dev environments without Docker.
AI tools (MCP)
grove mcp runs a local-only Model Context Protocol server that exposes your sites, request timeline, webhooks, logs and database schema to AI clients like Claude and Cursor. Read-only by default; opt-in agent-safe write tools (grove_migrate_sandboxed, grove_sql_sandboxed) appear only with --allow-write and run inside an automatic snapshot sandbox — each returns its blast radius (schema diff + causal chain) so you can see exactly what a write did before keeping it. Since 1.10.0 --allow-write also offers sandboxes: an agent gets a complete running copy of a site — a new branch in its own worktree, its own migrated copy of the database, its own HTTPS address — and every other tool takes the sandbox's name. Your checkout and your database are never involved. Closing keeps the branch with the agent's commits and refuses, naming the files, while anything is uncommitted. grove_routes tells an agent whether its change made a route slower.
Causal chain & explain
grove_request_chain correlates a captured request with the SQL, logs and errors Grove observed while handling it; grove explain <id> (and grove_explain) curate a full debugging bundle for one request — so an agent gets the whole story, not just the stack trace. Since 1.12.4 SQL capture sees a Laravel app's queries: PDO prepares statements on the server, and those are logged as Execute, which capture did not keep, so a request's causal chain showed the connection's use and SET NAMES and none of the app's SQL, and an N+1 never showed up. Executed prepared statements are kept now, with their values bound in, and nothing counts twice.
Docker & OrbStack sites
Grove auto-discovers running containers and Compose projects and serves them as name.test with trusted local HTTPS — 🐳 in the same dashboard, with start/stop/restart controls and grove share tunnelling. In 1.12.0 a proxy or Docker site answered 502 Bad Gateway over HTTP/2, which is every browser on an HTTPS site; since 1.12.1 requests are sent upstream as HTTP/1.1.
Public tunnels
Expose a local *.test site at a public URL with grove share — a native, self-hostable ngrok/Expose alternative. Zero-config via grove.elyracode.com.
Request inspector
A live table of recent tunnelled requests (method, path, status, duration) — ideal for debugging webhooks, in the GUI and the terminal. For a streaming response the duration column is time to headers, not total time, so a 16-second SSE stream reads as a few milliseconds.
And nothing is done twice
The sequel to that, and the more ordinary kind of waste. A proxied request built a new HTTP client per request — and a client is the connection pool, so every asset from a Vite dev server paid a fresh TCP handshake. Static files carried nothing to revalidate against, so an unchanged asset was re-read and re-sent on every reload; they now carry an ETag from size and mtime, needing no extra read, and answer If-None-Match with 304. DNS answers went out with TTL 0, which forbids caching, so the system resolver asked Grove again for every single connection — with mDNSResponder in that path on macOS, on every first byte. It is cached for five minutes now, the answer being always loopback. And the release profile optimised for binary size, which is the one thing that does not matter for a daemon on the request path.
One bad request stops being everybody's problem
The release build used panic = "abort", so a single unwrap on a poisoned mutex — in one request, for one site — took down the daemon and with it DNS, TLS and every other site you had open. Panics unwind now, so a failure stays inside the task that caused it. Two more of the same kind: the accept loop answered failure with a bare continue, which under EMFILE fails again immediately and forever, burning a core and never recovering — it backs off from 5 ms to 1 s now, which also gives descriptors time to be released. And neither the TLS handshake nor the wait for request headers had a deadline, so a crashed browser or a port scanner could hold a task and a file descriptor for as long as it liked. Worth knowing what that first change cost on the way in: the header deadline was configured without giving hyper a timer, so 1.4.0 and 1.4.1 panicked on every connection and served nothing — and because panics no longer killed the process, the daemon stayed up reporting itself healthy while no site rendered. Fixed in 1.4.2. Making a failure survivable also makes it quieter, and quiet is the harder half.
Nothing is buffered whole, either direction
Responses used to accumulate until PHP closed the request, so an SSE endpoint delivered nothing until it ended and a 2 GB download cost 2 GB of memory. Uploads were worse: the body was collected before Grove had even resolved which site the request belonged to, and then held in triplicate — 400 MB of upload cost 1.2 GB of RSS, now about 3 MB. Headers go out as soon as PHP flushes them, and request bodies stream into FastCGI with CONTENT_LENGTH taken from the body. A chunked upload declares no length and CGI insists on one, so Grove measures it — in memory to 1 MiB, then a 0600 spool file inside a 0700 directory, removed on Drop so neither an error nor a disconnect leaves upload contents on disk. Refusing chunked requests with 411 would have made Grove the reason a valid request fails; nginx and Apache spool, so Grove spools. Bodies beyond 2 GiB are refused with 413.
Hide & restore sites
grove forget removes a site from the list without deleting its files; grove restore brings it back.
Driver system
First-class drivers for Laravel, WordPress, generic PHP, static sites, and reverse proxy (Vite/Node). Until 1.12.1 PHP got only the last cookie over HTTP/2, so in a Laravel app with more than one cookie the session cookie went missing and logging in sent you back to /login; the cookie fields are now joined into one HTTP_COOKIE.
GUI + CLI in parity
Both are thin clients over the same daemon, plus a macOS menu-bar icon. Built with Tauri 2 + Svelte 5.
Zero external dependencies
DNS, proxy, FastCGI and TLS are all built into the Rust core. Nothing to install around it.
# Share a local site publicly — no config needed $ grove share myapp ✓ https://quiet-river-4821.grove.elyracode.com → myapp.test (HTTPS, streaming) # Live request inspector for webhooks 12:04:31 POST /webhooks/stripe 200 142ms 12:04:33 POST /webhooks/stripe 200 88ms
Share localhost,
without ngrok.
grove share exposes any local *.test site
at a public HTTPS URL for demos, real-device testing and webhooks — a native,
self-hostable alternative to Expose/ngrok, built right in.
- Zero-config via grove.elyracode.com — works out of the box
- Self-host your own server with the included deployment kit
- Live request inspector for debugging webhooks
- Streaming bodies proxied end-to-end over a single connection
Build it. Run it. Store it.
Ship it.
There is one place the ecosystem still asks you to leave it: getting code from your machine onto a server. Grove already provisions a whole environment locally, and Askr already reloads without dropping a request — rolling one worker at a time, and aborting the whole reload if the first one looks unhealthy.
grove deploy is the missing middle: immutable releases, an atomically swapped current symlink, migrations run before the swap, and a rollback that is one command — or automatic, when Askr's canary says no.
Deploying to your own box
No hosted control plane, no third party in the path — your machine to your box. Read the honest state of the whole stack on the architecture page.
Not shipping yet. Nothing above is in Grove today — the commands are the agreed design, not a changelog. When it lands, this section becomes a feature and the headline on the front page gains a fourth verb.
Park a folder,
open the browser.
Point Grove at a directory of projects and every subfolder is served at
name.test with HTTPS — or import an existing Valet setup.
# Install a PHP version $ grove php install 8.5 # Serve every project in a folder at *.test $ grove park ~/Code # Or link a single site $ cd ~/Code/app && grove link ✓ https://app.test
Free for solo. Pro for teams.
Everything you need for local development is free and open source, forever. Grove Pro adds shared, synced team infrastructure.
Grove
- Automatic *.test routing with local HTTPS
- Bundled PHP, Node, PostgreSQL, MySQL, Redis
- Mail-catcher, public tunnels, request timeline
- Database snapshots and the full toolchain on your PATH
Grove Pro
- Everything in the free version
- End-to-end encrypted team secret sync
- Reproducible team environments — clone to running in one command
- One-command onboarding and drift detection
- Priority support and commercial license
Your machine,
your dev environment.
Download Grove and serve your projects locally with HTTPS in minutes. Free, open source, and dependency-free.