Conventions your agent follows.
Architecture you still own.
WebJs is a full-stack web components framework with no build step. Nothing is hidden from your agent, or from you.
The first paint is the whole page
The server sends a finished page. It reads, its links navigate, and its forms submit before a single script runs. What loads afterwards is JavaScript, and only for the components that are actually interactive. That is progressive enhancement, and with WebJs it is the default rather than an effort.
P.S. Turn JavaScript off and reload. The page still reads, navigates, and respects your system theme. Then try that on the next framework's website that comes to mind. 😉
class LikeButton extends WebComponent({ count: Number }) {
render() {
return html`<button @click=${() => this.count++}>
♥ ${this.count}
</button>`;
}
}
LikeButton.register('like-button');
// No build step. No bundler. No virtual DOM.
// The live button is this file, server-rendered
// and upgraded in place. Click it.
<like-button count="3"></like-button>
Nothing is compiled away
The file in your editor and the file in the browser network tab are the same file. Here is a server action and the page that calls it. The page ships as you see it, because there is no build step. TypeScript is stripped to whitespace rather than compiled, so a stack trace points at the line you wrote. The action becomes an RPC call. Rails has shipped its default frontend without a bundler since Rails 7 in 2021, so the approach has production miles behind it.
Server action (RPC)
'use server';
import { eq } from 'drizzle-orm';
import { db } from '#db/connection.server.ts';
import { posts } from '#db/schema.server.ts';
// Import this from a page or component. In the
// browser the import becomes an RPC call. On the
// server it is just this function. No fetch by hand.
export async function getPost(id) {
const [post] = await db.select()
.from(posts)
.where(eq(posts.id, id));
return post;
}
SSR page
import { html, notFound } from '@webjsdev/core';
import { getPost } from '#actions/get-post.server.ts';
import '#components/like-button.ts';
export default async function Post({ params }) {
const post = await getPost(params.id);
if (!post) notFound();
return html`<article>
<h1>${post.title}</h1>
<like-button></like-button>
</article>`;
}
The browser is the framework. The rest is here.
Staying close to the platform is usually where a framework starts asking you to give things up, so each card is a place WebJs takes the standard and keeps the ergonomics anyway. Everything you need to ship, none of the build toolchain you don't.
Your folders are the routes
A page.ts is a route, a layout.ts wraps everything under it, and a route.ts is an HTTP handler. Dynamic segments, groups, catch-alls, and error boundaries all follow the folder tree, so the URL map is the directory listing.
Call the server like a function
Mark a file 'use server' and import it. The call site keeps the function's real argument and return types with no code generation in between, and Date, Map, Set, BigInt, and Blob round-trip across the wire. You never hand-write a fetch.
Some components ship no JavaScript
Components render on the server. An interactive one hydrates on its own when the browser upgrades its tag, and a display-only one is stripped from the browser entirely, module and vendor imports included.
Slow data never blocks the first byte
Wrap a slow region and the shell paints immediately while the data streams in behind it. Navigation is client-side already, with nothing to import and nothing to configure.
Auth is not a side quest
Login, a signed session, and a protected route ship in the scaffold, on a real database with a schema and migrations from the first command.
The unglamorous half, included
Caching, rate limiting, file storage, and WebSockets, sharing one pluggable store. Memory by default, Redis in one line. The parts nobody demos and every production app needs.
WebJs invents as little as possible. Routing follows Next.js file conventions, components follow lit's, and the rest is the platform, so what you know and what your agent was trained on both transfer. All of it is 43 KB gzipped, client router included, against about 99 KB for a minimal Next.js bundle.
The app has a shape before your agent starts
A scaffolded app arrives with a live demo of every feature WebJs ships, so your agent reads working code instead of guessing at an API. One command clears the demos and leaves the wiring, and the architecture stays decided either way, carried in a skill your agent reads on demand.
What arrives, and what leaves
$ npm create webjs@latest my-app
$ ls my-app/app/features
async-render boundaries
auth broadcast
caching client-router
... 26 in all
$ npm run gallery:clear
Gallery cleared (44 paths removed).
The skill and db wiring are kept.
Where the code goes
modules/auth/
actions/signup.server.ts
queries/current-user.server.ts
types.ts
modules/forms/
actions/send-message.server.ts
db/schema.server.ts
# one feature, one folder. reads in
# queries, writes in actions, one
# function per file.
How the UI is built
$ webjs ui add button
✔ Wrote components/ui/button.ts
--background --primary
--foreground --border
--card --muted
class="bg-background ..."
# the component is a file you own.
# the palette is tokens, so
# restyling is editing them.
The framework source is in your own project
A scaffolded app answers most questions with its demos and its agent skill. When those run out, your agent opens the framework itself. It reads the router or the renderer it is actually calling straight from node_modules, not a version recalled from training data. The files you write ship as written, and so do the framework's, so what it reads is what is running.
The renderer, client side
// Dispose the signal watcher so dependency edges drop. Without
// this the element holds references to module-scope signals
// (and vice versa) forever.
if (this.__signalWatcher) {
this.__signalWatcher.dispose();
this.__signalWatcher = undefined;
}
for (const c of this.__controllers) {
if (c.hostDisconnected) c.hostDisconnected();
}
The server that renders it
// 103 Early Hints: before running SSR, send preload hints for the
// page's module URLs so the browser can begin fetching them while
// the server is still computing the body. Skipped in dev (file churn
// would send stale URLs after rebuilds) and for non-GET/HEAD.
if (
!dev &&
(req.method === 'GET' || req.method === 'HEAD') &&
typeof res.writeEarlyHints === 'function'
) {
const match = app.routeFor(url.pathname);
Open node_modules/@webjsdev in your app and read any of it.
It works without a UI too
Two starting points, one command each. One is full-stack, the other is routes and modules with no UI at all. Either gives you a working app with live feature demos rather than an empty directory, and one command takes the demos out whenever you want to start clean.
Full Stack
DefaultSSR pages, web components, server actions, a database, streaming, and a browsable feature gallery. Auth (login, sessions, a protected route) ships as a gallery card.
app/page.ts components/counter.ts actions/posts.server.ts
Backend
A backend-only app, no UI or SSR. File-based route handlers, modules, middleware, rate limiting, WebSockets, a database, and a backend-features gallery.
app/api/users/route.ts app/api/chat/route.ts middleware.ts
Light DOM components, Tailwind CSS, Drizzle ORM, a modules layout, and design tokens are wired before you write a line. Every one of them is a default, not a lock-in. Swap what does not suit you.
Prefer Bun instead of Node.js? Flavor the whole scaffold for Bun by running
One command, then a prompt
Run the command below, then start your agent in the new app folder. The conventions, demos, and framework source are already there.