Skip to content

SoftN v0.0.25

Choose a tag to compare

@github-actions github-actions released this 23 Oct 03:40
· 231 commits to main since this release

SoftN v0.0.25

What's new

  • Upgrading from v0.0.24: the engine is ZIPP v0.0.29 (it was v0.0.27), in the browser and in the Rust backend host. What logic can see of it is in its entry below; among it, an error's stack now holds frames that count from the app's logic (at save (logic:12:5)), and a class written as a statement with no name (class { }) is a SyntaxError. Four entries marked "Upgrading" below change what an existing app may notice: a page may be written as HTML, with every existing .ui and .logic app reading as it did; an app's style block styles the app and nothing else; softn.net.fetch sends no cookie or referring address, and net without allowed_hosts no longer reaches this computer or its local network; and local AI loads GGUF checkpoints on ZIPP, with ONNX Runtime, transformers.js and Kokoro gone. Logic that opens with "use strict" now runs strict. A FormLogic that installs this release rebuilds its server sandbox from ZIPP v0.0.29 (with Rust 1.92.0, as for v0.0.27); FormLogic's checks, its UI and PHP suites and its hosted-runtime and ZIPP-sharing tests pass on this release's archive as FormLogic stands.
  • Size: the components every app downloads at start are the thirty most apps draw (the layout, text and images, a button, a text field, a form and a list), and the other thirty-nine light ones (Select, Checkbox, Modal, Tabs, Table, Avatar, Progress, Tooltip and the rest) are five groups, controls, feedback, data, display and navigation, each fetched by the page that draws one. A page with only the common components parses 147 KB less JavaScript before it is drawn (858 KB to 711 KB in the standalone runtime, 921 KB to 773 KB in SoftN Web) and is drawn about 90 to 150 ms sooner on a CPU slowed four times. A page's first draw waits for the groups its first screen names, for at most 300 ms, so it opens drawn rather than around placeholders, and the runtime remembers what each app's first screen waited for, so its next open starts those groups while the bundle is still arriving: a repeat visit to a page that draws all five is as quick as it was. A first visit to such a page is 20 to 60 ms slower at the median, within the measurement's run-to-run spread. SoftN Web keeps the groups in its precache, so an app that uses them works offline as before. @softn/components/minimal keeps every export it had; essentials and the five groups are new entries (docs/engineering/COMPONENT_LOADING.md).
  • While an app loads, the runtime says what it is downloading. After 400 ms the loading screen shows a bar and a line for each piece (the app's bundle, the 6.5 MB script engine, the torch package, the components its first screen draws) with sizes where they are known, in SoftN Web and the standalone runtime. A page is drawn before the engine is ready, and an app that draws its screen from its script is blank until then, so a thin line along the top of the app, with a small label ("Script engine · 34%"), then shows what is still arriving: on a first visit over a slow connection, the engine its taps are waiting for; later, a page's 3D. A load from the cache is over before either shows. A host can read the same list (core's loadProgressItems and subscribeLoadProgress, runtime-shell's LoadProgress).
  • The engine is ZIPP v0.0.29 (it was v0.0.27), in the browser and in the Rust backend host. What logic can see of it, each checked on the engine before it and on 0.0.29's: TextEncoder and TextDecoder are the engine's own, in every app's logic, where only a page given the document had them; they read and write UTF-8 exactly as the document's copies did (the two were run on the same inputs), read UTF-16LE besides, and a page given the document now has the engine's. Error.prototype.stack holds the error's frames, where it was empty, and each frame says where in the app's logic its code is: at save (logic:12:5) is line 12 of the logic, counted from its first line, however much the runtime compiles ahead of it, in the browser, in the script worker and on both backend hosts. The logic is the app's logic files joined, with their imports inlined, so for an app with one logic file those are that file's lines. A syntax error is placed by the engine itself, and the page and Studio's check quote the line it names as before (SyntaxError: unexpected token ';', at this line of the logic: let c = ;), now in the script worker too, where the engine's message stood alone. In the browser, a regular expression that backtracks past the engine's ceiling for one search is a RangeError the code can catch, where it ended the call (not in softn.sandbox.run, whose smaller budget runs out first). A class declared inside a block is not seen after it, and new on a Proxy around an arrow function throws a TypeError, as the language says. A class made more than once (a mixin applied to two bases) runs its own super calls, where each copy ran the last one's, and class { } written as a statement, with no name, is a SyntaxError, as in a browser; no repository example or published demo has one. In Python, x in b"..." raises ValueError for an int outside 0 to 255, b"bc" in bytearray(b"abc") is True, in on a range asks a value's own __eq__, and dividing two large ints rounds once, as CPython does (1 / (1 << 1020) was 0.0). ZIPP measures its interpreter two to four times faster on large bundled programs. The module every app downloads is 6.57 MB, 155 KB larger (1.63 MB with Brotli, from 1.59 MB). A FormLogic that installs a release with it rebuilds its server sandbox from ZIPP v0.0.29 (with Rust 1.92.0, as for v0.0.27).
  • Code whose only browser names are TextEncoder and TextDecoder is no longer given the document shell (about 60 KB compressed). The engine has both now and the shell leaves them in place, so such a page loaded the shell for nothing.
  • An app's "use strict" is in force. JavaScript logic that opens with it (comments may come first) ran sloppy everywhere: the runtime, its worker and both backend hosts put their own code ahead of the app's, and the engine reads a directive only at the very start, so this in a plain call was the global object and assigning an undeclared name quietly made a global. The runtime, the worker and the PHP host now put the directive first when the app's code opens with it, on the first line so no line of the app moves, and the Rust host compiles server logic with ZIPP's compile_script_with_preamble_and_origin, which reads the app's own prologue. SoftN's own code runs strict with it (every core test passes with every app made strict), and a strict app's $: values still need no declaration. Logic that does not open with the directive runs exactly as before; no repository example or published demo does.
  • The Rust backend host runs server logic on ZIPP's safe-sandbox profile, the one the browser engine and the PHP host's engine are built with and the one ZIPP gives for untrusted code. A regular expression that backtracks for ever (/^(a+)+$/ against forty a's and a !) is now a RangeError the handler can catch, in under a second; it held its worker for the 25-second wall time and then left the thread running on a core, so a request that reached such a pattern with the right input took a worker each time, and after eight the pool stopped replacing them. Server logic is held to the browser's other caps too: a regular expression's source of 16 KB, JSON nested 64 deep (the host already refuses a body nested past 62), arrays nested 4 deep in one join, and BigInts of 2^20 bits. The interpreter is the memory-safe one and the server binary carries no JIT (one was built in but never used: the step budget turned it off); a handler measured the same on both, about 160 ms of sorting, JSON, regex and objects in a release build.
  • Size: a production build carries no source maps: the site build, every release and the apps' production builds write none, 27 MB of the site with their compressed copies, read by no visitor (scripts/build/sourcemaps.mjs). npm run build:site:dev (--sourcemaps) builds them for a development deployment, BUILD-INFO.json records which, and package-site packages such a build only with --development, as softn-website-<tag>-zipp-<engine>-development.zip. The packages' own dist/ keep theirs for development.
  • Size: each app's assets/core-runtime/ holds only what the workers reach (packages/@softn/core/scripts/worker-files.mjs): the script, sandbox and local-model workers' modules, the model worker's WebAssembly and licences, and the torch package. It was the whole of core's dist, 4.1 MB of which the workers reach 2.8; core's own dist/core-runtime/ mirror is the same set.
  • Offline: Studio and the Builder are installed whole. Their service workers precache everything they ship, every feature, Three.js and its loaders, the QR decoder and the workers' files included, so an installed editor works offline with every feature, while a page still loads each one only when it draws it. The precache revisions the workers' files, whose names do not change from build to build.
  • Offline: an app opened once online in SoftN Web is installed with everything it can reach, so it opens and runs with no connection as an installed app does. The install now also fetches what a component asks for later than its chunk (every Three.js loader, control and effect a Scene3D may use, whatever models it is given; a QRReader's decoder), the sandbox worker for an app whose code uses softn.sandbox, and for an app that asks for ai the AI runtime and the local-model worker, so a model it has loaded before, or one in its own bundle, runs offline. e2e/tests/offline-app.spec.ts opens such an app, waits for Offline ready, takes the browser offline and uses all of it.
  • A tap before an app's script has loaded is answered once it has. The page is drawn as soon as it is parsed and its functions arrived with the script, so the first tap after opening an app on a slow device, or one right after Allow reloads the script, found no function and did nothing. The renderer hands the page a stand-in for each function the script declares, which waits for the script and calls it.
  • A worker-mode app reads a file of its own bundle in the host calls that read one: a model in its bundle (softn.ai.model.load({bundle})) said the bundle had no such file.
  • A worker-mode app's logic imports are placed as they are on the main thread. A file at the bundle's root importing ./b.logic asked for a.logic/b.logic, a ../ path was passed on unresolved, an indented import was left in the code, an import cycle, which the main thread places once, failed the whole load, and logic with no entry path had none of its imports placed. The worker now resolves imports with the main thread's own code (runtime/logic-imports.ts).
  • Size: Studio, the Builder, the desktop loader and the apps @softn/vite-plugin builds load components as the runtimes do, the minimal set with the shell and every other feature (charts, editors, smart views, media and the QR decoder, Scene3D with Three.js and its loaders) the first time a page draws it. They imported the whole library, Three.js included, whatever the page used. A Studio preview of a page with a chart fetches the charts and nothing else, and a page without a scene never fetches Three.js; the Builder's entry went from 2.4 MB to 1.1 MB. The three apps share one module for what is fetched on demand (scripts/build/on-demand-chunks.mjs). Studio's agent check loads every component a page names before it reads the page, and the Builder's canvas draws a component whose feature is still on its way as its stand-in. @softn/components/lazy adds ensureRuntimeComponents(), which registers once per registry.
  • Size: an app carries the ZIPP engine once. The page compiles its engine, the app's one hashed and precached asset, and posts the compiled module to every worker it starts: the script worker of an app that runs in one, the sandbox behind softn.sandbox.run and each local model's worker. Each of them fetched and compiled a second 6 MB copy from assets/core-runtime/, which every deployment carried with its compressed copies; it is gone, so softn-web, Studio and Builder are each about 9.7 MB smaller and core's dist 6 MB, an app that runs its script in a worker installs offline without the second download, and a first local model no longer downloads an engine. A host that configures the engine's bytes (configureZippWasmSource) has them compiled once for the page and every worker. The FormLogic runtime archive still carries a copy at that path in the hosted runtime and both editors, for FormLogic's release fetcher, which requires one there; nothing reads it.
  • Upgrading: a page can now be written as an HTML page is written, and its code kept in .js files, with every existing .ui and .logic app reading as it did (all of the repository's pages and the published demos parse to the same documents as before). Studio makes new projects as index.html and main.js; a page of your own site can run as the app with no bundle (softn/softn.js). The entries below say what changed where.
  • HTML in a page. Lower-case tags are HTML, capitalised ones components, mixed freely. <!DOCTYPE html>, <html>, <head> and <body> are read through: the head's <title> is the document's title, and a <body> with attributes keeps them on a <div>. <br>, <img …>, <input …> and the other void elements end with their tags, where each used to swallow the rest of its parent; a </br> is passed over. <style> and <script> may stand inside an element, and <Logic>, <Style> and <Script> are blocks in any case, where their content was dropped. An <svg> draws its paths, shapes, gradients, clip paths and filters, by SVG's names for them, and what they would load from another site goes through the app's network permission. for, readonly, tabindex, maxlength and HTML's other attribute names reach React by its names, so <input readonly> is read-only. <input value="…"> and checked are a starting value that can be changed; <textarea> starts with its text (it drew "[object Object]"); <pre> keeps its spacing; an <option>'s words reach React as one text; <my-card> is one tag name; and every HTML 4 named character reads (&euro;, &eacute;, &rsaquo;), where 33 did. A plain tag loses is="…" and any on… attribute that is not a handler SoftN made: React writes every attribute of an element it takes for custom, and is makes any element one, so <div is="x" ONCLICK="…"> reached the page as code.
  • A page's files, named as HTML names them. <script src="main.js"></script> is <logic src> and an inline <script> a <logic> block, in the bundle composer, and <script src="main.py"> links Python; a <script> that is data is passed over, and one from another site is not loaded and says so. <link rel="stylesheet" href="styles.css"> places a stylesheet the app carries. <img src="images/logo.png">, <video poster> and url(images/paper.png) in a page's styles show the bundle's file, as asset() does; they were read against the host page's address. A path the bundle does not carry is left as written. <a href="about.html"> to another page of the bundle shows that page in the app, composed as its main page and run afresh, its <title> the document's, with a history entry so the browser's Back returns; the standalone runtime and softn.com's play pages do this, and a page run with softn.js leaves its links to the browser, since each page of a site starts itself. Pages and logic are found by what names them (main, <import from>, <script src>), not by their extension, so a page may be index.html and its code main.js; the directory and the backend hosts take any name for the main page. Core exports the composer's readers of these references (pageLogicSources, pageStylesheetHrefs, logicImportPaths) for editors to share. A tag inside an HTML comment is commented out: a <script> there used to be run, or its file required, and a <link> there placed. The runtime's own softn.js is no part of the app's code, and a linked stylesheet's url(…)s are read from its own folder, as a browser reads them. Several <script> blocks on a page are one script, in the order they are written, where the last replaced the others when a page was parsed by itself.
  • Handlers and logic written as browser code. onclick="…" and the quoted @click="…" run their code with the event as event, for oninput and onchange too, and a closing return false is .prevent; a quoted handler used to be words with a warning. A <form> with no action keeps its page when submitted, where it reloaded the page and the app with it; one with an action still goes there. A logic function handed an event, onsubmit="send(event)" or @input={search}, gets target and currentTarget as what those elements hold (value, checked, name, dataset) and defaultPrevented, and e.preventDefault() and e.stopPropagation() can be called there and do nothing; the first line of such a handler used to throw. Logic written as a module reads as the script it is: export before a declaration is the declaration, and import { a } from "./b.js" is import "./b.js"; each kept the app from opening. A field with a starting value, <input value="" oninput="search(event.target.value)">, can be typed in whatever handles it; one with a handler stayed on its written value. A function named as a handler, @click={save}, is given the event's target and currentTarget on the main thread as in a worker; it was read after the wait for the engine, when React had cleared them.
  • A page's code written for a browser is given the page as its document. Code that names document gets one, built in JavaScript inside the sandbox and holding the page's own plain HTML from its first line: getElementById, querySelector and querySelectorAll with the selectors pages use, createElement, appendChild, remove and the rest, textContent, a real innerHTML, classList, style, dataset, addEventListener with capture and bubble, a field's value, FormData, and DOMContentLoaded and load once the page is drawn. A to-do list written as an HTML page with a <script> runs as it does in a browser. What the code changes is drawn through the renderer's one path for elements, so it is checked as markup is: no <script>, <iframe>, handler attribute or javascript: address gets through innerHTML. A component, a {value}, #if and #each stay SoftN's: the code can find, move and remove them, not change what is inside; an event inside one reaches the code with its real target, so e.target.closest('li').dataset.id on a list SoftN draws works. The code and the markup share state: {count} follows a count++ in a listener. Layout reads (offsetWidth, getBoundingClientRect) are zero, and Python and code in a worker are not given the document. The rest of a browser's window comes with it: fetch as a promise (the hosts it may reach are those the app's permission.json allows net), location (location.href = "about.html" goes to another page of the app, never to another site), URLSearchParams, structuredClone, queueMicrotask, requestAnimationFrame, sessionStorage and alert() (confirm() and prompt() say to ask on the page instead: nothing in the sandbox can wait for an answer); document.title names the tab in the standalone runtime; and onclick="…" in HTML the code writes runs as a browser runs it. A page whose code names none of these compiles and draws exactly as before.
  • Studio makes a new project as index.html, pages/<slug>.html and main.js (or main.py), with <script src> and .html imports, and its AI agent is taught that layout, HTML in a page and .js logic. A project made before (ui/*.ui, logic/*.logic) opens, previews, checks and exports as it did. Studio previews an .html page with the runtime, as it does a .ui one; an .html under assets/ is still shown as a file. Its check no longer reports <img src="assets/…"> on an HTML element, which now shows the file. The directory's source view shows .py files, and the editors highlight an .html page as a page. Its export files a .js as logic only when a page links it or logic imports it (a library, a service worker or softn.js kept beside the app is an asset), and keeps a file the manifest lists under assets there. A link in the preview to another page of the project shows that page, where it took Studio itself to that address.
  • The Builder opens a project written as HTML (index.html linked to main.js) on its main page with that file active (it looked for ui/main.ui and left none active), offers the page's functions as it does for <logic src>, and writes <script src> and the .html imports back as it found them. Its own new projects are still ui/main.ui and logic/main.logic, as is FormLogic's project adapter. Its file tree lists the files at the project's root, index.html and main.js among them, and renaming or moving a file follows <script src> and <link href> as it follows <logic src>.
  • A page of your own site can be the app with no bundle: put the standalone runtime's folder beside it and write <script src="softn/softn.js"></script> first in its head. softn.js keeps the rest of the document from being drawn or run and loads softn-page.js, which fetches the page and what it names (its code and what that imports, its stylesheets, the pages it imports, seed data, and a manifest.json and permission.json when the site has them, the manifest listing any Python modules) and runs it as the standalone runtime runs a bundle; data-page runs another page in its place. Checked in Chromium, Firefox and WebKit. An image's path is read from the page that runs the app, where data-page names another, and a host that answers a file it does not have with its index page (an app host's fallback) is taken to have no such file.
  • The theme writes its side as color-scheme beside its colours on the document. On the standalone page, which stamps no data-theme, the brand tokens' dark default stayed, and a light app's text fields, checkboxes and select menus were drawn dark on its white page.
  • Components, as a person meets them. A date with no time (2024-03-15) in SmartGrid, SmartView and SmartTimeline is the day it names west of UTC, where it showed the day before, and the timeline adds no hour to it. SmartForm sends with an optional number with a min left empty, and keeps what is typed when its data is written inline (data={{ status: 'new' }}), which it put back at every keystroke. SmartStats draws a trend given as a number. A Table with onRowClick leaves Enter and Space on a button or field in a row to that control. Focus goes back to what opened a Confirm, or a Modal whose content takes focus, where it went to the page's body. A bound ColorPicker takes a six-digit colour typed into it (a three-digit one is taken when the field is left), and the first Escape in a CodeEditor in a dialog is the editor's, as its hint says, where it closed the dialog.
  • More components, as review's probes met them. A DataGrid keeps its own filters when the page does not pass them back (they could not be typed in), and an editable column with no editor of its own gets a text field. The tab trap in a Modal or Drawer counts a group of radios as one stop. Tabs and a TreeView with defaultExpandAll take data that arrives after they are drawn. A dismissed Alert comes back when its words change, an Avatar tries a new src after one failed, a DPad hidden while a direction is held releases it, and a Split first drawn where it has no room measures itself once it has. SmartForm takes defaults for a new record. NavItem's page and navigate do what they say, where both were attributes nothing read: page="about" shows that page (it sets the app's currentPage, or page as Studio's projects keep it, and the item is drawn as the current one while its page shows), and navigate="about.html" is a link, shown in the app where the host shows that page itself and the browser's otherwise; an address that would run code is no link.
  • $: values written over several lines are read as the engine reads them: $: visible = todos with .filter(…) on the next line was cut at the first line, and every recompute gave the whole list. Keys an app captures are its own: one app closing no longer releases another's.
  • Peer-to-peer sync is for the app on screen. SoftN Web keeps every open app mounted and shows one tab at a time, and an app's rooms ran on behind the others, talking to its peers and writing what they sent into its records, and went on after the app closed, until the same app was opened again. An app's rooms now wait while another app is shown: nothing is sent or received, and what the app writes meanwhile is exchanged when it is shown again, as on any first connection. A room started while the app is hidden waits too, and one asked for as the app closed is not started. Closing the app leaves its rooms and keeps the one it saved (db.getSavedSyncRoom()). The name a person goes by in a room survives a hide and a show. Sync's signalling sockets close when no room is on them: y-webrtc kept a room's socket open, reconnecting, after the room left, and a room hidden and shown made the next room open a second socket to the same server. An app no longer hears its peers from a background tab; the standalone runtime and the desktop, which show one app, are unchanged. Opening an app that has synced before no longer fetches the sync code (195 KB, with yjs) to close a room an earlier open had left running, and opening a second view of an app no longer closes the room the first is in.
  • Server sync for an app behind another tab. Every app open in SoftN Web kept its socket to the sync server its manifest names, behind whichever tab was on screen. A hidden app now keeps it only while something it wrote is waiting to be sent or answered, and for 30 seconds after (restAfterMs), so a look at another tab and back costs no reconnect; what it writes while hidden is still queued, a hard delete included, and opens the socket to be sent; an app opened behind another tab with nothing to send opens none; and shown again it connects and pulls what changed while it was away. A shown app is live as before, and a host that never says whether an app is shown (FormLogic, the desktop) is unchanged. The files a person picks in a FileChooser are let go when its app closes, where they were kept for the life of the page.
  • Components, from a read of what each one starts and lets go of. Scene3D releases its game keys when the window loses focus and when the scene goes, where the next scene's player walked on by itself until the key was pressed again; one scene going fullscreen no longer resizes every other scene on the page to the window; and a sky is the scene's background, where the plain colour was drawn over it after the first frame. AudioStream runs one pump at a time across a sampleRate change, where a pump still waiting on a worker let a second start and their blocks overlapped. A Microphone reopened while its previous attempt was still loading no longer has that attempt close the new one's device. A Drawer closed behind a Modal opened from it leaves focus in the Modal, which gives it back to what opened the Drawer. PanView stops panning when the mouse was let go outside it before the drag began; Draggable follows only the pointer that began a drag; SortableList drops the item it picked up when the list changed during the drag; and CopyButton's fallback leaves no hidden text field holding the text in the page when the copy fails.
  • More of the same. SmartGrid closes a dialog when its save, add or delete comes back only if that dialog is still the one on screen and nothing was typed in it since: saving one row, then Cancel and Edit on another, closed the second row's dialog with what had been typed in it. Scene3D frees the GPU buffers of an instanced mesh or a particle system whose count changes, where they waited for the garbage collector, and a model or procedural texture kept for reuse lets go of what each scene uploaded of it once no scene shows it, so a closed scene's renderer and canvas are not kept alive by the cache; an evicted model's decoded images are closed. A model built while its textures were refused (asked for before the person chose Allow) is no longer kept for reuse, so the next open loads it whole. An Image whose src becomes the fallback it is already showing is shown, where it stayed hidden behind the placeholder.
  • Records kept in step with a server. An edit made offline survives reconnecting: what the server sends no longer overwrites an edit it has not answered, where the edit was undone and the next one sent the old value back. A record removed for good (hardDelete, db.prune, db.clearCollection) is deleted on the server too, where the next pull brought it back. One server write that failed no longer stops this device's edits from being sent. A script in a worker reads the app's records, where it saw an empty database, and keeps what it wrote. On the desktop a record updated and deleted in the same moment stays deleted, useCollection drops a record another device deleted, and the kept copy of a damaged collection is not copied again on every write.
  • Directory API: the address a bundle is cached under carries its digest (bundle.softn?v=N&sha256=…, on the play page and each version's link), is answered with those bytes or a 404, and is the only one held for a day; ?v=N alone is revalidated, so a demo the seeder rewrote, or a name purged and published again, no longer leaves the play page refusing the app for a day. A new version that fails before it is recorded leaves no bundle and no trust behind. A publish naming a parent, a remix and a new version check their bundle before taking the catalogue's lock, and a new version is counted against its limit first. The seeder no longer republishes and retires a demo on every request when a folder by its name holds no readable bundle, and a storage collection, record id, key or field with a line break after it is refused.
  • XDB is part of SoftN. The Rust crate the desktop runtime and the Rust host store records with is crates/softn-xdb in this repository (taken from xdb.org at f76727f, the commit SoftN pinned), its documents are under docs/engineering/xdb/, and CI runs its tests. Nothing fetches the xdb.org repository any more. The XDB names in SoftN's API (.xdb files, xdb/ folders, getXDB, the desktop commands) are unchanged. A source build of v0.0.24 or an earlier tag still fetches its pinned xdb.org commit.
  • Upgrading: an app's style block now styles the app and nothing else. What each rule styles must be inside the element the app is drawn in (data-softn-app), while what is around it may be anything, so .softn-theme-dark .x still answers to a theme the host sets around the app; html, body and :root are that app's own root as well, so a page's body { margin: 0 } and :root { --brand: … } mean what they meant. A rule that styled something of the host's no longer does. The app's @font-face and @keyframes names are its own (an inline font-family or animation using one follows), and @import, @property, @scope, @page, @view-transition and at-rules SoftN does not know are dropped.
  • Apps are kept to their own part of the page. A bundle's CSS reached the host's whole document: it could hide the permission bar, reword it, set it in a look-alike font, or undo the box the host keeps the app's paint in. The scoping above is read back by the browser's own CSS parser, which removes anything that would still reach outside the app (tested with a corpus of sheets written to make a parser and the browser disagree, in Chromium, Firefox and WebKit: e2e/tests/app-isolation.spec.ts). image-set(var(--u)), and any image function filled from a custom property or attribute, is treated as loading something unknown: it fetched whatever --u held whether or not the bundle had net. The desktop runtime and Studio's preview now keep an app's paint in its box, as the web runtime does, so a position: fixed element cannot cover the permission bar or Studio (the standalone runtime's page layout does while the bar is asking). An app's window.addEventListener('keydown', …) hears the keys aimed at the app, at the page when nothing has focus, and the pointer while the app is on screen: it heard every key pressed in the document, in the host's own fields and in another app's password field, with no permission and while the bar was still asking. softn.input.captureKeys holds back only keys the app hears, at most 32 of them.
  • Upgrading: softn.net.fetch sends no cookie and no referring address of the page an app is drawn in (credentials: 'omit', referrerPolicy: 'no-referrer'); an app that signs in sends its own Authorization header, as every app in use does, or asks the host's backend (softn.backend). net with no allowed_hosts no longer reaches this computer or its local network (localhost, 127.0.0.1, 192.168.…, 10.…, link-local, .local, .internal, .home.arpa): name such a host in allowed_hosts, as the apps that use one already do. That holds for every request the runtime judges, markup included, and in the desktop runtime, which also resolves a name and sends the request only to the addresses it checked. alert() is shown inside the app, not as the browser's dialog, and softn.files.saveFile saves only from a click or a key press.
  • More of what an app is kept from. alert() used the browser's own dialog, which says the host is speaking ("softn.com says…") and holds the whole tab: an app could word a host's request for a password, or raise one on a timer so nobody could reach the permission bar or leave. It is now drawn in the app, as the app's, never while the permission bar is asking, and at most 50 times. A model or image the ai capability downloads needs net too, goes with no cookie, follows no redirect, and a relative address (the host's own server) is refused; with ai alone, a model URL was a request to any https host. Each app may keep 1 MB in localStorage and 3 MB of records in the browser's storage: one bundle with no permissions filled the store the host and every app share, and every write after it failed (an app that already holds more may still write less, and so can always make room). A script's window.__… names are not written to the page's window where the host, its frameworks or SoftN may use them (__TAURI__ written from an app took the page for the desktop runtime, and every app opened after it lost its storage), and the desktop check now asks for Tauri's own bridge. ping, attributionsrc and SVG's xml:base are dropped from markup (each is a request the browser makes by itself, and ping="/a https://…" beaconed past the check), and so is an id or name starting __. A component's other style objects (tooltipStyle, overlayStyle) are judged as style is; a Popover's placement must be one of its four, a Spinner's thickness a number and a Switch's colours colours, since each was written into CSS where any other text loaded an image or added a rule for the whole page. A file an app saves is offered under a plain name: no right-to-left overrides or other invisible characters, no Windows device names, and a kind of file that runs when opened is saved as text. The document shell's marker for its elements can no longer be guessed by the app's markup.
  • HTML's boolean attributes are on when they are there, whatever they say, as in a browser: <input disabled="">, <p hidden=""> and <details open=""> were drawn as if the attribute were absent, since React reads the empty string as no. That is what a page's script writes, so el.hidden = true, button.disabled = true, toggleAttribute('hidden') and details.open = true did nothing on the page.
  • A second lock behind SoftN's own checks: the site, the web runtime, Studio, the Builder and the standalone runtime are built with a Content-Security-Policy (scripts/build/script-policy.mjs) under which nothing runs as script but the deployment's own files, its inline scripts by hash, and WebAssembly; no plug-ins, and no <base> that moves the page's addresses. If one of SoftN's checks ever let an onerror, a javascript: address or a <script> through, the browser would still run none of it. It says nothing about what an app may fetch, so it costs an app nothing. The AI capability's engine files now come from the site itself, as the speech worker's do, not from a CDN. (Hashes are taken of a script as the browser reads it, with its line endings made LF, so a build on Windows does not refuse its own.)
  • More of what a browser gives a page's code. Forms check themselves as a browser does (checkValidity(), reportValidity(), validity, validationMessage, setCustomValidity(), from required, type, pattern, minlength, maxlength, min, max and step), and requestSubmit() sends only a valid one; form.email and form.elements.email find a field; FormData, Headers and URLSearchParams can be listed (Object.fromEntries, for…of), and a <select multiple> gives every choice. URL, btoa/atob, TextEncoder/TextDecoder, crypto.randomUUID() and crypto.getRandomValues() (ChaCha20 keyed by the host's own random bytes: numbers a page can rely on), AbortController, EventTarget, DOMException, MutationObserver, IntersectionObserver, ResizeObserver, matchMedia(), innerWidth/innerHeight, and history with popstate and hashchange (within the page; the host's address is not changed). structuredClone copies dates, maps, sets and values that hold themselves. Layout is measured once asked for: an element's first offsetWidth is zero, and from then on it is its size on the page. <template> is never drawn and its content can be copied; <dialog> has show(), showModal() (drawn over the app inside its box, not in the browser's top layer), close(), returnValue, <form method="dialog"> and Escape; a <details> the person opens is open to the code. NodeList.item(), iterating a classList, Object.keys(el.dataset), whole a.href and img.src, <output>'s value and a date field's valueAsDate. What the code puts on document.body or document.documentElement (a theme class, a CSS variable) is drawn on the app's root, checked as any element's attributes are. fetch("data.json") reads the app's own file, with no network, as an image of its is read.
  • A page written for a browser behaves as one in two more ways: a script that throws as it starts stops there, and the page stays drawn with a note of why (it was replaced by an error); and a handler in its markup that SoftN does not read (onclick="this.classList.toggle('on')") runs as a browser runs it, with this and event, on a page with code of its own or none, where it did nothing. A backslash in a quoted attribute of an HTML element is a backslash: pattern="[A-Z]{3}-\d{3}" asked for the letter d. An HTML element whose end tag may be left out ends where HTML ends it: an <li> at the next <li> or the list's end, a <p> at a <div>, a cell at the next cell or row; an unclosed <li> held the rest of the page. Pointer events carry pointerId, pointerType and pressure, and a drag's dataTransfer holds what dragstart put in it until the drop. (Parser version 10; every repository page and published demo reads as it did.)
  • A page is read as HTML5 reads it, and SoftN's own syntax in any case. <DIV CLASS=card>, <BR>, ONCLICK="…" and </DIV> are HTML's, values may be unquoted (src=images/logo.png), <textarea> and <title> hold text, an SVG's CDATA is its text, and &amp and &copy read without their semicolons. #IF, #Else If, #EACH (item IN items), <IMPORT … FROM>, @CLICK.PREVENT and :BIND are SoftN's, and <datatable> is DataTable where no HTML element has the name. Misnested markup is built as a browser builds it, by HTML's list of active formatting elements and its adoption agency: <b><i>x</b>y</i> opens the <i> again after the <b>, <b><div>a</b>c</div> moves the div out of the <b> and leaves c unformatted in it (inside copies of any formatting elements between), an <em> a </ul> closed is opened again around the text after the list, a link ends at the next link, an end tag stops at a block or a table it may not reach through, an <svg> or <math> ends at a <p> or a <div>, a table moves what has no place in it to before it and ends at a table written in it, a ruby's annotation ends a paragraph left open in it, and the implied <tbody> and a row around stray cells are added. A differential test held 58,000 random fragments to Chromium's own parser with no difference but SoftN's deliberate ones: /> ends any tag, a form inside a form is kept, a form written in a table holds its rows, </form> closes what is open in the form, and space alone between tags (a page's layout) does not open a formatting element again, where HTML opens it around the space and puts every later block inside it. No more than 64 formatting elements are open again at once, where a thousand paragraphs each leaving a <b> of its own open made a million elements. Nesting deeper than 512 elements is placed beside the 512th with one warning, as Chromium places it; past about 2,000 levels reading a page ran out of stack, and the renderer stops drawing at 512 levels where it overflowed at about 800. Page after page of unclosed <b><i> made copies of copies (a second for 500); at most three of a kind are now open again, as in HTML. <div style>, style={busy && {…}} when false and style={3} are drawn with no style, where React threw and the page went with it, and an #if in a <textarea> gives it its text, where it threw too. Every repository page reads as it did. (Parser versions 11 and 12.)
  • Expressions are read as JavaScript reads them, held to it by a differential test of 20,000 random expressions and 8,000 random handlers. Numbers may be written .5, 3., 0b101, 0o17 and 1_000; 0b101 and 1_000 were 0 and 1 followed by a name, which an argument list then took as one more argument, so JSON.stringify(0o17) was "0" with nothing said. A missing comma between arguments, list items or record fields (f(a b), [1 2], { a: 1 b: 2 }) is an error where it was read as if it were there, and ?? beside a bare || or &&, which JavaScript refuses, is a warning asking for parentheses. <!-->, <!---> and a comment ended by --!> end where HTML ends them, where the first two hid the rest of the page; <xmp>, <iframe>, <noembed> and <noframes> hold text, as in a browser. (Parser version 13.)
  • A quoted attribute may run over lines, as in HTML: <path d="M0 0 with the rest of the path on the next lines, or a long class="…" split in two, ended at the first line break, read the rest as attributes and lost the element's children. A page saved with Windows line endings reads as one saved with LF: a <pre>, a <textarea>, a template literal and every text kept a \r at each line's end. Strings and template literals read every escape JavaScript does, where a template read `\x41` as "x41" and a line ending in \ (a continuation) was a line break, or with CR LF ended the string. A byte-order mark at the start of a page is passed over, without counting as a column. Every repository page, demo and FormLogic page, and 260 pages of two other projects, read as they did. Expressions read the bitwise operators and shifts (flags & 4, n | 0, ~x, 1 << n, >>>, and |= and the rest in a handler) at JavaScript's levels, where each was an unexpected token that cut the expression short; {hidden?.3:1} is a choice of .3 or 1, as in JavaScript, where it was read as ?. and refused; and Intl is there to format a price or a date (new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" }).format(total)), where it was undefined and showed nothing. Held to JavaScript by 80,000 more random expressions and 8,000 handlers. (Parser version 14.)
  • What a person brings from another way of writing pages reads, or is named with what SoftN writes instead. #if ok and #each item in items may leave out their parentheses, the header running to the end of its line; #if ok read the rest of the page as its condition, and the page drew nothing. #each (item of items) is the loop in writes. title=`Hi ${name}` is the template literal it looks like, where it was the attribute title and one attribute per word inside it. Svelte's and Handlebars' {#if}, {:else} and {/each} are said once each, with SoftN's #if (…) … #end, where they were a run of errors (one about regular expressions) and Svelte's {#if took the rest of the page as its condition; {{ name }}, which shows a record, is a warning; markup written inside braces ({ok && <p>yes</p>}) is told to use #if or #each; Angular's *ngIf, [value], (click) and [(ngModel)], which were read in pieces with nothing said, and Vue's v-if, v-for, v-model and :value="…" are each named with the SoftN to write.
  • An HTML page's <script src> and stylesheet <link> are found where the parser reads tags. One whose attribute held a > (onload="if (a > b) go()", title="a>b") lost its src or href, and the page its code or its rules; one shown as text in a <textarea>, <title>, <style>, <xmp> or CDATA, or a <link> a script writes, was loaded as the app's; and a script shown in a textarea ran on and swallowed the real one after it. Every demo bundle composes as it did.
  • The document a page's code is given behaves as a browser's in many more places, held to Chromium by an audit's 4,367 selector checks and 533 innerHTML cases. A list built a row at a time no longer kills the page's script past about 2,000 rows (each parent's children are sent once a turn, and what the host cannot read in one piece is read in pages, never dropped), and the usual while (items.length) items[0].remove() no longer runs the engine out of time: children, childNodes, getElementsBy… and attributes are live. A click listener sees a checkbox or radio already changed, as in a browser; a style with ; in a url() is kept whole, with !important; HTML's entity rules apply in innerHTML; SVG keeps its names' case (viewBox) and />; a <template>'s content is its content; outerHTML can be set; innerText keeps its line breaks; classList can be indexed; TextDecoder decodes as the Encoding Standard says; selectors understand escapes, :has(), :nth-of-type and every other common pseudo-class, and refuse a bad one with a SyntaxError; a disabled <fieldset> disables its fields; capture listeners run first at the target; and URLs, URLSearchParams, FormData and structuredClone follow their standards more closely. Nothing found could make the page itself run code. The shell is 60 KB compressed where it was 34 KB, and is loaded only for an app whose code names document.
  • Records kept in step with a server lose no edit to a reload, a retry or one record too large. The queue of edits the server has not answered is kept with the records (it was in memory, and an app closed before reconnecting lost its offline edits to the next pull); one record has one edit in flight, so a retried push no longer costs the next edit; one record that cannot be saved here, or one edit larger than the host's 4 MB message limit, no longer stops every edit after it. A record deleted on another device while this one was away now leaves it: pulls name where the last ended, and the server sends the deletions since. A Date reaches peers as its date, not {}; clear() and a second hardDelete reach the server and the room; a peer rejoining with nothing no longer writes an older copy over the room's; useCollection shows a record that comes back; and db.prune and db.clearCollection are one write, where they took seconds for a few thousand records. Found by an audit that model-tested 1.8 million operations and fuzzed both kinds of sync.
  • Directory API: a form body is held to its route's limit as a JSON one is (a 2 MB comment sent as a form was taken where the same as JSON was a 413); the storage summary, a count over the whole store, is counted as a read of it, and past the limit the app's page still answers with its storage unavailable; one invalid UTF-8 byte in a field no longer empties the field; a linked app cannot be named as a parent; hidden categories no longer fill the room for new ones; the thousandth app whose name has no Latin letter gets a name of its own; a new version that fails to be recorded leaves the app's icon as it was; a backup leaves out the temp files a killed writer left on Windows too; and the development router's redirect cannot name another site. Found by an audit that also tried zip-slip and zip-bomb archives, keys across apps, origins and headers, with nothing found there.
  • Components take a number as a page writes it. An HTML page writes every attribute as text, and <Progress value="40"> showed 0%, <Rating max="7"> drew five stars, <Pagination siblingCount="1"> added 5 + "1" and showed page 51, and a number where a yes or no goes (required={0}, checked={count}) drew a stray "0" and wrote aria-checked="1": an audit found 49 number props in 30 components and 64 yes-or-no props in 39 that did so. The renderer now gives each registered built-in's props the kind its props interface declares (prop-kinds.generated.ts, written with the component manifest): a number written as text is the number, a number where a yes or no goes is that yes or no. Text that is not a number, and "false", reach the component as written, and a host's own component by a built-in's name gets nothing of this. Props that take a number or CSS text read "300" as 300 pixels (<Image width="300">, <Grid columns="3">, <Avatar size="64">).
  • Components no longer take a page down over a value of the wrong shape, found by drawing each of the 105 about 700 times with wrong and edge values. A list where a Set goes (<DataGrid selectedKeys={[1]}>, <TreeView expandedIds={["1"]}>, which a page cannot write otherwise), a list where text goes (<SmartView fields={["name"]}>), a number where a list goes, NaN or a function in a prop CodeEditor, MarkdownEditor, DataGrid, Table or SmartForm follow (which re-rendered without end), a callback written as text, and a QR code with more text than one holds each threw; <RadarChart levels={1e7}> drew a ring per level until the page's memory ran out (it draws at most 20). Charts at width="100%" draw at their own size where every coordinate was NaN; Accordion and Tabs items without keys are keyed by place, where one click opened them all; text given as a list of names is read as whole names (hideColumns="nameLong" also hid name); Slider marks may be numbers; GaugeChart, Drawer, Skeleton and the EmptyState presets take an unknown choice as their default; a Radio option with no value answers to its label; Pagination without onPageChange no longer throws on each press; and holes in Kbd's and SortableList's lists show nothing where they showed "null".
  • HTML an app hands a component to show (a RichTextEditor's value, an Icon's SVG) loses what the renderer already takes off markup: ping, which with two URLs (ping="/x https://…") was judged as one local address and beaconed to any host on a click, from an app with no net; an id or name starting __, which made the element a global of the host's window (window.__TAURI__); and attributionsrc, xml:base and is. One rule (dropsFromMarkup) now serves both. Found by an audit that ran about 32,000 inputs through the sanitizer, the renderer and the CSS scoping in Chromium, with no script run in any.
  • softn.com, from an audit of every page and flow. Start over during a batch publish stops it after the upload in flight and says which apps went live, where the rest of the batch kept publishing; Publish another starts an empty form, where one more press published a duplicate; a folder of bundles can be dropped, as the page said; an app that does not exist is titled so and kept out of search results; the rating bars count a vote at once; a second press on Share closes it, and cancelling the phone's share sheet opens nothing. The landing page loads its live app only when it scrolls into view, and on a phone only when asked (about 1.8 MB it fetched for every visitor). Pages have a skip link, the directory's search a visible focus ring, and docs code in light mode passes contrast; an edit key on the update page is hidden until shown. The guides' SmartGrid, SmartStats and SmartTimeline examples use the props those components have, document and Studio's new project layout are described as they are, and the docs' Copy button no longer sticks.
  • The Builder, from an audit of every panel. Delete or Backspace on a collection in Data asks first, as the Delete collection button does, where one key took the collection and its records with no undo. The property panel takes each prop's default and choices from the component itself (the component manifest now records them, and a test fails if the two ever disagree): LineChart's interactive could never be turned on, Card's dividers never off, and "Default (md)" gap drew no gap, with about a hundred defaults like them. The Export dialog's hosts field keeps what is typed, separators included; looking at a logic file no longer clears the page's Undo; a page written with Windows line endings is written back with them; an HTML page keeps its <body> and </html> through a canvas edit. Focus stays near what was deleted, every palette row has its icon, the file tree is one Tab stop, the warnings an opened bundle raised can be read, the hosts field says a local host must be listed, and the code editor's focus ring is visible.
  • Studio, from an audit of every view. Its check parses every page and compiles the logic, and names the file and line of what does not read: a page missing a </Card> or function broken( { in main.js said "No problems", left Run and Publish on and showed the app as Running. Any error now holds Run and Publish, and the preview says "Does not parse" or "Does not compile". The "is ready for…" notice no longer covers the preview's tools. Each style's swatch is what it makes: Clean, Bold and Playful were drawn light and made dark apps, and now make light ones; Dark makes dark. On a phone Studio has Files, Pages and History too; the preview has a Desktop, Tablet and Phone switch; Ctrl+S saves anywhere in the editor; a skip link, a proper Settings tablist, focus that lands somewhere after a rename, a delete or Approve, and History rows that say what each was. The agent and the bundled examples write the HTML layout new projects use (index.html, pages/, main.js).
  • The runtimes, from an audit of the web runtime, the desktop loader and the standalone runtime. A bundle's remote logic import (import "https://192.168.1.1/x.js") is judged by the rule softn.net.fetch follows, so it no longer reaches this computer or its local network unless allowed_hosts names the host; it went out after Allow. A page run with softn.js lays out at a phone's width, where the head's viewport meta was swallowed and it drew at 980 pixels. The web runtime's server sync sends the bundle's auth_token and keeps a query in the server's address, and every host builds the sync address one way (https://h was wss://h//sync on the desktop). The installed web runtime shows Home from its cache when the network stalls for 3 seconds, where it waited as long as the stall. A library card is a button with its actions beside it, not inside it; the consent dialog describes a private collection as private and its scopes in words; removing a running app keeps Home and offers to stop it; a missing app says it was not found; /play/ follows the light or dark theme; and /web/ has skip links. The desktop loader reads bundles with the same pipeline as the web runtime, and CI runs its Rust tests.
  • Records kept with a server, from an audit of the client against the Rust and PHP hosts. An edit the host overruled reaches the device that made it, in pieces under the message limit, where a 2.4 MB correction was dropped as too large and the losing edit stayed; a pull never steps past a record it had to skip. Records written before sync started (it starts after Allow) are sent when it does, where they were never sent and every later edit to them was refused. A pull the host is too busy for is asked again, and a push it cannot take is retried, where the client waited until the socket dropped. What a host's hook does to a record, and what a route writes, reach every connected device. A client keeps only the collections it asked for, and pulls a collection the first time it writes to one. An answer about a pull no longer sends pushes twice, and the host takes a resent push once. A message over the limit, invalid text or a binary frame closes with a code that says so. Each side works with the other's earlier version; the limits both keep are in one file both test suites check (apps/softn-host-rust/sync-protocol.json). The PHP host's 404 carries code: "not_found", as the Rust host's does.
  • The theme file reader (generateThemeCss, exported and used by nothing here) writes no value that would end its CSS rule or load something (red; } body { … }, url(…)), and no theme whose name would leave its selector.
  • Upgrading: local AI runs on ZIPP, and ONNX Runtime, transformers.js and Kokoro are gone. softn.ai.model.load reads a GGUF checkpoint (a Hugging Face repository such as "owner/name" or "owner/name:Q4_K_M", {huggingface, file}, {url} or {bundle}), not an ONNX repository, which it refuses by name; generate takes the same messages and options (max_new_tokens, temperature, top_p, top_k, do_sample, repetition_penalty, enable_thinking) and answers {text} as before. softn.ai.onnx.*, softn.ai.pipeline, generate, embed, classify and run answer an error saying they were removed and what replaces them. softn.audio.speak({provider: "kokoro"}) is spoken by a system voice and says so (fallbackFrom: "kokoro"), loadSpeechModel answers neural-speech-removed, and speech asks for no permission, since it downloads nothing. None of the repository's apps used the removed calls; the AIChat and TheOffice demos name an ONNX model and need a GGUF one.
  • Local language models on ZIPP (docs/engineering/LOCAL_MODELS.md). The checkpoint's architecture is a ZIPP Python plugin compiled by SoftN's own engine, and ZIPP's compute runtime runs it on WebGPU, WebGL2, compiled WebAssembly kernels or plain JavaScript, falling back from the GPU to the CPU kernels when the GPU refuses the program. Quantized matrices stay in the file's Q4_K/Q6_K blocks on the device and a token's embedding is one row read when it is needed, so Qwen3-0.6B holds about 400 MB, not 2.3 GB; on an NVIDIA GPU in Chromium it generates about 60 tokens a second. Each model lives in a Worker of its own (core-runtime/runtime/model-worker.js, 290 KB with two WebAssembly modules of 234 KB), so releasing it, or stopping a reply that will not end, is a terminate; softn.ai.model.cancel stops a reply where it is. The demos come from softn-Examples v1.0.3, in which AI Chat and TheOffice load Qwen3-0.6B as a GGUF checkpoint (unsloth/Qwen3-0.6B-GGUF:Q4_K_M, about 400 MB) where they loaded an ONNX model load now refuses; AI Chat takes text only, and the unlisted AIDemo, which exercised removed calls, is gone. A conversation that grows by a message reads only the new part, and a long prompt is read in chunks. A context the compute runtime's budget will not hold is halved until it fits (Qwen3-0.6B asked for 4096 was refused outright before), and a chunk program that does not fit beside the decode program leaves the prompt to be read a token at a time. A model is downloaded once into Cache Storage and read from disk by range, never held whole in memory; ai.maxModelSizeMB (default 1024) is checked against the declared size and against the bytes as they arrive, a hub download is refused unless its bytes came from Hugging Face's own hosts, and a model URL needs net as softn.net.fetch does. A message that spells one of the model's control tokens (<|im_end|>, <think>) is encoded as plain characters, so it cannot end its own turn or open a system one. ZIPP's model stack is vendored as source from the v0.0.28 tag (packages/@softn/core/vendor/zipp-models/, every file's digest in SOURCE.json, the GPU runtime checked against the release's embed kit) and is in the licence inventory.
  • A model in an app's own bundle loads: {bundle: "assets/m.gguf"} is read where the app's markup would find the file. No host passed the runtime a bundle reader, so a bundled model could never load before.
  • Slimmer: each deployment drops about 80 MB of ONNX Runtime WebAssembly, the speech worker, its voices and transformers.js; a checkout's node_modules is 850 MB smaller (44 packages). @types/node is declared at the root, where it had only been present because transformers.js pulled it in.
  • A model download is the app's: it stops when the app closes (and no Worker is started for it), and when nothing has arrived for a minute. The site's model cache, which every app shares, keeps four files and 4 GB at most, letting the oldest go, so no app can fill the storage other apps' records live in by loading one URL after another; when the origin has no room, or the cache refuses the file (a private window's small quota does), the model is downloaded again and held for that load only rather than failing it.
  • The desktop runtime's build copies core's runtime files as the web hosts do (scripts/build/core-worker-assets.mjs): without core's nested core-runtime/ mirror, 12.9 MB of duplicates, and without the host-JavaScript engine, which its own copy step shipped in every desktop build although no Worker can reach it and no app runtime may carry it.
  • The Builder's code editor is SoftN's own (@softn/components' CodeEditor: a real text field over a highlighted copy of its text, in the brand's colours for both themes) instead of Monaco. The Builder's build is 24.1 MB where it was 33.8 MB, its main script 2.4 MB where it was 5.5 MB, and it no longer precaches Monaco's 5.6 MB TypeScript worker or its icon font; a checkout's node_modules loses 99 MB. What goes with Monaco is its autocompletion and its JavaScript diagnostics. Tab still indents and Enter keeps the line's indent; the key that lets Tab leave the editor is Esc, on every platform (it was Ctrl+M, or ⌃⇧M on a Mac).
  • @napi-rs/canvas (38 MB of native code) is no longer installed with the repository. It draws the committed PWA icons, which change when the mark does: scripts/brand/generate-icons.mjs says to install it for that run, and the test that redraws them is skipped without it. The check of the desktop icon packs reads their PNGs with Node's zlib instead, so it runs everywhere.
  • Core's core-runtime/ mirror and every app's copy of it leave out source maps, which no Worker loads: 3.1 MB fewer in each (the mirror is 9.9 MB where it was 13.5 MB). The dev server still serves them from core's dist.
  • The web runtime, Studio and the Builder build their service workers themselves (scripts/build/pwa.mjs) instead of through vite-plugin-pwa, whose build half bundled each worker with Babel, Rollup plugins and terser: 200 packages and 30 MB of node_modules. The worker is still Workbox's runtime, bundled with esbuild into one sw.js; what each app precaches, the revision each file gets, the web runtime's coi.js (imported first, so documents keep their isolation headers), its four runtime caches and immediate take-over, Studio's and the Builder's wait for Reload, the navigation fallback and the manifests are as they were, checked file by file against the previous build and in Chromium offline. workbox-window is no longer shipped: the registration hook (virtual:softn-pwa/react) is thirty lines over navigator.serviceWorker. A checkout's third-party packages are 228 MB in 555 packages, from about 400 MB at the start of this release's slimming.

Downloads

Download What it is Pick it when
softn-website-v0.0.25-zipp-v0.0.29.zip The complete softn.com website. The whole softn.com in one folder: the landing page, the app directory, the web runtime, Builder, Studio, the documentation and the directory API. You want to run your own SoftN site: a directory people can publish apps to and play them from.
softn-app-static-v0.0.25.zip One app on a page (static). The smallest way to host one SoftN app: a plain page with a loading spinner, a permission bar and your app, with no SoftN branding and no server code. You have a .softn app and a web host, and you want the app on a page of your own site.
softn-app-static-with-backend-linux-x64-v0.0.25.zip One app on a page, with an optional PHP server backend. The static single-app page above, plus a backend/ folder that lets the app run its own private server logic and SQLite database on an Apache/PHP host, with no service to keep running. Your app has a private server part (server/ logic, SQL migrations, routes) and your host is Apache with PHP on Linux x86-64.
softn-app-private-v0.0.25.zip One app served privately by PHP. The single-app page again, but PHP renders it and hands the app's files to the browser piece by piece behind a viewer cookie, so the .softn file itself is never at a URL. You do not want visitors (or crawlers) to be able to download your app as one file.
softn-app-private-with-backend-linux-x64-v0.0.25.zip One app served privately by PHP, with an optional server backend. The privately served page above together with the PHP server backend: three folders, webroot/, private/ and backend/. The app works at once without the backend; you enable the backend when the app needs it. You want the .softn file kept off every URL and the app has a private server part.
softn-formlogic-runtime-v0.0.25.zip The SoftN runtime for FormLogic. Everything FormLogic embeds from SoftN, already built: the sandboxed frame FormLogic runs an app in, Builder and Studio as editors inside FormLogic, the server-side runtime for native apps, and the starter adapter. FormLogic’s build downloads this from the latest SoftN release instead of building SoftN from source. You run FormLogic and its build cannot reach GitHub; download this and point SOFTN_RELEASE_ARCHIVE at it.

Each archive unzips with a README.md at the top that explains it; RELEASE-GUIDE.md below compares them all.

The backend download targets Linux x86-64 with Apache/PHP and process execution enabled. It is a generic, unconfigured distribution: install your own public and private app bundles and preserve existing configuration, keys and databases when upgrading. Native XDB synchronization remains a Rust-host feature.

The complete website archive's name carries the tag of the zipp engine inside it. It ships no
example apps: the directory starts empty, and .softn files dropped on any
page of the site publish into it — one, or a folder at once, with the admin
key from data/config.json lifting the hourly limit for the site owner. The
example apps are published as .softn downloads with every
softn-Examples release.
The desktop loader and builder are not attached to releases; they build from
this tag's manifests with npm run tauri build in their apps. RELEASE-GUIDE.md
below compares every download and says which one to take.

The single-app archive includes a small counter example; replace it with your own bundle. It has no app download controls, but browser-delivered app bytes remain extractable. Required license notices are included in all archives.

Built with ZIPP v0.0.29 (its javascript-python-base engine, javascript engine and torch package; the release SHA256SUMS has sha256 d92c1d763caf703143a52291fee3723f2d486745640adc2db3aa3812f521f4a7).