HOW IT WORKS

How Ink works.

Ink is a private memory for your writing. This is what it does with it, and what it never does.

01

The idea.

Ink is a place to write, and a memory for what you have written. You write. Ink keeps it. Later, while you write something new, Ink quietly shows other pieces of yours that are close to it, including ones you forgot and ones in another language.

That is all it does. It never writes for you, and it never tells you what your words are worth.

Ink is a React 19 and Tiptap editor on a Fastify API, with Postgres (Supabase) underneath. A piece is a Yjs document plus two derived copies: a search copy of its words, and a 1,024-number vector of its meaning.

Everything that looks smart is read-only. It ranks pieces that already exist. Nothing in Ink generates or edits text.

As of October 2026. The numbers and settings below are today’s, tuned on small samples, and may change as Ink is tuned or the model is replaced.

02

Saved on your device first.

A hand-made diagram on sticky notes: You write, then Your device (saved first), then Ink (in the background), then Your other devices (catch up). A dashed note says: Offline? It waits, then goes through. An engineering diagram: Edit, then Browser storage (written first), then Fastify API (PUT /api/pieces/:id), then Other devices (Yjs merge, any order). A dashed branch from storage reads: Offline, it waits, retry on 401, 408, 429.
The change is kept on your device before it goes anywhere.Local first. Sync is async and idempotent.

Every change is kept on your device first, then sent to Ink in the background. If your connection drops, nothing is lost: the change waits, and goes through when you are back.

Ink never reads your words while you type.

Each edit is first written to IndexedDB in the browser, then synced to the API in the background. Pieces are Yjs documents, so edits from two devices merge in any order, and a repeated sync changes nothing (pieces/merge.ts).

A 4xx answer is final for that change, except 401, 408 and 429, which retry. No model is called while you type: vectors are made after a save, off the request.

03

How Ink reads it.

A hand-made diagram: a page called A piece is turned into numbers and becomes a point on a map. The Hindi word बारिश and the word rain sit close together, joined by green threads. Same idea, two languages: close together. An engineering diagram: a piece becomes 1,024 floats and a point in a vector space. The Hindi word बारिश and the word rain sit close together, joined by threads. 1,024 dimensions, drawn here in 2D.
Pieces about the same thing end up close together.Cosine similarity over BGE-M3 vectors.

After you save, Ink reads the piece once, quietly, in the background. It turns the piece into a long list of numbers that stands for what it is about. Two pieces about the same thing end up close together, even when they use different words, or different languages.

The model that does this is open, and it runs on a computer Ink runs itself. Your writing is not sent to an outside company.

The model is BGE-M3 (open weights, 1,024 dimensions). It runs in apps/embedder, a small Python server on sentence-transformers, with the weights pinned to a commit and loaded from safetensors.

The API reaches it only on localhost, Tailscale, or a name in EMBEDDER_PRIVATE_HOSTS, with a shared secret and a 1 MB cap. Each copy handles one request at a time. A search waits at most 5 seconds for it, then shows words only.

Vectors live in Postgres next to the piece, per writer (pgvector, HNSW index, cosine), and are deleted with it.

04

Beside a piece.

A hand-made diagram of circles around This piece: Very close in the middle, Close around it, and grey dots outside marked Too far, not shown. An engineering diagram of similarity floors around the open piece: at 0.53 and above, Forgotten and Loose; at 0.45 and above, Related; below 0.45, dropped.
The closer a piece is, the more likely it appears.Floors: 0.45 for Related, 0.53 for Forgotten and Loose.

Open “Ink sees this too” next to a piece, and you may see these notes:

  • Related. Other pieces of yours that are close in meaning to the one you are writing. Up to five.
  • Forgotten. A piece you have not touched in six months or more, brought back. Up to two. Once shown, it rests for a month.
  • Loose lines. A very short piece, a line or two, that belongs with this one. Up to two.
  • Noticed. On All writing, one quiet remark: three pieces about the same thing, the same idea in Hindi and in English, or an old piece coming back. It is a fixed sentence, not something a model writes.

If nothing is close, Ink shows nothing. It never explains why two pieces are close.

Related ranks your pieces by cosine similarity to the open piece. Floor 0.45 (MIN_SIMILARITY), limit 5.

