@loadbare/app 0.7.3 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/dist/build/assemble.d.ts.map +1 -1
  2. package/dist/build/assemble.js +67 -6
  3. package/dist/build/assemble.js.map +1 -1
  4. package/dist/core/lb-constants.d.ts +2 -2
  5. package/dist/core/lb-constants.d.ts.map +1 -1
  6. package/dist/core/lb-constants.js +28 -14
  7. package/dist/core/lb-constants.js.map +1 -1
  8. package/dist/core/lb-types.d.ts +4 -10
  9. package/dist/core/lb-types.d.ts.map +1 -1
  10. package/dist/core/lb-types.js.map +1 -1
  11. package/dist/hub/lb-apply.d.ts +2 -2
  12. package/dist/hub/lb-apply.d.ts.map +1 -1
  13. package/dist/hub/lb-apply.js +98 -8
  14. package/dist/hub/lb-apply.js.map +1 -1
  15. package/dist/hub/lb-hub.browser.d.ts.map +1 -1
  16. package/dist/hub/lb-hub.browser.js +61 -26
  17. package/dist/hub/lb-hub.browser.js.map +1 -1
  18. package/dist/server/lb-express.d.ts.map +1 -1
  19. package/dist/server/lb-express.js +2 -7
  20. package/dist/server/lb-express.js.map +1 -1
  21. package/dist/server/lb-server.d.ts +8 -15
  22. package/dist/server/lb-server.d.ts.map +1 -1
  23. package/dist/server/lb-server.js +0 -3
  24. package/dist/server/lb-server.js.map +1 -1
  25. package/docs/TECHREF-1.0.md +141 -100
  26. package/docs/analysis-closed-set.md +210 -0
  27. package/docs/comparison.md +1124 -0
  28. package/docs/prior-art.md +216 -0
  29. package/docs/reference/custom-elements.md +21 -6
  30. package/docs/reference/data-binding.md +120 -40
  31. package/docs/reference/page-files.md +13 -9
  32. package/docs/reference/widgets.md +2 -2
  33. package/docs/roadmap.md +1 -1
  34. package/docs/testing.md +7 -1
  35. package/docs/theory.md +737 -408
  36. package/docs/tutorials/080-widget-requests.md +9 -28
  37. package/package.json +1 -1
package/docs/theory.md CHANGED
@@ -1,53 +1,68 @@
1
1
  # Theory of Loadbare App
2
2
 
3
- by Ken Downs, August 29, 2026.
4
-
5
- Loadbare is one man's answer to the extreme expense and slow performance
6
- of modern web apps. When using modern frameworks, it seems to me that
7
- they are composed entirely of accidental complexity, I seem to be always working
8
- for the tools instead of the other way around.
9
-
10
- I wanted a new way to develop web apps that reduced accidental complexity
11
- as far as possible to zero, and gave me snappy performant apps.
12
-
13
- This essay describes the analysis and conclusions that led to the
14
- design and implementation of the Loadbare application layer.
3
+ by Ken Downs, August 29, 2026. Revised September 14, 2026.
4
+
5
+ Loadbare is one man's answer to the expensive development and slow performance
6
+ of modern web apps. When using modern frameworks, it often feels like
7
+ I am working
8
+ for the tools instead of the other way around. After all of that work,
9
+ I have an application that does not respect the user's time, it is
10
+ slow.
11
+
12
+ My first goal in creating `@loadbare/app` was performance. I want my
13
+ applications to be very snappy, I want them to respect the user's
14
+ time.
15
+
16
+ Efficient development is my second goal, so long as it leads to a
17
+ snappy and performant application. My intuitive sense here is that
18
+ the enemy of efficient development is what Fred Brooks called
19
+ "accidental difficulties", the time you spend servicing the tools that
20
+ allow you to build the application. Driving accidental complexity
21
+ as close as possible to zero therefore became the next goal, as that
22
+ would naturally result in developer efficiency.
23
+
24
+ To begin, I had to redefine some of the popular concepts that govern
25
+ discussion and decisions around front-end development.
15
26
 
16
27
  ## The Principle of Developer Experience
17
28
 
18
- First I had to find my way out of the cruft that clutters
19
- the front end discussions of today, and establish a "Principle zero" that
20
- could be a starting point. This is it:
29
+ Developer experience is often defined in terms of developer ergonomics,
30
+ leaving out the developer's motivation for wanting to be comfortable.
31
+ So I began by establishing a personal definition of the
32
+ ideal developer experience:
21
33
 
22
34
  > The Principle of Developer Experience: The best developer experience is
23
35
  > creating an application that people use and appreciate.
24
36
 
37
+ This principle pre-empts arguments for elegant abstractions and tooling
38
+ that are not obviously and directly contributing to a satisfied
39
+ end user.
40
+
25
41
  This principle combines and aligns business concerns with engineering
26
- concerns, grounding any solution in why we do what we do.
42
+ concerns, grounding any solution in the desire to make a useful application.
27
43
 
28
- This principle also
29
- pre-empts arguments for seemingly elegant abstractions and tooling that
30
- cannot be obviously and easily shown to contribute directly to an
31
- application that end users use and appreciate.
44
+ While no technical solution can magically provide me with the product
45
+ skills to produce an application that users appreciate, I do not
46
+ want the solution to make demands on my time that take me away from
47
+ that goal.
32
48
 
33
49
  ## The Principle of User Experience
34
50
 
35
- The Principle of Developer experience grounds us in the end user,
51
+ The Principle of Developer Experience grounds us in the end user,
36
52
  and from here I considered my own largest single complaint about
37
- web apps: they are so damn slow.
38
-
39
- It seems to me that the front end industry, as represented by the
40
- fat browser frameworks, has abandoned performance completely.
53
+ web apps: they are so damn slow.
41
54
 
42
- I wanted to put performance front and center, and design around
43
- it instead of optimizing for it later.
55
+ The path to a performant solution puts performance first and all
56
+ design decisions follow from that primacy. Performance is not something
57
+ to be optimized after a solution has been found by following other
58
+ priorities.
44
59
 
45
60
  After some research into what people perceive as performant, I came
46
- upon the number of 300ms. Faster than that seems abrupt, slower
61
+ upon the number of 300ms. Faster than that seems abrupt, slower
47
62
  seems sluggish. This led to the next foundation principle:
48
63
 
49
64
  > The Principle of User Experience: a full server round trip should
50
- > complete, from user action through durable persitence to final
65
+ > complete, from user action through durable persistence to final
51
66
  > paint, in a median time of 300ms.
52
67
 
53
68
  This scopes the goal to performance and performance only. All other
@@ -56,414 +71,728 @@ for user display and interaction. We do not need to invent new
56
71
  solutions for accessibility, tabs, buttons and so forth. What we need
57
72
  is a way to use the mature solution and respect the user's time.
58
73
 
59
- But we do have a practical implication: if we have a 300ms budget,
74
+ There is a direct practical implication: if we have a 300ms budget,
60
75
  and roughly assume 200ms median wire time, we must split the remaining
61
76
  100ms between database, non-db server, and browser. Since the database
62
77
  in the limiting case must provide durable persistence, our ideal
63
- processing time for browser and non-db server activity must
78
+ processing time for browser and app server activity must
64
79
  approach zero. In other words, our solution must be a model citizen,
65
80
  chewing up as few cycles as possible while providing the full modern
