OpenGrammar
Privacy-first, self-hosted writing assistant: a browser extension with a 156,000-word offline engine and more than 40 rule patterns, an AI router that only sees what local rules miss, and a Hono backend that deploys to five runtimes.
- offline dictionary
- 156,000 words
- rule categories
- 30
- backend tests
- 98
- ai providers
- 5 (BYOK)
- deploy targets
- 5
- writing score
- 0-100
- editors supported
- 4
The whole system on one page: local rules first, AI second, and a backend that runs anywhere. Open the image for the full-size animated version.
OpenGrammar started with a simple annoyance. Every writing assistant sends what a person types to a server, keeps a copy for training, and charges a subscription for the privilege. We wanted the same quality without the upload, so the first engine runs in the browser and an AI provider only sees the sentences the local rules could not fix.
The project is a Chrome extension plus a self-hostable backend. The extension injects into Gmail, Google Docs, Notion and Reddit, underlines issues as they are typed, and offers rewrites, tone changes and writing statistics. The backend is one Hono app that runs on Node, Bun, Deno, Cloudflare Workers, Vercel and Netlify.

What it does
- Checks as text is typed. A content script watches rich-text editors, extracts the text, and paints underlines with suggestions in place.
- Runs the first pass offline. A 156,000-word dictionary and more than 40 rule patterns across 30 categories work with no network call at all.
- Sends only the leftovers to AI. The local findings are removed before any provider call, so tokens are spent on context, tone and phrasing rather than typos.
- Brings an AI key of choice. OpenAI, Groq, Together, OpenRouter or a local model through Ollama. The key lives in browser storage and never reaches our servers.
- Adapts to the writing. The engine detects casual, technical and formal contexts and scales its strictness to match.
- Scores the result. A 0-100 writing score covers correctness, readability, engagement and sentence delivery, with trends over time in the stats page.
Two engines, one pass
The local engine is the interesting half. It holds a 156,000-word dictionary and a rule set organized into 30 category modules, from articles, verb tense and subject-verb agreement through to inclusive language, formality and readability. Everything in that layer is a regular expression with a suggestion and a reason, which is why it returns in milliseconds and works on a plane.
The AI router handles what rules cannot judge: whether a sentence carries the right tone, whether a paragraph needs a tighter rewrite, whether a phrase reads as passive in context. Requests go to the provider configured in the options page, and the response is merged with the local findings. Deduplication matters here. Without it, the AI re-checks every typo the dictionary already caught, which wastes money and doubles the underline noise.
Tone rewriting, autocomplete and custom prompts ride on the same router. The whole engine is behind a /analyze endpoint that returns issues with original, suggestion, reason, offset and a confidence score.
Deploy it anywhere
The backend has no database and no session state, so it fits almost any runtime:
- Cloudflare Workers with Wrangler, live at
cf.opengrammer.eu.cc - Netlify Functions, live at
nl.opengrammer.eu.cc - Vercel Edge through the adapter in the repository
- Docker from
swdhinbiswas/opengrammar-backend, a multi-architecture image - NPM as
opengrammar-server, which starts withnpx opengrammar-serveron Node, Bun or Deno
All five speak the same four routes:
| Method | Endpoint | Purpose |
|---|---|---|
| GET | / |
status dashboard and engine version |
| GET | /health |
health check |
| POST | /analyze |
grammar and style analysis |
| POST | /autocomplete |
context-aware completion |
The NPM route is the one we use most. It runs on an old laptop, a Raspberry Pi, or an Android phone through Termux, which makes a private grammar API available on a network nobody else can reach.
Privacy by architecture
The privacy claims are structural rather than promises:
- No database. The backend stores nothing, so there is nothing to leak, subpoena or migrate.
- No API key on the server. Keys live in browser storage and travel with the request per call.
- Stateless processing. Text in a request is analysed in memory and discarded when the response is sent.
- Open source under Apache-2.0. Every rule, route and permission in the manifest can be read and changed.
That model is also the reason the project can stay free. There is no inference bill to cover, because the heavy model runs on the writer's own key, and the local engine costs nothing to run.
Quality
The backend ships 98 tests through bun test: 92 cover the engine, including rule behavior, deduplication and the dictionary, and 6 cover the server routes. CI type-checks and builds both the backend and the extension on every push, and the repository keeps a contribution guide for the rule library so non-developers can suggest patterns through an issue template.
Stack
| Part | Technology |
|---|---|
| Extension | TypeScript, React, Vite, CRXJS |
| Backend | Hono, Zod, compromise |
| AI providers | OpenAI, Groq, Together, OpenRouter, Ollama |
| Runtimes | Node, Bun, Deno, Cloudflare Workers, Netlify |
| Packaging | npm, Docker Hub |
| Tests | bun test, 98 cases |
Links
- Website and extension download: opengrammer.eu.cc
- Documentation: github.com/swadhinbiswas/opengrammar/tree/main/docs
- Source: github.com/swadhinbiswas/opengrammar
- NPM: opengrammar-server
- Docker: opengrammar-backend
- Public edge API: cf.opengrammer.eu.cc/health and nl.opengrammer.eu.cc/health
The hard part
What made it hard
Rules that fire on everything
Early versions matched "very unique" in poetry, flagged passive voice in a changelog, and marked short sentences as errors in a text thread. The fix was a context layer: every rule carries a category, the analyzer detects the writing mode, and strictness scales with it. Precision matters more than recall when the underlines appear inside someone's email.
Keeping the local and AI layers from double-reporting
Both engines can find the same problem, and the AI version arrives with a different offset because the local suggestions changed the text. The router now receives the text with local issues already suppressed, and the merge step maps AI offsets back onto the original string. Without that mapping, underlines landed on the wrong words.
One backend, five runtimes
Node, Bun, Deno and edge workers differ in ways that the Hono layer mostly hides, but the entry points do not: filesystem access, environment variables and port binding all needed a separate server file. The repository keeps server-node.ts, server-bun.ts, server-deno.ts and server.ts and the shared logic lives underneath, so a fix lands once.
Chrome extension rules are unforgiving
Manifest V3 content scripts cannot hold state across page navigations, service workers are killed when idle, and injecting into Google Docs means fighting a canvas editor with its own clipboard model. The extension keeps state in storage, re-injects on route changes, and treats each supported editor as a separate adapter with its own text extractor.
A key that never leaves the browser
Sending an API key from the extension to our backend so it could call a provider would have simplified the code and broken the whole promise. Instead the backend accepts a key per request, uses it for that call only, and never logs it. The rule library and the provider routing had to be written so that no path stores a credential.
Outcome
What exists today
- A Chrome extension that checks Gmail, Google Docs, Notion and Reddit, with a rewrite panel, tone controls, custom prompts and a writing statistics page
- An offline engine with a 156,000-word dictionary and more than 40 rule patterns across 30 categories
- A BYOK AI router for OpenAI, Groq, Together, OpenRouter and Ollama, with deduplication against local findings
- A single Hono backend that deploys to Cloudflare Workers, Netlify Functions, Vercel Edge, Docker and NPM
- Two public edge APIs that answer
/analyzeand/healthtoday - 98 backend tests, 92 of them on the engine, plus type-checked builds for both packages in CI
- The whole project under Apache-2.0, with a rule contribution guide that does not require writing TypeScript
What I'd do differently
If we built it again
- Split the engine and the server into separate packages from the first commit. The shared types moved twice before settling, and each move broke the extension build.
- Put rule categorization in the data model, not in the checker functions. The context filter works, but it had to be retrofitted onto rules that assumed one level of strictness.
- Treat each rich-text editor as an adapter with a test page from day one. Google Docs alone changed its DOM twice during development.
- Write the key handling path first. Building the happy path and then removing every trace of the credential was more work than designing the stateless flow upfront.
- Keep the dictionary behind an interface. It started as a module import, which made the extension bundle heavier than it needed to be for users who only wanted AI checks.