youneed docs
← youneed
Menu ☰

Docs

One paradigm, ~170 packages in the monorepo. The sidebar groups them by ecosystem — dom, server, ssr, cli — each core package followed by adapters, providers, middleware and plugins that snap into it. Everything below is copied from the packages' own READMEs — nothing here is aspirational.

Introduction

youneed is a TypeScript-first toolkit for building web apps on native platform primitives — Custom Elements, Shadow DOM, the HTTP server, the Speculation Rules API — instead of a virtual DOM and a heavy runtime. Every package shares one paradigm: a factory returns a base class you extend, TC39 standard decorators register members into per-class registries, and a fluent builder wires it together. Component(tag), Controller(path), Page(url) and Test() all feel the same — learn the shape once on the client, reuse it on the server, in pages, in tests.

The dependency graph stays shallow on purpose: dom has zero dependencies, ssr builds on dom + server, devtools builds on dom + ssr. Install one package, or compose the stack — nothing drags the rest in.

Installation & quick start

Add the core client package and define a component.

$ pnpm add @youneed/dom
counter.ts @youneed/dom
@Component.define()
class Counter extends Component("x-counter") {
  @Component.prop() count = 0;
  @Component.event() inc() { this.count++; }
  render() {
    return html`<button @click=${this.inc}>${this.count}</button>`;
  }
}
`; } }' aria-label="Copy code sample">Copy
A note on decorators. youneed uses TC39 standard decorators (@Component.define()), which no browser runs natively yet and which Vite's default oxc/esbuild target leaves untransformed. Running with tsx / node --import tsx handles them out of the box. Bundling with Vite needs @youneed/vite-plugin added so they're lowered.

dom @youneed/dom

A component framework built directly on native Custom Elements and Shadow DOM, with no virtual DOM. It combines Lit-style tagged-template rendering and scoped styles, Angular-style decorators, tasks and signals, and platform-native lifecycle — producing real custom elements that drop into plain HTML, React, Vue, or SSR markup without extra glue.

counter.ts @youneed/dom
import { Component, html, css } from "@youneed/dom";

@Component.define()
class Counter extends Component("x-counter") {
  static styles = css`button { font: inherit }`;

  @Component.prop() count = 0;          // reactive: assigning re-renders
  @Component.prop({ attribute: true }) label = "count"; // reflects <x-counter label="…">

  @Component.event() inc() { this.count++; } // auto-bound for @click

  render() {
    return html`<button @click=${this.inc}>${this.label}: ${this.count}</button>`;
  }
}
packages/dom on GitHub →

dom-router @youneed/dom-router

A small client-side SPA router that mounts a Custom Element into a DOM outlet when the current URL matches a route. It supports three URL strategies — hash, history, query — behind a single API, and routes can target either a component class or a tag-name string. It pairs naturally with @youneed/dom components but has no hard dependency on it.

router.ts @youneed/dom-router
import { createRouter } from "@youneed/dom-router";

const router = createRouter({
  outlet: document.getElementById("app")!,
  mode: "history",                 // "hash" (default) · "history" · "query"
  routes: [
    { path: "/",            component: HomePage },       // a component CLASS…
    { path: "/users/:id",   component: "user-page" },    // …or a tag string. params: { id }
    { path: "*",            component: "not-found" },     // catch-all
  ],
});

router.navigate("/users/42");      // updates the URL + mounts <user-page>
router.current?.params;            // { id: "42" }
packages/dom-router on GitHub →

devtools @youneed/devtools

A floating, React-DevTools-style inspector for @youneed/dom applications. It provides a live, searchable component tree, a detail view with props, time-travel over state snapshots, a props diff, emitted events, live scheduler swapping, and per-element style editing — extensible with additional tabs (@youneed/ssr adds Page/Routes/Map views).

devtools.ts @youneed/devtools
import { installDevtools, mountDevtoolsPanel } from "@youneed/devtools";

installDevtools();        // capture per-component state/props/events/styles
mountDevtoolsPanel();     // floating, dockable, interactive panel (state persists)
packages/devtools on GitHub →

dom-adapter-react @youneed/dom-adapter-react

Bridge @youneed/dom and React in both directions. toReact renders a dom component inside a React tree — props are type-checked against the component's own @prop fields and on<Event> handlers receive its CustomEvents. fromReact wraps an existing React component as a custom element that drops into a dom tree — no rewrite. Part of the adapter family (dom-adapter-vue, -preact, -svelte, -astro, -angular).

bridge.tsx @youneed/dom-adapter-react
import { toReact, fromReact } from "@youneed/dom-adapter-react";

// dom → React: a real React component with typed props
const ReactUserCard = toReact(UserCard);
function Profile({ user }) {
  return <ReactUserCard user={user} onSelect={(e) => console.log(e.detail)} />;
}

// React → dom: a custom-element class wrapping a React component
const ReactChart = fromReact(Chart);
html`<${ReactChart.tagName} .props=${{ data }}></${ReactChart.tagName}>`;
packages/dom-adapter-react on GitHub →

dom-provider-i18n @youneed/dom-provider-i18n

Use @youneed/i18n translations inside components — call i18n("key") straight in an html template and re-render on every locale change. The i18nProvider form plugs into the component's providers slot and adds a typed this.i18n (key autocomplete) with automatic reactivity — no boilerplate, and it composes with other providers in the same array.

greeting.ts @youneed/dom-provider-i18n
import { Component, html } from "@youneed/dom";
import { createI18n } from "@youneed/i18n";
import { i18nProvider } from "@youneed/dom-provider-i18n";

const appI18n = createI18n({
  resources: { en: { hello: "Hello {name}" }, de: { hello: "Hallo {name}" } },
  locale: "en",
});

class Greeting extends Component("x-greeting", { providers: [i18nProvider(appI18n)] }) {
  render() {
    return html`<div>${this.i18n("hello", { name: "Ada" })}</div>`;
    //                       ^ typed: autocompletes "hello" and checks params
  }
}
// setLocale("de") → every subscribed component re-renders
packages/dom-provider-i18n on GitHub →

dom-provider-timers @youneed/dom-provider-timers

Lifecycle-scoped timers: this.timers wraps setTimeout / setInterval / rAF / idle / delay + the Scheduler API (postTask, yield) + debounce / throttle — everything is cancelled automatically when the component disconnects, and every handle (and the registry itself) implements Symbol.dispose, so using scopes it. The search box in this page's sidebar is debounced through it.

clock.ts @youneed/dom-provider-timers
import { Component, html } from "@youneed/dom";
import { timersProvider } from "@youneed/dom-provider-timers";

class Clock extends Component("x-clock", { providers: [timersProvider()] }) {
  time = this.signal(new Date());

  onMount() {
    this.timers.setInterval(() => this.time.set(new Date()), 1_000);
    // no teardown code — cancelled on disconnect
  }

  render() {
    return html`<time>${this.time.get().toLocaleTimeString()}</time>`;
  }
}

// delay rejects AbortError on disconnect; postTask maps to scheduler.postTask.
// Everything cancellable is Disposable:
{
  using tick = this.timers.setInterval(render, 100);
} // ← cancelled at end of scope, even on throw
packages/dom-provider-timers on GitHub →

server @youneed/server

A small, typed HTTP server built on node:http, offering decorator-based controllers, schema validation with type inference, guards, Express-style middleware, and content negotiation. It follows the same paradigm as the rest of the toolkit — extend a base class, mark methods with decorators, compose behavior with a fluent builder.

cats-controller.ts @youneed/server
import { Application, Controller, t, HttpError } from "@youneed/server";

const Cat = t.object({ name: t.string(), age: t.number() });

class Cats extends Controller("/cats", { guards: [requireApiKey] }) {
  @Controller.get("/:name", { params: t.object({ name: t.string() }), response: { 200: Cat } })
  async byName(ctx: Context) {
    const cat = lookup(ctx.params.name);
    if (!cat) throw new HttpError(404, { error: "Not found" }); // throw any status
    return cat;                                                  // validated against 200: Cat
  }

  @Controller.guard(isAdmin)            // per-method guard (stacks with class guards)
  @Controller.post({ body: Cat, response: { 201: Cat } })
  async create(ctx: Context) {
    return this.Response.json(ctx.body, { status: 201 });        // ctx.body is typed + validated
  }
}

Application(Cats)
  .use(requestLogger(), cors())          // global middleware (onion model)
  .openapi({ title: "Cats", version: "1.0.0" })   // → GET /openapi.json
  .listen(3000, (ctx) => console.log(`:${ctx.port}`));
packages/server on GitHub →

schema @youneed/schema

class-validator-style DTO validation built on standard TC39 decorators rather than reflect-metadata or legacy experimental decorators, so the same decorated class validates identically in TypeScript and plain compiled JS. Fields are annotated with constraint decorators like @IsEmail(), @MinLength(), and @Min(), and validate() checks a class or plain object against those rules.

schema.ts @youneed/schema
import { IsEmail, IsNotEmpty, MinLength, IsOptional, IsInt, Min, validate } from "@youneed/schema";

class CreateUserDTO {
  @IsEmail() email!: string;
  @IsNotEmpty() @MinLength(8) password!: string;
  @IsOptional() @IsInt() @Min(18) age?: number;
}

const errors = validate(CreateUserDTO, await req.json());
// [] when valid, else:
// [{ property: "email", value: "nope", constraints: { isEmail: "email must be an email" } }]
packages/schema on GitHub →

orm-sql @youneed/orm-sql

A small TypeORM-style SQL ORM built on standard TC39 decorators, with no reflect-metadata or legacy TypeScript decorator flags required. Entities are plain classes and databases plug in as adapters, with a zero-dependency SQLite adapter (node:sqlite) included as the reference engine — repositories give CRUD, relations, transactions, and schema migrations via a Migrator.

orm-sql.ts @youneed/orm-sql
import { Table, Orm, getRepository } from "@youneed/orm-sql";

class UsersTable extends Table("users") {
  @Table.primaryGeneratedColumn() id!: number;

  @Table.field("string")
  @Table.index({ group: "user_action" })
  userId!: string;

  @Table.field("string", { unique: true }) email!: string;
  @Table.column({ type: "boolean", default: true }) isActive!: boolean;

  @Table.oneToMany(() => Photo, (p) => p.user) photos!: Photo[];
}

await Orm({
  type: "sqlite",
  database: ":memory:",
  tables: [UsersTable, Photo],
  synchronize: true,
});

const users = getRepository(UsersTable);
const ada = await users.insert({ userId: "u1", email: "ada@x.com" });
await users.findOne({ email: "ada@x.com" }); // → UsersTable instance
packages/orm-sql on GitHub →

server-middleware-rate-limit @youneed/server-middleware-rate-limit

Rate-limit requests with a pluggable strategy — fixed window, sliding log, token bucket, exponential backoff — emitting standard X-RateLimit-* and Retry-After headers. The KV-backed KvFixedWindow keeps one shared limit across a whole fleet via a single atomic incr per request. One of ~30 server-middleware-* packages that compose the same way (cors, helmet, etag, session, metrics, …).

rate-limit.ts @youneed/server-middleware-rate-limit
import { Application } from "@youneed/server";
import { rateLimit, TokenBucket, KvFixedWindow } from "@youneed/server-middleware-rate-limit";
import { RedisKV } from "@youneed/kv-redis";

Application()
  .use(rateLimit({ windowMs: 60_000, max: 100 }))   // fixed window, global
  .use("/api", rateLimit({ strategy: new TokenBucket({ capacity: 50, refillPerSec: 5 }) }))
  // one shared limit across N instances — the counter lives in Redis
  .use("/auth", rateLimit({ strategy: new KvFixedWindow(new RedisKV({ url: process.env.REDIS_URL }), { windowMs: 60_000, max: 20 }) }))
  .listen(3000, () => {});
packages/server-middleware-rate-limit on GitHub →

server-plugin-jobs @youneed/server-plugin-jobs

A zero-dependency job scheduler — cron expressions (5- or 6-field), fixed intervals, one-off delays — shipped standalone (createScheduler) and as a server plugin that starts on listen and stops during graceful drain. A leader-lock over a shared KV store makes a job fire exactly once per occurrence across a fleet, and injectable clock/timers make tests run instantly.

jobs.ts @youneed/server-plugin-jobs
import { Application } from "@youneed/server";
import { jobs } from "@youneed/server-plugin-jobs";

const cron = jobs({
  jobs: [{ name: "cleanup", schedule: "0 */6 * * *", handler: purge }],
});

app.plugin(cron).listen(3000, () => {}); // start() on listen, stop() on drain

// still mutable after registration
cron.scheduler.add({ name: "heartbeat", schedule: { every: 30_000 }, handler: ping });
cron.scheduler.trigger("cleanup");       // run one now, bypassing the schedule
packages/server-plugin-jobs on GitHub →

ssr @youneed/ssr

Server-side rendering for @youneed/dom components, emitted as native Declarative Shadow DOM so markup hydrates without JavaScript, plus a Page entity that acts as the document-level counterpart to a Controller, with first-class Speculation Rules support. It requires registering a server DOM (happy-dom) before importing the package, since components extend HTMLElement at import time.

home.ts @youneed/ssr
import { Page, mountPages, enablePageDevtools } from "@youneed/ssr";
import { Application } from "@youneed/server";

class About extends Page("/about", { title: "About" }) {
  render() { return AboutApp; }                  // a component class, instance, or HTML string
}

class Home extends Page("/", {
  title: "Home",
  clientScript: () => import("./client.ts"),     // type-checked; resolved to a URL
  speculation: { prerender: [{ source: "list", urls: [About.url], eagerness: "moderate" }] },
}) {
  render() { return HomeApp; }
}

enablePageDevtools();                            // embed page+routes payload (dev)
mountPages(Application(), Home, About).listen(3010,);
packages/ssr on GitHub →

ssr-plugin-meta @youneed/ssr-plugin-meta

SEO <meta> + OpenGraph + Twitter Card tags as SSR page middleware. A page declares metadata via the meta option; the module renders the tags, resolving og:url / og:image to absolute URLs against the SSR origin. meta can also be a function of the request context for per-request tags.

post.ts @youneed/ssr-plugin-meta
import { ssr } from "@youneed/server-plugin-ssr";
import { meta } from "@youneed/ssr-plugin-meta";

class Post extends Page("/blog/:slug", {
  title: "Hello world",
  meta: {
    description: "An introductory post.",
    og: { type: "article", image: "/og/hello.png" },
    twitter: { card: "summary_large_image" },
  },
}) { /* … */ }

app.plugin(ssr({
  origin: "https://example.com",
  pages: [Post],
  modules: [meta({ siteName: "Example", twitterSite: "@example" })],
}));
packages/ssr-plugin-meta on GitHub →

ssr-plugin-sitemap @youneed/ssr-plugin-sitemap

A sitemap.xml module for the SSR plugin. Static page routes are enumerated automatically; dynamic routes (/users/:id) are listed via entries — a value or an async function, so the feed reflects fresh data on each request. All <loc>s resolve absolute against origin.

sitemap.ts @youneed/ssr-plugin-sitemap
import { ssr } from "@youneed/server-plugin-ssr";
import { sitemap } from "@youneed/ssr-plugin-sitemap";

app.plugin(ssr({
  origin: "https://example.com",
  pages: [Home, About, Pricing],
  modules: [
    sitemap({
      exclude: ["/admin", /^\/internal/],
      entries: [{ url: "/blog/launch", lastmod: "2026-06-01", priority: 0.8 }],
      defaults: { changefreq: "weekly", priority: 0.5 },
    }),
  ],
}));
packages/ssr-plugin-sitemap on GitHub →

ssr-plugin-robots @youneed/ssr-plugin-robots

A robots.txt module: per-user-agent allow/disallow policies, a Sitemap: line resolved against origin, and the permissive "allow everything" file when no policies are given.

robots.ts @youneed/ssr-plugin-robots
import { ssr } from "@youneed/server-plugin-ssr";
import { robots } from "@youneed/ssr-plugin-robots";

app.plugin(ssr({
  origin: "https://example.com",
  modules: [
    robots({
      policies: [
        { userAgent: "*", disallow: ["/admin", "/api"], allow: "/api/public" },
        { userAgent: ["GPTBot", "CCBot"], disallow: "/" },
      ],
      sitemap: true, // → Sitemap: https://example.com/sitemap.xml
    }),
  ],
}));
packages/ssr-plugin-robots on GitHub →

cli @youneed/cli

A type-safe, Commander-style CLI framework built on the same factory-class pattern as @youneed/dom's Component and @youneed/server's Controller. Options and commands are defined as classes and composed into an Application, with this.options and execute(...) typed directly from flag/argument strings — plus a small reactive layer, graceful shutdown, middleware, and plugins.

cli.ts @youneed/cli
import { Application, Command, Option, defaultOptions } from "@youneed/cli";

// A reusable, named option (its key + value type flow into `this.options`).
class FirstOption extends Option("--first", {
  short: "f",
  description: "display just the first substring",
}) {}

class SplitCommand extends Command({
  name: "split <string>", // grammar: a word + positional args
  description: "Split a string into substrings and display as an array",
  options: [FirstOption, { name: "-s, --separator <char>", default: "," }, ...defaultOptions()],
}) {
  execute(value: string) {
    // `value` is typed from `<string>`; `this.options` from the options tuple.
    const limit = this.options.first ? 1 : undefined;
    console.log(value.split(this.options.separator, limit));
  }
}

Application({
  name: "string-util",
  description: "CLI to some JavaScript string utilities",
  version: "0.0.8",
  commands: [SplitCommand],
  options: [...defaultOptions()],
});
packages/cli on GitHub →

cli-middleware-prompt @youneed/cli-middleware-prompt

Interactive prompts for CLI commands: the middleware adds this.prompt with ask (free text), confirm (y/n), choice (single-select), list (multi-select), alert and spinner. Prompts draw through the core LiveRenderer in raw-key mode, and everything binds to one terminal — inject a scripted double and they're testable.

setup.ts @youneed/cli-middleware-prompt
import { Application, Command } from "@youneed/cli";
import { prompts } from "@youneed/cli-middleware-prompt";

class Setup extends Command("setup", { middleware: [prompts()] }) {
  async execute() {
    const name = await this.prompt.ask("Project name?", { default: "app" });
    const env = await this.prompt.choice("Environment", ["dev", "staging", "prod"]);
    const feats = await this.prompt.list("Features", ["ts", "lint", "tests"]);
    if (await this.prompt.confirm(`Create ${name}?`, { default: true })) {
      await this.prompt.spinner("scaffolding", () => scaffold(name, env, feats));
      await this.prompt.alert("Done!");
    }
  }
}

Application({ name: "create", commands: [Setup] }).run(["setup"]);
packages/cli-middleware-prompt on GitHub →

cli-plugin-help @youneed/cli-plugin-help

Enhanced help: registers a help [command] command that replaces the built-in output with a grouped command list and per-command examples — the interactive, in-terminal usage screen. For offline man(1) documentation, pair it with cli-plugin-man.

ops.ts @youneed/cli-plugin-help
import { Application } from "@youneed/cli";
import { help } from "@youneed/cli-plugin-help";

Application({
  name: "ops",
  version: "1.0.0",
  description: "Operations toolkit",
  commands: [/* … */],
  plugins: [
    help({ examples: { split: ["ops split a,b,c --first"] } }),
  ],
}).run();

// ops help          → grouped command list with examples
// ops help split    → usage, options and examples for one command
packages/cli-plugin-help on GitHub →

test @youneed/test

A class-and-decorator test framework built in the same paradigm as @youneed/dom and @youneed/server: a factory returns a base class you extend, TC39 decorators register test members (cases, fixtures, hooks), and a fluent TestApplication builder wires everything up and runs it. Fixtures give scoped setup/teardown, TestContext carries steps/annotations/abort signals, and reporters, plugins, and parallelization (worker processes, CI sharding) are pluggable.

calculator.test.ts @youneed/test
import { Test, Fixture, TestApplication, expect } from "@youneed/test";

class Calculator {
  add(a: number, b: number) { return a + b; }
}

class CalcFixture extends Fixture<Calculator>({ name: "calc", scope: "test" }) {
  setup() { return new Calculator(); }
}

class CalcTest extends Test({ name: "Calculator" }) {
  @Test.use(CalcFixture) calc!: Calculator;

  @Test.it("adds two numbers")
  adds() {
    expect(this.calc.add(2, 3)).toBe(5);
  }
}

await TestApplication().addTests(CalcTest).run();
packages/test on GitHub →

logger @youneed/logger

A zero-dependency, Winston-style structured logger with pluggable transports and a composable format pipeline. The core touches no Node-only APIs, so the same bundle runs in the browser, in SSR/SSG, on the server, in workers, and at the edge — the only built-in destination is a universal ConsoleTransport, while environment-specific destinations ship as separate @youneed/logger-transport-<name> packages. Child loggers, secret redaction, and TC39 resource management for disposal round it out.

logger.ts @youneed/logger
import { createLogger, format, ConsoleTransport } from "@youneed/logger";

const log = createLogger({
  level: "info",
  format: format.combine(format.timestamp(), format.redact(["ssn"]), format.json()),
  defaultMeta: { service: "api" },
  transports: [new ConsoleTransport()], // works in the browser and on the server
});

log.info("listening", { port: 3000 });
// {"level":"info","message":"listening","timestamp":"…","service":"api","port":3000}

const reqLog = log.child({ requestId: "r-42" }); // bindings on every record
reqLog.error("db down", { password: "hunter2" }); // password → "[REDACTED]"
packages/logger on GitHub →

Naming & the full index

Around 170 packages live in the monorepo. Most are an extension of a core package, named <core>-<kind>-<name>:

dom-provider-* A this.*-style extension for Component — e.g. dom-provider-a11y, dom-provider-i18n, dom-provider-rbac.
dom-adapter-* Interop with another view layer — dom-adapter-react, dom-adapter-vue, dom-adapter-svelte, dom-adapter-astro.
server-plugin-* A ServerPlugin bolt-on for @youneed/serverserver-plugin-graphql, server-plugin-oauth2, server-plugin-jobs.
server-middleware-* Onion-model middleware for the core server — server-middleware-logger, server-middleware-idempotency.
cli-middleware-* Middleware for @youneed/cli commands — cli-middleware-prompt, cli-middleware-progress, cli-middleware-color.
logger-transport-* A destination for @youneed/loggerlogger-transport-stdout, -file, -http.
orm-adapter-* A dialect/driver for the ORMs — orm-adapter-mysql, orm-adapter-postgres, orm-adapter-mongo.
test-reporter-* / test-plugin-* Output formats and plugins for @youneed/test — reporters render results, plugins add behavior (benchmark, snapshot, resilience).

Every package follows the same shape underneath the name: a factory, a base class, standard decorators. Install one, or compose the stack — nothing drags the rest in.

The full index

Every package of every ecosystem, one block each — the left sidebar links straight into this list (filter it from the sidebar's search box). Descriptions come straight from each package's package.json; the interactive searchable catalog lives on the landing page.

@youneed/ai-skill

Agent Skills that turn Claude into a youneed-framework expert: the `youneed` skill (components, server, organization, performance, server-optimizations/security/best-practices, middleware, realtime/pub-sub, plugins & infra, a11y, i18n, auth/login (OAuth2/OTP/JWT/webhooks), migration) plus focused skills for the CLI framework (cli), server-side rendering & static generation (ssr), devtools/ts-plugin (develop), logger/transports/config (logging), orm-sql/kv (orm), writing/running tests (testing), migrating an existing app onto the youneed stack — frontend/backend/data/tests + build switch + interop adapters (migration), authorization + secrets (security), feature flags (feature-flags), application server plugins graphql/grpc/mailer/storage/queue/otlp (server-plugins), the shad UI library + component providers (ui), typed API client / resilient fetch / runtime adapters (clients), and the shared @youneed/core primitives + build tooling (foundation).

README →

@youneed/api-client

Typed API client runtime + OpenAPI → TypeScript client codegen (consumes the OpenAPI @youneed/server generates). Dependency-free, uses fetch (or @youneed/http-client).

import { generateClient } from "@youneed/api-client/codegen";
const code = generateClient(openApiDoc, { className: "PetStore" }); // → .ts source string
README →

@youneed/cli

Type-safe, Commander-style CLI framework on the @youneed factory-class pattern.

import { Application, Command, Option, defaultOptions } from "@youneed/cli";

// A reusable, named option (its key + value type flow into `this.options`).
class FirstOption extends Option("--first", {
  short: "f",
  description: "display just the first substring",
}) {}

class SplitCommand extends Command({
  name: "split <string>", // grammar: a word + positional args
  description: "Split a string into substrings and display as an array",
  options: [FirstOption, { name: "-s, --separator <char>", default: "," }, ...defaultOptions()],
}) {
  execute(value: string) {
    // `value` is typed from `<string>`; `this.options` from the options tuple.
    const limit = this.options.first ? 1 : undefined;
    console.log(value.split(this.options.separator, limit));
  }
}

Application({
  name: "string-util",
  description: "CLI to some JavaScript string utilities",
  version: "0.0.8",
  commands: [SplitCommand],
  options: [...defaultOptions()],
});
README →

@youneed/cli-middleware-cache

Disk cache middleware: this.cache.get/set/wrap, TTL, JSON on disk.

import { Command } from "@youneed/cli";
import { cache } from "@youneed/cli-middleware-cache";

class Build extends Command("build", { middleware: [cache()] }) {
  async execute() {
    // get-or-compute, expiring after 60s
    const deps = await this.cache.wrap("deps", () => resolveDeps(), 60_000);
    console.log(deps);
  }
}
README →

@youneed/cli-middleware-childprocess

Child-process middleware for @youneed/cli: this.childprocess.spawn / .exec wrap node:child_process in a task — reactive (pending/output/exit), killed on shutdown or teardown.

import { Command } from "@youneed/cli";
import { childprocess } from "@youneed/cli-middleware-childprocess";

class Build extends Command("build", { middleware: [childprocess()] }) {
  async execute() {
    const tsc = this.childprocess.spawn("tsc", ["-p", "."]);
    const { code } = (await tsc.exited) ?? {};
    if (code !== 0) console.error(tsc.stderr);

    // shell shorthand: run and await the collected output
    const out = await this.childprocess.exec("git rev-parse HEAD");
    console.log(out?.stdout.trim());
  }
}
README →

@youneed/cli-middleware-clipboard

Clipboard middleware: this.clipboard.write/read via system clipboard.

import { Command } from "@youneed/cli";
import { clipboard } from "@youneed/cli-middleware-clipboard";

class Token extends Command("token", { middleware: [clipboard()] }) {
  async execute() {
    const t = generate();
    await this.clipboard.write(t);
    console.log("copied to clipboard");
  }
}
README →

@youneed/cli-middleware-color

Terminal color & styling middleware for @youneed/cli: adds this.color with ANSI styles, honouring NO_COLOR / --no-color / TTY detection.

import { Command } from "@youneed/cli";
import { color } from "@youneed/cli-middleware-color";

class Build extends Command({ name: "build", middleware: [color()] }) {
  execute() {
    console.log(this.color.green("done"), this.color.bold(this.color.cyan("✓")));
    if (this.color.enabled) console.log(this.color.background.magenta(" NEW "));
  }
}
README →

@youneed/cli-middleware-env

Environment-variable middleware for @youneed/cli: adds a typed, validated this.env parsed from process.env via @youneed/schema, failing fast on bad config.

import { Command } from "@youneed/cli";
import { env, t } from "@youneed/cli-middleware-env";

class Serve extends Command({
  name: "serve",
  middleware: [env({ PORT: t.port().default(3000), NODE_ENV: t.enum(["dev", "prod"]) })],
}) {
  execute() {
    this.server.listen(this.env.PORT); // this.env.PORT: number
    // this.env.NODE_ENV: "dev" | "prod"
  }
}
README →

@youneed/cli-middleware-fs

Filesystem middleware: this.fs read/write + temp dirs auto-removed on teardown.

import { Command } from "@youneed/cli";
import { fs } from "@youneed/cli-middleware-fs";

class Gen extends Command("gen", { middleware: [fs()] }) {
  async execute() {
    const dir = this.fs.tempDir();             // removed automatically on teardown
    this.fs.writeJson(`${dir}/out.json`, { ok: true });

    if (this.fs.exists("config.json")) {
      const cfg = this.fs.readJson<{ name: string }>("config.json");
      this.fs.writeText("README.md", `# ${cfg.name}\n`);
    }
  }
}
README →