66
81
  experience of a web app, leaving
67
- as much of the 100ms as possible to the database.
82
+ as much as possible of the 100ms to the database.
68
83
 
69
- ## The Primacy of Essential Complexity
84
+ ## The Principle of Essential Complexity
70
85
 
71
86
  Now we move on to developer efficiency. Fred Brooks gave us a powerful
72
87
  way to consider this, when he made the distinction between "essential difficulties"
73
- and "accidental difficulties". These terms have since been glossed into
74
- "essential complexity" and "accidental complexity", and I will
75
- use the complexity versions to be intelligble to the casual reader.
88
+ and "accidental difficulties", later glossed into
89
+ "essential complexity" and "accidental complexity".
76
90
 
77
91
  Essential complexity is the inherent difficulty of solving the user's
78
92
  problem. This is where we want to spend our time and effort.
79
93
  Accidental complexity is the work demanded by the tools we use to solve
80
- the problem. We want to minimize this to being vanishingly small.
94
+ the problem. We want to minimize this, always pushing to zero.
81
95
 
82
- Because I believe there is some objective truth to my subjective feeling
83
- that the fat browser frameworks have become nothing but accidental complexity,
84
- it seemed necessary to state the princple of the Primacy of
85
- Essential Complexity, which borrows phrasing from both Brooks and
86
- Pike to establish that:
96
+ So, with apologies to Brooks and Pike, here is my mash-up and restatement
97
+ of their ideas, tuned to the current task:
87
98
 
88
- > Essential Complexity Dominates. Show me your abstractions and tooling and
89
- > I shall remain mystified. Show me your tables and your visual vocabulary
99
+ > Essential complexity should dominate. Show me your abstractions and tooling and
100
+ > I shall remain mystified. Show me your tables and your rendering vocabulary
90
101
  > and I shall be enlightened - the organization of the code will be self-evident.
91
102
 
92
-
93
103
  ## The Search For a Solution
94
104
 
95
- The three principles stated above, once articulated, allowed me to
96
- clarify my rejection of the fat browser frameworks: nowhere do they
97
- state the same motivations that I have, and their illustrations and
98
- examples make clear their motivations are irrelevent to mine and
99
- vice-versa. In fact, my
100
- extensive experience with React, Angular and VueJS contributed to
101
- my articulation of these principles, the principles state what I found
102
- lacking in the fat browser frameworks.
105
+ The three principles above give some weight to my intuitive rejection
106
+ of the fat browser frameworks, though it would be truer to say the
107
+ rejection came first. My extensive experience
108
+ with React, Angular and VueJS left me able to state what I
109
+ had found lacking. Once stated, the principles made it plain that the
110
+ frameworks' motivations and mine are irrelevant to each other.
103
111
 
104
112
  This led me to look at the Hypermedia libraries. After several weeks
105
- of iterations, I found myself frustrated. These were very clearly
113
+ of iterations, I found myself frustrated. These were very clearly
106
114
  closer to what I wanted, and the various authors' motivations seemed
107
115
  very close to my own, but I was still seeing more tooling concerns
108
- than I wanted. I do not blame the hypermedia libraries, I think I was
109
- fighting them. But that simply told me we differed on an assumption,
110
- and that assumption remained unidentified and unstated.
116
+ than I wanted. I seemed to be fighting the libraries, which could
117
+ mean there was some unstated assumption we did not share, or perhaps
118
+ they were solving a problem I did not have.
119
+
120
+ ## Different Views of The Problem
111
121
 
112
- It turns out that all of the tools and frameworks I could find are
113
- supporting something I don't need: a DOM that is fully mutatable at
114
- any time, for any reason deemed necessary by the developer. They
122
+ The lightbulb moment came when I found the unstated assumption,
123
+ and found it served a problem I don't have. The solutions I had
124
+ tried all support a DOM that is fully mutable
125
+ at any time for any reason. They
115
126
  all provide or expect an HTML templating system that combines
116
127
  interpolation of data values with conditional and list rendering,
