An in-app assistant for your own website, embedded with one script tag. Users ask in plain language; OpenWalk answers right on the page.
Walks users through tasks. Ask "how do I invite a teammate?" and OpenWalk highlights the one next element to click, explains why, and replans after every action against the live DOM – guidance never goes stale. It gets better with use: the server passively learns which screens exist and how they connect (element labels and paths only, never anything anyone types), so it can route people to a setting buried three screens deep.
Explains the screen. Ask "what is this?" or "which plan am I on?" and the vision route reads the current page – visible text and form state, ephemeral, nothing stored – then answers with notes pinned to the exact fields and sections it talks about, one at a time.
Helps fill forms. On a confusing form, OpenWalk explains each field and proposes values; clicking a note's Insert button places the suggestion into the field. The user stays in charge: insertion only ever happens on that explicit click, never into password fields, and nothing is ever submitted – the widget highlights and narrates, users do every click.
Bring your own LLM key (Google, Anthropic, or OpenAI), point it at any Postgres, and it is yours: no gateway, no telemetry, no lock-in. MIT licensed.
Want this on other people's sites instead of your own? That's openwalk.ai – a hosted service with a browser extension, built on this exact code.
Requires Node ≥ 24 and Docker.
-
Run the server:
npm install cp .env.example .env # fill in the three required lines, see below docker compose up -d db # local Postgres on :54320 npm start
.envneeds a provider key (GOOGLE_GENERATIVE_AI_API_KEYfor the default model), anOPENWALK_ADMIN_TOKEN(any secret, e.g.openssl rand -hex 24), andOPENWALK_ALLOWED_DOMAINS– the sites this server is willing to serve. The server refuses to start without the last two; an accidentally open deployment is not a thing. -
Embed the widget in the app you allowed:
<script src="http://localhost:3900/widget.js" defer></script>
The widget appears bottom-right; ask it something like "how do I invite a teammate?".
-
Inspect your data: open http://localhost:3900 for the admin UI – see what has been collected per site, watch routing decisions and token spend, and purge anything.
GOOGLE_GENERATIVE_AI_API_KEY=... OPENWALK_ADMIN_TOKEN=... \
OPENWALK_ALLOWED_DOMAINS=yourapp.com docker compose upOne command brings up the server and its Postgres (compose also reads a
./.env file, which beats inlining secrets). To use an external Postgres,
run the image directly:
docker build -t openwalk .
docker run -p 3900:3900 \
-e DATABASE_URL=postgres://… \
-e GOOGLE_GENERATIVE_AI_API_KEY=... \
-e OPENWALK_ADMIN_TOKEN=... \
-e OPENWALK_ALLOWED_DOMAINS=yourapp.com \
openwalkThe server is a single plain-Node process; it runs anywhere Node 24 and a Postgres connection exist — Fly.io with Neon Postgres is a proven combination.
Say you run yourapp.com and deploy OpenWalk at openwalk.yourapp.com:
-
Deploy the server with
OPENWALK_ALLOWED_DOMAINS=yourapp.com. It will plan, store, and answer CORS for your product's pages and refuse everything else – nobody can quietly point your instance (and your LLM bill) at other sites. -
Add the tag to your app's pages:
<script src="https://openwalk.yourapp.com/widget.js" defer ></script>
The widget talks to the origin the script came from (
data-serveroverrides it). Privacy rules are built in – element labels only, never typed values – and chat state lives in the page's own sessionStorage.
By default every widget user's browsing feeds the site knowledge (pages and
navigation edges – labels and structure only). You may not want your users
contributing anything stored: seed the knowledge yourself by clicking through
the app with the widget once, then turn off Collect site knowledge for the
origin in the admin UI (Sites → your site). The server then drops /observe
payloads for that origin – guidance keeps using the knowledge you seeded, it
just stops growing. Add data-observe="off" to the script tag so your users'
widgets don't even send observations; the admin switch stays the actual
guarantee, enforced server-side.
Plainly, so you can decide what to rely on:
OPENWALK_ADMIN_TOKEN(required). Bearer token for the admin API (/api/*): everything collected, and the power to purge it. The server refuses to boot without one.OPENWALK_ALLOWED_DOMAINS(required). The widget API (/guide,/observe) only serves pages on these domains, enforced server-side on the page URL of every request – it bounds what your deployment will ever plan for or store, no matter who calls it. It also drives CORS: only allowlisted origins get cross-origin browser access, so other websites can't use their visitors' browsers against your server. (*exists for personal any-site servers and is always an explicit choice.)OPENWALK_TOKEN(optional). Shared Bearer token on the widget API. Real protection for a private server; on an embed it sits in your page source, so treat it as an app id, not a secret.- API keys never reach the browser. The LLM provider key lives in server env only. The widget talks to your server, nothing else.
- What the server stores is deliberately boring: page digests (path, title, element labels), navigation edges, walkthrough recipes (goal + instructions), routing traces (goal keywords only), and token usage counts. Values typed into the page are never stored. Explain questions send the visible page text and field values (passwords masked in the browser) for the answer only – rendered into the prompt, persisted nowhere.
OPENWALK_BLOCKED_DOMAINS(optional). Subtracts from the allowlist – a kill switch that always wins.
One honest caveat: an allowlisted deployment still exposes /guide to
anonymous callers claiming to be on your domain (they can burn your LLM
tokens, they just can't do anything else). If that risk matters to you, put a
rate limiter or your CDN's bot protection in front. The composable server
(createOpenwalkHandler – auth, model resolution, and usage metering are
injectable) is how openwalk.ai adds per-user
authentication on top.
| Env var | Default | Purpose |
|---|---|---|
DATABASE_URL |
required | postgres://… storage connection |
OPENWALK_ADMIN_TOKEN |
required | Bearer token for the admin API (/api/*) |
OPENWALK_ALLOWED_DOMAINS |
required | comma-separated domains the widget API serves (subdomains included); * = any |
GOOGLE_GENERATIVE_AI_API_KEY |
– | key for google/… models (the default model is Gemini) |
ANTHROPIC_API_KEY |
– | key for anthropic/… models |
OPENAI_API_KEY |
– | key for openai/… models |
OPENWALK_MODEL |
google/gemini-2.5-flash |
provider/model for the planner and router |
OPENWALK_VISION_MODEL |
OPENWALK_MODEL |
optional smarter model for explain answers (planning stays on the fast model) |
OPENWALK_TOKEN |
(off) | optional Bearer token on the widget API |
OPENWALK_BLOCKED_DOMAINS |
(none) | domains to refuse even if allowed (kill switch) |
PORT |
3900 |
server port |
- On pages carrying the widget, it passively records a digest of each screen a user visits – path, title, element labels – one row per page (site knowledge).
- When someone asks something fresh, a tiny router call decides between two paths: explain – the question is about the current screen, so the vision model reads the page's visible text and answers, optionally pinning note bubbles to the fields it explains – or navigate – the step planner takes over.
- For walkthroughs, the LLM picks the one element on the current page to act on next – site knowledge steers navigation, but only live elements are ever highlighted.
- The widget outlines it with an instruction bubble; clicking the element (or pressing Next) advances, closing the bubble stops the walkthrough.
- After each action – including full page navigations – the page is rescanned and the next step is planned from what is actually on screen.
- Every routing decision (route, reason, model, goal keywords) is recorded and shown in the admin dashboard's Routing area, and every LLM call's exact token usage in its Usage area.
npm run dev:server # node --watch, runs the TypeScript directly
npm run dev:admin # vite dev server for the admin UI, proxies /api
npm run build # server + admin + embeddable widget
npm run typecheck
npm test # unit tests (db suite skipped without TEST_DATABASE_URL)
npm run test:pg # full suite against a throwaway Dockerized PostgresLayout: server/ (plain-Node API + composable handler), widget/ (the
in-page widget, built into admin/dist/widget.js), admin/ (React admin
UI, served by the server at /).
- Shared community walkthrough library (opt-in publishing, versioned per app)
- Knowledge export/import between origins (seed production from staging)
MIT.
Guided automation of a third-party app may conflict with that app's terms of service. OpenWalk only ever highlights and explains – users do the clicking – but use it on apps you're authorised to use.