@youneed/cli-middleware-hotkeys

Hotkey middleware: this.keys.on(name, handler) over the raw terminal.

import { Application, Command, text } from "@youneed/cli";
import { hotkeys } from "@youneed/cli-middleware-hotkeys";

class Watch extends Command("watch", { middleware: [hotkeys()] }) {
  #status = "watching — press r to rebuild, q to quit";

  async execute() {
    await new Promise<void>((done) => {
      this.keys.on("r", () => {
        this.#status = "rebuilding…";
        this.requestUpdate();
        rebuild().then(() => {
          this.#status = "watching — press r to rebuild, q to quit";
          this.requestUpdate();
        });
      });
      this.keys.on("q", done);       // q ends the command
      this.keys.on("ctrl-c", done);  // so does Ctrl-C
    });
  }

  render() {
    return text`${this.#status}`;
  }
}

Application({ name: "dev", commands: [Watch] });
README →

@youneed/cli-middleware-i18n

i18n middleware for @youneed/cli: this.i18n (translate/locale) backed by @youneed/i18n.

import { Application, Command } from "@youneed/cli";
import { i18n } from "@youneed/cli-middleware-i18n";

const messages = {
  en: { hi: "Hello {name}", bye: "Goodbye" },
  ru: { hi: "Привет {name}", bye: "Пока" },
};

class Greet extends Command({
  name: "greet <name>",
  options: [{ name: "--locale <l>" }],
  middleware: [i18n({ resources: messages, locale: "en" })],
}) {
  execute(name: string) {
    console.log(this.i18n.t("hi", { name }));  // `greet Ada --locale ru` → "Привет Ada"
  }
}

Application({ name: "app", commands: [Greet] });
README →

@youneed/cli-middleware-logger

Logger middleware for @youneed/cli: adds this.logger (a @youneed/logger instance) with level wired from --verbose/--quiet flags.

import { Application, Command } from "@youneed/cli";
import { logger } from "@youneed/cli-middleware-logger";

class Deploy extends Command({
  name: "deploy <env>",
  options: [{ name: "-v, --verbose" }, { name: "-q, --quiet" }],
  middleware: [logger()],
}) {
  execute(target: string) {
    this.logger.info("deploying", { target });
    this.logger.debug("resolved config", { region: "eu" }); // shown only with -v
  }
}

Application({ name: "ops", commands: [Deploy] });
README →

@youneed/cli-middleware-markdown

Markdown middleware: this.markdown(md) renders Markdown to terminal.

import { Application, Command } from "@youneed/cli";
import { markdown } from "@youneed/cli-middleware-markdown";

class Readme extends Command("readme", { middleware: [markdown()] }) {
  execute() {
    console.log(
      this.markdown("# Title\n\nSome **bold** text and a `code` span.\n\n- one\n- two"),
    );
  }
}

Application({ name: "docs", commands: [Readme] });
README →

@youneed/cli-middleware-music

Music transport for @youneed/cli: adds this.player (track metadata + elapsed/duration/play/pause clock), with an optional system-player backend for real audio.

import { Command, task, text } from "@youneed/cli";
import { formatTime, music, systemPlayer } from "@youneed/cli-middleware-music";

const TRACK = { title: "Midnight City", artist: "M83", duration: 30 };

export class Play extends Command("play", {
  middleware: [music(TRACK, { autoplay: true, backend: systemPlayer("track.mp3") })],
}) {
  // Keep the live region alive until the track ends.
  #ended = task(this, () =>
    new Promise<void>((resolve) => {
      const poll = () =>
        void (this.player?.ended || this.abortSignal.aborted ? resolve() : setTimeout(poll, 100));
      setTimeout(poll, 100);
    }),
  );

  constructor() {
    super();
    this.#ended.run();
    // Advance the transport clock at 12fps; the runtime disposes the timer on exit.
    this.scheduler.frame((dt) => this.player?.tick(dt), 12);
  }

  render() {
    const p = this.player;
    return text`${p.track.title} — ${formatTime(p.elapsed)} / ${formatTime(p.duration)}`;
  }
}
README →

@youneed/cli-middleware-notification

Desktop notification middleware for @youneed/cli: adds this.notify (send/info/success/warn/error) backed by node-notifier, with a graceful fallback when it isn't installed.

import { Application, Command } from "@youneed/cli";
import { notifications } from "@youneed/cli-middleware-notification";

class Build extends Command("build", { middleware: [notifications()] }) {
  async execute() {
    await doWork();
    await this.notify.success("Build complete");          // OS notification, no sound
    // or, fully specified:
    await this.notify.send({ message: "Deployed", subtitle: "prod", sound: true });
  }
}

Application({ name: "ci", commands: [Build] });
README →

@youneed/cli-middleware-oscillator

A synthetic spectrum source + cava-style bar renderer for @youneed/cli: adds this.oscillator and spectrumBars() for terminal audio visualisers.

import { Application, Command, task, text } from "@youneed/cli";
import { oscillator, spectrumBars } from "@youneed/cli-middleware-oscillator";

class Vis extends Command("vis", { middleware: [oscillator({ bands: 32, speed: 1.5 })] }) {
  t = 0;

  execute() {
    // animate: bump `t` on a timer and repaint via a task/render loop
    task(this, async () => {
      for (let i = 0; i < 300; i++) {
        this.t = i / 30;
        this.requestUpdate?.();
        await new Promise((r) => setTimeout(r, 33));
      }
    }).run();
  }

  render() {
    return text`${spectrumBars(this.oscillator.sample(this.t), { height: 10 })}`;
  }
}

const app = Application({ name: "demo", commands: [Vis] });
app.run(["vis"]);
README →

@youneed/cli-middleware-pages

Pager middleware: this.pages.show(text) pages long output.

import { Application, Command } from "@youneed/cli";
import { pages } from "@youneed/cli-middleware-pages";

class Log extends Command("log", { middleware: [pages()] }) {
  async execute() {
    const bigLogString = await readSomeLongLog();
    await this.pages.show(bigLogString); // takes over the screen until `q`
  }
}

const app = Application({ name: "tool", commands: [Log] });
app.run(["log"]);
README →

@youneed/cli-middleware-progress

Progress-bar middleware: this.progress.bar() with percent/ETA.

import { Application, Command } from "@youneed/cli";
import { progress } from "@youneed/cli-middleware-progress";

class Download extends Command("download", { middleware: [progress()] }) {
  async execute() {
    const files = await listFiles();
    const bar = this.progress.bar({ total: files.length, label: "downloading" });
    for (const f of files) {
      await fetchFile(f);
      bar.tick(); // repaints the live region
    }
    bar.complete();
  }
}

const app = Application({ name: "tool", commands: [Download] });
app.run(["download"]);
README →

@youneed/cli-middleware-prompt

