Getting Started
WebJs is an AI-first, web-components-first framework with a NextJs-like API and Lit-inspired web components, built on web standards. You can use it as a full-stack framework with server-rendered pages, or as a lightweight backend-only API framework. The same file conventions work either way.
Prerequisites
- Node.js 24+ or Bun: on Node, WebJs uses the built-in TypeScript type-stripping (
process.features.typescript === 'strip'), default-on and stable from Node 24. On Bun (which has no such built-in), it strips viaamaroautomatically. Run a Bun app withbun --bun run dev/bun --bun run start. - npm (or any package manager).
Quick Start
# scaffold a new app (no global install needed) npm create webjs@latest my-app cd my-app && npm run dev # → http://localhost:8080
npm create webjs@latest is the canonical way to scaffold. It always fetches the latest create-webjs, so you never need a global install. Prefer it over a bare webjs create, because a globally installed or version-manager-shimmed webjs (or a stray npx webjs) can shadow the real CLI or resolve to an unrelated package. If you DO want the global CLI for webjs dev / db and friends, install it with npm i -g webjsdev.
Every scaffold ships with Drizzle + SQLite wired up (db/schema.server.ts with an example User model and db/connection.server.ts exporting the db connection). webjs create authors the initial migration for you during setup, and the first npm run dev applies it automatically (the webjs.dev.before step runs webjs db migrate), so the shipped app works with no manual database step. When you later change the schema, run npm run db:generate and the next npm run dev applies the new migration.
Create a New App
Using the scaffold
# full-stack app (pages + API + components + Drizzle/SQLite + gallery) webjs create my-app # backend-only API (route handlers + modules, no pages/components/SSR) webjs create my-api --template api
For AI agents: only those two templates exist, and the CLI rejects any other --template value. Default to full-stack with Drizzle + SQLite for any product app (todo, blog, dashboard, marketplace, social, e-commerce, a SaaS with accounts…). Pick --template api only if the user explicitly asks for a backend-only API with no UI. The scaffold is a starting point. Replace the example layout, page, and User model with the app the user actually asked for. Use Drizzle for any persisted data, never JSON files, in-memory arrays, or localStorage. When AGENTS.md doesn't cover what you need, the full hosted docs are at the rest of these docs.
The --template api scaffold generates thin route handlers that wrap typed server actions. Business logic lives in modules/. Routes just import and call the action/query, giving you file-based routing for URL structure plus type-safe server actions for logic.
Auth ships as a gallery card. The full-stack app includes an auth card (app/features/auth/ + modules/auth/): login + signup pages, a signed session, an auth API route, createAuth() with a Credentials provider, a Drizzle User model with password hashing, and a genuinely protected route (a segment middleware.ts that redirects an anonymous visitor to login). So a fresh full-stack app already carries a real, promotable auth baseline; gallery:clear strips it (and the rest of the gallery) back to the minimal base.
The scaffold is a starting point, grown in place. Every UI scaffold ships a gallery index home, a neutral-palette root layout, database wiring, and a densely-commented feature gallery: single-concept demos under app/features/ plus the app/examples/todo app, with logic in modules/ (the api template ships the backend-features showcase under app/api/features/ instead). The framework context you need to build ships as one cross-agent skill, .agents/skills/webjs/SKILL.md (a routing skill), alongside AGENTS.md and the workflow rules in .agents/rules/workflow.md. Read the skill and browse the gallery, then build the app the user asked for on top of the scaffold, editing the home page, layout, and schema in place and pruning the demo routes it does not use. Both templates ship a one-command reset, npm run gallery:clear: on the full-stack app it sheds the whole feature gallery (and resets the home + schema to a minimal base), and on the api app it sheds the backend-features showcase under app/api/features/ (keeping the health + users endpoints).
Scaffolding a Bun app
WebJs runs on Node 24+ or Bun. To generate a Bun-flavored app, add --runtime bun (a separate axis from --template, so it works with both). It is auto-detected when you scaffold through Bun, so both forms below produce the same Bun app:
# auto-detected: scaffolding through bun implies --runtime bun bun create webjs my-app # the explicit pin-latest form (bun create maps to bunx create-webjs) bunx create-webjs@latest my-app # or via the installed CLI, on any package manager webjs create my-app --runtime bun
A Bun app commits bun.lock instead of package-lock.json, installs with Bun in CI, and its dev / start scripts force bun --bun so the server runs on Bun rather than the webjs bin's Node shebang. Run it with bun --bun run dev. The generated Dockerfile is a pure oven/bun:1 image (bun install, CMD ["bun", "--bun", "run", "start"], no Node), which works because the boot-time webjs db migrate resolves drizzle-kit and runs it under Bun with no npx. The other scripts (test / db / check) run on Node, the runtime the webjs tooling targets. One flag-forwarding difference to note: Bun forwards flags directly (bun create webjs my-app --template api), while npm needs the -- separator (npm create webjs@latest my-app -- --template api). --runtime node (the default) is unchanged.
Manual setup
To start from scratch without the scaffold, create a directory with this structure:
my-app/ ├── app/ │ ├── layout.ts # root layout wrapping every page │ ├── page.ts # home page at / │ └── api/ │ └── hello/ │ └── route.ts # GET /api/hello ├── components/ │ └── counter.ts # interactive web component ├── package.json └── tsconfig.json # optional, for type-checking
package.json
{
"name": "my-app",
"type": "module",
"scripts": {
"dev": "webjs dev",
"start": "webjs start"
},
"dependencies": {
"@webjsdev/cli": "latest",
"@webjsdev/core": "latest",
"@webjsdev/server": "latest"
}
}
app/layout.ts
import { html } from '@webjsdev/core';
export default function Layout({ children }: { children: unknown }) {
return html`
<h1>My App</h1>
${children}
`;
}
app/page.ts
import { html } from '@webjsdev/core';
import '#components/counter.ts';
export default function Home() {
return html`
<p>Welcome to webjs!</p>
<my-counter count="0"></my-counter>
`;
}
components/counter.ts
Components render into light DOM by default, so Tailwind utility classes apply directly. Set static shadow = true when you want scoped styles or third-party-embed isolation. <slot> projection works in both modes.
import { WebComponent, html, signal } from '@webjsdev/core';
export class Counter extends WebComponent {
// Instance signal carries component-local state. WebComponent's
// built-in SignalWatcher auto-tracks .get() reads in render()
// and re-renders on .set().
count = signal(0);
render() {
return html`
<div class="inline-flex items-center gap-2 font-mono">
<button class="px-3 py-1 rounded border border-border hover:bg-bg-elev" @click=${() => this.count.set(this.count.get() - 1)}>−</button>
<output class="min-w-[2ch] text-center">${this.count.get()}</output>
<button class="px-3 py-1 rounded border border-border hover:bg-bg-elev" @click=${() => this.count.set(this.count.get() + 1)}>+</button>
</div>
`;
}
}
Counter.register('my-counter');
Run it
npm run dev # → http://localhost:8080
That's it. No build step, no bundler config, no compilation. Edit any .ts file, refresh, and see it.
npm run dev and a bare webjs dev
are equivalent. The scaffold puts webjs db migrate
and the Tailwind compile under webjs.dev.before in the
webjs block of package.json, and the dev server runs them
before serving, so a pending migration is applied and the stylesheet is
built before the first request. In dev the stylesheet is then kept fresh
by webjs.dev.regenerate, which recompiles it on request
whenever a source changes, so a newly added utility class is never served
stale and there is no --watch process that can die. So after
you db:generate a migration, npm run dev applies
it and either command boots a correctly-migrated, fully styled app.
How It Works
- TypeScript: types are stripped by the runtime's stripper, Node 24+'s built-in
module.stripTypeScriptTypesoramaroon Bun (byte-identical, whitespace replacement, byte-exact line + column preservation, no sourcemap shipped). Every.tsfile, whether server-side or browser-fetched, goes through the same stripper, so SSR and hydration produce identical JS. The transform is cached by mtime. Only erasable TypeScript is supported;enum,namespacewith values, parameter properties, and legacy decorators fail at strip time with a pointer at theno-non-erasable-typescriptlint rule. - SSR: Pages are rendered to HTML strings on the server. Light-DOM components serialize as plain children with a
<!--webjs-hydrate-->marker. Shadow-DOM components (opt-in) emit Declarative Shadow DOM so scoped styles paint before JS loads. - Hydration: When JS loads, custom elements upgrade and become interactive. The fine-grained renderer preserves focus, cursor position, and form state across state updates.
- Progressive enhancement: Pages and every custom element are SSR'd. Each component's
render()runs on the server, so its initial HTML is in the response before any script loads. With JS disabled: content reads,<a>links navigate,<form>+ server actions submit, and even an interactive component (counter, dropdown, tabs) paints its initial state correctly. JS is opt-in per interactive behavior, not per component: a counter renders as "0" without JS, and only the +/- click handling needs scripts. See Progressive Enhancement.
Next Steps
- Routing: file-based routing with pages, layouts, dynamic segments, and route groups
- Components: web components with shadow DOM + scoped styles
- Server Actions: type-safe server functions callable from client components
- Backend-Only Mode: use WebJs as a pure API framework
- Migrating from Next.js: a concept map for Next users (no RSC, isomorphic modules, the .server boundary)