Source profileQuality 94/100

webjsdev/webjs/.agents/skills/webjs/SKILL.md

webjs

Build and review WebJs applications. Use when working on WebJs app structure, pages, layouts, routes, server actions, components, signals, data and validation, auth, sessions, styling, the client router, streaming, or tests. WebJs is AI-first, web-components-first, and has no build step.

Source repository stars
109
Declared platforms
0
Static risk flags
2
Last source update
2026-08-21
Source checked
2026-08-28

Decision brief

What it does: where it fits

Use this skill for end-to-end WebJs app work. It helps you choose the right layer first, reach for the right export, and avoid the WebJs-specific mistakes that Next.js or Lit muscle memory causes. WebJs is its own framework: the component API matches Lit and the routing feels li…

Best for

  • New features or refactors touching pages, routes, actions, components, data, auth, sessions, styling, or tests
  • Reviewing WebJs code for correctness or framework usage
  • Answering "how should this be structured in WebJs?"

Not for

  • Treating a page or layout like a React component and expecting its markup to hydrate. It runs server-only; put interactivity in a component.
  • Promoting a whole page section to a component so that one control inside it can be interactive. The island should wrap the control and the state it reads. An oversized island ships its own JS AND un-elides every display…

Compatibility matrix

Platform support, with evidence labels

PlatformStatusEvidenceWhat to check
CodexNot declaredNo explicit evidencePortability before use
Claude CodeNot declaredNo explicit evidencePortability before use
CursorNot declaredNo explicit evidencePortability before use
Gemini CLINot declaredNo explicit evidencePortability before use
Open the compatibility checker

Installation

Inspect first. Install second.

The source command is displayed only when detected. A safe inspection prompt is always available so your agent can explain every action before execution.

Source-detected install commandSource
npx skills add https://github.com/webjsdev/webjs --skill ".agents/skills/webjs"
Safe inspection promptEditorial

Inspect the Agent Skill "webjs" from https://github.com/webjsdev/webjs/blob/5bfa596a4e92397961addc27d52e5d00bd6f5eb9/.agents/skills/webjs/SKILL.md at commit 5bfa596a4e92397961addc27d52e5d00bd6f5eb9. List every install step, command, network request, credential, file read/write, external action, and rollback step. Explain whether it fits my task. Do not install or execute anything until I approve.

Workflow