Interactive prompts for @youneed/cli: adds this.prompt with ask/confirm/choice/list/alert — raw-key TUI primitives drawn via the core live renderer.

import { Application, Command } from "@youneed/cli";
import { prompts } from "@youneed/cli-middleware-prompt";

class Setup extends Command("setup", { middleware: [prompts()] }) {
  async execute() {
    const name = await this.prompt.ask("Project name?", { default: "app" });
    const env = await this.prompt.choice("Environment", ["dev", "staging", "prod"]);
    const feats = await this.prompt.list("Features", ["ts", "lint", "tests"]);
    if (await this.prompt.confirm(`Create ${name}?`, { default: true })) {
      await this.prompt.spinner("scaffolding", () => scaffold(name, env, feats));
      await this.prompt.alert("Done!");
    }
  }
}

const app = Application({ name: "create", commands: [Setup] });
app.run(["setup"]);
README →

@youneed/cli-middleware-screen

Alternate-screen middleware: full-screen TUI buffer; restores on teardown.

import { Application, Command } from "@youneed/cli";
import { screen } from "@youneed/cli-middleware-screen";

class Top extends Command("top", { middleware: [screen()] }) {
  async execute() {
    const paint = () => this.screen.draw(renderDashboard(this.screen.columns, this.screen.rows));
    paint();
    this.screen.onResize(paint); // repaint on terminal resize
    await untilQuit(); // your own quit signal
  }
}

const app = Application({ name: "monitor", commands: [Top] });
app.run(["top"]);
README →

@youneed/cli-middleware-worker

Worker-thread middleware for @youneed/cli: this.worker.run / .spawn wrap node:worker_threads in a task — offload CPU work, reactive, terminated on shutdown or teardown.

import { Application, Command } from "@youneed/cli";
import { worker } from "@youneed/cli-middleware-worker";

class Hash extends Command("hash <file>", { middleware: [worker()] }) {
  async execute(file: string) {
    const job = this.worker.run((path, require) => {
      const { readFileSync } = require("node:fs");
      const { createHash } = require("node:crypto");
      return createHash("sha256").update(readFileSync(path)).digest("hex");
    }, file);
    console.log(await job.exited); // resolves with the digest (undefined on error)
  }
}

const app = Application({ name: "tool", commands: [Hash] });
app.run(["hash", "./package.json"]);
README →

@youneed/cli-plugin-completion

Shell-completion plugin for @youneed/cli: registers a `completion` command that emits bash/zsh/fish scripts generated from the command catalogue.

import { Application } from "@youneed/cli";
import { completion } from "@youneed/cli-plugin-completion";

Application({
  name: "ops",
  commands: [/* … */],
  plugins: [completion()],
}).run();

// then, in the user's shell:
//   ops completion >> ~/.bashrc          # bash
//   ops completion zsh >> ~/.zshrc       # zsh
//   eval "$(ops completion)"             # current session
README →

@youneed/cli-plugin-config

Config-file plugin for @youneed/cli: loads a config file (rc / *.config.json / package.json field) and merges its values into option defaults app-wide.

import { Application } from "@youneed/cli";
import { config } from "@youneed/cli-plugin-config";

Application({
  name: "ops",
  commands: [/* … */],
  plugins: [config()],
}).run();
README →

@youneed/cli-plugin-devtools

Devtools server for @youneed/cli: serves the unified <youneed-devtools> shell (same UI as the server devtools) — a shad command/option builder that lists commands, fills in an invocation from a form, and copies or runs it over the devtools protocol.

import { Application } from "@youneed/cli";
import { devtools } from "@youneed/cli-plugin-devtools";

Application({
  name: "ops",
  commands: [/* … */],
  plugins: [devtools()],
}).run();

// ops devtools   →   http://127.0.0.1:7331
README →

@youneed/cli-plugin-error

Error-formatting plugin for @youneed/cli: pretty stderr output, hints, and optional stack traces via the onError lifecycle hook.

import { Application } from "@youneed/cli";
import { errorReporter, CliError } from "@youneed/cli-plugin-error";

Application({
  name: "ops",
  commands: [/* … */],
  plugins: [errorReporter()],
}).run();

// In a command, throw a CliError for richer output:
throw new CliError("config not found", {
  code: "ENOENT",
  hint: "run `ops init` first",
});
// ✖ config not found [ENOENT]
//   hint: run `ops init` first
README →

@youneed/cli-plugin-feature-flags

Feature-flag plugin for @youneed/cli: a `flags` command to list/inspect/override flags plus middleware that adds this.flags (isEnabled/variant/value/evaluate) so any command can gate behavior on a flag.

import { Application, Command } from "@youneed/cli";
import { createFlags } from "@youneed/feature-flags";
import { featureFlags, flagsMiddleware } from "@youneed/cli-plugin-feature-flags";

const flags = createFlags([
  { key: "beta", defaultValue: false, rollout: 20 },
  { key: "theme", defaultValue: "light", variants: { light: "light", dark: "dark" } },
]);

class Deploy extends Command({ name: "deploy", middleware: [flagsMiddleware(flags)] }) {
  execute() {
    if (this.flags.isEnabled("beta")) console.log("beta path");
  }
}

Application({
  name: "ops",
  commands: [Deploy],
  plugins: [featureFlags(flags)],
}).run();
README →

@youneed/cli-plugin-help

Enhanced help plugin: a richer help command with examples and grouped commands.

import { Application } from "@youneed/cli";
import { help } from "@youneed/cli-plugin-help";

Application({
  name: "ops",
  version: "1.0.0",
  description: "Operations toolkit",
  commands: [/* … */],
  plugins: [
    help({ examples: { split: ["ops split a,b,c --first"] } }),
  ],
}).run();

// ops help          → grouped command list with examples
// ops help split    → usage, options and examples for one command
README →

@youneed/cli-plugin-man

Man-page plugin: a man command emitting roff documentation from the catalogue.

import { Application } from "@youneed/cli";
import { man } from "@youneed/cli-plugin-man";

Application({
  name: "ops",
  version: "1.0.0",
  description: "Operations toolkit",
  commands: [/* … */],
  plugins: [man()],
}).run();

// ops man > ops.1   →   man ./ops.1
README →

@youneed/cli-plugin-otel

@youneed/cli plugin + middleware: real OpenTelemetry SDK — a span per command execution, cli.command metrics, OTLP/HTTP export flushed before exit.

import { Application, Command } from "@youneed/cli";
import { otelCommand, otelPlugin } from "@youneed/cli-plugin-otel";

class Deploy extends Command({
  name: "deploy <env>",
  middleware: [otelCommand()],          // 1. per-command span + this.otel
}) {
  async execute(env: string) {
    const plan = await this.otel.spanAsync("plan", async () => buildPlan(env));
    await this.otel.spanAsync("apply", async () => applyPlan(plan));
  }
}

Application({
  name: "ops",
  version: "1.4.0",
  commands: [Deploy],
  plugins: [otelPlugin({ serviceName: "ops-cli" })], // 2. SDK lifecycle + metrics
});
README →

@youneed/cli-plugin-update-notifier

Update-notifier plugin: checks npm for a newer version, notifies after commands.

import { Application } from "@youneed/cli";
import { updateNotifier } from "@youneed/cli-plugin-update-notifier";

Application({
  name: "ops",
  version: "1.0.0",
  commands: [/* … */],
  plugins: [updateNotifier({ current: "1.0.0" })],
}).run();

// after a command, if a newer version is on npm:
//   Update available: 1.0.0 → 1.2.0
//   Run `npm i -g ops` to update.
README →

@youneed/core

Foundational primitives shared across @youneed packages: shared types, the class-metadata registry (TC39 addInitializer + WeakMap, esbuild/tsx-safe), and disposal helpers.

import { createRegistry, ctorOf, classChain } from "@youneed/core";

interface FieldMeta { name: string; prop: string; }
const FIELDS = createRegistry<FieldMeta[]>(() => []);

// A field decorator that records itself into the most-derived class's entry.
function field(name: string) {
  return function (_v: unknown, ctx: ClassFieldDecoratorContext) {
    ctx.addInitializer(function (this: object) {
      FIELDS.for(ctorOf(this)).push({ name, prop: String(ctx.name) });
    });
  };
}

// The runtime reads it back, walking the inheritance chain most-derived first.
function fieldsOf(instance: object): FieldMeta[] {
  const all: FieldMeta[] = [];
  for (const c of classChain(ctorOf(instance))) all.push(...(FIELDS.read(c) ?? []));
  return all;
}
README →

create-youneedpackage

