The local loop
The workflow Groveback is built around: a real backend on your machine, your project’s shape in git, and a diff you confirm before anything reaches a deployed project.
The picture
Section titled “The picture” YOUR REPO THE PROJECT (committed, reviewed in PRs) (api.example.com)
grove init ──────▶ grove.json ─────────────────────▶ linked url · project · outDir
grove gen ◀────── <outDir>/grove.ts ◀───────────── live schema typed client (shape only) │ │ imported by your app ▼ ┌──────────┐ GROVE_URL=http://localhost:8099 │ your app │ ───────────────────────────────┐ └──────────┘ (env var — no regeneration) │ │ grove pull ◀────── groveback/ ◀───────────────────── shape │ collections · functions · roles │ │ │ │ applied at boot │ ▼ │ grove dev ──────▶ ┌──────────────┐ ◀───────────────────────────┘ │ localhost: │ grove seed ──────▶ │ 8099 │ in-memory · one project (--seed) │ │ (--persist writes it all back) ▲ │ + dashboard │ key-only dashboard │ └──────────────┘ seeds/ │ fixtures │ you edit groveback/ while it runs ▼ grove status ─────▶ what would change? grove push ──────▶ groveback/ ─────────────────────▶ shape updated (plans first · refuses if the project moved)Two columns: everything on the left is a file in your repo, everything on the right is the running project. The CLI is what crosses between them.
Local-first collapses the right column — there is no deployed project yet, so grove dev is
the only server and groveback/ grows from what you build in it. Add the right column later
with grove init and grove push; nothing you built has to change.
Shape and data follow different rules
Section titled “Shape and data follow different rules”| What it is | Which way it travels | |
|---|---|---|
| Shape | collections, policies, functions, roles, integrations | both ways — pull down, push up |
| Data | the actual documents | never between projects; local data comes from committed fixtures |
That split is deliberate. A config export is shape-only, so there is no path that pulls documents out of production or writes documents into it.
grove pull
Section titled “grove pull”Mirrors the project’s shape onto disk, one file per resource:
groveback/ collections/ posts.json functions/ ingest.ts ← the real handler, editable in your editor ingest.json ← everything else; points at the .ts via codeFile integrations/ roles.jsonFunctions land as actual .ts files, so a handler is something you edit and test with
@groveback/testkit instead of a string field in a JSON blob.
Commit groveback/ — shape changes then show up as a reviewable diff in a pull request.
Two behaviors to know: pull replaces, so a resource deleted upstream is deleted locally;
and it refuses to overwrite a bundle with uncommitted changes (--force overrides).
Every pull records a sha256 baseline per file in .grove/pull.json, which is how push
later detects that someone else moved the project.
grove dev
Section titled “grove dev”grove dev --seed --persist # what you almost always wantgrove dev # port 8099, in-memory, nothing loaded or savedgrove dev --mongo mongodb://localhost:27017/dev # real Mongo (indexes, vector search)--seed and --persist are one pair: load seeds/ on start, write everything back on exit.
Alone, --persist saves work you will never see again, and --seed loads fixtures you will
lose.
What happens, in order:
- The CLI mints a local admin key (
gb_sk_<projectId>.<secret>) — it has to exist before the server does, because the server installs it at boot. - It spawns
@groveback/serveras a separate process. Not embedded: the server is the whole backend (MongoDB, GraphQL, a Bun runtime) shipped as one standalone binary per platform, while the CLI stays plain Node and dependency-free. npm installs only the binary matching your machine. - The server refuses to start in a production posture — a real
JWT_SECRETwithoutGROVEBACK_DEV=1aborts the boot. A known admin key on a production deployment is not a warning-level mistake. - It applies
groveback/to the fresh, empty server. - It writes
.grove/and prints the dashboard URL with?key=…in it.
It also refuses to start if something already answers /api/v1/health on the port —
otherwise everything downstream would be talking to a stranger.
Storage is in-memory by default, so data is gone when you stop. That is the point: your fixtures are the source of truth, not a local database you have been mutating for a month.
--seed --persist makes that painless. On exit, --persist runs both halves of “save my
work” — the shape into groveback/, the documents into seeds/ — and --seed loads them
back next time:
saved: 1 config file(s), 3 document(s)Persistence made of files, not a database volume: what you built goes into the repo, and the rest of the team gets it from git.
The local dashboard
Section titled “The local dashboard”grove dev prints:
http://localhost:8099/dashboard?key=gb_sk_proj_dev.…#/There is no login form, because there is nothing to log into: the dashboard’s login goes
through the control plane, and a grove dev server has no control plane — it has exactly one
project, and the key embeds its id. The dashboard stores the key and strips it from the
address bar so it does not linger in history. After the first visit,
http://localhost:8099/dashboard#/ is enough.
Sections that need the control plane (Connections, Environments) are hidden rather than left to fail with a 401 you cannot act on.
The Local section shows how your bundle differs from the real project. That comparison is
computed by the CLI, not the browser — it needs the project’s admin key, and that key must
never reach a web page. grove dev runs the comparison, writes a credential-free snapshot,
and the dashboard renders it. Applying stays in the terminal.
grove push
Section titled “grove push”grove status # what would change, without changing anythinggrove push --dry-rungrove pushAlways plans first and asks before applying. A non-TTY stdin answers “no” — silence is never consent.
Three guards worth knowing:
- It refuses if the project moved since your last
pull. Pull records a hash per file; if someone else pushed in the meantime, applying yours would silently clobber theirs.--forceoverrides. - Config import never deletes. Removing a resource from the bundle does not remove it from
the project. Deletion is
--prune, which names every resource before removing it, and which never offers to delete a collection or a bucket. --dry-runexits 2 when there are changes, so CI can gate on it without parsing output.
Fixtures
Section titled “Fixtures”Shape comes from groveback/. Data comes from fixtures you commit:
seeds/ posts.json # a JSON array, or NDJSON — the format is sniffed authors.jsongrove seed # load seeds/ into the local projectgrove seed --reset # wipe those collections firstgrove seed --dump # local documents → seeds/grove dev --seed --persist does the load and the dump for you at the two moments that
matter. These are the manual versions, for when you want one half on its own.
Fixtures keep their id, which makes a seed run idempotent and keeps cross-references between
fixtures stable — but it also means seeding a collection that already has them collides.
--reset is the way to reload.
--reset deletes documents, so it refuses any target that is not demonstrably a local
server. A mistyped --target wiping production is the one catastrophic failure this command
could have.
One committed client, two environments
Section titled “One committed client, two environments”The URL baked into the generated client is always the one in grove.json, never the
--target you generated against. Baking localhost:8099 into a committed file would break
the app for everyone else on the team.
To point the app at a local server, set an environment variable — no regeneration:
GROVE_URL=http://localhost:8099 npm run devResolution order: an explicit baseUrl option → $GROVE_URL → the URL baked at generation.
What to commit
Section titled “What to commit”| Path | Commit? | Why |
|---|---|---|
grove.json |
yes | No secrets by design. |
groveback/ |
yes | The project’s shape, reviewed in PRs. |
seeds/ |
yes | Everyone starts from the same data. |
<outDir>/grove.ts |
yes | The generated client. |
.grove/ |
no | Local state — it is gitignored and writes its own .gitignore. |
.grove/ holds the running server and its key (dev.json), the divergence baseline
(pull.json) and the dashboard’s diff snapshot (local-status.json). It self-ignores with a
.gitignore containing *, so a missing app-level .gitignore can never leak the dev key.