What the source asks the agent to do

  1. 01

    Default Workflow

    1. Classify the change. Route contract, data model, server mutation, auth, or only UI? 2. Start from the server. Add the page/route and its server action or query before wiring interactive UI. A page render or a POST should already return correct HTML before any component hydrat…

    Classify the change. Route contract, data model, server mutation, auth, or only UI?Start from the server. Add the page/route and its server action or query before wiring interactive UI. A page render or a POST should already return correct HTML before any component hydrates.Put code in the narrowest owner. Route-local first (modules//), promote to lib/ or components/ only when reuse is real.
  2. 02

    Full Documentation

    This skill is the quick guide. When you need the full API reference for a surface, load the matching file in references/ (listed below). For even deeper framework detail, WebJs ships buildless, so the source you run IS the source you read: look in nodemodules/@webjsdev/{core,ser…

    This skill is the quick guide. When you need the full API reference for a surface, load the matching file in references/ (listed below). For even deeper framework detail, WebJs ships buildless, so the source you run IS…
  3. 03

    What WebJs Is

    WebJs is an AI-first, web-components-first framework with no build step: source files are served as native ES modules, and TypeScript is stripped in place (Node 24+ or Bun). It runs SSR + progressive enhancement by default.

    Pages and layouts (app//page.ts, app//layout.ts) run only on the server to produce HTML. They do NOT hydrate, so their own markup cannot be interactive (an @click in a page template is dropped at SSR). They still LOAD i…Components (WebComponent custom elements) hydrate per element, islands-style. All interactivity lives here: @event, reactive property assignment, signal mutation..server.ts is the one server boundary. With 'use server' its exports are RPC-callable from the client (the import is rewritten to a stub); without it the file is a server-only utility whose browser import throws at load…
  4. 04

    When To Use This Skill

    New features or refactors touching pages, routes, actions, components, data, auth, sessions, styling, or tests

    New features or refactors touching pages, routes, actions, components, data, auth, sessions, styling, or testsReviewing WebJs code for correctness or framework usageAnswering "how should this be structured in WebJs?"
  5. 05

    Reach For The Right Primitive

    Scan this BEFORE deciding how to build something, while the shape of the code is still open. WebJs ships a primitive for most of the jobs below, and the mistake to guard against is rarely choosing badly between them. It is that the primitive never comes to mind at all, so the Re…

    Scan this BEFORE deciding how to build something, while the shape of the code is still open. WebJs ships a primitive for most of the jobs below, and the mistake to guard against is rarely choosing badly between them. It…Rows point rather than explain. The reference is the authority on the rule, and it is the durable half: the demo lives in the scaffold gallery, which an app deletes with npm run gallery:clear once it has outgrown it.

Permission review

Static risk signals and limitations

Reads files

low · line 7

The documentation asks the agent to read local files, directories, or repositories.

This skill is the quick guide. When you need the full API reference for a surface, load the matching file in `references/` (listed below). For even deeper framework detail, WebJs ships buildless, so the source you run IS the source you read

Network access

medium · line 45

The documentation includes network, browsing, or remote request actions.

| call server code from the browser | import the `'use server'` function and call it | hand-writing `fetch()` against an endpoint | `references/data-and-actions.md` | `app/features/server-actions` |

Evidence record

Why each signal appears

EvidenceSourceComputedTestedEditorial
SignalValueEvidence typeMeaning
Quality score94/100ComputedDocumentation, specificity, maintenance, and trust rules
Repository stars109SourceRepository attention, not individual Skill quality
Compatibility0 platformsSourceDeclared in the catalog source record
Usage guideautomated source guideEditorialGenerated or reviewed according to the visible evidence level

Pinned source

Provenance and original SKILL.md

Repository
webjsdev/webjs
Skill path
.agents/skills/webjs/SKILL.md
Commit
5bfa596a4e92397961addc27d52e5d00bd6f5eb9
License
MIT
Collected
2026-08-28
Default branch
main
View the original SKILL.md

Build a WebJs App

Use this skill for end-to-end WebJs app work. It helps you choose the right layer first, reach for the right export, and avoid the WebJs-specific mistakes that Next.js or Lit muscle memory causes. WebJs is its own framework: the component API matches Lit and the routing feels like Next, but the execution model is neither.

Full Documentation

This skill is the quick guide. When you need the full API reference for a surface, load the matching file in references/ (listed below). For even deeper framework detail, WebJs ships buildless, so the source you run IS the source you read: look in node_modules/@webjsdev/{core,server,cli}/ (each package ships its own AGENTS.md). The complete hosted docs live at https://webjs.dev/docs.

What WebJs Is

WebJs is an AI-first, web-components-first framework with no build step: source files are served as native ES modules, and TypeScript is stripped in place (Node 24+ or Bun). It runs SSR + progressive enhancement by default.

There is no server/client component split. No RSC render tree, no Flight protocol, no "use client" boundary. Instead:

  • Pages and layouts (app/**/page.ts, app/**/layout.ts) run only on the server to produce HTML. They do NOT hydrate, so their own markup cannot be interactive (an @click in a page template is dropped at SSR). They still LOAD in the browser so imported components register.
  • Components (WebComponent custom elements) hydrate per element, islands-style. All interactivity lives here: @event, reactive property assignment, signal mutation.
  • *.server.ts is the one server boundary. With 'use server' its exports are RPC-callable from the client (the import is rewritten to a stub); without it the file is a server-only utility whose browser import throws at load. This, not a component annotation, is how a dependency (the DB driver, secrets, node:*) is kept off the client.
  • route.ts is a server-only HTTP handler (named GET/POST exports), the one routing file that is NOT isomorphic.

Progressive enhancement is the default architecture. With JS off, content reads, <a> navigates, and a <form action=${importedAction}> submits to its server action. JS is opt-in per interactive behaviour. Never write a first paint that depends on hydration.

Islands, and why their size is a decision. A WebJs page is server-rendered HTML with small interactive components embedded in it, each hydrating on its own when the browser upgrades its tag. That is the islands model, and what makes it pay is that the sea is free: static markup a page renders costs the browser nothing, because a page never hydrates. So an island is not a unit of code organisation, it is a unit of shipped JavaScript, and it should wrap the interactive part and stop. Absorbing a page's static markup into a component to keep things tidy converts free HTML into shipped JavaScript, and it takes every display-only child down with it, because a component rendered by a component that ships can no longer be elided. Size the island to the behaviour, not to the section of the design it happens to sit in. references/components.md has the stopping rule and a worked before/after.

When To Use This Skill

  • New features or refactors touching pages, routes, actions, components, data, auth, sessions, styling, or tests
  • Reviewing WebJs code for correctness or framework usage
  • Answering "how should this be structured in WebJs?"
  • Finding the right export, reference doc, or default pattern for a task

Reach For The Right Primitive

Scan this BEFORE deciding how to build something, while the shape of the code is still open. WebJs ships a primitive for most of the jobs below, and the mistake to guard against is rarely choosing badly between them. It is that the primitive never comes to mind at all, so the React-shaped version gets hand-rolled in its place. That version compiles, passes webjs check, and ships, so nothing catches it. The "reflex to resist" column is what the hand-rolled version usually looks like.

Rows point rather than explain. The reference is the authority on the rule, and it is the durable half: the demo lives in the scaffold gallery, which an app deletes with npm run gallery:clear once it has outgrown it.

I need to...Reach forReflex to resistReferenceDemo
add a URL, static or with a dynamic segmenta file at app/<path>/page.ts, [id] for a paramregistering the route in a table or configreferences/routing-and-pages.mdapp/features/routing
abandon a render because something is missing or not allowedthrow notFound() / forbidden() / unauthorized()returning an error object and branching in the templatereferences/routing-and-pages.mdapp/features/boundaries
set a page's title, description, or social previewexport const metadata or generateMetadata()writing <head> tags in the pagereferences/routing-and-pages.mdapp/features/metadata
make part of the page respond to a click or hold statea WebComponent custom elementexpecting the page's own markup to hydratereferences/components.mdapp/features/components
render a keyed list, or swap one node when state changesrepeat() / watch() from /directivesre-rendering the component or diffing by handreferences/components.mdapp/features/directives
get server data into a component's first paintasync render() awaiting an actionfetching in connectedCallback, which SSR never callsreferences/components.mdapp/features/async-render
call server code from the browserimport the 'use server' function and call ithand-writing fetch() against an endpointreferences/data-and-actions.mdapp/features/server-actions
expose JSON to a caller outside the approute.ts with named GET / POST exportsa server action, which is the in-app pathreferences/routing-and-pages.mdapp/features/route-handler
write data from a form, JS off included<form action=${importedAction}>an @submit handler calling fetch()references/data-and-actions.mdapp/features/forms
make a mutation feel instantoptimistic()a manual try-catch that restores a cached copyreferences/optimistic-ui.mdapp/features/optimistic-ui
navigate without a full page reloadnothing, the router is already onimporting or configuring a routerreferences/client-router-and-streaming.mdapp/features/client-router
cross-fade a navigation instead of snappingthe view-transition meta, via page metadataanimating the swap yourselfreferences/client-router-and-streaming.mdapp/features/view-transitions
show tokens or progress as the server produces theman action returning an async generatorpolling, or a socket for a one-shot answerreferences/client-router-and-streaming.mdapp/features/streaming
change ONE element after a write<webjs-stream>redrawing the whole list around itreferences/client-router-and-streaming.mdapp/features/stream
paint the page before a slow region is ready<webjs-suspense> with a fallbackblocking the whole page on the slow awaitreferences/client-router-and-streaming.mdapp/features/suspense
refresh one region on its own, with no navigation<webjs-frame>a stateful component that fetches and re-rendersreferences/client-router-and-streaming.mdapp/features/frames
hold a live two-way connectiona WS() route export plus connectWS()polling on an intervalreferences/client-router-and-streaming.mdapp/features/websockets
push one update to every client on a socket pathbroadcast()every client polling for changesreferences/client-router-and-streaming.mdapp/features/broadcast
add login and a signed-in-only routecreateAuth plus a redirect in the pagerolling password hashing and session cookiesreferences/auth-and-sessions.mdapp/features/auth
remember something per visitor across requestsgetSession() on a signed cookiea module-level map keyed by userreferences/auth-and-sessions.mdapp/features/sessions
stop re-rendering a page identical for everyoneexport const revalidatecaching by hand in a module variablereferences/built-ins.mdapp/features/caching
read config or a secret at runtimeprocess.env server-side, WEBJS_PUBLIC_ for the browserimporting a config module into a componentreferences/built-ins.mdapp/features/env
stop one caller hammering an endpointthe rateLimit() middlewarecounting requests inside the handlerreferences/built-ins.mdapp/features/rate-limit
accept an upload and serve it backFileStore plus a streaming routebuffering the file in memory or writing to public/references/built-ins.mdapp/features/file-storage
keep the app usable offlinethe opt-in service workercaching responses in localStoragereferences/service-worker.mdapp/features/service-worker
see these composed in one real featurethe todo example appstitching the single-feature demos togetherreferences/optimistic-ui.mdapp/examples/todo

Load Only The References You Need

The table above routes by the job; this one routes by the topic, for when you already know which surface you are working on. Classify the task first, then load the smallest useful reference set. Each reference starts with a "What This Covers" section; read that to confirm relevance before reading the rest. Loading more than two or three at once usually means the task is not narrowed yet.

Task involves...Start with
Pages, layouts, dynamic routes, route handlers, metadata, redirects, 404sreferences/routing-and-pages.md
Writing components: what a component owns, reactive props, signals, lifecycle, light vs shadow DOMreferences/components.md
Why a component's JS was or was not downloaded, webjs elision, static interactive = truereferences/components.md
Server actions, mutations, queries, validation, the ActionResult envelopereferences/data-and-actions.md
Sessions, login flows, route protection, forbidden() / unauthorized()references/auth-and-sessions.md
Tailwind, light-DOM tag-prefix rule, tokens, fixed headers, no-reflow layoutreferences/styling.md
Where a repeated markup helper lives (utils/ui/ vs lib/), and fragment vs display-only componentreferences/styling.md
Client router, prefetch, frames, view transitions, Suspense streamingreferences/client-router-and-streaming.md
Optimistic UI for a user-facing mutationreferences/optimistic-ui.md
The @webjsdev/ui component kit (a components.json is present): class helpers, tokens, add / view, the MCP ui toolreferences/ui-kit.md
TypeScript at runtime, erasable syntax, full-stack types, the derive-the-type rule (never unknown / any)references/typescript.md
Unit, browser, e2e tests, the handle() harness, Bun parityreferences/testing.md
Auth, caching, env vars, rate limit, file storage, the webjs config blockreferences/built-ins.md
Node vs Bun, running the app, deploying, runtime-specific differencesreferences/runtime.md
Offline support, an asset cache, the opt-in service workerreferences/service-worker.md
Splitting a large file, or how big a module may bereferences/module-structure.md
A pattern that feels like Next.js or Lit but might not transferreferences/muscle-memory-gotchas.md

Common bundles:

  • Form or CRUD feature then data-and-actions, routing-and-pages, testing; add auth if user-specific
  • Interactive widget then components, styling; add client-router-and-streaming only if it streams
  • Protected area then auth-and-sessions, routing-and-pages, testing
  • Instant-feeling mutation then data-and-actions, optimistic-ui

Default Workflow

  1. Classify the change. Route contract, data model, server mutation, auth, or only UI?
  2. Start from the server. Add the page/route and its server action or query before wiring interactive UI. A page render or a <form> POST should already return correct HTML before any component hydrates.
  3. Put code in the narrowest owner. Route-local first (modules/<feature>/), promote to lib/ or components/ only when reuse is real.
  4. Keep server-only code behind .server.ts. The DB driver, secrets, and node:* never belong in a page, layout, or component.
  5. Add interactivity per behaviour. Reach for a component (and a signal or @event) only where the UI is genuinely interactive. A display-only component is elided from the browser. Then wrap the interactive part and STOP: the static markup around it stays in the page, where it costs nothing.
  6. Validate input at the boundary. Declare export const validate on an action; the RPC and route() boundaries run it.
  7. Default mutations to optimistic UI where the client can predict the result (optimistic() from @webjsdev/core).
  8. Type every boundary from its source, never unknown or any. The row type comes from the schema (typeof todos.$inferSelect), the action's input from a named interface and its result from ActionResult<T>, the routing files from PageProps / LayoutProps / RouteHandlerContext. unknown belongs on a payload nothing has vouched for yet that the next line narrows, and on a parameter of your own helper that forwards into an html template hole. Everywhere else, including a layout's children, it is a missing type. See references/typescript.md.
  9. Test the narrowest meaningful layer, and render the app in a real browser for any UI change (static checks do not catch a collapsed layout).

Project Layout

app/                  ROUTING ONLY (thin adapters importing from modules/)
  layout.ts           root layout (the ONLY file that may write <html>/<head>/<body>)
  page.ts             /
  <segment>/page.ts   /<segment>
  [param]/page.ts     dynamic route (params.param)
  <path>/route.ts     HTTP handler at /<path>
  error.ts loading.ts not-found.ts forbidden.ts unauthorized.ts   boundaries (nearest wins)
middleware.ts         root middleware
modules/<feature>/    actions/ (mutations, *.server.ts), queries/ (reads, *.server.ts),
                      components/ (custom elements), types.ts,
                      utils/ (pure; returns data, or an html fragment under utils/ui/)
lib/                  lib/*.server.ts server-only infra, lib/utils/ browser-safe helpers,
                      lib/utils/ui.ts app-wide html fragments (lib/ui/ once they grow)
components/*.ts        shared presentational custom elements (one per file)
db/*.server.ts        Drizzle: schema, connection
public/*              static assets, served at /public/<name>

App-internal imports use the # root alias (import { db } from '#db/connection.server.ts'), Node's native package.json imports field, not deep ../../../ relatives. A same-directory import stays relative.

Core WebJs Rules (invariants)

  1. Server-only code lives in .server.ts, route.ts, or middleware.ts. Never in a page, layout, or component (it crashes the browser at module load).
  2. 'use server' exports are async functions returning serializer-safe values. Files without 'use server' are server-only utilities.
  3. Custom element tag names contain a hyphen. Pass the tag to Class.register('tag-name').
  4. Event (@), property (.), and boolean (?) holes in html are UNQUOTED: @click=${fn}, never @click="${fn}".
  5. Signals are the default state primitive. Import signal / computed from @webjsdev/core, read via signal.get() inside render(). The base-class factory WebComponent({ ... }) is only for values riding an HTML attribute or arriving via SSR hydration.
  6. Page and layout default exports are functions returning a value; they never call render() themselves.
  7. Light-DOM components with custom CSS prefix every class selector with their tag name. Prefer Tailwind (unique by construction).
  8. Only the root layout may write <!doctype> / <html> / <head> / <body>.
  9. No backtick characters inside an html\...`` body, even in comments (it closes the literal and 500s).
  10. TypeScript must be erasable (erasableSyntaxOnly: true): no enum, no value namespace, no constructor parameter properties, no legacy decorators.
  11. Reactive properties are declared ONLY through the base-class factory extends WebComponent({ count: Number }). Never a static properties block, never a class-field initializer (it clobbers the reactive accessor).
  12. A form that writes binds its action: <form action=${importedAction}>, or a per-button <button formaction=${importedAction}>. A bound submitter is SELF-SUFFICIENT (#1307): the renderer puts formmethod="post" and formenctype on the button itself, so it needs no bound form around it and works inside any form or none. Quoted bindings, non-submit controls, <input type="submit"> (the identity needs its value, which is also its label, so use a <button>), submitter name / value / form / static formaction attributes, a .prop spelling of any of those, action=${fn} off a <form>, a bound form with method="get", a BOUND submitter's own non-post formmethod or unparseable formenctype, and a non-action function all throw. A PLAIN button's own formmethod / formenctype is a legal native override and is left alone. A page has no action export, so a bare <form method="post"> is a 405.

Export Map

Find the right export fast. Load the linked reference for full examples.

@webjsdev/core (browser + isomorphic)

  • html / css tagged templates. WebComponent({ ... }) base-class factory; prop(type?, opts?) declares one reactive property. register(tag, C) / Class.register('tag').
  • signal / computed reactive state, effect(fn) client-only reaction (returns a disposer), batch(fn) coalesced writes; render(v, el) client render.
  • notFound() / redirect(url[, status]) control-flow throws (page/layout/action only, NOT route.ts). forbidden() / unauthorized() render the nearest boundary.
  • Suspense({fallback, children}) page-level streaming; <webjs-suspense> component-level streaming.
  • optimistic() optimistic UI; navigate(url) / revalidate(url?) client-router control; connectWS / richFetch.
  • asset(path) content-hashes a public/ url so a deploy cannot serve stale bytes (href=${asset('/public/app.css')}), served immutable for a year. Page / layout / metadata route only, inside the render function. See references/built-ins.md.
  • Types: Metadata, PageProps<R>, LayoutProps<R>, RouteHandlerContext<R>, WebjsConfig.
  • @webjsdev/core/server: renderToString / renderToStream (Node side).
  • @webjsdev/core/directives: repeat, unsafeHTML (trusted only), live, keyed, guard, cache, until, watch(signal), ref / createRef, asyncAppend / asyncReplace, templateContent. Task / TaskStatus live at @webjsdev/core/task, context (createContext / ContextProvider / ContextConsumer) at /context. See references/components.md for the directive table + Task + context.

@webjsdev/server (server side)

  • createRequestHandler, cors(), route(action, opts?) REST adapter, sitemap() / sitemapIndex(), actionContext(), actionSignal(), requestId(), cache() / revalidateTag.
  • Route-handler toolkit: json(v) rich responder, readBody(req), clientIp(req), no-arg headers() / cookies() / cspNonce() (client counterpart richFetch is in @webjsdev/core). See references/routing-and-pages.md.
  • Auth + sessions: createAuth (+ Credentials / Google / GitHub), auth() / auth(req), session() + cookieSession / storeSession, getSession(req) (.get / .set / .flash / .destroy). File storage: getFileStore / diskStore / signedUrl. See references/auth-and-sessions.md + references/built-ins.md.
  • Data layer is Drizzle in db/*.server.ts. Auth, sessions, caching, rate limit, file storage are built in and pluggable (references/built-ins.md).

File conventions

page.ts (server-only fn), layout.ts (embeds children), route.ts (HTTP handler), middleware.ts, *.server.ts (server boundary), error.ts / loading.ts / not-found.ts / forbidden.ts / unauthorized.ts (boundaries), metadata routes (sitemap.ts, robots.ts, manifest.ts, icon.ts, opengraph-image.ts).

Canonical Patterns

A page

// app/about/page.ts
import { html } from '@webjsdev/core';
export default function About() {
  return html`<h1>About</h1>`;
}

A dynamic route reading data through an action

// app/users/[id]/page.ts
import { html } from '@webjsdev/core';
import { getUser } from '#modules/users/queries/get-user.server.ts';
export default async function User({ params }: { params: { id: string } }) {
  const user = await getUser(params.id); // never import the DB directly into a page
  return html`<h1>${user.name}</h1>`;
}

A server action

// modules/users/actions/update-profile.server.ts
'use server';
import { eq } from 'drizzle-orm';
import { db } from '#db/connection.server.ts';
import { users } from '#db/schema.server.ts';
export async function updateProfile(input: { id: string; name: string }) {
  const name = String(input?.name || '').trim();
  if (!name) return { success: false, error: 'name required', status: 400 };
  const [row] = await db.update(users).set({ name }).where(eq(users.id, input.id)).returning();
  return { success: true, data: row };
}

Call it from a component via a normal import (rewritten to a typed RPC stub). Never hand-write fetch().

An interactive component

// components/counter.ts
import { WebComponent, prop, html } from '@webjsdev/core';
class Counter extends WebComponent({ count: prop(Number) }) {
  constructor() { super(); this.count = 0; }
  render() {
    return html`<button @click=${() => this.count++}>${this.count}</button>`;
  }
}
Counter.register('my-counter');

The no-JS write path (a form-bound action)

// modules/contact/actions/send-message.server.ts
'use server';
export async function sendMessage(formData: FormData) {
  const email = String(formData.get('email') || '');
  if (!email) return { success: false, fieldErrors: { email: 'required' } };
  return { success: true, redirect: '/thanks' };
}

// app/contact/page.ts
import { sendMessage } from '#modules/contact/actions/send-message.server.ts';
export default function Contact({ actionData }) {
  return html`<form action=${sendMessage}><input name="email"></form>`;
}

Binding the action is the whole wiring: the renderer omits action (so the form posts to the page's own url), supplies method="post" and an enctype, and emits a hidden __webjs_action identity field. A form-bound action always receives the FormData.

Success is a 303 (PRG); failure re-renders the page at 422 with the result on actionData. With JS the client router applies the response in place. A submission that binds nothing is a 405, and the submission is Origin-verified like an RPC call.

Security And Session Defaults

  • Never ship demo secrets. Require session and provider secrets from the environment and fail fast if missing.
  • CSRF is an Origin / Sec-Fetch-Site check on both the action RPC and the form-submit path, not a token cookie. A safe GET action is CSRF-exempt. A route.ts REST endpoint is NOT covered: authenticate every mutating endpoint, validate, rate-limit.
  • Prod action errors are sanitized to a generic message plus a digest. Put a user-facing message on the ActionResult { success: false, error } envelope, never on a raw throw.
  • Use forbidden() for an authenticated user lacking permission, unauthorized() for an unauthenticated request. Inside a 'use server' RPC action, return an ActionResult for an auth failure instead of throwing.
  • For CORS use cors() from @webjsdev/server; credentials: true REQUIRES an explicit origin allowlist, never '*'.

Testing Defaults

  • Prefer server/handler tests first: drive the app with handle() from @webjsdev/server/testing and assert on the Response.
  • Add a browser test (npm run test:browser) for anything touching hydration, the client router, slots, or custom-element upgrade. A unit test is necessary but NOT sufficient for a browser-facing change.
  • Render the app and LOOK for any UI change: npm run check and npm run typecheck pass even when a layout collapses. Static tools give no signal for a visual defect.
  • WebJs runs on Node 24+ AND Bun. Prove a runtime-sensitive change (serializer, listener, streams, node:crypto, the TS stripper) on both.

Common Mistakes To Avoid

  • Treating a page or layout like a React component and expecting its markup to hydrate. It runs server-only; put interactivity in a component.
  • Promoting a whole page section to a component so that one control inside it can be interactive. The island should wrap the control and the state it reads. An oversized island ships its own JS AND un-elides every display-only component inside it, so the cost is not linear in what you moved.
  • Importing a .server.ts utility (no 'use server') directly into a shipping component. Its browser stub throws at load; reach it through a 'use server' action.
  • Using a static properties block or a class-field initializer for reactive props instead of the WebComponent({ ... }) factory.
  • Quoting an event / property / boolean hole (@click="${fn}").
  • Writing fetch() to call your own server instead of importing the action.
  • Writing a bare <form method="post"> and expecting a page action export to catch it. There is no such export; bind the action with action=${fn} or the submission is a 405.
  • Putting a submitter's formaction=${fn} on anything that is not a submit control, or on a button carrying its own name / value. The identity IS the button's name/value pair, so both halves are spoken for.
  • Writing formmethod="get" or formenctype="text/plain" on a button that BINDS an action. Neither can carry that action's body, so the pair contradicts itself and throws. On a button that binds nothing it is a legal native override and is honoured.
  • Binding an action whose file declares export const method = 'GET'. Form-bound actions strictly require POST (default). Binding a GET action to a form is a 405 at runtime and a webjs check error (form-action-not-a-get-action).
  • Leaving read-only RPC server query actions as default POST. Always export export const method = 'GET' for RPC data queries so arguments ride URL params, ETags/304 caching work, and CSRF is safely bypassed.
  • Writing method="get" on a bound <form action=${fn}>. WebJs supplies method="post" and formenctype automatically, and a bound form declaring method="get" is REFUSED at render (a thrown error, not a warning), because a GET sends no body for the action to read.
  • Throwing redirect() / notFound() inside a route.ts handler (uncaught 500). Return a Response there.
  • A placeholder first paint that fetches in connectedCallback. SSR does not call connectedCallback; put first-paint data in the constructor (server-known inputs) or use async render().
  • A browser global (window, document, localStorage) in the constructor or render(). It throws at SSR; do browser-only work in connectedCallback.
  • Interpolating into a component's <style> / <script> body. Use static styles or Tailwind.
  • Driving a component's markup from a delegated document listener in a page or layout, coupled by a class selector. The markup, the state, and the listener belong in one component.
  • Parking one component's UI state on <body> or <html>. The router's swap range never covers the document shell, so the flag outlives the markup it described. A document-wide SETTING such as the theme is the exception, and a transient effect such as a scroll lock is released in disconnectedCallback.

Frequently asked questions

What to verify before installation and use

What does the webjs source document cover?

Use this skill for end-to-end WebJs app work. It helps you choose the right layer first, reach for the right export, and avoid the WebJs-specific mistakes that Next.js or Lit muscle memory causes. WebJs is its own framework: the component API matches Lit and the routing feels li…

How do I install webjs?

The source record exposes this install command: npx skills add https://github.com/webjsdev/webjs --skill ".agents/skills/webjs". Inspect the command and pinned source before running it.

Which permission-related actions were detected?

Static rules flagged read-files, network in the source; the page lists the matching lines and excerpts.

Alternatives

Compare before choosing

Computed 100147

oaustegard/claude-skills

featuring

Generate hierarchical _FEATURES.md files that describe what a codebase DOES from a user/consumer perspective, anchored to source symbols via tree-sitting. Supports large complex codebases through feature-driven decomposition into sub-feature files. Uses a multi-pass synthesis: orientation → detail → overview rewrite. Use when someone says "what does this do", "document features", "feature inventory", "_FEATURES.md", or needs to understand a codebase's purpose before modifying it. Complements tre

Computed 10017

dancingteeth/unified-code-review

unified-code-review

Risk-first code review for PRs and branch audits: blast-radius triage, agent-authored discipline (tests first, intent evidence), call-graph pincer for integration defects between modules, then structural code-judo bar. Use when reviewing PRs, auditing agent-written diffs, catching rubber-stamp green CI, or wiring bugs single-file review misses. Prefer over structure-only thermo-nuclear review alone. Do not use for unrelated coding tasks or as an always-on rule.

Computed 1009

Postpartum-genushyacinthus29/dotnet-skills

dotnet-worker-services

Build long-running .NET background services with `BackgroundService`, Generic Host, graceful shutdown, configuration, logging, and deployment patterns suited to workers and daemons.

Computed 99241

enuno/unifi-mcp-server

unifi-mcp-tool-builder

Specialized guide for adding new MCP tools to the UniFi MCP Server following project standards, UniFi API patterns, and test-driven development practices. Use when implementing new UniFi Network Controller features as MCP tools.