Skip to content

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.

npm create webjs@latest my-app

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. 😉

components/like-button.ts
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.
live
Server-rendered, then upgraded. Click it.
No whole-page hydrationContent readsLinks navigateForms submitDisplay components ship 0 KB

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)

actions/get-post.server.ts
'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

app/posts/[id]/page.ts
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.

app/
  page.ts → /
  posts/[id]/page.ts → /posts/7
  api/hooks/route.ts → POST

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.

Client
RPC
Server action

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.

<price-tag> 0 KB
<add-to-cart> hydrates

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.

streaming data chunk...

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.

Login & sessions
Database & migrations

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.

Caching Rate limits File storage WebSockets

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

terminal
$ 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/
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

design system
$ 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

node_modules/@webjsdev/core/src/component.js
// 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

node_modules/@webjsdev/server/src/dev.js
// 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

Default

SSR 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
npm create webjs@latest my-app

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
npm create webjs@latest my-api -- --template api

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 bun create webjs@latest my-app

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.

npm create webjs@latest my-app