Local development
Running the frontend against mocks, then the full backend stack (anvil, Postgres, Redis) when you need real state.
Botanary's local dev setup is layered on purpose: you can build against the frontend alone with no backend at all, or bring up the full stack when you need real on-chain state behind it.
Frontend: mocks by default
pnpm install
cp .env.example .env.local
pnpm devThat's the whole loop for MSW-only mode, and it's the default: the app runs fully against mocks, with
no backend. Mocking is gated by one flag - NEXT_PUBLIC_API_MOCKING=enabled - and when it's set, a
MockGate component dynamically imports the mock worker and starts it before anything else renders:
useEffect(() => {
if (!MOCKING) return;
void import('@/mocks/browser')
.then(({ worker }) => worker.start({ onUnhandledRequest: 'bypass', quiet: true }))
.finally(() => { if (active) setReady(true); });
}, []);The worker itself (setupWorker(...handlers) from msw/browser) is defined in src/mocks/handlers.ts
against fixtures in src/mocks/fixtures.ts, both typed with openapi-msw's createOpenApiHttp<paths>
- the same generated schema the real client uses (see API client). If you
ever need to reinstall the MSW service worker file itself:
pnpm msw:init(msw init public/ --save).
Real Privy auth only gets wired up when mocking is off and an app ID is configured - it's never
active while NEXT_PUBLIC_API_MOCKING=enabled, so nothing in this mode ever depends on a live Privy
connection.
Frontend env vars
.env.example ships non-secret defaults, safe as-is for the MSW-only build:
| Variable | Default |
|---|---|
NEXT_PUBLIC_API_BASE_URL | http://localhost:3000/v1 |
NEXT_PUBLIC_API_MOCKING | enabled |
NEXT_PUBLIC_AGENT_BASE_URL | https://api.app.botanary.xyz/agent |
NEXT_PUBLIC_WAITLIST_API_BASE | https://api.app.botanary.xyz/v1 |
NEXT_PUBLIC_PRIVY_APP_ID ships commented out - it's deferred until you actually need real Privy auth
locally.
Frontend scripts
pnpm dev / build / start (Next dev, build, serve), pnpm typecheck (tsc --noEmit), pnpm test
(vitest + Testing Library), pnpm test:e2e (Playwright, run against MSW), pnpm lint / pnpm format,
and pnpm api:generate to regenerate the client from the OpenAPI contract.
Backend: the full stack
The backend's local integration stack is a docker-compose file with two entry points:
docker compose up --build # core: anvil + one-shot contract deploy + postgres + redis + backend
docker compose --profile frontend up # + the Next.js frontend, talking to the real backend, mocking offThe core stack boots an anvil chain (--chain-id 31337, one-second blocks), then a one-shot
contracts-deploy job deploys AgentGuard and a mock USDT to it and writes the resulting deployment
addresses to a shared volume - signed with anvil's own well-known default test account, explicitly
called out in the compose file as never a real key. A comment on the file explains why the backend
itself still runs against fake bundler/paymaster adapters even here: the canonical ERC-4337 singletons
(EntryPoint v0.7, SmartSession, the ERC-7484 registry) aren't deployed on a fresh anvil chain, so the
backend's bundler and paymaster ports stay on fakes while it reads real AgentGuard state and events
from anvil directly. Postgres 16 and Redis 7 round out the core stack; the optional frontend profile
adds the Next.js app pointed at this real backend with mocking disabled.
Ports: the backend serves http://localhost:3000/v1 (Swagger UI at /docs), anvil listens on
:8545, and the optional frontend runs on :3001. On boot, the backend logs exactly what it's doing:
its listen URL, where to find /openapi.json and /docs, and which adapter set it's running -
fake-adapter runs are explicitly logged as running entirely against in-memory fakes.
Backend env vars
.env.example groups its variables by concern - names only here, since values are either placeholders
or genuine secrets that never belong in a docs page:
- Server -
NODE_ENV,PORT,API_PREFIX - Adapters -
ADAPTERS(selects fake vs. real implementations behind everyports/interface) - CORS / rate limiting -
CORS_ORIGINS,THROTTLE_TTL_MS,THROTTLE_LIMIT - Data -
DATA_BACKEND,DATABASE_URL,REDIS_URL - Chain -
ACTIVE_CHAIN_ID,DEPLOYMENTS_DIR,RPC_URL - Privy -
PRIVY_APP_ID, plus its paired secret and verification-key vars (not reproduced here) - Bundler / paymaster - project and RPC endpoint vars for the ERC-4337 bundler/paymaster vendors used, plus a dedicated revoke-only-paymaster toggle; the API-key-bearing vars in this group are deliberately omitted from this page
- Simulation and a handful of other read-only third-party integrations - block-explorer lookups, a depeg-price oracle, and outbound email for the waitlist flow
For local development against the core docker-compose stack, the backend container itself is already configured with sane local values (fake adapters, a Postgres data backend, anvil's chain id, CORS open to the local frontend ports) - you don't need to hand-configure any of this to get the stack running.
Testing
Frontend: pnpm test (vitest + Testing Library) for unit/component tests, pnpm test:e2e (Playwright,
run against the MSW mock layer) for end-to-end. Backend: pnpm test (vitest) for unit tests,
pnpm test:int (RUN_DB_TESTS=1 vitest run) for integration tests that need a real datastore behind
them.
You can build and test the entire frontend - including everything under src/features - without
ever running the backend, a database, or a real chain. Reach for the docker-compose stack only when
you actually need real on-chain state or backend behavior behind your changes.