Internal scaffolder for @youneed/* workspace packages.

README →

@youneed/devtools

Inspector panel (component tree, time-travel, schedulers) + Page devtools.

import { installDevtools, mountDevtoolsPanel } from "@youneed/devtools";

installDevtools();        // capture per-component state/props/events/styles
mountDevtoolsPanel();     // floating, dockable, interactive panel (state persists)
README →

@youneed/devtools-protocol

Universal, CDP-style devtools protocol for every youneed surface (frontend, server, ssr, cli): JSON-RPC 2.0 envelopes, targets, domains, commands + events, pluggable transports, and a domain-keyed UI extension registry (one protocol, per-surface UI).

README →

@youneed/dom

Reactive components on native Custom Elements + Shadow DOM.

import { Component, html, css } from "@youneed/dom";

@Component.define()
class Counter extends Component("x-counter") {
  static styles = css`button { font: inherit }`;

  @Component.prop() count = 0;          // reactive: assigning re-renders
  @Component.prop({ attribute: true }) label = "count"; // reflects <x-counter label="…">

  @Component.event() inc() { this.count++; } // auto-bound for @click

  render() {
    return html`<button @click=${this.inc}>${this.label}: ${this.count}</button>`;
  }
}
README →

@youneed/dom-adapter-angular

Use @youneed/dom components inside Angular and Angular components inside @youneed/dom — type-safe and refactor-friendly (pass the class, not a tag string).

import { toAngular } from "@youneed/dom-adapter-angular";
import { UserCard } from "./user-card";

// In an Angular component, append the element where you need it:
@Component({ selector: "app-profile", template: `<section #host></section>` })
class Profile {
  @ViewChild("host") host!: ElementRef<HTMLElement>;
  @Input() user!: User;
  ngAfterViewInit() {
    this.host.nativeElement.append(toAngular(UserCard, { user: this.user }));
  }
}
README →

@youneed/dom-adapter-astro

Render @youneed/dom components to SSR HTML for Astro islands — Declarative Shadow DOM + client hydration, type-safe (pass the component, not a tag string).

import { registerDOM } from "@youneed/dom/register";
registerDOM();
README →

@youneed/dom-adapter-preact

Use @youneed/dom components inside Preact and Preact components inside @youneed/dom — type-safe and refactor-friendly (pass the component, not a tag string).

import { toPreact } from "@youneed/dom-adapter-preact";
import { UserCard } from "./user-card";

const PreactUserCard = toPreact(UserCard);

function Profile({ user }) {
  return <PreactUserCard user={user} onSelect={e => console.log(e.detail)} />;
}
README →

@youneed/dom-adapter-react

Render @youneed/dom components inside React — type-safe and refactor-friendly (pass the class, not a tag string).

import { toReact } from "@youneed/dom-adapter-react";
import { UserCard } from "./user-card";

const ReactUserCard = toReact(UserCard); // ← a real React component

function Profile({ user }) {
  return (
    <section>
      <ReactUserCard user={user} onSelect={(e) => console.log(e.detail)} />
    </section>
  );
}
README →

@youneed/dom-adapter-svelte

Use @youneed/dom components inside Svelte (a use: action) and Svelte components inside @youneed/dom — type-safe and refactor-friendly (pass the component, not a tag string).

import { fromSvelte } from "@youneed/dom-adapter-svelte";
import Chart from "./Chart.svelte";

const SvelteChart = fromSvelte(Chart, { events: ["select"] }); // ← a custom-element class
README →

@youneed/dom-adapter-vue

Use @youneed/dom components inside Vue and Vue components inside @youneed/dom — type-safe and refactor-friendly (pass the component, not a tag string).

import { fromVue } from "@youneed/dom-adapter-vue";
import Chart from "some-vue-charts";

const VueChart = fromVue(Chart); // ← a custom-element class
README →

@youneed/dom-provider-a11y

Accessibility helpers for @youneed/dom components — a composable provider for screen-reader announcements, focus trapping, and reduced-motion.

import { Component, html } from "@youneed/dom";
import { a11yProvider } from "@youneed/dom-provider-a11y";

class Dialog extends Component("x-dialog", { providers: [a11yProvider()] }) {
  onMount() {
    this.onCleanup(this.a11y.trapFocus()); // Tab stays inside; focus restored on close
    this.a11y.announce("Dialog opened");   // spoken by screen readers
  }
  render() {
    return html`<button>OK</button><button>Cancel</button>`;
  }
}
README →

@youneed/dom-provider-color-scheme

Light/dark/auto color scheme for @youneed/dom — a composable provider that reflects CSS color-scheme globally or per component, toggled via this.setColorScheme.

import { Component, html } from "@youneed/dom";
import { colorSchemeProvider } from "@youneed/dom-provider-color-scheme";

class Card extends Component("x-card", { providers: [colorSchemeProvider("auto")] }) {
  render() {
    return html`
      <button @click=${() => this.colorScheme.toggle()}>theme</button>
      <p>scheme: ${this.colorScheme.value} (${this.colorScheme.resolved})</p>`;
    //          ^ this.colorScheme.{ value, resolved, set, toggle } are typed
  }
}
README →

@youneed/dom-provider-direction

Per-component text direction (LTR/RTL) for @youneed/dom: a composable provider that reflects `dir` and toggles via this.setDirection.

import { Component, html } from "@youneed/dom";
import { directionProvider } from "@youneed/dom-provider-direction";

class Panel extends Component("x-panel", { providers: [directionProvider("ltr")] }) {
  render() {
    return html`
      <button @click=${() => this.direction.toggle()}>flip</button>
      <p>dir: ${this.direction.value}</p>`;
    //          ^ this.direction.{ value, set, toggle } are typed
  }
}
README →

@youneed/dom-provider-env

Type-safe, fail-fast environment variables for @youneed/dom — coerce + validate import.meta.env (or any source) against a @youneed/schema spec, read it via this.env, inspect it in devtools.

import { Component, html } from "@youneed/dom";
import { defineEnvironmentVariables, envProvider, t } from "@youneed/dom-provider-env";

export const env = defineEnvironmentVariables(import.meta.env, {
  schema: {
    API_URL: t.url(),
    FEATURE_X: t.boolean().default(false),
  },
});
//    ^ typed: { API_URL: string; FEATURE_X: boolean }

class Widget extends Component("x-widget", { providers: [envProvider(env)] }) {
  render() {
    return html`<a href=${this.env.API_URL}>open</a>`; // ← typed this.env
  }
}
README →

@youneed/dom-provider-feature-flags

Use @youneed/feature-flags inside @youneed/dom components — evaluate flags in html templates with reactive re-render on flag change, plus SSR hydration.

import { Component, html, when } from "@youneed/dom";
import { createFlags } from "@youneed/feature-flags";
import { provideFlags, flags, flagged } from "@youneed/dom-provider-feature-flags";

provideFlags(createFlags([{ key: "new-dashboard", defaultValue: false }]));

class Dashboard extends Component() {
  constructor() {
    super();
    flagged(this); // ← re-render on every flag change
  }
  render() {
    return when(flags().isEnabled("new-dashboard"), () => html`<new-ui></new-ui>`);
  }
}
README →

@youneed/dom-provider-i18n

Use @youneed/i18n translations inside @youneed/dom components — i18n() in html templates with reactive re-render on locale change.

import { Component, html } from "@youneed/dom";
import { createI18n } from "@youneed/i18n";
import { provideI18n, i18n, localized } from "@youneed/dom-provider-i18n";

provideI18n(createI18n({
  resources: { en: { hello: "Hello {name}" }, de: { hello: "Hallo {name}" } },
  locale: "en",
}));

class Greeting extends Component() {
  constructor() {
    super();
    localized(this); // ← re-render on every setLocale(...)
  }
  render() {
    return html`<div>${i18n("hello", { name: "Ada" })}</div>`;
  }
}
README →

@youneed/dom-provider-logger

A scoped @youneed/logger child on every @youneed/dom component — this.logger, stamped with the component tag, via a composable provider.

import { Component, html } from "@youneed/dom";
import { loggerProvider } from "@youneed/dom-provider-logger";

class Cart extends Component("x-cart", { providers: [loggerProvider()] }) {
  onMount() { this.logger.info("mounted"); }      // → { component: "x-cart", message: "mounted", … }
  checkout() { this.logger.warn("empty cart"); }
}
README →

@youneed/dom-provider-otel

@youneed/dom provider: real OpenTelemetry Web SDK — render spans per component, dom.render metrics, OTLP/HTTP export flushed on pagehide.

import { initDomOtel } from "@youneed/dom-provider-otel";

initDomOtel({ serviceName: "web-app", endpoint: "https://otel.example.com" });
README →

@youneed/dom-provider-rbac

Use @youneed/rbac inside @youneed/dom components — gate UI in html templates with `this.can(action, resource, instance?)`, re-rendering when the current subject changes.

import { Component, html, when } from "@youneed/dom";
import { createRBAC } from "@youneed/rbac";
import { provideRBAC, can, setSubject } from "@youneed/dom-provider-rbac";

provideRBAC(createRBAC((role) => {
  role("editor").can(["create", "update"], "post");
}));
setSubject({ roles: ["editor"], id: "u1" }); // the logged-in user

class PostView extends Component() {
  render() {
    return when(can("update", "post", this.post), () => html`<button>Edit</button>`);
  }
}
README →

@youneed/dom-provider-timers

Lifecycle-scoped timers for @youneed/dom — a composable provider contributing this.timers: setTimeout/setInterval/rAF/idle/delay + the Scheduler API (postTask, yield) + debounce/throttle, all auto-cancelled when the component disconnects.

import { Component, html } from "@youneed/dom";
import { timersProvider } from "@youneed/dom-provider-timers";

class Clock extends Component("x-clock", { providers: [timersProvider()] }) {
  time = this.signal(new Date());

  onMount() {
    this.timers.setInterval(() => this.time.set(new Date()), 1_000);
    // no teardown code — cancelled on disconnect
  }

  render() {
    return html`<time>${this.time.get().toLocaleTimeString()}</time>`;
  }
}
README →

@youneed/dom-provider-virtual

Composable @youneed/dom provider for IntersectionObserver-driven list virtualization (this.virtual) — only visible chunks render.

import { Component, html } from "@youneed/dom";
import { virtualProvider } from "@youneed/dom-provider-virtual";

class Feed extends Component("x-feed", { providers: [virtualProvider()] }) {
  @Component.prop() rows: Row[] = [];

  render() {
    return html`${this.virtual({
      items: this.rows,
      render: (r) => html`<div class="row">${r.title}</div>`,
      estimateHeight: 40,
    })}`;
  }
}
README →

@youneed/dom-provider-zustand

Bind a Zustand store to @youneed/dom components — a composable provider for reactive this.store with optional selector-gated re-renders.

import { createStore } from "zustand/vanilla";
import { Component, html } from "@youneed/dom";
import { zustandProvider } from "@youneed/dom-provider-zustand";

const cart = createStore((set) => ({
  items: [],
  add: (item) => set((s) => ({ items: [...s.items, item] })),
}));

class Cart extends Component("x-cart", { providers: [zustandProvider(cart)] }) {
  render() {
    return html`
      <span>items: ${this.store.state.items.length}</span>
      <button @click=${() => this.store.state.add(newItem)}>add</button>`;
  }
}
README →

@youneed/dom-router

Tiny client-side SPA router (hash / history / query).

import { createRouter } from "@youneed/dom-router";

const router = createRouter({
  outlet: document.getElementById("app")!,
  mode: "history",                 // "hash" (default) · "history" · "query"
  routes: [
    { path: "/",            component: HomePage },       // a component CLASS…
    { path: "/users/:id",   component: "user-page" },    // …or a tag string. params: { id }
    { path: "*",            component: "not-found" },     // catch-all
  ],
});

router.navigate("/users/42");      // updates the URL + mounts <user-page>
router.current?.params;            // { id: "42" }
README →

@youneed/dom-scheduler

Prioritized, batching render scheduler (DOM/Node-agnostic; rAF/idle with setTimeout fallback).

README →

@youneed/dom-ui-shad

shadcn-style component library on @youneed/dom (Custom Elements + Tailwind), with a copy-the-source CLI.

import { ShadButton, registerTailwind } from "@youneed/dom-ui-shad";
README →

@youneed/feature-flags

Tiny framework-agnostic feature-flag engine: boolean/variant/value flags, attribute targeting + deterministic percentage rollout, synchronous evaluation, SSR snapshot hydration.

import { createFlags } from "@youneed/feature-flags";

const flags = createFlags([
  { key: "new-dashboard", defaultValue: false, rollout: 20 },        // 20% of users
  {
    key: "checkout",
    defaultValue: "control",
    variants: { control: "control", fast: "fast" },
    rules: [{ attributes: { plan: "pro" }, variant: "fast" }],       // pro users → "fast"
  },
]);

flags.isEnabled("new-dashboard", { targetingKey: user.id });          // stable 20% bucket
flags.variant("checkout", { targetingKey: user.id, attributes: { plan: user.plan } });
flags.value<string>("checkout", { attributes: { plan: "free" } });    // "control"
README →

@youneed/feature-flags-datadog

Framework-agnostic @youneed/feature-flags adapter: batch flag EXPOSURES to the Datadog Logs intake (no Datadog SDK, plain fetch) via flags.onEvaluation.

import { createFlags } from "@youneed/feature-flags";
import { attachDatadog } from "@youneed/feature-flags-datadog";

const flags = createFlags([
  { key: "new-dashboard", defaultValue: false, rollout: 20 },
]);

// one call wires flags.onEvaluation(...) up for you:
const exp = attachDatadog(flags, {
  apiKey: process.env.DD_API_KEY!,
  service: "web",
  env: "production",
});

flags.isEnabled("new-dashboard", { targetingKey: user.id }); // → buffered exposure

// on shutdown, flush and stop the timer:
await exp.stop();
README →

@youneed/feature-flags-launchdarkly

LaunchDarkly provider adapter for @youneed/feature-flags (Node server SDK-backed remote evaluator).

import { FeatureFlags } from "@youneed/feature-flags";
import { launchDarklyProvider } from "@youneed/feature-flags-launchdarkly";

const flags = new FeatureFlags([], {
  provider: launchDarklyProvider({ sdkKey: "sdk-…" }),
});

// authoritative (awaits the SDK)
const ev = await flags.evaluateAsync("new-dashboard", { targetingKey: user.id });

// synchronous (best-effort until the provider warms the per-context cache)
flags.isEnabled("new-dashboard", { targetingKey: user.id });
README →

@youneed/feature-flags-posthog

PostHog provider for @youneed/feature-flags — a framework-agnostic remote evaluator over PostHog's /decide HTTP API (no SDK, pure fetch).

import { FeatureFlags } from "@youneed/feature-flags";
import { posthogProvider } from "@youneed/feature-flags-posthog";

const flags = new FeatureFlags([], {
  provider: posthogProvider({ apiKey: process.env.POSTHOG_KEY! }),
});

// Await the authoritative value from PostHog:
await flags.evaluateAsync("new-dashboard", { targetingKey: user.id });
await flags.evaluateAsync<string>("checkout", {
  targetingKey: user.id,
  attributes: { plan: user.plan },
}); // → { value: "fast", variant: "fast", reason: "TARGETING_MATCH" }

// Or pre-warm a context so the synchronous `evaluate`/`isEnabled` hit the cache:
await flags.warm({ targetingKey: user.id }, ["new-dashboard", "checkout"]);
flags.isEnabled("new-dashboard", { targetingKey: user.id });
README →

@youneed/feature-flags-vercel

Vercel Edge Config source for @youneed/feature-flags: pulls flag definitions from an Edge Config store (plain fetch, no SDK), with polling for live updates.

import { createFlags } from "@youneed/feature-flags";
import { vercelSource } from "@youneed/feature-flags-vercel";

const flags = createFlags(
  vercelSource({
    connectionString: process.env.EDGE_CONFIG, // https://edge-config.vercel.com/<id>?token=<token>
    prefix: "flag:", // optional: only `flag:*` items, prefix stripped from the key
  }),
);

await flags.load(); // async source → fill the snapshot from Edge Config

flags.isEnabled("new-dashboard", { targetingKey: user.id }); // 20% rollout
flags.value("checkout", { targetingKey: user.id, attributes: { plan: user.plan } });
README →

@youneed/http-client

Zero-dep resilient fetch: timeout, retry + backoff (honors Retry-After), and a circuit breaker.

import { createClient } from "@youneed/http-client";

const client = createClient({
  timeout: 5_000,        // per-attempt deadline (ms)
  retries: 3,            // up to 4 attempts
  failureThreshold: 5,   // trip the breaker after 5 consecutive failures
  resetTimeout: 30_000,  // stay OPEN this long before a half-open trial
});

// Callable like fetch …
const res = await client("https://api.example.com/users");

// … plus method helpers.
const created = await client.post("https://api.example.com/users", {
  body: JSON.stringify({ name: "Ada" }),
  headers: { "content-type": "application/json" },
});

// Introspect the breaker.
console.log(client.breaker.state); // "closed" | "open" | "half-open"
README →

@youneed/i18n

Tiny, fully-typed translation core: dotted-path keys with autocomplete, interpolation, locale switching, subscriptions.

import { createI18n } from "@youneed/i18n";

const i18n = createI18n({
  resources: {
    en: { greeting: "Hello {name}", nav: { home: "Home" } },
    de: { greeting: "Hallo {name}", nav: { home: "Startseite" } },
  },
  locale: "en",
  fallbackLocale: "en",
});

i18n("greeting", { name: "Ada" }); // "Hello Ada"  ← autocompletes "greeting" | "nav.home"
i18n.setLocale("de");
i18n("nav.home");                   // "Startseite"
README →

@youneed/kv

Back-compat alias for @youneed/server-plugin-store (the KV contract + MemoryKV).

// Both of these resolve to the same exports:
import { MemoryKV, namespaced, type KV } from "@youneed/kv";
import { MemoryKV, namespaced, type KV } from "@youneed/server-plugin-store";
README →

@youneed/kv-redis

Back-compat alias for @youneed/server-plugin-pubsub-redis (RedisKV + RedisPubSub).

import { RedisKV, redisKV } from "@youneed/kv-redis";
import { namespaced } from "@youneed/server-plugin-store";

const kv = new RedisKV({ host: "127.0.0.1", port: 6379, password: "…" });
// or from a URL:  new RedisKV({ url: "redis://:pw@host:6379/0" })
// or the factory:  const kv = redisKV({ url: process.env.REDIS_URL });

await kv.set("user:1", JSON.stringify({ name: "Ada" }), { ttl: 60 });
await kv.incr("hits", { ttl: 60 });

const sessions = namespaced(kv, "sess");  // share one Redis across consumers
README →

@youneed/logger

Zero-dep structured logger: levels, JSON lines, child bindings, secret redaction.

import { createLogger, format, ConsoleTransport } from "@youneed/logger";

const log = createLogger({
  level: "info",
  format: format.combine(format.timestamp(), format.redact(["ssn"]), format.json()),
  defaultMeta: { service: "api" },
  transports: [new ConsoleTransport()], // works in the browser and on the server
});

log.info("listening", { port: 3000 });
// {"level":"info","message":"listening","timestamp":"…","service":"api","port":3000}

const reqLog = log.child({ requestId: "r-42" }); // bindings on every record
reqLog.error("db down", { password: "hunter2" }); // password → "[REDACTED]"
README →

@youneed/logger-plugin-datadog

@youneed/logger plugin: stamp Datadog-standard default fields (ddsource/service/ddtags) on every record.

import { createLogger, format } from "@youneed/logger";
import { datadog } from "@youneed/logger-plugin-datadog";

const log = createLogger({
  format: format.combine(format.timestamp(), format.json()),
  plugins: [datadog({ service: "api", env: "prod", version: "1.4.0", tags: { team: "core" } })],
});

log.info("listening", { port: 3000 });
// {"level":"info","message":"listening","timestamp":"…","ddsource":"nodejs",
//  "service":"api","ddtags":"env:prod,version:1.4.0,team:core","port":3000}
README →

@youneed/logger-plugin-exception

@youneed/logger plugin: log uncaughtException/unhandledRejection (Winston-style exception handlers).

import { createLogger } from "@youneed/logger";
import { exceptionHandler } from "@youneed/logger-plugin-exception";

const log = createLogger({ plugins: [exceptionHandler()] });
// later: throw new Error("boom") anywhere →
// { level: "error", message: "uncaughtException", exception: true,
//   error: { name, message, stack } }  then process exits 1
README →

@youneed/logger-plugin-i18n

@youneed/logger plugin: translate log message keys through an @youneed/i18n translator at format time.

import { createLogger, format } from "@youneed/logger";
import { createI18n } from "@youneed/i18n";
import { i18nPlugin } from "@youneed/logger-plugin-i18n";

const i18n = createI18n({
  resources: { en: { "server.started": "Listening on :{port}" } },
  locale: "en",
});

const log = createLogger({ format: format.combine(format.timestamp(), format.json()) });
log.use(i18nPlugin(i18n));

log.info("server.started", { port: 3000 }); // → message: "Listening on :3000"
README →

@youneed/logger-plugin-location

@youneed/logger plugin: stamp each record with the call site (file:line:column) it was logged from.

import { createLogger, format } from "@youneed/logger";
import { locationPlugin } from "@youneed/logger-plugin-location";

const log = createLogger({
  format: format.combine(
    format.timestamp(),
    format.printf((i) => `[${i.timestamp}] ${i.location} ${String(i.message)}`),
  ),
  plugins: [locationPlugin()],
});

log.info("starting up");
// [2026-06-22T15:36:00Z] app.ts:24:7 starting up
README →

@youneed/logger-plugin-otel

@youneed/logger plugin: stamp trace_id / span_id / trace_flags of the active OpenTelemetry span on every log record.

import { createLogger, format } from "@youneed/logger";
import { otel } from "@youneed/logger-plugin-otel";
import { withSpanAsync } from "@youneed/otel";
import { startNodeOtel } from "@youneed/otel/node";

const handle = startNodeOtel({ serviceName: "api" }); // once per process

const log = createLogger({
  format: format.combine(format.timestamp(), format.json()),
  plugins: [otel()], // or later: log.use(otel())
});

await withSpanAsync("GET /users", {}, async () => {
  log.info("listening", { port: 3000 });
  // {"level":"info","message":"listening","timestamp":"…","port":3000,
  //  "trace_id":"4bf92f3577b34da6a3ce929d0e0e4736",
  //  "span_id":"00f067aa0ba902b7","trace_flags":"01"}
});
README →

@youneed/logger-transport-file

@youneed/logger transport: append log lines to a file (sync or buffered stream).

import { createLogger, format } from "@youneed/logger";
import { FileTransport } from "@youneed/logger-transport-file";

const log = createLogger({
  format: format.combine(format.timestamp(), format.json()),
  transports: [new FileTransport({ filename: "app.log" })],
});
README →

@youneed/logger-transport-http

@youneed/logger transport: batch-ship log records to an HTTP endpoint via fetch/sendBeacon (universal).

import { createLogger, format } from "@youneed/logger";
import { HttpTransport } from "@youneed/logger-transport-http";

const log = createLogger({
  format: format.combine(format.timestamp(), format.json()),
  transports: [new HttpTransport({ url: "/_logs", batchSize: 50, flushInterval: 2000 })],
});
README →

@youneed/logger-transport-stdout

@youneed/logger transport: fast Node process.stdout/stderr writer for high-throughput servers.

import { createLogger, format } from "@youneed/logger";
import { StdoutTransport } from "@youneed/logger-transport-stdout";

const log = createLogger({
  format: format.combine(format.timestamp(), format.json()),
  transports: [new StdoutTransport()], // error/warn → stderr, rest → stdout
});
README →

@youneed/orm-adapter-mongo

MongoDB adapter for @youneed/orm-nosql (official mongodb driver).

import { Nosql, Collection, getCollectionRepository } from "@youneed/orm-nosql";
import { mongoAdapter } from "@youneed/orm-adapter-mongo";

class Note extends Collection("notes") {
  @Collection.id() id!: string;
  @Collection.field("string") title!: string;
}

const db = await Nosql({
  adapter: mongoAdapter,
  url: "mongodb://localhost:27017",
  database: "app",
  collections: [Note],
  synchronize: true, // ensures collections + unique indexes
});

const notes = getCollectionRepository(Note);
await notes.insertOne({ title: "Hello" });
await notes.find({ title: { $regex: "^He" } });
README →

@youneed/orm-adapter-mysql

MySQL adapter for @youneed/orm-sql (mysql2-backed).

import { Orm, getRepository } from "@youneed/orm-sql";
import { mysqlAdapter } from "@youneed/orm-adapter-mysql";

await Orm({
  adapter: mysqlAdapter,
  host: "localhost",
  port: 3306,
  username: "root",
  password: "root",
  database: "test",
  tables: [UsersTable],
  synchronize: true,
});

const users = getRepository(UsersTable);
await users.insert({ userId: "u1", email: "ada@x.com" });
README →

@youneed/orm-adapter-postgres

PostgreSQL adapter for @youneed/orm-sql (node-postgres/`pg`-backed).

import { Orm, getRepository } from "@youneed/orm-sql";
import { postgresAdapter } from "@youneed/orm-adapter-postgres";

await Orm({
  adapter: postgresAdapter,
  host: "localhost",
  port: 5432,
  username: "postgres",
  password: "postgres",
  database: "test",
  tables: [UsersTable],
  synchronize: true,
});

const users = getRepository(UsersTable);
await users.insert({ userId: "u1", email: "ada@x.com" }); // generated id populated
README →

@youneed/orm-nosql

Tiny document/NoSQL ORM on standard TC39 decorators (Mongoose-style collections, Mongo-style query filters, pluggable document-store adapters; ships an in-memory store).

import { Collection, Nosql, getCollectionRepository } from "@youneed/orm-nosql";

class Users extends Collection("users") {
  @Collection.id() id!: string;                          // store-assigned key
  @Collection.field("string", { unique: true }) email!: string;
  @Collection.field("string") name!: string;
  @Collection.field("boolean", { default: true }) active!: boolean;
  @Collection.index({ group: "by_name" }) sortName!: string;
}

await Nosql({ type: "memory", collections: [Users], synchronize: true });

const users = getCollectionRepository(Users);
const ada = await users.insertOne({ email: "ada@x.com", name: "Ada" }); // → Users instance w/ id
await users.findOne({ email: "ada@x.com" });            // Mongo-style filter
await users.find({ active: true }, { limit: 20, sort: { name: 1 } });
README →

@youneed/orm-sql

Tiny SQL ORM on standard TC39 decorators (TypeORM-style entities, pluggable DB adapters).

import { Table, Orm, getRepository } from "@youneed/orm-sql";

class UsersTable extends Table("users") {
  @Table.primaryGeneratedColumn() id!: number;

  @Table.field("string")
  @Table.index({ group: "user_action" })
  userId!: string;

  @Table.field("string", { unique: true }) email!: string;
  @Table.column({ type: "boolean", default: true }) isActive!: boolean;

  @Table.oneToMany(() => Photo, (p) => p.user) photos!: Photo[];
}

await Orm({
  type: "sqlite",
  database: ":memory:",
  tables: [UsersTable, Photo],
  synchronize: true,
});

const users = getRepository(UsersTable);
const ada = await users.insert({ userId: "u1", email: "ada@x.com" });
await users.findOne({ email: "ada@x.com" }); // → UsersTable instance
README →

@youneed/otel

Shared OpenTelemetry setup for @youneed/* framework levels: real OTel SDK (node + web), OTLP/HTTP traces + metrics, W3C propagation helpers, instrumented fetch.

import { startNodeOtel } from "@youneed/otel/node";

const handle = startNodeOtel({ serviceName: "api", endpoint: "http://localhost:4318" });
// …app runs…
await handle.shutdown(); // force-flushes spans + metrics
README →

@youneed/rbac

Tiny framework-agnostic authorization engine: roles + permissions (action × resource), role inheritance, ownership/attribute conditions, deny-over-allow, synchronous can().

import { createRBAC, owns, attr } from "@youneed/rbac";

const rbac = createRBAC((role) => {
  role("admin").can("*", "*");                          // superuser
  role("viewer").can("read", "post");
  role("editor")
    .inherits("viewer")                                 // + everything viewer can
    .can(["create", "update"], "post")
    .can("delete", "post", owns("authorId"))            // only own posts
    .cannot("update", "post", attr("locked", true));    // never edit locked posts
});

rbac.can({ roles: ["editor"], id: "u1" }, "delete", "post", { authorId: "u1" }); // true
rbac.can({ roles: ["editor"], id: "u1" }, "delete", "post", { authorId: "u2" }); // false
rbac.can({ roles: ["viewer"] }, "delete", "post");                               // false
rbac.check({ roles: ["editor"] }, "update", "post", { locked: true });           // { granted:false, reason:"DENY" }
README →

@youneed/schema

class-validator-style DTO validation on standard TC39 decorators (no reflect-metadata, works in JS).

import { IsEmail, IsNotEmpty, MinLength, IsOptional, IsInt, Min, validate } from "@youneed/schema";

class CreateUserDTO {
  @IsEmail() email!: string;
  @IsNotEmpty() @MinLength(8) password!: string;
  @IsOptional() @IsInt() @Min(18) age?: number;
}

const errors = validate(CreateUserDTO, await req.json());
// [] when valid, else:
// [{ property: "email", value: "nope", constraints: { isEmail: "email must be an email" } }]
README →

@youneed/secrets

Tiny framework-agnostic secrets manager: one SecretsProvider contract, caching, `secret://` reference resolution, env/memory/file built-ins; Vault & AWS adapters ship separately.

import { createSecrets, EnvSecrets } from "@youneed/secrets";

const secrets = createSecrets(new EnvSecrets(), { cacheTtlMs: 60_000 });

const dbUrl = await secrets.require("DATABASE_URL");          // throws if unset
const cfg = await secrets.resolveAll({                        // deep-resolve secret:// refs
  db: { url: "secret://DATABASE_URL" },
  stripe: "secret://STRIPE_KEY",
  port: 3000,
});
README →

@youneed/secrets-aws

AWS Secrets Manager provider for @youneed/secrets (pure fetch + SigV4, no aws-sdk).

import { createSecrets } from "@youneed/secrets";
import { awsSecrets } from "@youneed/secrets-aws";

const secrets = createSecrets(
  awsSecrets({
    region: "us-east-1",
    accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
    secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
    // sessionToken: process.env.AWS_SESSION_TOKEN, // STS temp creds
  }),
  { cacheTtlMs: 60_000 },
);

const dbUrl = await secrets.require("prod/db/url");                 // GetSecretValue
const cfg = await secrets.resolveAll({ db: "secret://prod/db/url" }); // deep-resolve
const names = await secrets.list();                                 // NAMES only (audit)
README →

@youneed/secrets-vault

HashiCorp Vault (KV v2) SecretsProvider for @youneed/secrets — pure fetch, no SDK.

import { createSecrets } from "@youneed/secrets";
import { vaultSecrets } from "@youneed/secrets-vault";

const secrets = createSecrets(
  vaultSecrets({
    address: "https://vault:8200",
    token: process.env.VAULT_TOKEN!,
    mount: "secret", // default
    // namespace: "team-a",   // Vault Enterprise → X-Vault-Namespace
  }),
);

const dbUrl = await secrets.require("db");        // reads secret/data/db
const pw = await secrets.get("db#password");      // one field from that path
const names = await secrets.list();               // secret NAMES only
README →

@youneed/server

Tiny typed HTTP server (controllers, middleware, cache).

import { Application, Controller, t, HttpError } from "@youneed/server";

const Cat = t.object({ name: t.string(), age: t.number() });

class Cats extends Controller("/cats", { guards: [requireApiKey] }) {
  @Controller.get("/:name", { params: t.object({ name: t.string() }), response: { 200: Cat } })
  async byName(ctx: Context) {
    const cat = lookup(ctx.params.name);
    if (!cat) throw new HttpError(404, { error: "Not found" }); // throw any status
    return cat;                                                  // validated against 200: Cat
  }

  @Controller.guard(isAdmin)            // per-method guard (stacks with class guards)
  @Controller.post({ body: Cat, response: { 201: Cat } })
  async create(ctx: Context) {
    return this.Response.json(ctx.body, { status: 201 });        // ctx.body is typed + validated
  }
}

Application(Cats)
  .use(requestLogger(), cors())          // global middleware (onion model)
  .openapi({ title: "Cats", version: "1.0.0" })   // → GET /openapi.json
  .listen(3000, (ctx) => console.log(`:${ctx.port}`));
README →

@youneed/server-adapter

Run a @youneed/server app on any runtime: a Web `fetch` bridge (Bun / Deno / edge / Workers) over the app's Node request listener, plus node/bun/deno serve adapters.

import { Application } from "@youneed/server";
import { serve, toFetchHandler } from "@youneed/server-adapter";

const app = Application().get("/hello", () => ({ hello: "world" }));

// 1) One-liner that works on Node / Bun / Deno (auto-detects the runtime):
const server = await serve(app, { port: 3000 });
console.log(server.runtime, server.url); // "node http://localhost:3000"

// 2) A raw Web fetch handler for edge / serverless:
export default { fetch: toFetchHandler(app) };          // Cloudflare Worker
// Bun:  Bun.serve({ fetch: toFetchHandler(app) })
// Deno: Deno.serve(toFetchHandler(app))
README →

@youneed/server-middleware-accept-language

@youneed/server middleware: negotiate the request locale from the Accept-Language header (HTTP content negotiation).

import { Application, Response } from "@youneed/server";
import { acceptLanguage } from "@youneed/server-middleware-accept-language";
import { i18n } from "./i18n.ts"; // an @youneed/i18n instance

Application()
  .use(acceptLanguage({ supported: ["en", "de", "fr"], default: "en", i18n }))
  .get("/", (ctx) => Response.text(`locale: ${ctx.state.locale}`))
  .listen(3000, () => {});
README →

@youneed/server-middleware-api-key

@youneed/server middleware: shared-secret API key auth (header/query/scheme), SHA-256 matched, principal mapping.

import { Application } from "@youneed/server";
import { apiKey } from "@youneed/server-middleware-api-key";

// Flat allowlist (key sent as `X-API-Key: <key>`):
app.use(apiKey({ keys: [process.env.PARTNER_KEY!] }));

// Keys mapped to a principal → ctx.state.apiClient:
app.use(apiKey({ keys: { k_live_abc: { name: "billing", scopes: ["read"] } } }));

// Store only HASHES at rest (never plaintext keys):
app.use(apiKey({ hashed: true, keys: [sha256hex(process.env.PARTNER_KEY!)] }));

// Dynamic lookup (DB / cache):
app.use(apiKey({ verify: async (key) => db.clientByKey(key) }));
README →

@youneed/server-middleware-authorization

@youneed/server middleware: generic Authorization-header auth with a pluggable signing algorithm (bring your own sign/verify/generatePair) + self-contained signed tokens.

import { sign, verify, generatePair } from "my-signing-algo";
import { authorization, createTokens } from "@youneed/server-middleware-authorization";

// 1. Wrap your algorithm (the package never inspects what it does):
const kuznyechik = { name: "Kuznyechik", sign, verify, generatePair };
const { publicKey, privateKey } = kuznyechik.generatePair();

// 2. Verify side — the middleware (any prefix you like):
app.use(authorization({ prefix: "Bearer", algorithm: kuznyechik, key: publicKey }));

// 3. Issue side — your login route:
const tokens = createTokens({ algorithm: kuznyechik, privateKey, publicKey });
const token = await tokens.sign({ sub: "u1", scope: "read" }, { expiresInSec: 3600 });

// 4. Handlers read the verified payload:
app.get("/me", (ctx) => ctx.state.user);
README →

@youneed/server-middleware-basic-auth

@youneed/server middleware: HTTP Basic auth + API-key auth.

import { Application, Response } from "@youneed/server";
import { basicAuth, apiKey } from "@youneed/server-middleware-basic-auth";

const app = Application()
  // Static user → password map (passwords compared in constant time).
  .use("/admin", basicAuth({ users: { alice: "s3cret" }, realm: "Admin" }))
  // …or your own verifier, returning a principal (or false to reject).
  .use("/api", apiKey({ verify: (key) => lookup(key), header: "x-api-key" }))
  .get("/admin/me", (ctx) => Response.json({ you: ctx.state.user }))
  .get("/api/data", (ctx) => Response.json({ key: ctx.state.apiKey }));
README →

@youneed/server-middleware-bearer

@youneed/server middleware: Bearer-token authentication.

import { Application } from "@youneed/server";
import { bearer } from "@youneed/server-middleware-bearer";

Application()
  .use(bearer({
    verify: async (token) => {
      const user = await lookupSession(token);
      return user ?? false; // false/null → 401
    },
  }))
  .listen(3000, () => {});
README →

@youneed/server-middleware-body-limit

@youneed/server middleware: reject oversized request bodies.

import { Application } from "@youneed/server";
import { bodyLimit } from "@youneed/server-middleware-body-limit";

Application()
  .use(bodyLimit("5mb"))
  .listen(3000, () => {});
README →

@youneed/server-middleware-compression

@youneed/server middleware: gzip/brotli response compression.

import { Application } from "@youneed/server";
import { compression } from "@youneed/server-middleware-compression";

Application()
  .use(compression({ threshold: 1024 }))
  .listen(3000, () => {});
README →

@youneed/server-middleware-cors

@youneed/server middleware: CORS headers + preflight.

import { Application } from "@youneed/server";
import { cors } from "@youneed/server-middleware-cors";

Application()
  .use(cors({ origin: "*" }))
  .listen(3000, () => {});
README →

@youneed/server-middleware-csrf

@youneed/server middleware: stateless CSRF (double-submit cookie).

import { Application } from "@youneed/server";
import { csrf } from "@youneed/server-middleware-csrf";

Application()
  .use(csrf())                                  // global, default options
  .use("/admin", csrf({ cookieName: "admin_csrf" })) // scoped, custom cookie
  .listen(3000, () => {});
README →

@youneed/server-middleware-etag

@youneed/server middleware: ETag + conditional GET (304).

import { Application } from "@youneed/server";
import { etag } from "@youneed/server-middleware-etag";

Application()
  .use(etag())                  // weak validators, global
  .use("/assets", etag({ weak: false })) // strong, scoped
  .listen(3000, () => {});
README →

@youneed/server-middleware-health

@youneed/server middleware: Kubernetes-style liveness/readiness probe endpoints.

import { Application, Response } from "@youneed/server";
import { health } from "@youneed/server-middleware-health";

const h = health({
  checks: {
    db: () => pool.totalCount > 0,
    cache: async () => (await redis.ping()) === "PONG",
  },
});

const app = Application()
  .use(h)                                   // register early
  .get("/users", () => Response.json([/* … */]));

