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.
Running Grove’s ElyraSQL? Upgrade to 1.10.1 — 1.10.1 moves it to 1.11.4 for you, and leaves your database file alone.
Every ElyraSQL through 1.11.3 shipped a rustls whose TLS 1.3 handshake accepted handshake messages across encryption-level boundaries (RUSTSEC-2026-0285), and Grove 1.8.0 to 1.10.0 installed 1.11.3.
Moving the pin alone would have made things worse: service builds unpack to a directory named for their version, so Grove would have seen ElyraSQL as not installed, autostart would have skipped it silently, and grove service start would have said “not installed” with your data still sitting there.
So when the 1.10.1 daemon starts it finds the 1.11.3 build, fetches 1.11.4 in the background and starts it if it was running before, on the same port. The old build stays on disk until you remove services/elyrasql/elyrasql-1.11.3-*.
Downloaded the macOS CLI from a browser and watched it die? Up to 1.10.0 the standalone aarch64-apple-darwin archive held ad hoc signed binaries, and Gatekeeper killed them on first run — 1.10.0’s grove --version exited 137. They are signed with the Developer ID and notarized now, and the disk image itself is notarized and stapled rather than only the app inside it. The CLI inside Grove.app was always signed.
Also: on 1.4.x? Upgrade, then run sudo grove ca rotate once.
1.5.0 closes a security review of the daemon, the proxy, the tunnel and the secret
client, and one finding needs a command from you.
The root CA Grove installs in your trust store was unconstrained, so it
could sign a certificate for any hostname at all rather than only the TLD Grove serves.
A certificate cannot gain a name constraint after the fact, so the existing one has to be
replaced — grove doctor warns until it is, and every
site’s certificate is re-issued on next use. As with every private key this site has
had to write about, rotating closes the door and tells you nothing about whether anybody
went through it. Since 1.6.0 that command refuses without sudo
rather than untrusting nothing, deleting the old CA, minting a new one and then failing to
trust it — a missed sudo used to leave the old CA in your
keychain and the new one untrusted on disk, which is broken HTTPS on the one command everyone
here is told to run.
1.8.0 finishes this. grove doctor now finds a stale
unconstrained CA that a missed sudo left behind and prints the command that
removes it — and it asks the system’s trust evaluator rather than searching the keychain, so a
certificate that is merely still stored and no longer believed raises nothing. It reads the constraint off the
certificate too, rather than the note beside it, so a leftover note cannot vouch for a CA that constrains nothing.
Three more that need no action beyond upgrading, all of them local:
the daemon’s IPC socket was 0777 with no authentication, so any
process on the machine could drive the thing that runs as root — and since
1.8.0 there is no such thing: one sudo grove install moves the daemon off root
for good, with launchd or systemd binding 53, 80 and 443 while it is root and handing the listening descriptors
to a process that never had privilege at all, and since 1.9.0 the daemon refuses to start as root: a machine that updated past 1.8.0 without running sudo grove install stops at startup and names that command, which is the fix; root executed
binaries out of a directory it did not own; and the request log kept
credentials in the clear — Grove is the proxy, so it sees every header of every
request your sites receive. Downloads are verified against the publisher’s SHA-256
before use now, which they were not, and leaf certificates actually renew, which they did
not.
Running the tunnel? An empty --token used
to disable authentication, so the simplest way to start a server was also the open one.
It is required now; --allow-anonymous keeps the old
behaviour on purpose. The control channel is still unencrypted, which the tunnel
documentation now says in a Security section rather than leaving to be discovered.
Every connection was reset, on every site:
ERR_CONNECTION_RESET and no page at all. The header-read
timeout added in 1.4.0 was configured without giving hyper a timer, and hyper neither falls
back to a default nor complains at setup — it panics the first time a connection reaches that
code, which is every connection. 1.4.2 installs the timer.
The daemon reported itself healthy the whole time.
grove status and the GUI both said Running while not
one site rendered, because 1.4.0 also stopped a panic from killing the process. That change
was right and we said so on this page — it is also what turned a total outage into a quiet
one. Under the previous behaviour the first request would have taken the daemon down and the
fault would have been obvious.
On 1.3.0 or earlier? Update.
1.3.1 closes two disclosures in the request path. A plain PHP project's document root is the
project root, so /.env was served in full — APP_KEY
included — and any existing .php file was handed back as source rather
than executed, disclosing the front controller. Locally that is bad enough; with
grove share both were reachable from the internet.
Dot-prefixed paths are now refused outright (.well-known/ excepted, so ACME
still works) and .php goes to PHP-FPM as nginx and Apache do.
If you shared a site from an older build, rotate anything that was in that .env.
macOS · Apple Silicon
Grove_1.10.1_aarch64.dmg
12.8 MB
macOS · Apple Silicon
grove-v1.10.1-aarch64-apple-darwin.tar.gz
7.6 MB
Linux · x86_64
grove-v1.10.1-x86_64-unknown-linux-gnu.tar.gz
8.6 MB
Linux · x86_64
Grove_1.10.1_amd64.deb
13.9 MB
Linux · x86_64
Grove-1.10.1-1.x86_64.rpm
13.9 MB
Linux · x86_64
Grove_1.10.1_amd64.AppImage
87.1 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.
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.
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. Since Grove 1.10.1 it is pinned to 1.11.4, for a rustls advisory in the TLS that ElyraSQL terminates. 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 1.11.3 to 1.11.4 by itself when it starts: a patch release of what you had is fetched in the background and started the way it ran before, with autostart and on-demand kept, while a minor or major move is only logged with the command to run, because MySQL and PostgreSQL data directories do not always carry across one. 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.
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.
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.
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).
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. The design is public while it is being built: read the deploy contract, or 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.