Conventions your agent follows.
Architecture you still own.
WebJs is a full-stack JavaScript web components framework with no build step. You get production-ready architecture from your very first prompt.
Native web components the browser already understands, with better DX
A component here is a real custom element. The browser owns registration, upgrade, and the lifecycle, so nothing sits between your class and the DOM it renders. What WebJs adds is the ergonomics. Reactive properties are declared in the class signature, state runs on signals, markup is a tagged template literal, and none of it needs a decorator or a build step. Every component is server-rendered first, so the page reads and its forms submit before a script runs, which makes progressive enhancement 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>
What you write is what runs
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.
Model agnostic by construction
An agent does not need to have seen WebJs before. There is no build step, so the framework sits in your node_modules as plain JavaScript at the version you installed, and a model opens the router or the renderer it is calling instead of recalling an API. Switching models does not change the answer.
The architecture arrives decided
Where a page lives, where a form submission is handled, and which code is allowed to touch the server are settled by the framework rather than improvised per app. What comes back is in the shape a reviewer expects.
A design system, not scattered values
A palette and a type scale ship as design tokens rather than values spread through components, so every screen the app grows shares them and restyling the whole thing means editing the tokens instead of hunting through markup.
Types run the whole way through
A component importing a server function keeps that function's argument and return types at the call site, and a database row carries its schema type into the markup that renders it, with no code generation anywhere in between.
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
--foreground
--primary
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.