[{"data":1,"prerenderedAt":435},["ShallowReactive",2],{"blog-list-en-GB-3-any":3},[4,108,195],{"id":5,"title":6,"author":7,"body":8,"date":94,"description":95,"draft":96,"extension":97,"meta":98,"minutes":99,"navigation":100,"path":101,"seo":102,"stem":103,"tags":104,"__hash__":107},"blog_en\u002Fblog\u002Fwhy-there-will-never-be-ai-in-numori.md","Why there will never be AI in Numori","Erik Bjerke",{"type":9,"value":10,"toc":85},"minimark",[11,15,20,23,30,36,42,46,49,52,56,68,71,75,78],[12,13,14],"p",{},"Every few weeks someone asks when Numori Notes will get a summarise button. The\nanswer is never, and it is worth explaining why, because \"no AI\" sounds like a\nstylistic preference and it is not one.",[16,17,19],"h2",{"id":18},"the-three-costs-nobody-puts-on-the-feature-list","The three costs nobody puts on the feature list",[12,21,22],{},"A generative feature inside a note-taking app carries three costs that are\nusually left out of the announcement post.",[12,24,25,29],{},[26,27,28],"strong",{},"Your data has to go somewhere it can be processed."," Numori Notes stores your\nnotes locally and syncs them through a relay that holds no keys. A model cannot\nsummarise something it cannot read. Adding a summarise button means either\nshipping your notes to a server that can decrypt them, or shipping a model large\nenough to be useful onto your laptop. The first breaks the architecture. The\nsecond is a 4 GB download to save you reading your own shopping list.",[12,31,32,35],{},[26,33,34],{},"Someone has to pay for the inference."," Inference is not free and it does not\nget cheaper per user as you grow — it scales with usage, unlike storage, which\ngets cheaper per gigabyte every year. A company that adds AI features to a\nsubscription priced like ours has three options: raise the price, cap the feature so tightly it is\nuseless, or find another way to make money from the people using it. All three\nare worse than not having the feature.",[12,37,38,41],{},[26,39,40],{},"It changes what the product is for."," Once a feature is expensive, it has to be\npromoted. It gets a place in the toolbar, then a prompt, then a tooltip\nsuggesting you try it. Software that has to justify a running cost stops being a\ntool and starts being a funnel.",[16,43,45],{"id":44},"but-it-is-opt-in","\"But it is opt-in\"",[12,47,48],{},"Opt-in is a real distinction and I do not want to dismiss it. But it decays. The\nfeature that shipped behind a toggle in version 1 is on by default in version 4,\nbecause the metrics said adoption was low and adoption was the thing being\nmeasured.",[12,50,51],{},"More importantly, opt-in does not undo the architectural change. If the server\ncan decrypt your notes when you press the button, it can decrypt your notes. The\ncapability is what matters, not the current policy about when it is used. This is\nthe same argument we make about privacy policies generally: I would rather be\nunable to read your data than promise not to.",[16,53,55],{"id":54},"what-we-do-instead","What we do instead",[12,57,58,59,63,64,67],{},"The calculator in Numori Notes is a reasonable illustration of the alternative.\nIt parses ",[60,61,62],"code",{},"48 GBP in EUR"," and ",[60,65,66],{},"12% of 340"," and gives you an answer. It is a\nparser and a unit graph. It runs in a fraction of a millisecond, entirely on your\ndevice, it gives the same answer every time, and when it does not understand\nsomething it says so rather than inventing a plausible number.",[12,69,70],{},"That last property is the one worth dwelling on. A calculator that is wrong 2% of\nthe time is not a calculator. A great deal of what gets shipped as an AI feature\nis a function that used to be exact and is now approximate, with a friendlier\ntone of voice.",[16,72,74],{"id":73},"the-commitment","The commitment",[12,76,77],{},"No assistants, no summarisation, no generation, no classification of your content,\nno \"smart\" reordering, and no embedding models running over your files to build an\nindex we can query.",[12,79,80,81,84],{},"It is an easy promise to verify, which is the point of publishing everything:\n",[60,82,83],{},"Numori-Notes"," has no inference dependency in its lock file, and it never will.\nIf that changes, you will see it in a pull request before you see it in a\nrelease, and you are welcome to say so loudly.",{"title":86,"searchDepth":87,"depth":87,"links":88},"",3,[89,91,92,93],{"id":18,"depth":90,"text":19},2,{"id":44,"depth":90,"text":45},{"id":54,"depth":90,"text":55},{"id":73,"depth":90,"text":74},"2026-08-18","Refusing to add AI features is not nostalgia or a marketing angle. It is the only position consistent with the other promises we have made.",false,"md",{},6,true,"\u002Fblog\u002Fwhy-there-will-never-be-ai-in-numori",{"title":6,"description":95},"blog\u002Fwhy-there-will-never-be-ai-in-numori",[105,106],"principles","privacy","cvyM0lz5-fr4lHQvGQwem064Ho9fV9nISUdHiDfQbdk",{"id":109,"title":110,"author":7,"body":111,"date":185,"description":186,"draft":96,"extension":97,"meta":187,"minutes":188,"navigation":100,"path":189,"seo":190,"stem":191,"tags":192,"__hash__":194},"blog_en\u002Fblog\u002Fopen-source-has-to-include-the-servers.md","Open source has to include the servers",{"type":9,"value":112,"toc":178},[113,116,120,123,127,130,133,136,140,143,146,150,161,164,168,175],[12,114,115],{},"A lot of privacy-focused companies describe themselves as open source. Usually\nwhat that means is the apps are public and the backend is not. That is better\nthan nothing, and it is much less than it sounds.",[16,117,119],{"id":118},"what-an-open-client-proves","What an open client proves",[12,121,122],{},"Quite a lot, actually. If the client is open and reproducibly built, you can\nverify that it encrypts before sending, that the keys are derived where the\ndocumentation says, and that it does not contain a third-party analytics SDK. For\nend-to-end encrypted content, this is most of the story: if the ciphertext leaves\nyour device correctly, the server's honesty matters much less.",[16,124,126],{"id":125},"what-it-does-not-prove","What it does not prove",[12,128,129],{},"Everything about the parts that are not end-to-end encrypted, which is always\nmore than the marketing implies.",[12,131,132],{},"Metadata is the obvious one. Who you email, when, how often, from which IP, and\nhow large the message was. None of that is in the client's threat model, and all\nof it is retained or not retained by code you cannot see. \"We do not log IP\naddresses\" is a sentence about a config file you are not allowed to read.",[12,134,135],{},"Then there is everything operational: retention windows, backup lifetimes,\nwhether deletion is real, which third parties the service calls, and what happens\nto an account flagged for abuse. All decided in a repository you have no access\nto.",[16,137,139],{"id":138},"the-uncomfortable-version","The uncomfortable version",[12,141,142],{},"The reason server code stays closed is rarely that it contains secrets. Secrets\nbelong in a vault, not in source. It stays closed because it is where the\ncompromises live: the analytics that were supposed to be temporary, the vendor\nthat needed plaintext, the retention window that is longer than the policy page\nimplies.",[12,144,145],{},"I am not accusing anyone in particular. I am saying that \"trust us, the server is\nfine\" is the one claim in a privacy pitch that cannot be checked, and it is\nconsistently the claim that is left uncheckable.",[16,147,149],{"id":148},"what-we-are-doing-about-it","What we are doing about it",[12,151,152,153,156,157,160],{},"Every Numori server is public under the AGPL-3.0. ",[60,154,155],{},"Numori-CRDT"," is the sync\nrelay, ",[60,158,159],{},"Numori-Auth"," is the identity service, and both are readable today. The\nAGPL matters here rather than a permissive licence: it means anyone can run\nNumori as a service, but they have to publish their modifications too. If we are\never acquired and someone decides the sync relay should start keeping a little\nmore, the licence makes that visible.",[12,162,163],{},"This does not make us trustworthy. It makes us checkable, which is a more useful\nproperty, because it does not depend on our intentions staying good.",[16,165,167],{"id":166},"the-catch-stated-plainly","The catch, stated plainly",[12,169,170,171,174],{},"Publishing the server does not prove that the code we published is the code we\nare running. That gap is real and I am not going to pretend otherwise. Closing it\nproperly needs reproducible builds and attestation, which is on the roadmap for\n",[60,172,173],{},"Numori-Updater"," and is genuinely hard.",[12,176,177],{},"What publishing does give you is a specification precise enough to hold us to, an\nindependent implementation you can run yourself, and the ability to leave for\nyour own server without losing anything. That is a much better position than\ntaking a policy page at face value.",{"title":86,"searchDepth":87,"depth":87,"links":179},[180,181,182,183,184],{"id":118,"depth":90,"text":119},{"id":125,"depth":90,"text":126},{"id":138,"depth":90,"text":139},{"id":148,"depth":90,"text":149},{"id":166,"depth":90,"text":167},"2026-08-04","Publishing a client while the server stays closed proves almost nothing. Here is what an open client actually tells you, and what it does not.",{},5,"\u002Fblog\u002Fopen-source-has-to-include-the-servers",{"title":110,"description":186},"blog\u002Fopen-source-has-to-include-the-servers",[105,193],"open-source","otfuyB2w5qHulKJ3ckmNfUKC8qph4SATA-xYe7rNy4Y",{"id":196,"title":197,"author":7,"body":198,"date":423,"description":424,"draft":96,"extension":97,"meta":425,"minutes":426,"navigation":100,"path":427,"seo":428,"stem":429,"tags":430,"__hash__":434},"blog_en\u002Fblog\u002Fbuilding-eighteen-apps-with-one-design-system.md","Building eighteen apps with one design system",{"type":9,"value":199,"toc":417},[200,207,214,218,229,235,239,242,245,286,303,307,318,339,400,403,407,410,413],[12,201,202,203,206],{},"An ecosystem of eighteen applications built by a very small team only works if\nalmost none of the interface is written twice. ",[60,204,205],{},"Numori-UI"," is how we get there: a\nTailwind 4 and Vue 3 component library that every Numori interface, including this\nwebsite, is assembled from.",[12,208,209,210,213],{},"It did not start as a library. It started as ",[60,211,212],{},"components\u002Fui\u002F"," inside Numori Notes,\nand it stayed there for a year before we pulled it out.",[16,215,217],{"id":216},"extracting-it-was-mostly-deleting-things","Extracting it was mostly deleting things",[12,219,220,221,224,225,228],{},"The notes app had twenty-eight components in that folder. Roughly a third of them\nhad a prop that existed for exactly one call site. A ",[60,222,223],{},"Modal"," with a\n",[60,226,227],{},"hideCloseButtonOnMobileWhenScrolled"," boolean is not a design system, it is a\nchangelog.",[12,230,231,232,234],{},"The rule we settled on: a prop earns its place if two unrelated screens need it,\nor if leaving it out forces a consumer to reimplement the component. Everything\nelse became a slot. ",[60,233,223],{}," went from eleven props to six plus a slot, and the\ncall sites got shorter rather than longer.",[16,236,238],{"id":237},"accessibility-is-the-reason-to-extract-not-a-bonus","Accessibility is the reason to extract, not a bonus",[12,240,241],{},"This was the genuinely useful outcome and it was not the one we set out for.",[12,243,244],{},"When keyboard handling lives in each screen, it is done well in the screens\nsomeone was thinking about it in and forgotten everywhere else. Our notes app had\na modal that trapped focus and a settings dialog that did not, because they were\nwritten four months apart.",[12,246,247,248,251,252,255,256,259,260,263,264,267,268,271,272,271,275,63,278,281,282,285],{},"Moving that behaviour into the component makes it structural. ",[60,249,250],{},"Toggle"," renders a\nreal ",[60,253,254],{},"\u003Cbutton role=\"switch\">"," with ",[60,257,258],{},"aria-checked",", so keyboard activation comes\nfree from the platform rather than from a ",[60,261,262],{},"@keydown.space"," handler somebody\nremembered. ",[60,265,266],{},"Button"," takes ",[60,269,270],{},"ariaLabel",", ",[60,273,274],{},"ariaChecked",[60,276,277],{},"role",[60,279,280],{},"tabindex"," as\nfirst-class props, because the alternative — dropping to a raw ",[60,283,284],{},"\u003Cbutton>"," when you\nneed an ARIA attribute — is exactly how a design system gets bypassed.",[12,287,288,289,291,292,63,295,298,299,302],{},"The honest part: it is not finished. ",[60,290,223],{}," does not yet trap focus or set\n",[60,293,294],{},"role=\"dialog\"",[60,296,297],{},"aria-modal",", and it does not close on Escape. Those are real\ngaps, they are written down, and this website deliberately does not use ",[60,300,301],{},"UiModal","\nfor anything that matters until they are closed.",[16,304,306],{"id":305},"tree-shaking-because-a-marketing-site-should-not-ship-a-form-library","Tree-shaking, because a marketing site should not ship a form library",[12,308,309,310,313,314,317],{},"The library ships no precompiled CSS and no barrel file that drags everything in.\nImport ",[60,311,312],{},"UiKbd"," and you get 0.4 kB. Import ",[60,315,316],{},"UiButton"," and you get 16 kB, about 7 kB\nof which is the icon path table.",[12,319,320,321,324,325,63,328,331,332,334,335,338],{},"That last number is why this site delegates icons to ",[60,322,323],{},"@nuxt\u002Ficon"," instead. The\nlibrary bundles the thirty-six MDI glyphs its own components need; a website needs\n",[60,326,327],{},"mdi:github",[60,329,330],{},"mdi:robot-off-outline"," and thirty others that no component uses.\nSo ",[60,333,205],{},"'s Nuxt module takes an ",[60,336,337],{},"icons.component"," option and hands icon\nrendering to whatever the host app already has:",[340,341,345],"pre",{"className":342,"code":343,"language":344,"meta":86,"style":86},"language-js shiki shiki-themes github-light github-dark","numoriUi: {\n  prefix: 'Ui',\n  icons: { component: 'Icon' },\n}\n","js",[60,346,347,360,375,394],{"__ignoreMap":86},[348,349,352,356],"span",{"class":350,"line":351},"line",1,[348,353,355],{"class":354},"sScJk","numoriUi",[348,357,359],{"class":358},"sVt8B",": {\n",[348,361,362,365,368,372],{"class":350,"line":90},[348,363,364],{"class":354},"  prefix",[348,366,367],{"class":358},": ",[348,369,371],{"class":370},"sZZnC","'Ui'",[348,373,374],{"class":358},",\n",[348,376,377,380,383,386,388,391],{"class":350,"line":87},[348,378,379],{"class":354},"  icons",[348,381,382],{"class":358},": { ",[348,384,385],{"class":354},"component",[348,387,367],{"class":358},[348,389,390],{"class":370},"'Icon'",[348,392,393],{"class":358}," },\n",[348,395,397],{"class":350,"line":396},4,[348,398,399],{"class":358},"}\n",[12,401,402],{},"Design systems usually get this backwards and insist on owning the icon layer.",[16,404,406],{"id":405},"what-it-cost","What it cost",[12,408,409],{},"Two weeks, and one genuinely annoying class of bug. Because components are\nconsumed as raw single-file components rather than as a prebuilt bundle, Vite has\nto be told not to pre-bundle or externalise the package. That is three lines in a\nVite plugin now, and it was an afternoon of confusion first.",[12,411,412],{},"Worth it. The next application starts with a working, accessible, translated\ninterface on day one, and an accessibility fix lands in eighteen apps at once\ninstead of in the one somebody filed a bug against.",[414,415,416],"style",{},"html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":86,"searchDepth":87,"depth":87,"links":418},[419,420,421,422],{"id":216,"depth":90,"text":217},{"id":237,"depth":90,"text":238},{"id":305,"depth":90,"text":306},{"id":405,"depth":90,"text":406},"2026-07-21","Numori UI started as a components folder inside the notes app. Extracting it taught us more about accessibility than about component APIs.",{},7,"\u002Fblog\u002Fbuilding-eighteen-apps-with-one-design-system",{"title":197,"description":424},"blog\u002Fbuilding-eighteen-apps-with-one-design-system",[431,432,433],"engineering","design-system","accessibility","LMvkyx75RH1zQuR_oWS9MzEMaE64YOmicZ92a83ietU",1788170791774]