// GET /healthz → 200 { status: "ok" }                      (liveness)
// GET /readyz  → 200 { status: "ready", checks: { … } }    (readiness, all pass)
//             → 503 { status: "not ready", checks: { … } } (any check failed)

app.listen(3000, (s) =>
  s.gracefulShutdown({ onShutdown: () => h.setReady(false) }),
);
README →

@youneed/server-middleware-helmet

@youneed/server middleware: security response headers (helmet-style).

import { Application } from "@youneed/server";
import { helmet } from "@youneed/server-middleware-helmet";

Application()
  .use(helmet())                                     // strict defaults, global
  .use(helmet({ contentSecurityPolicy: false }))     // opt out of CSP
  .listen(3000, () => {});
README →

@youneed/server-middleware-http2-guard

@youneed/server middleware: HTTP/2 DoS protection (Rapid Reset, stream floods).

import { Application } from "@youneed/server";
import { http2Guard } from "@youneed/server-middleware-http2-guard";

Application()
  .use(http2Guard())                                   // defaults, global
  .use(http2Guard({ maxResetsPerWindow: 50, onAbuse: (i) => console.warn(i) }))
  .listen(3000, () => {});
README →

@youneed/server-middleware-https-redirect

@youneed/server middleware: force HTTPS + canonical host/trailing-slash redirects.

import { Application, Response } from "@youneed/server";
import { httpsRedirect } from "@youneed/server-middleware-https-redirect";

const app = Application()
  .use(httpsRedirect({ host: "example.com", trailingSlash: "never" }))
  .get("/users", () => Response.json([/* … */]));

// http://www.example.com/users/  →  308  Location: https://example.com/users
README →

@youneed/server-middleware-idempotency

@youneed/server middleware: Idempotency-Key — safe retries of unsafe requests via a pluggable KV store.

import { Application, Response } from "@youneed/server";
import { idempotency } from "@youneed/server-middleware-idempotency";

const app = Application()
  .use(idempotency({ ttl: 86400 }))               // 24h replay window (in-process store)
  .post("/charges", () => Response.json(charge())); // POST twice with one key → one charge
README →

@youneed/server-middleware-ip-filter

@youneed/server middleware: allow/deny requests by client IP (CIDR, IPv4/IPv6), proxy-aware.

import { Application } from "@youneed/server";
import { ipFilter } from "@youneed/server-middleware-ip-filter";
import { trustProxy } from "@youneed/server-middleware-trust-proxy";

// Allowlist — only these IPs/ranges may reach /admin (everything else 403):
app.use("/admin", ipFilter({ allow: ["10.0.0.0/8", "192.168.1.5", "2001:db8::/32"] }));

// Denylist — block known-bad ranges, everyone else passes:
app.use(ipFilter({ deny: ["203.0.113.0/24"] }));
README →

@youneed/server-middleware-jwt

@youneed/server middleware: JWT (JWS) authentication — verifies signature (HS/RS/PS/ES) + claims, with JWKS support. Zero dependencies.

import { Application } from "@youneed/server";
import { jwt } from "@youneed/server-middleware-jwt";

// Symmetric (HS256) — shared secret:
app.use(jwt({ secret: process.env.JWT_SECRET!, issuer: "auth.acme.dev", audience: "api" }));

// Asymmetric, single fixed key (RS256/ES256):
app.use(jwt({ publicKey: pemOrJwkOrKeyObject, algorithms: ["RS256"] }));

// Asymmetric via JWKS — keys rotate by `kid`, fetched and cached:
app.use(jwt({ jwks: "https://auth.acme.dev/.well-known/jwks.json", algorithms: ["RS256"] }));

// Handlers read the verified claims:
app.get("/me", (ctx) => ctx.state.user);
README →

@youneed/server-middleware-keep-alive

@youneed/server middleware: advertise a Keep-Alive header + drop connections programmatically.

import { Application, Response, HttpError } from "@youneed/server";
import { keepAlive, connection } from "@youneed/server-middleware-keep-alive";

const app = Application()
  .use(keepAlive({ timeout: 10, max: 1000 }))      // → Keep-Alive: timeout=10, max=1000
  .use((ctx, next) => {
    if (ctx.request.headers["x-malware"]) {
      connection(ctx).destroy();                    // sever the socket immediately
      throw new HttpError(403, { error: "blocked" });
    }
    return next();
  })
  .get("/users", () => Response.json([/* … */]));
README →

@youneed/server-middleware-load-shed

@youneed/server middleware: load-shedding (global concurrency limit) — fast-fail surplus requests with 503 under overload.

import { Application, Response } from "@youneed/server";
import { loadShed } from "@youneed/server-middleware-load-shed";

const app = Application()
  .use(loadShed({ maxConcurrent: 500, retryAfter: 2 }))
  .get("/work", () => doExpensiveWork());
// once 500 requests are in flight, the 501st gets:
//   503 Service Unavailable
//   Retry-After: 2
//   { "error": "Service Unavailable" }
README →

@youneed/server-middleware-logger

@youneed/server middleware: attach a request-scoped @youneed/logger child (bound to requestId/traceId) — log(ctx).

import { Application, Response } from "@youneed/server";
import { createLogger } from "@youneed/logger";
import { logger, log } from "@youneed/server-middleware-logger";

const base = createLogger();

const app = Application()
  .use(logger(base, { bindings: (ctx) => ({ method: ctx.request.method, url: ctx.request.url }) }))
  .get("/users", (ctx) => {
    log(ctx).info("listing users", { count: 3 });
    // → {"level":"info","message":"listing users","count":3,"requestId":"…","method":"GET","url":"/users"}
    return Response.json([/* … */]);
  });