Forgotten and Loose use a stricter floor of 0.53 (MIN_SIDE_SIMILARITY), limit 2 each. Forgotten needs 180 days since the last edit and rests 30 days after it is shown. Loose is 160 characters or fewer.

Noise is dropped first: pieces under 3 words, near-copies, and repeats of the same words. The panel looks at your 1,000 most recent pieces and the 1,000 closest. A piece with no vector yet falls back to shared words.

Noticed (GET /api/noticed) has three kinds in order: three of the same thing, the same idea in Hindi and English, an old piece returns. Each is a fixed sentence pattern built on your device, never generated.

06

Why it feels fast.

Ink is built so that nothing you do waits for something else.

  • Writing never waits for the internet. Every change lands on your device first, so a slow connection cannot slow you down.
  • Nothing runs while you type. Ink reads your piece after you save, in the background, and only once you have gone quiet.
  • Only the change is sent, not the whole piece.
  • Search answers in two steps. Your words appear straight away. Pieces close in meaning follow a moment later, so you never wait for the clever part.

Meaning search can take a few seconds today. Your words never wait for it.

  • Local first. The typing path ends at IndexedDB. There is no network round trip before the editor responds.
  • Diffs, not documents. Sync sends Y.diffUpdate against the server’s state vector, so only what the other side is missing moves.
  • No model on the request path. An embedding job is queued after a save and runs once the piece has been quiet (EMBED_QUIET_SECONDS), so a burst of saves is one job.
  • Two search requests. ?part=words (PGroonga, after a 200 ms pause) renders at once. ?part=close (HNSW, cosine, after 500 ms) follows. If the embedder is slow or down, the words still answer.

Meaning search takes about 3 seconds on a busy local model. The words request never waits for it.

07

What Ink never does.

  • It never writes, rewrites, corrects or scores your writing.
  • It never translates it. Your words stay in your own script.
  • It has no streaks, goals or word-count pressure.
  • It never uses your writing to train anyone’s models.

No model is called per keystroke, and no text is generated anywhere. Only open-weights models are used, and your writing is never sent to a third-party model API.

08

Honest limits.

  • Hinglish, which is Hindi in Latin letters, is found by its words, not yet by meaning.
  • Pieces under four words are left out of search by meaning.
  • How close is “close” is tuned on small samples of writing, and will change as it is tuned.
  • Search by meaning can take a few seconds. If it cannot answer, you still get your words.
  • There is no phone app yet. Ink works in your phone’s browser.

Hinglish (hi-Latn) is left out of vectors and ranking, because BGE-M3 cannot read Latin-script Hindi. MIN_WORDS is 4 for search and 3 for the panel.

The cutoffs came from small samples and are due to be retuned. The embedder serves one request at a time, so search falls back to words after 5 seconds. Each writer may search 120 times a minute and open the panel 60 times a minute.

09

You stay in control.

Every piece has an “Ink remembers this” switch. Turn it off, and Ink leaves that piece out of Related, Forgotten, Loose lines, Noticed and search by meaning. You can still find it by its exact words, because that is only you searching your own writing. “Delete this piece” removes it for good. Ink keeps only its id, so an old device cannot bring it back.

Only you can read your writing.

Every query is scoped to the writer in the database. A CI job (api-checks) runs a two-writer privacy suite in Bruno on every pull request: writer B must never see, search, relate to or delete writer A’s pieces, and expired or broken tokens are refused.

Signing out ends API access at once, because each request checks auth.sessions. A deleted piece leaves only its id behind (deleted_pieces).

10

Read the code.

Everything described here is in the open. If you want to check any of it, read the code. Open it on GitHub

Source: github.com/NaveenKharwar/Ink. docs/search.md covers search in detail, with the name of the constant behind every number on this page.

Ready to try it?

A note from Naveen

Hello, and thank you.

01  Why I made it

I’m Naveen, and I make Ink. A line you write at night shouldn’t be lost by the morning, and an old poem shouldn’t stay hidden just because you forgot where you put it.

02  What I care about

I wanted one private place where nobody reads your words but you. I’ve put a lot of care and a lot of hours into every small thing, from how a page saves to how the paintings move. I built it with love, for writers like you.

03  Thank you

Thank you for trying it, and for supporting it. It means more than I can say.

Naveen

Naveen KharwarFounder, Ink