AI-first. Web components first.
Full-stack web framework inspired by NextJs, Lit, and Rails. cache() for
queries, HTTP Cache-Control for pages, Session class with SessionStorage,
NextAuth-style auth with providers, WebSocket broadcast, rate limiting.
Swap the in-memory cache store for Redis with a single setStore() call
(no config files, no build step in between). Web components first,
TypeScript with zero build step, real SSR with Declarative Shadow DOM.
- AI-first. Predictable file conventions, one function per file, explicit
.server.tsboundary,AGENTS.mdcontract — designed so LLMs modify code without loading the entire codebase into context. - No build.
.tsfiles served directly. Node 23.6+ strips types at runtime; the dev server strips types via esbuild for the browser (~1ms/file, cached). Edit, refresh, done. - Web components, light DOM by default. Pages and components render as light DOM so global CSS and Tailwind utilities apply directly — no
::part, no:host, no CSS-var plumbing. Shadow DOM is opt-in (static shadow = true) when you need scoped styles or real<slot>projection. Both modes SSR fully, no hydration runtime. - Tailwind CSS by default. The scaffold ships with the Tailwind browser runtime +
@themedesign tokens. Prefer hand-written CSS? Opt out entirely — the framework works just as well with vanilla CSS when you follow the wrapper-scoping convention (.page-<route>,.layout-<name>, component-tag scoped). Full recipe in the Styling docs. - Full-stack type safety. Import a
.server.tsfunction from a component — TypeScript sees the real signature. superjson on the wire preservesDate,Map,Set,BigInt. - Server-file source is unreachable from the browser. Framework invariant: any file ending
.server.{js,ts}or starting with'use server'is always served as an RPC stub, never its real source. Enforced in the HTTP layer with regression tests. - NextJs-style routing.
page.ts,layout.ts,route.ts,error.ts,middleware.ts,[params],(groups),_private. Layouts persist across navigations. - Client router. Turbo-Drive-style link interception. Shadow-DOM-aware via
composedPath(). Layouts stay mounted, only page content swaps. No white flash. - WebSockets built in. Export
WSfromroute.ts→ WebSocket endpoint.connectWS()on the client auto-reconnects. - Backend-only mode. Skip pages entirely — use webjs as a lightweight API framework with file routing, middleware, rate limiting, and TypeScript.
- Built-in essentials. Auth, sessions, caching, WebSocket broadcast, rate limiting — all built in, sharing one pluggable cache store. In-memory by default; call
setStore(redisStore({ url: process.env.REDIS_URL }))once at startup to put all four on Redis for horizontal scaling. - Lazy loading.
static lazy = truedefers module download until the component scrolls into the viewport. SSR content stays visible — only the JS is lazy. - Error boundaries & loading states.
error.tscatches render failures at any route level.loading.tsauto-wraps pages in Suspense boundaries. - Metadata routes.
sitemap.ts,robots.ts,manifest.ts,icon.ts,opengraph-image.ts— dynamic SEO/PWA metadata from functions, not static files. expose()for REST. Tag a server action withexpose('POST /api/posts', fn)to make it reachable over HTTP and via RPC. Optional input validation.- Production ready. CSRF, gzip/brotli, HTTP/2, 103 Early Hints, CSP nonces, modulepreload, rate limiting, health probes, graceful shutdown, streaming Suspense.
# scaffold a new app
npx webjs create my-app # full-stack (pages + API + components)
npx webjs create my-api --template api # backend-only (routes + modules, no SSR)
npx webjs create my-app --template saas # auth + dashboard + Prisma User model
# or run everything in the monorepo (website + docs + blog together)
git clone https://github.com/vivek7405/webjs
cd webjs && npm install
cd examples/blog && npx prisma migrate dev --name init && cd ..
npm run dev
# → Website → http://localhost:5000
# → Docs → http://localhost:4000
# → Blog → http://localhost:3456
#
# Or run any one individually:
cd examples/blog && npm run dev # just the blog
cd docs && npm run dev # just the docs
cd website && npm run dev # just the websitepackages/
core/ # webjs — html, css, WebComponent, renderers, client router
server/ # @webjskit/server — dev/prod server, router, SSR, actions, WS
cli/ # @webjskit/cli — webjs dev/start/build/db
examples/
blog/ # full-featured reference app (auth, posts, comments, chat)
docs/ # documentation site (built on webjs itself)
AGENTS.md # AI-agent contract for the framework
CLAUDE.md # Claude Code quick-reference
// app/page.ts — server-rendered, async data fetching
import { html, repeat } from '@webjskit/core';
import '../components/counter.ts';
import { listPosts } from '../modules/posts/queries/list-posts.server.ts';
export const metadata = { title: 'home' };
export default async function Home() {
const posts = await listPosts();
return html`
<h1>posts</h1>
<ul>
${repeat(posts, p => p.id, p => html`<li>${p.title}</li>`)}
</ul>
<my-counter count="3"></my-counter>
`;
}// components/counter.ts — interactive web component, light DOM + Tailwind
import { WebComponent, html } from '@webjskit/core';
export class Counter extends WebComponent {
// Light DOM is the default; Tailwind utility classes apply directly.
static properties = { count: { type: Number } };
count = 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--; this.requestUpdate(); }}>−</button>
<output class="min-w-[2ch] text-center">${this.count}</output>
<button class="px-3 py-1 rounded border border-border hover:bg-bg-elev" @click=${() => { this.count++; this.requestUpdate(); }}>+</button>
</div>
`;
}
}
Counter.register('my-counter');Need scoped styles, <slot> projection, or embed-ready isolation? Opt
in to shadow DOM with static shadow = true and author styles via
static styles = css\…``.
// modules/posts/queries/list-posts.server.ts — one function per file
'use server';
import { prisma } from '../../../lib/prisma.ts';
export async function listPosts() {
return prisma.post.findMany({ orderBy: { createdAt: 'desc' } });
}npx webjs build # optional: bundle for fewer HTTP requests
npx webjs start --port 8080 # JSON logs, gzip/brotli, ETag, streamingHealth: GET /__webjs/health. Graceful shutdown on SIGTERM.
Embed in Express/Fastify/Bun/Deno:
import { createRequestHandler } from '@webjskit/server';
const app = await createRequestHandler({ appDir: process.cwd() });
const resp = await app.handle(new Request('http://x/api/hello'));The docs site is built on webjs itself:
cd docs && npx webjs dev --port 400037 pages covering: getting started, AI-first development, routing, components, SSR, styling, Suspense, loading states, error handling, client router, server actions, expose() REST endpoints, API routes, WebSockets, database, authentication, TypeScript, middleware, rate limiting, lazy loading, metadata routes, caching, sessions, controllers, context protocol, task, deployment, backend-only mode, testing, conventions, configuration, editor setup.
Pre-1.0. 632 unit tests (96.6% line coverage, 87.5% branch, 93.6% function), 36 puppeteer e2e tests, 27 WTR browser tests. Key features:
- Core: SSR with DSD (opt-in) + light-DOM hydration (default), fine-grained client renderer,
repeat(),Suspense(), client router withcomposedPath()for shadow DOM, mixed-attribute interpolation, MutationObserver upgrade safety net - Data: server actions + superjson (Date/Map/Set/BigInt survive the wire),
expose()for REST,json()+richFetch()for content-negotiated APIs,cache()for server-side query caching with TTL +invalidate() - Server: file router, per-segment middleware,
rateLimit(), WebSockets +broadcast(), CSRF, compression, HTTP/2, 103 Early Hints, health probes, graceful shutdown,Sessionclass withSessionStorage(cookie or store-backed), NextAuth-stylecreateAuth()(Credentials, Google, GitHub) - DX: TypeScript with zero build,
AGENTS.mdcontract,CLAUDE.md, live reload in dev, optional esbuild bundle for prod,@webjskit/ts-pluginfor tsserver — tag-name and CSS-class-name go-to-definition insidehtml\`` templates.
MIT