README →

@youneed/server-middleware-metrics

@youneed/server middleware: dependency-free Prometheus metrics + /metrics exposition.

import { Application, Response } from "@youneed/server";
import { metrics } from "@youneed/server-middleware-metrics";

const app = Application()
  .use(metrics())                              // GET /metrics → text exposition
  .get("/users", () => Response.json([/* … */]));

// → http_requests_total{method="GET",status="200"} 1
//   http_request_duration_seconds_bucket{method="GET",status="200",le="0.05"} 1
//   http_request_duration_seconds_sum{method="GET",status="200"} 0.0012
//   http_request_duration_seconds_count{method="GET",status="200"} 1
//   http_requests_in_flight 0
README →

@youneed/server-middleware-rate-limit

@youneed/server middleware: rate limiting with pluggable strategies (fixed/sliding/token-bucket/leaky-bucket/exponential + KV).

import { Application } from "@youneed/server";
import { rateLimit, fixedWindow, tokenBucket } from "@youneed/server-middleware-rate-limit";

Application()
  .use(rateLimit({ strategy: fixedWindow({ windowMs: 60_000, max: 100 }) }))  // global
  .use("/api", rateLimit({ strategy: tokenBucket({ capacity: 50, refillPerSec: 5 }) }))
  .listen(3000, () => {});
README →

@youneed/server-middleware-request-id

@youneed/server middleware: per-request correlation id (trusted inbound X-Request-Id or generated), echoed + log-correlated.

import { requestId, getRequestId } from "@youneed/server-middleware-request-id";
import { requestLogger } from "@youneed/server-middleware-request-logger";

app.use(requestId());          // mount EARLY (before the logger)
app.use(requestLogger());

app.get("/x", (ctx) => ({ id: getRequestId(ctx) }));
README →

@youneed/server-middleware-request-logger

@youneed/server middleware: per-request access logging.

import { Application } from "@youneed/server";
import { requestLogger } from "@youneed/server-middleware-request-logger";

Application()
  .use(requestLogger())
  .listen(3000, () => {});
README →

@youneed/server-middleware-server-timing

@youneed/server middleware: emit a Server-Timing response header (DevTools timings).

import { Application, Response } from "@youneed/server";
import { serverTiming, timing } from "@youneed/server-middleware-server-timing";

const app = Application()
  .use(serverTiming())                       // register EARLY → accurate `total`
  .get("/users", async (ctx) => {
    const m = timing(ctx).metric("db");        // start a timer…
    const rows = await db.query("…");
    m.desc(`SQL · ${rows.length} rows`).stop(); // …set desc from the result, then stop
    return Response.json(rows);
  });
//  Server-Timing: db;dur=12.3;desc="SQL · 42 rows", total;dur=14.1
README →

@youneed/server-middleware-session

@youneed/server middleware: signed-cookie sessions with a pluggable store.

import { Application, Response } from "@youneed/server";
import { session, getSession } from "@youneed/server-middleware-session";

const app = Application()
  .use(session({ secret: process.env.SESSION_SECRET! }))
  .get("/login", (ctx) => {
    getSession(ctx)!.set("user", "ada");          // persisted on the way out
    return Response.json({ ok: true });
  })
  .get("/me", (ctx) => Response.json({ user: getSession(ctx)?.get("user") }))
  .get("/logout", (ctx) => {
    getSession(ctx)!.destroy();                    // clears store + cookie
    return Response.json({ ok: true });
  });
README →

@youneed/server-middleware-static

@youneed/server middleware: serve static files from disk with HTTP Range, ETag, and conditional-request support.

import { Application, Response } from "@youneed/server";
import { staticFiles } from "@youneed/server-middleware-static";

const app = Application()
  .use(staticFiles("public", { cacheControl: "public, max-age=3600" }))
  .get("/api/health", () => Response.json({ ok: true }));
README →

@youneed/server-middleware-timeout

@youneed/server middleware: fail requests that exceed a deadline (503).

import { Application } from "@youneed/server";
import { timeout } from "@youneed/server-middleware-timeout";

Application()
  .use(timeout(5000))                              // 5s, global
  .use("/report", timeout(30_000, { status: 504 })) // scoped, custom status
  .listen(3000, () => {});
README →

@youneed/server-middleware-trace

@youneed/server middleware: W3C traceparent distributed tracing (dependency-free, OpenTelemetry-compatible IDs).

import { Application, Response } from "@youneed/server";
import { tracing, span } from "@youneed/server-middleware-trace";

const app = Application()
  .use(tracing({ onEnd: (s) => exporter.push(s) }))   // → traceparent: 00-<traceId>-<spanId>-01
  .get("/users", (ctx) => {
    span(ctx).setAttribute("user.count", 3);
    span(ctx).addEvent("queried-db");
    return Response.json([/* … */]);
  });
README →

@youneed/server-middleware-trust-proxy

@youneed/server middleware: resolve the real client IP/protocol/host from X-Forwarded-* headers behind a proxy/CDN.

import { Application, Response } from "@youneed/server";
import { trustProxy, clientInfo } from "@youneed/server-middleware-trust-proxy";

const app = Application()
  .use(trustProxy({ hops: 1 }))                  // 1 trusted proxy in front
  .get("/whoami", (ctx) => Response.json(clientInfo(ctx)));
  // → { ip: "1.2.3.4", protocol: "https", host: "api.example.com" }
README →

@youneed/server-middleware-webhook-signature

@youneed/server middleware: verify inbound webhook HMAC signatures over the raw body. Pure verify function + generic builder + per-provider subpaths (stripe/github/shopify).

// 1. A pure validation FUNCTION — no Context. Reusable anywhere (queue consumers,
//    tests, non-HTTP transports). Returns { valid, reason?, timestamp? }, never throws.
import { verifyWebhookSignature } from "@youneed/server-middleware-webhook-signature";

const result = await verifyWebhookSignature(
  { secret, header: "x-signature", prefix: "sha256=" },
  { rawBody, headers },
);
if (!result.valid) reject(result.reason);

// 2. The MIDDLEWARE wrapping it (reads the raw bytes via the core's memoized
//    rawBody(ctx), so the handler still gets a parsed ctx.body).
import { webhookSignature } from "@youneed/server-middleware-webhook-signature";
app.use("/hooks", webhookSignature({ secret, header: "x-signature", prefix: "sha256=" }));
README →

@youneed/server-plugin-cluster

@youneed/server plugin: multi-core supervisor — fork workers across CPUs, restart crashes, graceful drain on SIGTERM.

import { Application, Response } from "@youneed/server";
import { cluster } from "@youneed/server-plugin-cluster";

Application()
  .get("/", () => Response.text("ok"))
  .plugin(cluster({ workers: 4 }))     // default: os.availableParallelism() ?? CPU count
  .listen(3000, (s) => s.gracefulShutdown());
README →

@youneed/server-plugin-devtools

@youneed/server plugin: dev-time topology, OWASP security audit, OpenAPI generation and microbenchmarks — mounts a devtools UI on a live app.

import { topology, externalServer, securityAudit, toOpenApi, microbench } from "@youneed/server-plugin-devtools";

const t = topology([
  { name: "api", middleware: ["cors", "helmet", "rate-limit"], routes: [/* … */] },
  externalServer({ name: "billing", url: "https://billing.acme.dev" }), // not behind our API
]);

securityAudit(t.servers[0]); // OWASP API Top 10 findings
toOpenApi(t.servers[0]);      // OpenAPI 3.1 document from route schemas
microbench(() => serialize(payload)); // ops/sec + p50/p99
README →

@youneed/server-plugin-docker

Wrap a @youneed/server or SSR app in Docker: generate a Dockerfile + .dockerignore + docker-compose.yml, with backing services (Mongo/MySQL/Postgres/Redis) inferred from the app's mounted plugins. Ships a devtools tab to view the artifacts.

README →

@youneed/server-plugin-env

Type-safe, fail-fast environment variables for @youneed/server — coerce + validate process.env against a @youneed/schema spec, with a devtools-visible ServerPlugin.

import { defineEnvironmentVariables, t } from "@youneed/server-plugin-env";

export const env = defineEnvironmentVariables(process.env, {
  schema: {
    PORT: t.port().default(3000),
    DATABASE_URL: t.url().secret(),
    NODE_ENV: t.enum(["development", "production", "test"] as const).default("development"),
  },
});
//    ^ typed: { PORT: number; DATABASE_URL: string; NODE_ENV: "development" | "production" | "test" }
README →

@youneed/server-plugin-feature-flags

@youneed/server plugin + controller provider for @youneed/feature-flags: request-scoped `this.flags`, a client-bootstrap snapshot route, dev override toggles, and a devtools Feature Flags tab.

import { Application, Controller } from "@youneed/server";
import { createFlags, featureFlags, flagsProvider } from "@youneed/server-plugin-feature-flags";
import type { Context } from "@youneed/server";

const flags = createFlags([
  { key: "new-checkout", defaultValue: false, rollout: 20 }, // stable 20% of users
  {
    key: "pricing",
    defaultValue: "control",
    variants: { control: "control", fast: "fast" },
    rules: [{ attributes: { plan: "pro" }, variant: "fast" }],
  },
]);

// Derive the per-request evaluation context (bucket by the authenticated user).
const ctxOf = (ctx: Context) => ({
  targetingKey: (ctx.state.user as { id?: string } | undefined)?.id,
  attributes: { plan: (ctx.state.user as { plan?: string } | undefined)?.plan },
});

class CheckoutController extends Controller("/checkout", {
  providers: [flagsProvider(flags, { context: ctxOf })],
}) {
  @Controller.get()
  index() {
    if (this.flags.isEnabled("new-checkout")) return { ui: "v2" };
    return { ui: "v1", variant: this.flags.variant("pricing") };
  }
}

const app = Application(CheckoutController).plugin(featureFlags(flags, { context: ctxOf }));
app.listen(3000);
README →

@youneed/server-plugin-graphql

@youneed/server plugin: a spec-compliant GraphQL HTTP endpoint over graphql-js (schema-first SDL or programmatic schema, GraphiQL, resolvers, per-request context), with a devtools tab (playground, SDL viewer, recent ops).

import { Application } from "@youneed/server";
import { graphql } from "@youneed/server-plugin-graphql";

const app = Application().plugin(
  graphql({
    schema: /* GraphQL */ `
      type Query {
        hello: String
        add(a: Int, b: Int): Int
      }
    `,
    rootValue: {
      hello: () => "hi",
      add: ({ a, b }: { a: number; b: number }) => a + b,
    },
    // context: (ctx) => ({ user: ctx.state.user }),   // per-request contextValue
    // path: "/graphql",                                // default
    // graphiql: true,                                  // GraphiQL on GET (browsers)
  }),
);
app.listen(3000);

// POST /graphql  { "query": "{ hello }" }        → { "data": { "hello": "hi" } }
// GET  /graphql  (from a browser)                → GraphiQL IDE
// GET  /graphql?query={hello}                    → { "data": { "hello": "hi" } }
README →

@youneed/server-plugin-grpc

@youneed/server plugin: run a gRPC (HTTP/2) server alongside your @youneed/server app on its lifecycle, with introspection + a unary call-tester over HTTP and a devtools tab.

import { Application } from "@youneed/server";
import { grpc } from "@youneed/server-plugin-grpc";

const app = Application().plugin(
  grpc({
    protoPath: new URL("./greeter.proto", import.meta.url).pathname,
    package: "greet",
    port: 50051,
    services: {
      // serviceName → { method → handler } (grpc-js unary style)
      Greeter: {
        // (call, callback) — or return a value from an async handler
        SayHello(call, callback) {
          callback(null, { message: `Hello, ${call.request.name}!` });
        },
      },
    },
  }),
);

app.listen(3000); // starts the gRPC server on :50051 too, drains both on shutdown
README →

@youneed/server-plugin-jobs

@youneed/server plugin: job scheduler (cron, intervals, delays) wired to the server lifecycle, with an optional KV leader-lock for fleets.

import { Application } from "@youneed/server";
import { jobs } from "@youneed/server-plugin-jobs";

const cron = jobs({ jobs: [{ name: "cleanup", schedule: "0 */6 * * *", handler: purge }] });
app.plugin(cron).listen(3000, () => {}); // scheduler.start() on listen, stop() on drain

// still mutable after registration
cron.scheduler.add({ name: "heartbeat", schedule: { every: 30_000 }, handler: ping });
README →

@youneed/server-plugin-jsonrpc

JSON-RPC 2.0 for @youneed/server: class + TC39-decorator endpoints (@JsonRPC.method) with schema-validated params, served over a POST request or a Chrome-CDP-style WebSocket, plus a @youneed/server-plugin-devtools panel to inspect methods and debug calls.

import { JsonRPC, JsonRPCResponse, JsonRPCErrorResponse } from "@youneed/server-plugin-jsonrpc";
import { t } from "@youneed/schema";
import type { Context } from "@youneed/server";

class MathEndpoint extends JsonRPC({
  providers: [loggerProvider(), anotherProvider()], // add private `this.<member>`
  guards: [authRequired()],                         // run before every method
}) {
  @JsonRPC.method("sum", { args: [t.number(), t.number()] })
  sum(a: number, b: number, ctx?: Context) {        // ctx optional, always last
    if (a > 10) return JsonRPCResponse.error({ code: -32000, message: "something went wrong" });
    // return JsonRPCResponse.error(JsonRPCErrorResponse.InternalError); // predefined map
    return JsonRPCResponse.success({ result: a + b });
  }
}
README →

@youneed/server-plugin-kv

Mount a KV store (from @youneed/server-plugin-store) as a ServerPlugin for @youneed/server: tracks reads/writes/hit-rate + surfaces a live key browser in @youneed/server-plugin-devtools (Infra card, header tab, flow node). Re-exports the KV contract.

import { Application } from "@youneed/server";
import { createKV, kv } from "@youneed/server-plugin-kv";
// re-exported for convenience: MemoryKV, namespaced, type KV …

const store = createKV();        // TrackedKV around an in-process MemoryKV
// const store = createKV(new RedisKV({ url: process.env.REDIS_URL }));

const app = Application().plugin(kv(store));

// Pass the SAME tracked instance to your consumers so all traffic is counted:
await store.set("user:1", JSON.stringify({ name: "Ada" }), { ttl: 60 });
await store.get("user:1");       // recorded as a hit
store.stats();                   // → { gets, sets, deletes, incrs, hits, misses }
README →

@youneed/server-plugin-mailer

@youneed/server plugin: transactional email with pluggable transports — built-in dependency-free SMTP plus AWS SES, SendGrid and Postmark (fetch-based, no SDKs), with a devtools tab.

import { Application } from "@youneed/server";
import { mailer } from "@youneed/server-plugin-mailer";
import { smtpTransport } from "@youneed/server-plugin-mailer/smtp";

const mail = mailer(
  smtpTransport({ host: "smtp.acme.dev", port: 587, auth: { user, pass }, from: "no-reply@acme.dev" }),
);

const app = Application().plugin(mail);
app.listen(3000);

// send from anywhere — use the plugin's tracked transport so devtools sees it
await mail.transport.send({
  to: "ada@x.dev",
  subject: "Welcome",
  text: "Hello!",
  html: "<b>Hello!</b>",
});
README →

@youneed/server-plugin-oauth2

OAuth2/OIDC login (Authorization Code + PKCE) for @youneed/server: a ServerPlugin core + universal, named providers (github/google/facebook/yandex/vk) you can extend, plus Telegram Login Widget.

import { Application } from "@youneed/server";
import { oauth2, redirect } from "@youneed/server-plugin-oauth2";
import { github } from "@youneed/server-plugin-oauth2/github";
import { google } from "@youneed/server-plugin-oauth2/google";
import { facebook } from "@youneed/server-plugin-oauth2/facebook";
import { yandex } from "@youneed/server-plugin-oauth2/yandex";
import { vk } from "@youneed/server-plugin-oauth2/vk";

const app = Application().plugin(
  oauth2({
    secret: process.env.OAUTH_SECRET!,                  // signs the state cookie
    providers: {
      github: github({ clientId: process.env.GH_ID!, clientSecret: process.env.GH_SECRET! }),
      google: google({ clientId: process.env.G_ID!, clientSecret: process.env.G_SECRET!, offline: true }),
      facebook: facebook({ clientId: process.env.FB_ID!, clientSecret: process.env.FB_SECRET! }),
      yandex: yandex({ clientId: process.env.YA_ID!, clientSecret: process.env.YA_SECRET! }),
      vk: vk({ clientId: process.env.VK_ID!, clientSecret: process.env.VK_SECRET! }),
    },
    async onLogin(ctx, { provider, profile, tokens }) {
      const user = await db.upsert(provider, profile);  // YOU decide what a session is
      ctx.cookies.set("uid", user.id, { httpOnly: true });
      return redirect("/");
    },
  }),
);
// Mounts:  GET /auth/<provider>  &  /auth/<provider>/callback  for each provider
README →

@youneed/server-plugin-otel

@youneed/server plugin + middleware: real OpenTelemetry SDK — SERVER spans per request with W3C propagation, http.server metrics, OTLP/HTTP export.

import { Application } from "@youneed/server";
import { otel } from "@youneed/server-plugin-otel";

const app = Application(MyController).plugin(
  otel({
    serviceName: "my-api",
    endpoint: "http://localhost:4318", // OTLP/HTTP receiver (collector :4318, Tempo, …)
  }),
);
app.listen(3000);
README →

@youneed/server-plugin-otlp

