A full-stack toolkit for applications whose state is a document. An observable document is the model, the wire and the storage format at once: assign to it, and the change is a commit that a page renders, a peer receives and a store keeps. Every piece is a package with one job, and a package that does not need another does not know it exists.
A document:
import { atomic, createObject, observer } from '@aweftjs/core';
const doc = createObject({ title: 'plan', done: 0 });
const stop = observer(doc).path('title').watch(() => console.log(doc.title));
doc.title = 'plan b'; // one commit; the watcher runs once
atomic(() => { doc.title = 'plan c'; doc.done = 1; }); // one commit, however much it writes
stop();A page:
import { mutable } from '@aweftjs/core';
import { h, mount } from '@aweftjs/ui';
const Counter = () => {
const clicks = mutable(0);
return <button theme="button" onClick={() => clicks.set(clicks.get() + 1)}>clicked {clicks} times</button>;
};
mount(document.body, <Counter />);A server:
import { auth, paths } from '@aweftjs/auth';
import { fromDirectory } from '@aweftjs/modules/node';
import { createServer } from '@aweftjs/server';
import { node } from '@aweftjs/server/node';
import { createStore, memoryDriver } from '@aweftjs/store';
const store = createStore({ driver: memoryDriver(), declare: { ...paths } });
const server = createServer({ sources: [fromDirectory('./modules'), auth], store, gate: 'auth/Gate', listener: node({ port: 8080 }) });
await server.start();That is the whole boot. Everything else the server does is a module in ./modules, and the
page reaches it over one socket that carries both the shared documents and the calls.
recipes/full-stack/ is the two halves in one directory, with the page reaching the server in
development through the dev server's proxy; its README carries the manifest and tsconfig an
application starts from.
Everything from codec to modules runs anywhere, and so does a sandbox compute room. dom,
ui, icons and client are the page, and the two page halves of sandbox sit over them;
server and jobs are the Node side; auth has a half on each. A package imports only from
its own tier or below, never across that line.
| Package | What it is |
|---|---|
@aweftjs/codec |
the encoding: values, deltas, commits, ids, positions |
@aweftjs/core |
observables, the deltas they produce, commits, scopes, identity |
@aweftjs/schema |
the shape a document must keep, checked before a commit lands |
@aweftjs/sync |
commits between two documents over any channel, both ends equal |
@aweftjs/store |
persistence over a driver; a memory driver and a Postgres driver ship |
@aweftjs/modules |
modules from directories, bundles or documents, loaded in dependency order |
@aweftjs/sandbox |
isolated rooms for module code that came from a document; @aweftjs/sandbox/page and @aweftjs/sandbox/room for a room with a page in it, an act in a frame |
@aweftjs/dom |
direct DOM binding, hydration and static render; @aweftjs/dom/router for URLs |
@aweftjs/ui |
components, theming, a stage for routed pages, head tags |
@aweftjs/icons |
icon sets as modules, one icon per import |
@aweftjs/client |
one connection to a server for the life of a page |
@aweftjs/server |
connections and requests behind a gate, over a listener you supply |
@aweftjs/auth |
the first battery: the gate, sessions, sign-in and sign-up, the names a person holds, per-user state, and the mail flows over notify |
@aweftjs/health |
the health battery: GET /api/health, 200 when the process and its store answer, with the application's own info beside |
@aweftjs/static |
the static battery: a directory of files served for what no route matched |
@aweftjs/logs |
the logs battery: what a page and the server did, per visit, in the store, with readers any process imports |
@aweftjs/uploads |
the uploads battery: a file from a page or a module, kept in a directory or a bucket and served at /files/<id>, with the application's rules in configuration |
@aweftjs/notify |
the notify battery: one send to a person over inbox, email and push, the inbox shared live on the page |
@aweftjs/jobs |
a scheduler over an array of jobs the application holds |
@aweftjs/ssg |
a routed site written out as files, taken over in place when the browser arrives |
@aweftjs/build |
the vite plugin and the Node loader: JSX and markup to h, static hoisting, asserts out of a release |
@aweftjs/testing |
the harnesses the stack tests itself with, for a driver or a listener of your own |
@aweftjs/debug |
a document or a commit read back as text |
Getting them. npm i @aweftjs/core @aweftjs/ui, and so on for whichever the application
needs. Node 24.12 or later. A published package carries compiled JavaScript, because Node will
not strip types from a file under node_modules (design 256).
An application that also changes the stack carries this repo as a git submodule
(git submodule add <this repo's url> aweft) and resolves each @aweftjs/* name through a
file: dependency on the package's directory instead. That application reads the TypeScript
source, so a fix is live on the next run with nothing to build: it asks for the source by name,
with node-options=--conditions=aweft-source in its .npmrc and aweft-source in its
tsconfig's customConditions. recipes/full-stack/README.md shows the manifest, the two
configs and the two skills under .claude/skills/ an application links.
recipes/README.md: everything that works, as programs the gate runs, indexed by the task you arrived with. Each ends with what it does not do for you.AGENTS.md: the rules, in two halves. The first is for building an application with the stack, the second for changing the stack.spec/: the wire format, the id scheme and the conformance fixtures, which are what a second implementation in another language would be held to.
Each package's README says what it is for and what it will not do for you, and every public
export carries a block comment with its parameters, what it returns, what it throws and an
example. docs/architecture.md is the plan, and docs/design/ is one note per decision.
MIT.