BotanaryBotanary
Guides

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 dev

That'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:

VariableDefault
NEXT_PUBLIC_API_BASE_URLhttp://localhost:3000/v1
NEXT_PUBLIC_API_MOCKINGenabled
NEXT_PUBLIC_AGENT_BASE_URLhttps://api.app.botanary.xyz/agent
NEXT_PUBLIC_WAITLIST_API_BASEhttps://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 off

The 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 every ports/ 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.

On this page