@youneed/server plugin: export per-request traces over OTLP/HTTP (JSON) to an OpenTelemetry collector / Jaeger / Tempo — no OTel SDK — with a devtools tab.

import { Application } from "@youneed/server";
import { otlp } from "@youneed/server-plugin-otlp";

const app = Application(MyController).plugin(
  otlp({
    endpoint: "http://localhost:4318", // OTLP/HTTP receiver (collector :4318, Tempo, …)
    serviceName: "my-api",
    headers: { "x-honeycomb-team": process.env.HONEYCOMB_KEY! }, // optional auth
    batchSize: 100, // flush when buffered
    flushMs: 5000, // …and on this interval
  }),
);
app.listen(3000);
README →

@youneed/server-plugin-otp

One-time-password login (passwordless / 2FA) for @youneed/server: a ServerPlugin core + pluggable channels — email (built-in SMTP) and SMS.

import { Application } from "@youneed/server";
import { otp } from "@youneed/server-plugin-otp";
import { emailChannel } from "@youneed/server-plugin-otp/email";
import { smsChannel, twilioSms } from "@youneed/server-plugin-otp/sms";

const app = Application().plugin(
  otp({
    secret: process.env.OTP_SECRET!,                  // HMACs the stored code hash
    channels: {
      email: emailChannel({ host: "smtp.acme.dev", port: 587, auth: { user, pass }, from: "no-reply@acme.dev" }),
      sms:   smsChannel({ send: twilioSms({ accountSid, authToken, from: "+15550000000" }) }),
    },
    async onVerify(ctx, { channel, to }) {            // identity proven → your session
      const user = await db.upsertByContact(channel, to);
      ctx.cookies.set("uid", user.id, { httpOnly: true });
      return { ok: true };
    },
  }),
);
README →

@youneed/server-plugin-pubsub

Backend-agnostic publish/subscribe for @youneed/server: PubSub contract + MemoryPubSub + a ServerPlugin that surfaces channels in @youneed/server-plugin-devtools (flow graph, header tab, message sender). Re-exports the KV store.

import { Application } from "@youneed/server";
import { createPubSub, pubsub } from "@youneed/server-plugin-pubsub";

// Wrap a backend (default = in-process MemoryPubSub) in a tracker for devtools.
const bus = createPubSub();

const app = Application()
  .plugin(pubsub(bus)) // exposes /__pubsub/channels + /__pubsub/publish, and inspect()
  .listen(3000, () => {});

// Subscribe — handler receives (message, channel). Messages are strings, so serialize.
const sub = await bus.subscribe("orders", (message, channel) => {
  const order = JSON.parse(message);
  console.log(`[${channel}]`, order.id);
});

// Publish from anywhere (a handler, a job, …).
await bus.publish("orders", JSON.stringify({ id: 42 }));

// Later: stop delivery.
await sub.close();
README →

@youneed/server-plugin-pubsub-deno

Deno KV adapter for @youneed/server-plugin-pubsub + -store: DenoKV and DenoPubSub (Deno KV queues). Runs on the Deno runtime / Deno Deploy.

import { Application } from "@youneed/server";
import { createPubSub, pubsub } from "@youneed/server-plugin-pubsub";
import { denoPubSub } from "@youneed/server-plugin-pubsub-deno";

// Wire the adapter into the core pubsub plugin.
const bus = createPubSub(denoPubSub()); // uses Deno.openKv()

Application().plugin(pubsub(bus)).listen(3000, () => {});

await bus.subscribe("orders", (message, channel) => {
  console.log(`[${channel}]`, JSON.parse(message));
});
await bus.publish("orders", JSON.stringify({ id: 42 }));
README →

@youneed/server-plugin-pubsub-kafka

Kafka adapter for @youneed/server-plugin-pubsub: KafkaPubSub over topics, via the official `kafkajs`. Pub/sub only (pair with a KV adapter for state).

import { Application } from "@youneed/server";
import { createPubSub, pubsub } from "@youneed/server-plugin-pubsub";
import { kafkaPubSub } from "@youneed/server-plugin-pubsub-kafka";

// Wire the adapter into the core pubsub plugin.
const bus = createPubSub(
  kafkaPubSub({ clientId: "orders-svc", brokers: ["localhost:9092"] }),
);

Application().plugin(pubsub(bus)).listen(3000, () => {});

// channel === Kafka topic. handler receives (message, topic).
await bus.subscribe("orders", (message, topic) => {
  console.log(`[${topic}]`, JSON.parse(message));
});
await bus.publish("orders", JSON.stringify({ id: 42 }));
README →

@youneed/server-plugin-pubsub-nats

NATS adapter for @youneed/server-plugin-pubsub: NatsPubSub over subjects, via the official `nats` (nats.js). Pub/sub only (pair with a KV adapter for state).

import { Application } from "@youneed/server";
import { createPubSub, pubsub } from "@youneed/server-plugin-pubsub";
import { natsPubSub } from "@youneed/server-plugin-pubsub-nats";

// Wire the adapter into the core pubsub plugin.
const bus = createPubSub(
  natsPubSub({ servers: "localhost:4222" }),
);

Application().plugin(pubsub(bus)).listen(3000, () => {});

// channel === NATS subject. handler receives (message, subject).
await bus.subscribe("orders", (message, subject) => {
  console.log(`[${subject}]`, JSON.parse(message));
});
await bus.publish("orders", JSON.stringify({ id: 42 }));
README →

@youneed/server-plugin-pubsub-postgres

Postgres adapter for @youneed/server-plugin-pubsub + -store: PostgresKV (table) and PostgresPubSub (LISTEN/NOTIFY), via the official `pg` driver.

import { Application } from "@youneed/server";
import { createPubSub, pubsub } from "@youneed/server-plugin-pubsub";
import { postgresPubSub } from "@youneed/server-plugin-pubsub-postgres";

// Wire the adapter into the core pubsub plugin.
const bus = createPubSub(postgresPubSub({ connectionString: process.env.DATABASE_URL }));

Application().plugin(pubsub(bus)).listen(3000, () => {});

// Cross-instance: any process LISTENing on "orders" gets this NOTIFY.
await bus.subscribe("orders", (message) => console.log(JSON.parse(message)));
await bus.publish("orders", JSON.stringify({ id: 42 }));
README →

@youneed/server-plugin-pubsub-rabbitmq

RabbitMQ adapter for @youneed/server-plugin-pubsub: RabbitMQPubSub over an AMQP exchange, via the official `amqplib`. Pub/sub only (pair with a KV adapter for state).

import { Application } from "@youneed/server";
import { createPubSub, pubsub } from "@youneed/server-plugin-pubsub";
import { rabbitmqPubSub } from "@youneed/server-plugin-pubsub-rabbitmq";

// Wire the adapter into the core pubsub plugin.
const bus = createPubSub(
  rabbitmqPubSub({ url: "amqp://localhost", exchange: "orders-svc" }),
);

Application().plugin(pubsub(bus)).listen(3000, () => {});

// channel === routing key on the exchange. handler receives (message, channel).
await bus.subscribe("orders", (message, channel) => {
  console.log(`[${channel}]`, JSON.parse(message));
});
await bus.publish("orders", JSON.stringify({ id: 42 }));
README →

@youneed/server-plugin-pubsub-redis

Redis/Valkey adapter — RedisKV (@youneed/server-plugin-store) + RedisPubSub (@youneed/server-plugin-pubsub) over a hand-rolled minimal RESP2 client (zero external deps).

import { redisKV } from "@youneed/kv-redis";
import { namespaced } from "@youneed/kv";

const kv = redisKV({ host: "127.0.0.1", port: 6379 });
// …or from a URL: redisKV({ url: "redis://:secret@cache.internal:6379/2" })

// Share one backend across consumers without key collisions:
const sessions = namespaced(kv, "sess");
const rate = namespaced(kv, "rl");

// Session store
await sessions.set(sid, JSON.stringify(data), { ttl: 1800 });
const raw = await sessions.get(sid);

// Rate limit — atomic, race-free counter; ttl applies only on creation:
const hits = await rate.incr(`ip:${ip}`, { by: 1, ttl: 60 });
if (hits > 100) throw new Error("rate limited");
README →

@youneed/server-plugin-pubsub-sqs

AWS SQS adapter for @youneed/server-plugin-pubsub: SqsPubSub over SQS queues (channel = queue), pure fetch + SigV4 (no aws-sdk). Long-poll consumer.

import { Application } from "@youneed/server";
import { createPubSub, pubsub } from "@youneed/server-plugin-pubsub";
import { sqsPubSub } from "@youneed/server-plugin-pubsub-sqs";

const bus = createPubSub(
  sqsPubSub({
    region: "us-east-1",
    accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
    secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
    // channel === queue name, appended to this base URL prefix.
    queuePrefix: "https://sqs.us-east-1.amazonaws.com/123456789012/",
  }),
);

Application().plugin(pubsub(bus)).listen(3000, () => {});

// channel === SQS queue. handler receives (message, channel).
await bus.subscribe("orders", (message, channel) => {
  console.log(`[${channel}]`, JSON.parse(message));
});
await bus.publish("orders", JSON.stringify({ id: 42 }));
README →

@youneed/server-plugin-queue

@youneed/server plugin: a durable background job queue (retries, backoff, dead-letter, delayed jobs, concurrent workers) persisted to a KV store, with a devtools tab.

import { Application } from "@youneed/server";
import { createQueue, queue } from "@youneed/server-plugin-queue";
import { redisKV } from "@youneed/kv-redis"; // or omit for the in-process MemoryKV

const jobs = createQueue({
  store: redisKV({ url: process.env.REDIS_URL }), // durable + shared across instances
  concurrency: 5,
  maxAttempts: 3,
  backoff: (attempt) => 1000 * 2 ** (attempt - 1), // 1s, 2s, 4s …
}).register("email", async ({ to }: { to: string }) => {
  await sendEmail(to); // throw → retried, then dead-lettered after maxAttempts
});

const app = Application().plugin(queue(jobs)); // starts workers on listen, drains on shutdown
app.listen(3000);

// enqueue from anywhere
await jobs.add("email", { to: "ada@x.dev" });
await jobs.add("email", { to: "grace@x.dev" }, { delayMs: 60_000 }); // run in 1 min
README →

@youneed/server-plugin-rbac

@youneed/server plugin + controller provider + guard for @youneed/rbac: request-scoped `this.can`, an `authorize(action, resource)` guard, a roles/check introspection API, and a devtools RBAC tab.

import { Application, Controller } from "@youneed/server";
import { createRBAC, owns, rbac, rbacProvider, authorizeWith } from "@youneed/server-plugin-rbac";

const engine = createRBAC((role) => {
  role("admin").can("*", "*");
  role("editor").inherits("viewer").can(["create", "update"], "post").can("delete", "post", owns("authorId"));
  role("viewer").can("read", "post");
});

// Bind the engine once so route guards need not carry it.
const authorize = authorizeWith(engine);

class PostController extends Controller("/posts", {
  providers: [rbacProvider(engine)],
}) {
  @Controller.guard(authorize("update", "post"))
  @Controller.put("/:id")
  update() {
    /* only reached if the current subject may update a post */
  }

  @Controller.get()
  list() {
    // `this.can` is bound to the current request's subject
    return this.can("read", "post") ? loadPosts() : [];
  }
}

const app = Application(PostController).plugin(rbac(engine));
app.listen(3000);
README →

@youneed/server-plugin-secrets

@youneed/server plugin + controller provider for @youneed/secrets: request-safe `this.secrets`, SAFE introspection routes (names + masked presence probe — values NEVER exposed), and a devtools Secrets tab.

import { Application, Controller } from "@youneed/server";
import { createSecrets, EnvSecrets, secrets, secretsProvider } from "@youneed/server-plugin-secrets";

const engine = createSecrets(new EnvSecrets(), { cacheTtlMs: 60_000 });

class BillingController extends Controller("/billing", {
  providers: [secretsProvider(engine)],
}) {
  @Controller.post()
  async charge() {
    const key = await this.secrets.require("STRIPE_KEY"); // resolved server-side; never returned
    // … call Stripe with `key` …
    return { ok: true };
  }
}

const app = Application(BillingController).plugin(secrets(engine));
app.listen(3000);
README →

@youneed/server-plugin-ssr

@youneed/server plugin: add SSR pages and satellite SSR modules (robots/sitemap/rss/llms/structured-data) to a server from the outside.

import { Application } from "@youneed/server";
import { ssr } from "@youneed/server-plugin-ssr";
import { robots } from "@youneed/ssr-plugin-robots";
import { sitemap } from "@youneed/ssr-plugin-sitemap";
import { rss } from "@youneed/ssr-plugin-rss";
import { llms } from "@youneed/ssr-plugin-llms";
import { structuredData, organization } from "@youneed/ssr-plugin-structured-data";

Application()
  .plugin(
    ssr({
      origin: "https://example.com",
      pages: [Home, About, Pricing], // @youneed/ssr Page classes
      devtools: true,
      modules: [
        robots({ sitemap: true }),
        sitemap({ defaults: { changefreq: "weekly" } }),
        rss({ title: "Blog", description: "Latest", items: loadItems }),
        llms({ title: "Example", includePages: true }),
        structuredData({ schemas: organization({ name: "Example" }) }),
      ],
    }),
  )
  .listen(3000, () => {});
README →

@youneed/server-plugin-storage

@youneed/server plugin: pluggable object/blob storage (memory, filesystem, S3) behind one StorageAdapter contract, with a devtools object-browser tab.

import { Application } from "@youneed/server";
import { storage, FileStorage, s3Storage, MemoryStorage } from "@youneed/server-plugin-storage";

// pick a backend
const files = new FileStorage("./data");                     // local disk
// const files = s3Storage({ bucket: "my-bucket", region: "us-east-1" }); // S3
// const files = new MemoryStorage();                        // dev / single instance

const app = Application().plugin(storage(files)); // exposes control routes + devtools tab
app.listen(3000);

// use it from anywhere
await files.put("docs/readme.txt", "hello", { contentType: "text/plain" });
const obj = await files.get("docs/readme.txt"); // { data: Uint8Array, contentType }
await files.list("docs/");                       // [{ key, size, contentType, updatedAt }]
await files.delete("docs/readme.txt");
README →

@youneed/server-plugin-store

Distributed key-value contract (KV) + in-process MemoryKV. The backing store is chosen by adapter (redis/postgres/deno via @youneed/server-plugin-pubsub-*).

import { MemoryKV, namespaced, type KV } from "@youneed/server-plugin-store";

const kv: KV = new MemoryKV();

await kv.set("user:1", JSON.stringify({ name: "Ada" }), { ttl: 60 });
await kv.get("user:1");          // → '{"name":"Ada"}'  (undefined once expired)
await kv.incr("hits", { ttl: 60 }); // → 1, atomic, expiry set only on creation
await kv.ttl("hits");            // → remaining seconds (-1 no expiry, -2 missing)

// Share one backend across consumers without key collisions:
const sessions = namespaced(kv, "sess");   // every key becomes "sess:<key>"
const rate = namespaced(kv, "rl");
README →

@youneed/server-upload

Streaming multipart/form-data file uploads for @youneed/server: web streams, progress, and size/extension/type/name/content guards.

import { Response } from "@youneed/server";
import { parseUpload, UploadError } from "@youneed/server-upload";
import { pipeline } from "node:stream/promises";
import { Readable } from "node:stream";
import fs from "node:fs";

app.post("/upload", async (ctx) => {
  const form: Record<string, string> = {};
  try {
    for await (const part of parseUpload(ctx, {
      maxFileSize: 10 * 1024 * 1024,
      maxFiles: 5,
      allowedExtensions: [".png", ".jpg", ".pdf"],
      allowedTypes: ["image/png", "image/jpeg", "application/pdf"],
      sniff: true,                                  // magic bytes vs declared type
      filenamePattern: /^[\w.\- ]+$/,
      onProgress: (p) => report(p.bytesReceived, p.totalBytes),
    })) {
      if (part.kind === "field") form[part.name] = part.value;
      else await pipeline(Readable.fromWeb(part.stream), fs.createWriteStream(`/tmp/${part.filename}`));
    }
    return { ok: true, fields: form };
  } catch (e) {
    if (e instanceof UploadError) return Response.json({ error: e.message }, { status: e.status });
    throw e;
  }
}, { body: false });                                // ← required: handler reads the stream
README →

@youneed/ssr

Server-side rendering + Page entity for @youneed components.

import { renderToString, renderPage } from "@youneed/ssr";

renderToString(MyComponent);             // → "<my-el><template shadowrootmode>…</template></my-el>"
renderPage(MyComponent, { title: "Home", clientScript: "/client.js" }); // full <!doctype html>
README →

@youneed/ssr-plugin-canonical

SSR page middleware for @youneed: emit <link rel="canonical"> and hreflang alternates per page.

import { ssr } from "@youneed/server-plugin-ssr";
import { canonical } from "@youneed/ssr-plugin-canonical";

class Pricing extends Page("/pricing", {
  canonical: "/pricing",                 // string | false | (ctx) => string
  alternates: [{ hreflang: "de", href: "/de/preise" }],
}) { /* … */ }

app.plugin(ssr({
  origin: "https://example.com",
  pages: [Pricing],
  modules: [canonical({ trailingSlash: "strip" })],
}));
README →

@youneed/ssr-plugin-csp

SSR Content-Security-Policy for @youneed: per-request nonce, header builder, and inline-script nonce injection for document responses.

import { ssr } from "@youneed/server-plugin-ssr";
import { csp } from "@youneed/ssr-plugin-csp";

app.plugin(ssr({
  pages: [Home],
  modules: [csp({ directives: { "img-src": ["'self'", "https://cdn.example.com"] } })],
}));
README →