117
- controlled by run-time attributes. All of them must chase down the
118
- consequences of the decision to allow a fully mutatable DOM. Those
119
- consequences spill into the usage and tooling around the frameworks
120
- and libraries. The arbitrarily mutatable DOM was the source of my
121
- intuitive sense that I was dealing with too much accidental complexity.
122
-
123
- ## The Loadbare Split
124
-
125
- Finally I had the foundational idea for a framework that splits code
126
- between a static "host" application and an active data channel, with
127
- some small framework library updating only data values in the DOM
128
- by following data-binding attributes in the HTML.
129
-
130
- (As a side note, I originally considered calling Loadbare "liveload"
131
- to draw the association to a bridge. The static HTML is the bridge,
132
- the data that refreshes the page is the "live load" (traffic) travelling
133
- on it. Alas, it seemed too close to 'livereload' and I gave it up.)
134
-
135
- The static host application is pure HTML, including custom elements
136
- with Light DOM. User interactions send requests to the server which
137
- returns data objects. The HTML contains attributes identifying how
138
- elements are bound to data, so the framework can update them.
139
-
140
- This should be self-evidently efficacious for scalar cells and
141
- rows. Something like `<span lb-row="siteStats" lb-cell="visitCount"></span>`
142
- or the same type of addressing for an input makes it trivial to
143
- both hyrdrate and refresh forms, and to gather a form's values
144
- to send to the server.
145
-
146
- List processing, such as for an HTML SELECT and complex tables took a bit
147
- longer to figure out, but the principle could be extended. Any list
148
- element can accept a `<template>` that describes how to build its
149
- items. The data channel must expand to allow "patch" operations,
150
- removing or adding one row instead of reshipping the entire
151
- query, but that is also straightforward.
152
-
153
- So a purist might say, "hey you are still manipulating the DOM".
154
- Of course we are, it is an application, not a static site. But the
155
- point is that the HTML can be written, read, and reasoned
156
- about as a static artefact, while still being dynamic at runtime.
157
-
158
- The model can also be extended for trees, but my current projects
159
- do not require this, so it is not implemented as of this writing.
160
-
161
- ## Choosing the Mechanisms
162
-
163
- So now I had three principles and one architectural decision that
164
- seemed to make it all possible. The next step was to pick the
165
- fewest number of mechanisms that would work.
166
-
167
- On the benefit side, the fewer mechanisms there are the easier
168
- it would be to create the solution and to use the solution. Fewer
169
- mechanisms makes for a system that is easier to learn, and easier
170
- to teach to an LLM.
171
-
172
- On the cost-avoidance side, each mechanism is
173
- an invitation to accidental complexity around the use of the
174
- mechanism, so they must be kept at a minimum.
175
-
176
- ### Browser Application Code
177
-
178
- Using plain HTML, extended with custom elements that do not use Shadow
179
- DOM has so far, apparently, completely solved the browser for me.
180
-
181
- In my humble opinion,
182
- Javascript belongs in the browser to enhance default behavior, and
183
- the enhancements ought to always be scoped to a custom element and
184
- its children. A custom element neatly satisfies this.
185
-
186
- Custom elements give an nice answer to the question, why can't we just
187
- use HTML like we used to? The Loadbare answer is: you can, go ahead.
188
- If you need something special, make a custom element, it is still
189
- just HTML.
190
-
191
- A simple builder also gives server-side HTML includes for free with
192
- no extra syntax. The builder scans for one fixed file (the chrome),
193
- and all pages. Anywhere a custom element is found, it looks for
194
- an HTML file to match, and inserts it into the built HTML. This is recursive
195
- of course.
196
-
197
- The builder became "tree shaking" for free. During HTML assembly
198
- and composition, any Typescript file whose name matches a custom
199
- element that was actually used gets added to the monolithic
200
- `client.js`. No import statement required. The Loadbare code is
201
- loading no extra Javascript outside of the single `client.js` named
202
- in the main HTML document.
203
-
204
- The validation during a build also fell out for free: if a custom
205
- element is used, it must have either a matching Typescript file
206
- or a matching HTML file, or both. The only error is if neither
207
- are present.
128
+ controlled by run-time attributes.
129
+
130
+ But I always think of the front-end as an interface to data that
131
+ is either in a relational store or can be sent to the browser
132
+ in relational shapes: single rows or sets of rows. I expect the
133
+ DOM to mutate in response to data hydration and refreshes, which
134
+ dramatically reduces the set of required mutations from "could be
135
+ anything at any time" to exactly "refresh these representations
136
+ of the data returned by the server".
137
+
138
+ There are, of course, other mutations besides
139
+ data. A button may be disabled after click when a request is in
140
+ flight, and must be enabled when the request completes. Some DOM
141
+ nodes become visible or invisible as a result of
142
+ user interaction or data refreshes. A response may contain error
143
+ information that must be presented to the user. But adding these cases
144
+ only reinforced the emerging hypothesis: there is a knowable
145
+ and closed set of required mutations for the DOM in a
146
+ data-oriented application. A solution that optimizes for that closed
147
+ set does not need the
148
+ complexity required to support arbitrary DOM updates. This could
149
+ be the path to a solution that is performant and reduces accidental
150
+ complexity.
151
+
152
+ ## The Solution Outline
153
+
154
+ Now it was time to iterate on a framework for data-oriented applications
155
+ with propositions narrowed to that space:
156
+
157
+ 1. If HTML carries some type of data binding attributes,
158
+ then the framework can perform all data-driven DOM mutations.
159
+ 2. Given a data binding attribute vocabulary, other
160
+ mutations could be handled with additional attributes, either as natural
161
+ extensions to data binding, or in other channels that are fully
162
+ independent and do not interfere with data binding.
163
+ 3. Most server code ought to be reducible to responding to requests with
164
+ data.
165
+
166
+ Propositions 1 and 3 give hope of meeting our performance requirement.
167
+ The framework task list is small, and tuning it for performance at
168
+ every iteration seems doable.
169
+
170
+ Proposition 2 gives hope of meeting our requirement that the application
171
+ satisfy the high expectations of a modern UI, with many flexible affordances.
172
+
173
+ But as promising as these propositions were, they do not yet fully
174
+ cover the requirements listed
175
+ at the top of this essay. At this point I only had a foundation and
176
+ general direction.
177
+ Going further required more iterations with demos and side projects,
178
+ tying the general ideas to many specific implementation
179
+ decisions.
180
+
181
+ ## Solution Particulars
182
+
183
+ From here it is no longer useful to trace through the specifics
184
+ of various iterations. A full recitation of dead ends, aha moments, revisions
185
+ and experiments would be tedious and uninformative.
186
+
187
+ It would be equally tedious to recap the [Technical Reference](./TECHREF-1.0.md),
188
+ or to flatly list the final mechanisms.
189
+
190
+ What follows instead is a basic outline of the mechanisms and
191
+ decisions as they finally stand, noting how each contributes to
192
+ the major goals and to other nice-to-have goals that are mentioned
193
+ in the course of the text.
194
+
195
+ ### Scope and Boundaries
196
+
197
+ Before I enumerate the mechanisms, I have to state how the framework
198
+ boundaries fell out of the Principle of Essential Complexity.
199
+
200
+ The framework does what we do not want to repeat in every application:
201
+ binding data to the DOM, carrying requests and responses, assembling the static
202
+ assets, wiring up the API. Everything else is the essential
203
+ complexity of creating a particular application: what the data is, how it
204
+ is stored and fetched, who may see it or change it, and how it looks.
205
+
206
+ Where the framework leaves something to the application, it provides an
207
+ empty slot and has no opinion about what goes in it. It does not:
208
+
209
+ - prescribe or prohibit any CSS architecture
210
+ - require or prevent any authentication solution
211
+ - expect or hinder the use of an ORM, or of `@loadbare/db`
212
+
213
+ The rule for one-time chores, such as standing up an Express Server,
214
+ is that they stay as close as possible to "set and forget", so that
215
+ their cost is paid once and they are not a tax on
216
+ application development tasks.
217
+
218
+ ### HTML and Javascript
219
+
220
+ HTML and Javascript are listed here first. They achieved the final
221
+ form long before the rest of the framework, drove the builder
222
+ design, and were the seed crystal that everything else formed
223
+ around.
224
+
225
+ The framework organizes HTML and Javascript together, expecting
226
+ the HTML to be composed of native elements and custom elements
227
+ with light DOM. Framework and application code are both built this
228
+ way.
229
+
230
+ The HTML is static, and is written in `.html` files, and once
231
+ delivered to the browser it remains static.
232
+
233
+ The framework manipulates the DOM by reading attributes.
234
+ When data arrives, it can do direct scalar assignments
235
+ or use `<template>` nodes
236
+ that are cloned for lists, with scalar assignments made to
237
+ each cloned instance.
238
+
239
+ This structure allows for a very simple builder. The builder
240
+ scans HTML, and when it finds a custom element, it makes sure
241
+ there is either a matching JavaScript file, a matching HTML file,
242
+ or both. Only the absence of both of them triggers an error, as
243
+ we assume a custom element without a JavaScript class or HTML must
244
+ be a mistake.
245
+
246
+ Nearly every benefit of the approach falls out of that one
247
+ scan. Because the builder finds everything by name, it can do
248
+ the following without any configuration and without any new syntax:
249
+
250
+ - HTML includes for free, no hacky syntax. When the builder
251
+ finds a custom element with a matching `.html` file, it expands
252
+ the element in place, recursively.
253
+ - Separation of build-time parameters from data binding.
254
+ An attribute on a custom element can
255
+ be substituted into its HTML at build time, where it becomes
256
+ permanent. This might be the label on an input or the text of a button.
257
+ Once the HTML is all chased down, build-time parameters are
258
+ constants in the shipped HTML code.
259
+ - Slots. A custom element can provide the equivalent of named
260
+ slots, filled by `<template>` elements that the builder resolves.
261
+ - No Javascript imports. A custom element that is used pulls in
262
+ its matching script by name. There is no import graph to maintain,
263
+ reason about, or break.
264
+ - Tree shaking for free. A script whose element is never used in
265
+ the HTML is never shipped. There is no dead code analyzer,
266
+ because dead code was never pulled in.
267
+ - Validation for free. A custom element with neither a script
268
+ nor an HTML file is a build error, and it catches the
269
+ typo'd tag that the browser would otherwise silently ignore.
270
+ - Three static assets. The build produces one HTML document
271
+ with every page inside it as a `<template>`, one `client.js`
272
+ holding the hub and every widget actually used, and one `app.css`
273
+ holding every stylesheet exactly as it was authored. This
274
+ is an easy builder to code, and leads to easy server code and
275
+ deployment configurations.
276
+ - CSS neutral. Because stylesheets are concatenated and nothing
277
+ is added, removed or scoped, the builder neither requires nor
278
+ prohibits any particular approach to CSS.
279
+ - JavaScript is part of the DOM tree. By coupling JavaScript
280
+ to custom elements, it can always reliably manipulate its
281
+ children, and find shared code in ancestors.
282
+
283
+ At runtime the same structure covers the rest of what a modern UI
284
+ needs. Lists are handled by the `<template>` mechanism and data
285
+ binding, as already mentioned. Conditional rendering is handled
286
+ by one more attribute, `lb-show`, naming the column that decides
287
+ whether an element is present. When the value is null or false,
288
+ the hub moves the element into a `<template>` standing where it
289
+ stood, and moves it back out when the value returns. (Note: this
290
+ last claim is not yet fully proven by a demo or the author's projects.)
291
+
292
+ Finally, and this is only my opinion, but I find static HTML with data
293
+ binding attributes to be far easier to author, inspect,
294
+ reason about, and maintain than systems like JSX that mix
295
+ together permanent items like a button label with interpolated
296
+ data, lists, and conditional rendering.
297
+
298
+ ### Why the HTML Solution is Performant
299
+
300
+ The HTML solution is one of two central pillars to my claim
301
+ that `@loadbare/app` can deliver an app that fits into the
302
+ median 300ms allotted to round trips from click to paint. Everything
303
+ listed in the previous HTML and Javascript shows that the
304
+ HTML and Javascript solution makes the framework the "model citizen"
305
+ that does as little work as possible, leaving
306
+ as much budget as we can to the database.
307
+
308
+ Most of the work happens once, at build time. Widget expansion,
309
+ build time parameters, slots, composition, validation and tree
310
+ shaking are all done by the builder. None of it is repeated in the
311
+ browser, and none of it is repeated per request on the server.
312
+
313
+ The solution means that the only runtime work left is work that
314
+ cannot be done until the data arrives.
315
+
316
+ The application loads once. A session begins with three static
317
+ files: one HTML document, one script and one stylesheet. They are
318
+ plain static assets, so they can be compressed, cached, and served
319
+ from anywhere. There are no per-route bundles, no lazy chunks, and
320
+ no waterfall of module requests discovering their imports one at a
321
+ time. The `--minify` flag shrinks the script and stylesheet further.
322
+
323
+ The framework is small. The entire browser framework is the
324
+ "hub" (explained below), which is under a thousand lines
325
+ of Typescript, comments
326
+ included (as of this writing). There is no virtual DOM, no component tree, no
327
+ reactivity system and no scheduler to download, parse, or run.
328
+
329
+ Navigation fetches no HTML and no code. Every page is already
330
+ in the document as a `<template>`, parsed once by the browser when
331
+ the document loaded. Moving to a page is a native `cloneNode` of
332
+ that template into `<main>`.
333
+
334
+ Only data crosses the wire. The server returns rows as JSON.
335
+ Rows are far smaller than the markup that displays
336
+ them, and the markup is already in the browser. One interaction is
337
+ one request and one response, carrying back any refreshed or changed
338
+ data.
339
+
340
+ Only changed data crosses the wire. A request handler names the queries
341
+ it refreshes, so the server re-runs only those, not the whole page.
342
+ Where even that is too much, a request can return a patch: the rows
343
+ that arrived or changed and the keys that went, rather than the
344
+ entire list shipped again.
345
+
346
+ The server renders nothing. There is no server-side rendering,
347
+ no server templating, and no hydration step. A page's server code
348
+ runs its queries and hands back what they returned. Nearly all of
349
+ the server's time for a request is therefore the database's time,
350
+ which is exactly where the Principle of User Experience wants it.
351
+
352
+ The framework mutates DOM in response to data refreshes using a
353
+ closed set of operations. The
354
+ hub finds its targets with the browser's own selector engine, by
355
+ the attributes written in the HTML, and sets a value, some text, or
356
+ an attribute, or moves an element into or out of a template. No tree is diffed, and no
357
+ element is rebuilt because its data arrived again. The framework
358
+ never has to
359
+ consider what might have changed, because the response says exactly
360
+ what did.
361
+
362
+ Nothing runs between interactions. Once data has landed, the
363
+ hub waits on events. It keeps no timers, no observers and no
364
+ polling loop, so an idle page costs the browser nothing.
365
+
366
+ Taken together, the framework's share of a round trip is a small,
367
+ fixed amount of work in the browser, a JSON serialization on the
368
+ server, and little else.
369
+
370
+ ### Chrome and Naming Conventions
371
+
372
+ Something else fell out of the HTML and Javascript solution fairly
373
+ early in framework development. It turned out that a handful of
374
+ hard-coded naming conventions could simplify the builder while
375
+ giving flexibility to file organization.
376
+
377
+ The builder would require exactly one file named 'chrome.html',
378
+ anywhere in the source tree, which contains the application skeleton,
379
+ and is processed the same as all other HTML files.
380
+
381
+ The builder would recognize any page ending in '.page.html' as a
382
+ navigation target.
383
+
384
+ A custom element's JavaScript, if present, should be named
385
+ `my-custom-element.browser.ts`. The '.browser.' segment requires
386
+ deliberate action to get the code into the browser, eliminating
387
+ configuration files or switches that specify which code in the
388
+ source tree is prohibited or forced into the browser.
389
+
390
+ A custom element's HTML, if present, should be named `my-custom-element.html`.
391
+ All matching is by name, no configuration required.
392
+
393
+ The builder scan begins with `.page.html` files and `chrome.html`. All
394
+ logic is simple and already stated: pick up code and HTML for custom
395
+ elements, and do so recursively. That is the entire algorithm.
396
+
397
+ This means that no special new mechanism was required for
398
+ application chrome. There is no requirement for how to
399
+ organize application code, and the destiny of every file is evident
400
+ from a directory view.
208
401
 
209
402
  ### Data Binding
210
403
 
211
- For the hub to know what data to apply where and how, we need
212
- custom attributes on the HTML. It was possible to define a small
213
- and coherent vocabulary because the actual number of operations
214
- afforded by a database is small and coherent.
215
-
216
- ### Browser Framework Code
217
-
218
- The entire browser framework code is in the hub `<lb-hub>`, and its
219
- only real job is to handle page navigation and the data channel.
220
-
221
- It also contains addressable methods to update DOM elements with
222
- data objects, so that custom elements do not have to reproduce that
223
- code.
224
-
225
- ### Application Server Code
226
-
227
- For server page code, this is where we truly got accidental complexity
228
- to zero. A page is just its HTML, its queries, and its request handlers.
229
-
230
- For the Express server, I was rather surprised myself to find that the
231
- simplest thing was to show example code for an Express server. Nowadays
232
- most frameworks provide their own Express server that is so completely
233
- invisible that a newcomer to the industry can be forgiven for not even
234
- knowing that Express exists. I wondered if pushing this concern to the
235
- dev team violated the Principle of Essential Complexity, but I really
236
- don't think so.
237
-
238
- The reasoning for not providing a shrink-wrapped
239
- server is simple: the boilerplate code is one-time effort, and the dev
240
- team will need to add in their own database handler, security and
241
- other concerns. A one-time effort may be seen as accidental
242
- complexity to a skeptic, but its one-time set up means it is not a
243
- continual tax being paid every time you write a new page. It is that
244
- sense of paying an ongoing tax that I'm really trying to avoid.
245
-
246
- ### The Database
247
-
248
- Database concerns are mostly out of scope for the application layer,
249
- but we must make mention of them because Loadbare is supposed to
250
- be optimized for database apps.
251
-
252
- Loadbare satisfies the minimal principle of not being rude by not
253
- requiring a specific database, neither prohibiting nor requiring
254
- the ORM of your choice if you use one.
255
-
256
- However, I should mention that I personally always use
257
- the `@loadbare/db` package to build
258
- my databases, which enriches them with calculated values that are
259
- declarative in the Loadbare db schema. For this reason, I need no
260
- ORM because the access patterns are very simple: most database reads
261
- come from one table or a fairly simple view, and user actions that
262
- update a single table automatically cascade calculations
263
- to related tables. For this reason, I do not need an ORM and
264
- my `@loadbare/app` applications connect directly to the database
265
- with no "business logic" code at all.
266
-
267
- I must also state that a team not using `@loadbare/db` will probably
268
- use an ORM or craft a business logic layer, and `@loadbare/app` does
269
- nothing special for this. It simply provides the empty slot where
270
- you plug it in.
271
-
272
- ### The Builder
273
-
274
- The builder kind of fell out for free from the architecture and
275
- mechanism choices.
276
-
277
- In addition to what was noted above, it is a very straightforward
278
- piece of code: it walks the `src/` tree, requiring only a 'chrome.html',
279
- and picking up all '*.page.html' files. It builds one Javascript file,
280
- one CSS file, and one HTML file. Having the files be sparse, containing
281
- only what was used, is trivial.
282
-
283
- It also builds the application's API from the page's processing
284
- code, and 4 lines in the Express server end up handling the entire
285
- API.
286
-
287
- In short, the builder satisfied the Principle of Essential Complexity, that
288
- if the code is organized around just application concerns, the builder
289
- algorithm is self-evident, straightforward, and easy to decorate with
290
- nice-to-haves.
291
-
292
- ## Assessing the Result
293
-
294
- I am a biased examiner, but I can say that `@loadbare/app` solves mutliple
295
- problems for me and achieves compliance with the principles stated
296
- at the top of this essay.
297
-
298
- The Principle of Developer Experience says my goal is to make something
299
- that users actually use and appreciate. Loadbare cannot do that for me,
300
- but it does free me from so many accidental concerns that my time is
301
- free to pursue that quality application. It reduces the cost of iteration
302
- and increases my ability to focus on the shape of the solution. That is
303
- as much as I can ask for from a productivity tool.
304
-
305
- In terms of the Principle of User Experience, that we must complete
306
- round trips from click to paint in a median time of 300ms, Loadbare gives
307
- me a better chance of doing this than any other tool I have seen.
308
- The Loadbare app is so light and does so little that if we allow a median
309
- time of 200ms for wire time, Loadbare likely needs 10ms or less between
310
- the browser and the server, leaving 90ms for the database and business
311
- logic. Making the budget in production can be reduced to careful modeling,
312
- tuning, and hardware spend. (Note: using `@loadbare/db` makes this almost
313
- a guaranteed win, provided we don't skimp on hardware and know how to tune
314
- Postgres for production).
315
-
316
- Finally, in terms of the Primacy of Essential Complexity, I may be a biased
317
- examiner. Because I wrote it, I see zero accidental complexity, inasmuch
318
- as everything seems to be there to serve the major principles. But I cannot
319
- trust my judgment there for obvious reasons. Instead I will propose that
320
- there are some reasonably obvious wins for Essential Complexity that even
321
- a skeptic might agree with.
322
-
323
- One big win is the ability to just write plain HTML. Mixing HTMl and
324
- Javascript seems cool if you need a fully mutatable DOM, but when you realize
325
- you don't need that, it's back to plain old reliable HTML.
326
-
327
- Another big win is the server code for pages. The clean organization
328
- of requests, actions, CRUD and queries without boilerplate is pure
329
- Essential Complexity. The automatic building of the API and 4 lines to
330
- handle it in Express is lower than I have seen in any other tool.
331
-
332
- Loadbare satisfies many other principles that do not merit a full explanation,
333
- such as using technology instead of fighting it, organizing the use of
334
- browser mechanisms instead of reimplementing them, and encouraging code
335
- that is likely to have a very long shelf life. Loadbare does not
336
- interfere with normal CSS usage, so you can bring any solution you like.
337
-
338
- In closing I would add one more claim. Loadbare is easy to learn because
339
- it has a small cognitive surface area. The main activity, coding pages,
340
- is highly repetive and driven by a handful of patterns. It can be learned
341
- by a person and taught to an LLM with just a few skills.
404
+ Once it looked like HTML authoring and building was solved, it was time
405
+ to ensure that Data Binding would not crash the party. If data binding
406
+ exploded into overlapping features and nasty edge cases, all progress would
407
+ halt.
408
+
409
+ If the framework browser code is to remain small, performant, and robust,
410
+ and fully expressive, then all of it must be achieved by reducing the
411
+ data binding to a closed set of non-overlapping features.
412
+
413
+ This meant that the earlier vague idea of "relationally shaped" had to
414
+ become a scoping decision. The framework browser code will only understand
415
+ rows and sets of rows. It will not understand a JSON document where child
416
+ rows are embedded in a row, and it will not understand a tree where there are
417
+ children in children in children. This does not force a relational
418
+ database, but it does force server code to return data in the shapes of
419
+ rows and sets of rows.
420
+
421
+ That scoping decision allowed us to reduce the data binding vocabulary
422
+ to four foundations:
423
+
424
+ | Attribute | Relational idea | HTML precedent |
425
+ | --------- | --------------- | -------------------------------------------------- |
426
+ | `lb-list` | A set of rows | `<select>` or `<ul>`, a container of its items |
427
+ | `lb-row` | A single row | `<form>`, one record of fields |
428
+ | `lb-cell` | A column | `name` on a form control, which field this is |
429
+ | `lb-key` | The primary key | `value` on an `<option>`, identity apart from text |
430
+
431
+ A highly simplified page showing
432
+ an invoice and its lines looks like this:
433
+
434
+ ```html
435
+ <section lb-row="invoice">
436
+ <h2 lb-cell="number"></h2>
437
+ <span lb-cell="customer"></span>
438
+ </section>
439
+
440
+ <table>
441
+ <tbody lb-list="invoiceLines">
442
+ <template lb-key="id">
443
+ <tr><td lb-cell="item"></td><td lb-cell="amount"></td></tr>
444
+ </template>
445
+ </tbody>
446
+ </table>
447
+ ```
448
+
449
+ This decision proved decisive in keeping the browser code robust,
450
+ expressive, and performant.
451
+
452
+
453
+ ### Requests and Request State
454
+
455
+ Proposition 2 said that once there is a data binding vocabulary, the
456
+ other mutations could be handled with additional attributes, either as
457
+ extensions to data binding or in channels that are independent of it.
458
+ The idea is that user interaction triggers framework browser code
459
+ that "knows what to do", assembling a request driven purely from
460
+ attributes.
461
+
462
+ This requirement is satisfied with a single new attribute, `lb-action`.
463
+ Three values are reserved, and any other value is interpreted as the
464
+ name of a routine on the server.
465
+
466
+ A request is an action and a position. The author writes the action,
467
+ and the hub supplies the position from the document, using the same
468
+ ancestor rule that decides where a value lands. To add a delete
469
+ button to the invoice lines from the previous section, the template
470
+ gains one element:
471
+
472
+ ```html
473
+ <template lb-key="id">
474
+ <tr>
475
+ <td lb-cell="item"></td>
476
+ <td lb-cell="amount"></td>
477
+ <td><button lb-action="lb-row-delete">Remove</button></td>
478
+ </tr>
479
+ </template>
480
+ ```
481
+
482
+ The button names neither the list nor the key. When it is clicked,
483
+ the hub reads `invoiceLines` from the nearest list scope and the key
484
+ from the live row around the button, and sends:
485
+
486
+ ```
487
+ { action: "lb-row-delete", list: "invoiceLines", key: "42" }
488
+ ```
489
+
490
+ But we need to be more modern than that. Nowadays we expect the
491
+ button to be disabled after it is clicked, and to display a wait
492
+ state in case the network is congested. In other words, the
493
+ framework browser code needs to notify the event target of its
494
+ request state.
495
+
496
+ To do this, framework browser code writes three attributes about a request, `lb-pending`,
497
+ `lb-error` and `lb-row-count`. The framework browser code ignores
498
+ a button or form performed again while stamped with `lb-pending`,
499
+ meaning the disabled state can be represented entirely by CSS, no
500
+ JavaScript needed.
501
+
502
+ Over many iterations I gradually reduced the core attributes to
503
+ these, from a larger set that had more overlapping behaviors and edge
504
+ cases. In the end this small set builds on and protects the
505
+ HTML/JavaScript solution.
506
+
507
+ ### The Hub
508
+
509
+ So far I have been using the term "framework browser code". In other
510
+ documents we call it the hub.
511
+ The hub is one custom element, `<lb-hub>`, and it is the entire
512
+ browser framework. It has three jobs: navigation, synthetic context, and the data channel.
513
+
514
+ #### Navigation
515
+
516
+ As previously stated, the 1.0 builder packages all page HTML into the
517
+ chrome and ships it as `app.html`. With all pages already present
518
+ in the browser, providing an SPA is fairly simple.
519
+
520
+ The hub catches anchor clicks, and checks if the anchor contains the
521
+ attribute `lb-nav-link`. If so, the hub interprets it as in-app
522
+ navigation, swaps the anchor's `href` into `<main>`, and sends a request
523
+ to the server for the page data.
524
+
525
+ Anchors without the attribute behave as normal links.
526
+
527
+ #### Synthetic Context
528
+
529
+ Synthetic context emerged as a useful feature near the very end of
530
+ development. Realizing that the hub knows about the current
531
+ navigation state, we had it publish a syntheic query result,
532
+ `lb-navigation`, that contains the URI and, if it can find it,
533
+ the anchor text for any link to that URI.
534
+
535
+ These can be bound to any HTML, same as any other query sent by
536
+ the application, but the resulting values are always supplied by
537
+ the hub.
538
+
539
+ There may be more synthetic context in the future.
540
+
541
+ #### The Data Channel
542
+
543
+ We have already worked out that the hub is crafting requests,
544
+ stamping request state, and mutating the DOM based on data received
545
+ in responses.
546
+
547
+ We have implicitly already assigned the hub responsibility for
548
+ the data channel, and here I simply want to say it out loud.
549
+
550
+ ### Server Page Code
551
+
552
+ Proposition 3 said that most server code ought to be reducible to
553
+ responding to requests with data. In Loadbare a page is up to three
554
+ files that share a name: `invoice.page.html`, `invoice.queries.ts`
555
+ and `invoice.requests.ts`. The HTML is the page as the user sees it,
556
+ the queries provide the data, and the requests are what it allows.
557
+
558
+ Each query is declared as a row or a list, the same two shapes the
559
+ HTML binds to. Each request declares the work it does and the
560
+ queries to run again afterward. Here is the delete button from
561
+ Requests and Request State, as the server sees it:
562
+
563
+ ```ts
564
+ // invoice.requests.ts
565
+ import { patch, type Requests } from "@loadbare/app/server";
566
+
567
+ export const requests: Requests = {
568
+ crud: {
569
+ invoiceLines: {
570
+ rowDelete: {
571
+ run: async (ctx, where) => {
572
+ await ctx.db.deleteLine(where.key);
573
+ return { invoiceLines: patch({ drop: [where.key] }) };
574
+ },
575
+ refresh: ["invoice"],
576
+ },
577
+ },
578
+ },
579
+ };
580
+ ```
581
+
582
+ Deleting a line changes the invoice total, so the entry refreshes
583
+ `invoice`. The list itself does not need to be queried again, since
584
+ the only change is one row that went away, and `run` says so with a
585
+ patch. The response carries both: the refreshed invoice row, and the
586
+ patch that removes one line.
587
+
588
+ Loadbare, as it turns out, does not have "endpoints" as such. If the
589
+ HTML scopes an `lb-row-delete` to `lb-list="invoice_lines"`, then the
590
+ server carries a CRUD entry `rowDelete` under object invoiceLines.
591
+ Since the browser and server exist to talk to each other in the same
592
+ language, no additional abstraction is needed.
593
+
594
+
595
+ ### The Express Server
596
+
597
+ Loadbare ships no server, and the application writes an ordinary
598
+ Express app. I was rather surprised myself to find that the simplest
599
+ solution here was to show example code, instead of shipping a shrink-wrapped
600
+ server. Most frameworks today provide a
601
+ server so completely hidden that a newcomer to the industry can be
602
+ forgiven for not knowing Express is underneath.
603
+
604
+ The build leaves the server four things to handle: the script, the
605
+ stylesheet, the hub's data channel, and the one HTML document.
606
+ The builder also writes a file,
607
+ `pages.ts`, that gathers every page's queries and requests into one
608
+ hub, so the server imports that hub and never lists the pages itself.
609
+ The part of the server that belongs to Loadbare is four lines:
610
+
611
+ ```ts
612
+ app.use(hubRoutes(hub, contextFor));
613
+ app.get("/client.js", (_req, res) => res.sendFile(path.join(DIST, "client.js")));
614
+ app.get("/app.css", (_req, res) => res.sendFile(path.join(DIST, "app.css")));
615
+ app.get(/.*/, (_req, res) => res.sendFile(path.join(DIST, "app.html")));
616
+ ```
617
+
618
+ These lines stay the same as pages are added. Everything else in the
619
+ file belongs to the application, and that is where authentication,
620
+ sessions, logging and the opening of the database go.
621
+
622
+ If we tried to ship a server, we'd have to load it with hooks for every
623
+ concern we don't handle, and we'd end up re-inventing Express. So
624
+ we don't do that.
625
+
626
+ ### What the Application Provides
627
+
628
+ What Loadbare leaves to the application is the essential complexity
629
+ named in Scope and Boundaries: what the data is, how it is stored and
630
+ fetched, who may see it, and how it looks. On the server, all of it
631
+ reaches the page code through one object.
632
+
633
+ For each request, the application builds a context, `ctx`, in a
634
+ function it hands to the hub in the Express server:
635
+
636
+ ```ts
637
+ function contextFor(req: Request): HubContext {
638
+ return { db: openDb(req) };
639
+ }
640
+ ```
641
+
642
+ Loadbare passes `ctx` to every query and every request.
643
+ A query finds exactly what the application put there,
644
+ which is usually a database handle opened for the user who made the
645
+ request. Authentication decides what goes into `ctx`, and the page
646
+ code sees only the result.
647
+
648
+ How the application reaches its database is also its own choice. I
649
+ always use `@loadbare/db`, which declares calculated values as part
650
+ of the schema. Most reads come from one table or a simple view, and
651
+ a write to one table cascades its calculations to related tables, so
652
+ my page code calls the database directly and there is no business
653
+ logic layer in the application at all. A team not using
654
+ `@loadbare/db` will probably use an ORM or write a business logic
655
+ layer, and it reaches the page code through `ctx` in the same way.
656
+
657
+
658
+ ### Conclusions Against the Three Principles
659
+
660
+ As we near the end of the essay, I want to summarize why I believe
661
+ that `@loadbare/app` meets the goals set out at the top of the
662
+ essay. I believe that overall we are somewhere well past
663
+ plausible for all claims, but only further iterations and usage
664
+ will tell the tale. This is a theory document, not a hard proof.
665
+ Performance stats and tables will be published separately.
666
+
667
+ The first principle stated in this essay was the Developer Experience
668
+ Principle, that the best developer experience was building something
669
+ that people use and appreciate.
670
+
671
+ No framework can directly give me that wonderful app, so I have to
672
+ elaborate that product talent must be leveraged and never blocked
673
+ by the framework. If the framework is to help instead of hinder,
674
+ it must smooth the path towards a great user experience, with
675
+ developer efficiency being a co-equal primary goal.
676
+
677
+ #### The User Experience
678
+
679
+ User experience, in the broad sense, is a solved problem. We have long had a rich
680
+ visual and interaction vocabulary, sufficiently expressive to
681
+ present any form of data and allow interaction with it. Loadbare need
682
+ invent nothing here, it must only take care not block the correct solutions.
683
+
684
+ But the orphan out in the barn is performance, which users arguably
685
+ care about more than anything.
686
+
687
+ In terms of performance, I believe the claim that Loadbare has a good
688
+ chunk of the answer and is deep into "undeniably plausible". The framework's
689
+ tasks, footprint and overhead are so small that it cannot help but
690
+ to be faster than the more elaborate alternatives. This is true in
691
+ both the browser and the server. Loadbare has precisely zero
692
+ "abstractions" that promise ergonomics while eroding performance. The
693
+ path from request to server to response to paint contains exactly the
694
+ steps that are essential to the job being done, and nothing more.
695
+
696
+ If we consider performance as settled as it can get in a theory
697
+ document, we move on to the rest of the User Experience, where
698
+ loadbare follows the principle of not blocking the developer's desired
699
+ solution. Anything around accessibility, theming or skinning, branding,
700
+ interaction, and responsiveness fit into a widget library and a
701
+ CSS solution.
702
+
703
+ Consider that HTML is highly accessible when used mindfully,
704
+ with a handful of aria-* attributes filling in the blanks. Concerns
705
+ such as contrast and target size are all solved in CSS. This is why
706
+ loadbare uses plain HTML, light DOM, and allows any structure of CSS: so that
707
+ a designer can specify an accessible design that becomes an
708
+ accessible widget library that is used to build an accessible app.
709
+
710
+ #### Essential Complexity
711
+
712
+ The essential complexity claim I believe is more vulnerable to
713
+ honest critique or different perspectives, so first we'll look at
714
+ it in a condensed form, and then address the best steel man argument
715
+ I can muster.
716
+
717
+ First, Loadbare has few requirements for project setup. They are
718
+ one-time, and do not grow at the scale of the app. Every attempt
719
+ has been made to make them "set and forget".
720
+
721
+ Second, Loadbare is missing entire categories of developer effort,
722
+ while providing expected benefits. HTML includes without imports,
723
+ declarative browser data binding and actions, free tree shaking,
724
+ extendable with custom elements, a small set of mechanisms, and
725
+ nothing to learn about the "the Loadbare way for CSS", among many
726
+ others.
727
+
728
+ Third, the essential activity of page and widget authoring, where
729
+ almost all of the work is, use nothing more than HTML and JavaScript
730
+ that is naturally coupled to the DOM. On the server, the queries
731
+ and requests have no boilerplate, setup or teardown requirements.
732
+
733
+ Fourth, the entire Loadbare vocabulary can fit in a single technical
734
+ reference document, which means it can fit into a person's head or
735
+ an LLM's context window.
736
+
737
+ > Full Disclosure: While I have had good success with LLM's coding
738
+ > pages and widgets with the current technical reference, as of
739
+ > this writing all Anthropic models consistently make one serious
740
+ > mistake in .requests.ts files. They refresh list queries after
741
+ > a patch, which is redundant, inefficient, and usually
742
+ > destroys scroll position.
743
+
744
+ With those four points made, a reasonable critic could still make an
745
+ argument that I would not care to dispute. They may say, "Sure Ken,
746
+ it looks like low accidental complexity to you because you wrote it,
747
+ and you understand it, but all of those data binding attributes and
748
+ that forced relational thinking are really just a swap of one set
749
+ of accidental complexity for another. The system is really
750
+ 'differently accidental' from other frameworks."
751
+
752
+ I would not dispute this argument because it is more about perspective
753
+ than measurable claims. It is true that Loadbare forces a declarative syntax
754
+ and "relational shaped" data. This appears "essential" to me because
755
+ I write database apps. By accepting it as essential, I get performant
756
+ apps and most of my effort is spent on the apps and not the tooling or
757
+ boilerplate. But if an existing stack does not easily allow the
758
+ data to be processed into to rows and lists of rows, if the applications affordances
759
+ do not seem to readily fit the vocabulary, then it is not a fit.
760
+
761
+ In conclusion, we can see simply that Loadbare's claims around
762
+ accidental and essential complexity are most likely to
763
+ be true when the existing stack can support an application that wants
764
+ data to be rows and tables, or easily mapped into them and out of them,
765
+ and the desired affordances can be easily mapped to the standard
766
+ set of operations against that data.
767
+
768
+ ## Appendix: Code Shelf Life
769
+
770
+ Loadbare has a benefit not stated elsewhere in the docs, because it
771
+ really has no place to land: Loadbare app code has a long shelf life.
772
+
773
+ Pages are HTML and CSS, widgets are custom elements, the server is Express,
774
+ and the page code is queries against the application's own database. Each of
775
+ those is a standard or a mature tool of its own, so the code written
776
+ today is exposed to very little framework churn tomorrow.
777
+
778
+ Every effort has also been made to scope release 1.0 so that future
779
+ features we can imagine now can be made additive. Consider for
780
+ example the current practice of a monolithic `app.html` containing
781
+ all pages for the app. If this proves unworkable, or threatens
782
+ the performance budget, we can add options for alternate solutions
783
+ that do not break existing code.
784
+
785
+ ## Appendix: Provenance and History
342
786
 
343
787
  I wrote Loadbare originally in 2003, as "Andromeda", before Node existed
344
- and even 4 years before we had JQuery. Andromeda was not nearly as
345
- optimized as Loadbare is now, but the principles were the same then as
346
- they are now. It has always worked for me, and I hope that it will work
347
- for you.
348
-
349
- ## A Consistency Model
350
-
351
- > **LLM-authored, not yet revised by a person.** Drafted by Claude on
352
- > 2026-09-13, from a design session against `@loadbare/app` 0.6.0. Treat
353
- > it as a proposal for this essay, not as the author's statement.
354
-
355
- ### From a subjective claim to a checkable one
356
-
357
- Whether Loadbare carries less accidental complexity than another tool will
358
- always be disputable. A developer fluent in React has muscle memory for its
359
- rules and will see Loadbare's rules as foreign, and foreign work feels
360
- accidental. That objection is about familiarity, and familiarity is a cost
361
- paid once, in the same way this essay treats the Express boilerplate.
362
-
363
- A stronger claim can be checked, and a single counterexample in markup
364
- would refute it. The model for applying data to HTML is:
365
-
366
- 1. **Internally consistent.** A few rules, each applied the same way
367
- everywhere, with no exceptions.
368
- 2. **Consistent with HTML.** Each rule has a counterpart in how HTML
369
- already behaves, and none contradicts it.
370
- 3. **Consistent with relational data.** What the server sends is the shape
371
- a SQL query already returns, so nothing is translated on the way.
372
-
373
- ### Where the vocabulary comes from
374
-
375
- This essay says the vocabulary is small because the operations a database
376
- affords are small and coherent. A sharper statement is that the vocabulary
377
- sits where two systems that are already coherent meet: the relational model
378
- and HTML's containment model. Each attribute is a relational idea placed on
379
- an element.
380
-
381
- | Loadbare | Relational | HTML precedent |
382
- | ---------------------------------------- | -------------------------------------------- | ------------------------------------------------------------ |
383
- | `lb-list` | a relation, a set of rows | `<select>`, `<ul>`: a container whose contents are its items |
384
- | `lb-row` | a single row | `<form>`: one record of fields |
385
- | `lb-cell` | a column | `name` on a control: which field this is |
386
- | `lb-key`, `lb-key-value` | the primary key | `value` on `<option>`: identity apart from the label |
387
- | `lb-action` | a closed set of writes, and named procedures | `action` on `<form>`: where a submission goes |
388
- | scope from ancestors | | form ownership, `lang`, `<fieldset disabled>` |
389
- | a nested scope begins a new one | | a nested element owns its own contents |
390
- | `lb-row-count`, `lb-pending`, `lb-error` | | state attributes such as `open` on `<details>` |
391
-
392
- ### The rules
393
-
394
- 1. **An element's own attributes describe what it displays. Its ancestors
395
- describe where it belongs.** A picker carrying `lb-list` displays that
396
- list, and its choice is addressed to the row it sits in, the way a
397
- `<select>` displays its options and submits to the form around it.
398
- 2. **A name answers with one shape, a row or a set of rows, and a cell holds
399
- one value.** Master-detail is a row and a list under two names. Many
400
- masters with their details is one list of joined rows, grouped for
401
- display.
402
- 3. **A value lands where the element shows its state, and a form gathers
403
- from the same places.** A widget receives `lb-value`, a form control its
404
- `value`, and any other element its text.
405
- 4. **A request is an action and a position.** The hub supplies the position
406
- from the document. Only an interaction supplies a value.
407
- 5. **The hub owns position and reconciliation. A widget owns interaction and
408
- placement.**
409
-
410
- ### Evidence
411
-
412
- The following decisions each removed an exception or a conflict, and none
413
- added an attribute. Over the same span the model gained pickers inside rows,
414
- native selects that show their own state, and nested lists that do not
415
- destroy each other.
416
-
417
- | Decision | Removed |
418
- | ------------------------------------------------------------- | ------------------------------------------------------------------------------------ |
419
- | The hub scopes every request | Native requests were scoped and widget requests were not |
420
- | Scope comes from ancestors, never the element's own `lb-list` | `lb-list` meant "display" in one place and "address" in another |
421
- | A form control receives its value as `value` | A `<select>` was treated as text, against HTML, and landing did not mirror gathering |
422
- | Row and template lookups stop at a nested scope | Cells respected nesting while rows and templates did not |
423
- | Master-detail is a row and a list | An open question born of document-shaped data |
424
- | `lb-value` on every cell | Only a widget's landed value was visible to a stylesheet |
425
- | An insert or update gathers its form or row \* | What sat beside a button decided what was sent |
426
- | Gathering finds cells the way landing does \* | Gathering read into nested scopes, and an element that was both a scope and a cell was gathered but never landed on |
427
-
428
- \* LLM-authored change, not yet reviewed by the author. Added by Claude on
429
- 2026-09-13: `lb-row-insert` and `lb-row-update` gather the nearest `<form>`,
430
- `<tr>` or live row inside their scope, rather than the nearest element
431
- holding a cell. Gathering and landing then share one test for which cells
432
- belong to a scope, judged from the element's ancestors and never its own
433
- attributes, following the first rule.
434
-
435
- Two signs suggest these are rules rather than patches. Fixing where a
436
- request's scope comes from also fixed an unrelated console error about insert
437
- forms, which was never worked on directly. Making rows respect nested scopes
438
- needed no change to the reference, which already described that behavior.
439
-
440
- ### Where the claim is not yet proven
441
-
442
- - **Parameters and view state.** A query takes no argument from the
443
- browser, so there is nowhere to put which record is selected, a filter, a
444
- sort, or a collapsed section. The model is incomplete here rather than
445
- inconsistent. If the answer is HTML's own, the URL, in the way
446
- `<form method="get">` puts its fields in the query string, the claim grows
447
- stronger. If it needs a state mechanism with no HTML precedent, that will
448
- be the model's first real exception.
449
- - **Checkboxes and radio buttons.** HTML's boolean convention is presence or
450
- absence, as with `checked`, `hidden` and `disabled`. A rule following it
451
- would avoid inventing truthiness, though it still touches data types.
452
- - **Data types.** HTML attributes are strings, so comparing values as strings
453
- in a stylesheet is consistent with HTML. What is not yet reconciled is
454
- that a database's typed values arrive untyped.
455
- - **A nested list shows the same rows in every outer row.** That follows
456
- from the second rule: one name is one relation. Only a new outer row
457
- starting with an empty nested list is a mechanical gap.
458
-
459
- ### A test for every change
460
-
461
- Before a change is made, ask:
462
-
463
- - Does it remove an exception, or add one?
464
- - Does it have an HTML precedent?
465
- - Does it have a relational counterpart?
466
-
467
- Stamping every column of a row onto the row element as `data-*` fails the
468
- first two: attributes nobody wrote, with no precedent in HTML. Writing
469
- `lb-value` on every cell passes all three.
788
+ and even 3 years before we had jQuery. The foundation of Andromeda
789
+ is now in `@loadbare/db`, the package that encodes business logic
790
+ declaratively and frees me from the ORM and the "logic layer" in the
791
+ application.
792
+
793
+ Andromeda carried a web layer too, meant to take advantage of the
794
+ enriched database. The conviction that led to Andromeda's UI in
795
+ 2003 was that we needed a framework optimized for database
796
+ applications. I am just as convinced today as I was then, and the effort
797
+ to provide that framework continues in `@loadbare/app`.
798
+