@youneed/ssr-plugin-feature-flags

SSR module for @youneed/server-plugin-ssr: evaluate feature flags on the server and inject the snapshot (window.__FLAGS__) into every page for client hydration.

import { ssr } from "@youneed/server-plugin-ssr";
import { createFlags } from "@youneed/feature-flags";
import { featureFlags } from "@youneed/ssr-plugin-feature-flags";

const flags = createFlags([
  { key: "new-dashboard", defaultValue: false, rollout: 20 },        // 20% of users
  { key: "checkout", defaultValue: "control",
    variants: { control: "control", fast: "fast" },
    rules: [{ attributes: { plan: "pro" }, variant: "fast" }] },     // pro → "fast"
]);

app.plugin(
  ssr({
    origin: "https://example.com",
    modules: [
      featureFlags(flags, {
        // derive the evaluation context from each request (cookie, session, …)
        context: (req) => ({ targetingKey: req.cookies?.uid, attributes: { plan: req.plan } }),
      }),
    ],
  }),
);
README →

@youneed/ssr-plugin-llms

SSR module for @youneed/server-plugin-ssr: serve an llms.txt (and optional llms-full.txt) guide for LLM crawlers.

import { ssr } from "@youneed/server-plugin-ssr";
import { llms } from "@youneed/ssr-plugin-llms";

app.plugin(
  ssr({
    origin: "https://example.com",
    pages: [Home, Docs, Pricing],
    modules: [
      llms({
        title: "Example",
        summary: "A widget store with a public API.",
        notes: ["All prices in USD."],
        sections: [
          { title: "Docs", links: [{ title: "API", url: "/docs/api", notes: "REST reference" }] },
        ],
        includePages: true, // append a "Pages" section from the mounted routes
      }),
    ],
  }),
);
README →

@youneed/ssr-plugin-meta

SSR page middleware for @youneed: emit SEO <meta> + OpenGraph + Twitter Card tags from per-page declarations.

import { ssr } from "@youneed/server-plugin-ssr";
import { meta } from "@youneed/ssr-plugin-meta";

class Post extends Page("/blog/:slug", {
  title: "Hello world",
  meta: {
    description: "An introductory post.",
    og: { type: "article", image: "/og/hello.png" },
    twitter: { card: "summary_large_image" },
  },
}) { /* … */ }

app.plugin(ssr({
  origin: "https://example.com",
  pages: [Post],
  modules: [meta({ siteName: "Example", twitterSite: "@example" })],
}));
README →

@youneed/ssr-plugin-otel

@youneed/ssr module: real OpenTelemetry SDK — ssr.render spans per static page render, ssr.render metrics, via the shared @youneed/otel core.

import { Application } from "@youneed/server";
import { ssr } from "@youneed/server-plugin-ssr";
import { otelModule } from "@youneed/ssr-plugin-otel";

Application()
  .plugin(ssr({
    pages: [Home, About],
    modules: [otelModule()],
  }))
  .listen(3000, () => {});
README →

@youneed/ssr-plugin-preload

SSR page middleware for @youneed: emit resource hints (<link rel=preload/modulepreload/preconnect/dns-prefetch/prefetch>) per page.

import { ssr } from "@youneed/server-plugin-ssr";
import { preload } from "@youneed/ssr-plugin-preload";

class Home extends Page("/", {
  preload: [
    { rel: "preload", href: "/fonts/inter.woff2", as: "font", type: "font/woff2", crossorigin: true },
    { rel: "modulepreload", href: "/client.js" },
  ],
}) { /* … */ }

app.plugin(ssr({
  origin: "https://example.com",
  pages: [Home],
  modules: [preload({ hints: [{ rel: "preconnect", href: "https://cdn.example.com" }] })],
}));
README →

@youneed/ssr-plugin-robots

SSR module for @youneed/server-plugin-ssr: generate robots.txt with per-agent allow/disallow rules and sitemap links.

import { ssr } from "@youneed/server-plugin-ssr";
import { robots } from "@youneed/ssr-plugin-robots";

app.plugin(
  ssr({
    origin: "https://example.com",
    modules: [
      robots({
        policies: [
          { userAgent: "*", disallow: ["/admin", "/api"], allow: "/api/public" },
          { userAgent: ["GPTBot", "CCBot"], disallow: "/" },
        ],
        sitemap: true, // → Sitemap: https://example.com/sitemap.xml
        host: "example.com",
      }),
    ],
  }),
);
README →

@youneed/ssr-plugin-rss

SSR module for @youneed/server-plugin-ssr: serve an RSS 2.0 or Atom feed from a list of items.

import { ssr } from "@youneed/server-plugin-ssr";
import { rss } from "@youneed/ssr-plugin-rss";

app.plugin(
  ssr({
    origin: "https://example.com",
    modules: [
      rss({
        title: "Example Blog",
        description: "Latest posts",
        // value or (async) function — feed reflects fresh data per request
        items: () =>
          loadPosts().then((p) =>
            p.map((post) => ({
              title: post.title,
              link: `/blog/${post.slug}`,
              description: post.excerpt,
              pubDate: post.publishedAt,
              guid: post.id,
            })),
          ),
      }),
    ],
  }),
);
README →

@youneed/ssr-plugin-sitemap

SSR module for @youneed/server-plugin-ssr: generate sitemap.xml from mounted page routes plus explicit entries.

import { ssr } from "@youneed/server-plugin-ssr";
import { sitemap } from "@youneed/ssr-plugin-sitemap";

app.plugin(
  ssr({
    origin: "https://example.com",
    pages: [Home, About, Pricing],
    modules: [
      sitemap({
        exclude: ["/admin", /^\/internal/],
        entries: [{ url: "/blog/launch", lastmod: "2026-06-01", priority: 0.8 }],
        defaults: { changefreq: "weekly", priority: 0.5 },
      }),
    ],
  }),
);
README →

@youneed/ssr-plugin-speculation

SSR page middleware for @youneed/ssr: inject the Speculation Rules API <script> (prefetch/prerender) from a page's declared rules.

class Home extends Page("/", {
  title: "Home",
  speculation: { prerender: [{ source: "list", urls: [About.url], eagerness: "moderate" }] },
}) {
  override render() { return HomeApp; }
}
README →

@youneed/ssr-plugin-structured-data

SSR module for @youneed/server-plugin-ssr: inject JSON-LD structured data (schema.org) into page <head>, with typed builders.

import { ssr } from "@youneed/server-plugin-ssr";
import { structuredData, organization, website } from "@youneed/ssr-plugin-structured-data";

app.plugin(
  ssr({
    origin: "https://example.com",
    modules: [
      structuredData({
        schemas: [
          organization({ name: "Example", url: "https://example.com", logo: "/logo.png" }),
          website({ name: "Example", url: "https://example.com", searchUrl: "https://example.com/search?q=" }),
        ],
      }),
    ],
  }),
);
README →

@youneed/ssr-router

SSR router for @youneed: an ssr() module adding Error/404 pages (server), plus the @youneed/dom-router for client SPA navigation.

import { Application } from "@youneed/server";
import { ssr } from "@youneed/server-plugin-ssr";
import { router } from "@youneed/ssr-router";
import { Home, Blog, NotFound, ErrorPage } from "./pages";

const app = Application();
app.plugin(
  ssr({
    pages: [Home, Blog],
    modules: [router({ notFound: NotFound, error: ErrorPage })],
  }),
);
app.listen(3000, () => {});
README →

@youneed/test

you need test framework

import { Test, Fixture, TestApplication, expect } from "@youneed/test";

class Calculator {
  add(a: number, b: number) { return a + b; }
}

class CalcFixture extends Fixture<Calculator>({ name: "calc", scope: "test" }) {
  setup() { return new Calculator(); }
}

class CalcTest extends Test({ name: "Calculator" }) {
  @Test.use(CalcFixture) calc!: Calculator;

  @Test.it("adds two numbers")
  adds() {
    expect(this.calc.add(2, 3)).toBe(5);
  }
}

await TestApplication().addTests(CalcTest).run();
README →

@youneed/test-devtools

Live web-UI devtools reporter for @youneed/test — streams the run to a browser over SSE

import { TestApplication } from "@youneed/test";
import { DevtoolsReporter } from "@youneed/test-devtools";

await TestApplication()
  .addTests(MyTest)
  .reporter(new DevtoolsReporter({ open: true }))   // opens the UI in your browser
  .run();
README →

@youneed/test-expect-extra

Extra expect matchers for @youneed/test

import { expect } from "@youneed/test-expect-extra";

expect({ id: 1, name: "a", extra: true }).toMatchObject({ id: 1, name: "a" });
expect(0.1 + 0.2).toBeCloseTo(0.3);
expect({ user: { role: "admin" } }).toHaveProperty("user.role", "admin");

await expect(fetchUser()).resolves.toMatchObject({ id: 1 });
await expect(boom()).rejects.toThrow(/failed/);
README →

@youneed/test-plugin-benchmark

Benchmark extension for @youneed/test (decorator + plugin + reporter)

import { TestApplication, Test } from "@youneed/test";
import { Benchmark, benchmark, BenchmarkReporter } from "@youneed/test-plugin-benchmark";

class Perf extends Test() {
  data = Array.from({ length: 1000 }, (_, i) => i);

  @Benchmark({ name: "sum 1k", iterations: 2000 })  // fixed count…
  @Test.it()
  reduceSum() { this.data.reduce((a, b) => a + b, 0); }

  @Benchmark({ timeBudgetMs: 200 })                 // …or sample for a time budget
  @Test.it()
  sort() { [...this.data].sort((a, b) => b - a); }
}

await TestApplication()
  .addTests(Perf)
  .use(benchmark())                  // ① the plugin — turns marked cases into timed loops
  .reporter(new BenchmarkReporter()) // ② its output (compose with ConsoleReporter etc.)
  .run();
//   ⚡ sum 1k  174,894 ops/sec ±2.1%  (mean 0.0057ms · p99 0.0134ms · 2000 samples)
README →

@youneed/test-plugin-feature-flags

Deterministic feature-flag helpers for @youneed/test: a fresh FeatureFlags fixture that resets overrides between tests, plus a scoped withFlags(...) override helper.

import { Test, TestApplication, expect } from "@youneed/test";
import { flagsFixture, withFlags, expectFlag } from "@youneed/test-plugin-feature-flags";
import type { FeatureFlags } from "@youneed/feature-flags";

const Flags = flagsFixture([
  { key: "new-checkout", defaultValue: false },
  { key: "theme", defaultValue: "light", variants: { light: "light", dark: "dark" } },
]);

class Checkout extends Test() {
  @Test.use(Flags) flags!: FeatureFlags; // …or decorator-free: flags = Flags.get();

  @Test.it("off by default") off() {
    expect(this.flags.isEnabled("new-checkout")).toBeFalsy();
  }

  @Test.it("forced on for THIS test only") on() {
    this.flags.override("new-checkout", true);
    expectFlag(this.flags, "new-checkout");
    // the next test gets a fresh engine — no leak.
  }

  @Test.it("scoped override restores after the block") scoped() {
    withFlags(this.flags, { "new-checkout": true }, () => {
      expectFlag(this.flags, "new-checkout"); // enabled inside
    });
    expect(this.flags.isEnabled("new-checkout")).toBeFalsy(); // restored
  }
}

TestApplication().addTests(Checkout).run();
README →

@youneed/test-plugin-i18n

Translation parity checks for @youneed/test: assert no missing/extra keys or drifted placeholders across locales.

import { Test, TestApplication, expect } from "@youneed/test";
import { assertParity, eachLocale } from "@youneed/test-plugin-i18n";
import { resources, i18n } from "./i18n.ts";

class I18n extends Test() {
  @Test.it("every locale is complete") complete() {
    assertParity(resources); // throws an AssertionError listing the gaps
  }

  @Test.it("greeting renders in every language") greet() {
    eachLocale(i18n, () => expect(i18n("greeting", { name: "x" })).toContain("x"));
  }
}
README →

@youneed/test-plugin-otel

@youneed/test plugin: real OpenTelemetry SDK — a span per test case with steps and failure status, test.* metrics, OTLP/HTTP export flushed at teardown.

import { TestApplication } from "@youneed/test";
import { otel } from "@youneed/test-plugin-otel";

await TestApplication()
  .addTests(MySuite)
  .use(otel({ serviceName: "my-tests", endpoint: "http://localhost:4318" }))
  .run();
README →

@youneed/test-plugin-rbac

Deterministic RBAC helpers for @youneed/test: a fresh RBAC fixture that resets role tweaks between tests, ergonomic Subject builders, expectCan/expectCannot assertions, and a scoped withRole(...) helper.

import { Test, TestApplication } from "@youneed/test";
import { rbacFixture, subject, expectCan, expectCannot, withRole } from "@youneed/test-plugin-rbac";
import { owns, type RBAC } from "@youneed/rbac";

const Rbac = rbacFixture((role) => {
  role("viewer").can("read", "post");
  role("editor")
    .inherits("viewer")
    .can(["create", "update"], "post")
    .can("delete", "post", owns("authorId")); // only own posts
});

class Posts extends Test() {
  @Test.use(Rbac) rbac!: RBAC; // …or decorator-free: rbac = Rbac.get();

  @Test.it("editor can update, viewer cannot") perms() {
    expectCan(this.rbac, subject(["editor"]), "update", "post");
    expectCannot(this.rbac, subject(["viewer"]), "update", "post");
  }

  @Test.it("ownership condition") owned() {
    const editor = subject(["editor"], { id: "u1" });
    expectCan(this.rbac, editor, "delete", "post", { authorId: "u1" }); // owns it
    expectCannot(this.rbac, editor, "delete", "post", { authorId: "u2" }); // not owner
  }

  @Test.it("setRole applies for THIS test only") mutate() {
    this.rbac.setRole({ name: "viewer", permissions: [{ action: "delete", resource: "post" }] });
    expectCan(this.rbac, subject(["viewer"]), "delete", "post");
    // the next test gets a fresh engine — no leak.
  }

  @Test.it("scoped role experiment restores after the block") scoped() {
    withRole(this.rbac, { name: "viewer", permissions: [{ action: "delete", resource: "post" }] }, () => {
      expectCan(this.rbac, subject(["viewer"]), "delete", "post"); // granted inside
    });
    expectCannot(this.rbac, subject(["viewer"]), "delete", "post"); // restored
  }
}

TestApplication().addTests(Posts).run();
README →

@youneed/test-reporter-console

Colored console reporter for @youneed/test

import { TestApplication } from "@youneed/test";
import { ConsoleReporter } from "@youneed/test-reporter-console";

await TestApplication()
  .addTests(MyTest)
  .reporter(new ConsoleReporter())   // plug it in
  .run();
README →

@youneed/test-reporter-html

HTML report reporter for @youneed/test

import { TestApplication } from "@youneed/test";
import { HTMLReporter } from "@youneed/test-reporter-html";

await TestApplication()
  .addTests(MyTest)
  .reporter(new HTMLReporter({ output: "report.html", title: "My suite" }))
  .run();
README →

@youneed/test-reporter-junit

JUnit XML reporter for @youneed/test

import { JUnitReporter } from "@youneed/test-reporter-junit";
TestApplication().addTests(...).reporter(new JUnitReporter({ output: "junit.xml" })).run();
README →

@youneed/test-reporter-progress

Live, interactive per-lane progress reporter for @youneed/test

import { TestApplication } from "@youneed/test";
import { ProgressReporter } from "@youneed/test-reporter-progress";

await TestApplication()
  .addTests(...suites)
  .parallel(4)                       // run across 4 in-process lanes
  .reporter(new ProgressReporter())  // live per-lane status
  .run();
README →

@youneed/test-reporter-tap

TAP reporter for @youneed/test

import { TapReporter } from "@youneed/test-reporter-tap";
TestApplication().addTests(...).reporter(new TapReporter()).run();
README →

@youneed/test-resilience

Timeout and retry plugins for @youneed/test

import { TestApplication } from "@youneed/test";
import { timeout, retry, Timeout, Retry } from "@youneed/test-resilience";

class Flaky extends Test() {
  @Retry(3) @Timeout(2000) @Test.it("eventually") work() { /* … */ }
}

TestApplication().addTests(Flaky)
  .use(retry(2))      // outer — re-runs on failure
  .use(timeout(5000)) // inner — each attempt times out
  .run();
README →

@youneed/test-snapshot

Snapshot testing for @youneed/test

import { snapshot, toMatchSnapshot } from "@youneed/test-snapshot";

class Render extends Test() {
  @Test.it() tree() { toMatchSnapshot(buildTree()); }
}

TestApplication().addTests(Render).use(snapshot()).run();
README →

@youneed/ts-plugin

TypeScript language-service plugin: completions inside html`` / css`` for @youneed/dom — custom-element tags, .props and @events.

import { defineComponentPreview, serveComponentPreview, runComponentPreview }
  from "@youneed/ts-plugin/preview";

const config = defineComponentPreview({
  file: "./src/components.ts",      // entry that registers your @youneed/dom components
  generate(c) {                     // optional: per-component props/markup/skip/width
    if (c.tag === "todo-item") return { props: { text: "Buy milk" } };
  },
});

await serveComponentPreview(config);          // live gallery at http://127.0.0.1:5757
// or: await runComponentPreview({ ...config, outDir: "preview" });  // → preview/<tag>.png
README →

@youneed/vite-plugin

Vite plugin for @youneed components.

import { defineConfig } from "vite";
import { domFramework } from "@youneed/vite-plugin";

export default defineConfig({
  plugins: [
    domFramework(),
    // react(), vue(), …
  ],
});
README →
packages/ on GitHub →