npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@dokebi/app

v0.4.5

Published

Multiple apps per project for Dokebi: pick the app and template by request domain, with EJS and Inertia skeletons.

Readme

@dokebi/app 0.4.4

An optional plugin for several apps in one project that picks the app and template by request domain. An optional package for Node >=24.0.0 and core ^0.4.0 (public on npm). The design is D-0030.

Install

dokebi add @dokebi/app

The required plugins dokebi-web, dokebi-controller and dokebi-template-ejs are installed in the same change (result autoInstalled), and the first app src/apps/main and domains: { main.<project>.localhost: { app: main } } are created (the project's own development host, from the package.json name or the folder name: two projects on one machine never share its cookies; dokebi app link main <host> adds another). template-ejs comes along at install time because the first app is an EJS skeleton. At run time app collects view engines (ejs, inertia) and realtime transports (websocket, sse) by name from @dokebi/web's contribution points and never imports those packages (D-0063 §5). Errors are AppError (VIEW_ENGINE_MISSING, REALTIME_UNAVAILABLE and so on, AI.md §6). To add more apps:

dokebi app add shop --domain a.com --domain www.a.com
dokebi app link shop b.com --template christmas   # add a domain to an existing app
dokebi app unlink b.com
dokebi app list
dokebi app remove shop        # remove the domains pointing at the app first; the folder stays

Domain links are recorded in both dokebi.yaml and dokebi.example.yaml. When an app uses Inertia pages, WebSocket or SSE, run dokebi add @dokebi/template-inertia, dokebi add @dokebi/websocket or dokebi add @dokebi/sse separately.

Folder layout

It follows D-0031. dokebi app add creates the app folder.

src/apps/<app>/
  app.ts            export default defineApp({ ... })
  routes.ts         routes
  controllers/      defineController tokens (this app only)
  models/           plain code (not plugins)
  views/default/    the default template (required). EJS apps: home.ejs; Inertia apps: Layout.vue, Pages/Home.vue
  views/<template>/ only the files that change; missing files are looked up in default, then shared src/shared/views
  views/app.js      (Inertia apps only) Vite entry: finds per-template pages and layouts in the same order
  sockets/          this app's WebSocket routes (defineSocket), created when needed
src/shared/         controllers, models, modules (dokebi-module) and views shared by several apps
  • dokebi app add <app> --inertia creates an Inertia skeleton and prepares the build (vite.config.ts, the build script, Vite and Vue dev dependencies, dist/ in .gitignore). Run npm run build afterwards.
  • With --ssr (D-0039) it also creates the server entry views/ssr.js, inertia.ssr: { bundle: 'dist/ssr/<app>/ssr.js' } in app.ts and the project's vite.ssr.config.ts, and build runs both the browser and the server build (a previously generated script with only vite build becomes both; a script you wrote yourself is kept and reported as BUILD_SCRIPT_KEPT). The browser entry views/app.js hydrates the server-rendered page with createSSRApp. Layouts are chosen with the layout option instead of changing the page module (requests with different templates render on the server at the same time).
  • @dokebi/template-inertia serves the build files under /build/. Details are in the CLI docs.

The app name is the folder name. The generated registration code (src/dokebi.generated.ts) imports ./apps/<app>/app.ts for every app listed in .dokebi/state.json and passes them to this plugin as entry { apps: { <app>: ... } }.

Configuration

dokebi-app:
  domains:
    a.com:     { app: shop }                       # no template = default
    b.com:     { app: shop, template: christmas }
    c.com:     { app: admin }
    s.com:     { app: shop, seal: true }           # seal every WebSocket frame of this host (below)
  unknownDomain: reject      # default; or a key of domains (answered with those settings)
  apps:                      # optional; keys are defined app names (otherwise the start fails with UNKNOWN_APP)
    shop:  { realtime: { namespaces: [front] } }          # default [<app>]
    admin: { realtime: { namespaces: [admin, front] } }   # see "Realtime namespaces" below

The host from dokebi-web's requestHost() (X-Forwarded-Host from a trusted proxy) is compared exactly, without the port and a trailing dot (a.com. is a.com) and in lower case. unknownDomain answers only a Host that is a host name, an IP address or empty; any other Host header (a.com/x, [email protected], markup) is refused like an unknown host. Behind a proxy, list the proxy addresses in dokebi-web trustProxy. There are no wildcards or path prefixes yet. The start refuses unknown apps, missing template folders and a missing views/default/.

defineApp

import { defineApp } from '@dokebi/app';
import { dependency as db } from '@dokebi/db';

export default defineApp({
  requires: [db],                              // plugins and shared modules this app uses
  controllers: [productController],
  views: new URL('./views/', import.meta.url),
  inertia: { vite: { manifest: 'dist/.vite/manifest.json', entry: 'src/apps/shop/views/app.js', base: '/build/' } },
  modules: { '/board': board },                  // mounts a feature module under a prefix (D-0032)
  shared: (request, page) => ({ menu: MENU }),   // data merged into every EJS page of this app
  routes(route, app) {
    route.get('/', (request, reply, page) =>
      page.view.reply(reply, 'home', app.controller(productController).list(), { layout: 'layouts/main' }),
    );
  },
  sockets: (app) => [chatSocket(app)],           // results of defineSocket(...)
  events: (app) => [ordersStream(app)],          // @dokebi/sse routes (SseRoute)
  setup(app) {}, start(app) {}, drain(app) {},
});
  • A handler's page is { app: { name, template, domain }, module, base, view, inertia, sealed }. In app routes module is undefined and base is ''. view is an EJS renderer that looks in <app>/<template> → <app>/default → shared src/shared/views; layouts and $include follow the same order. inertia uses the app's document settings (Vite entry and so on) and adds app to the shared props.
  • Error pages (D-0074): errors on an app's hosts (unknown routes included) go through the app first. An Inertia app answers Inertia validation errors with the redirect back (errors) and other errors with inertia.errorPage (for example Error); for a browser page an EJS app renders views/<template>/errors/<status>.ejs, else errors/error.ejs (same lookup as pages, with status, title, error, message, hint, fields), else @dokebi/web's small page. JSON clients always get JSON. The app add skeletons include errors/404.ejs and errors/error.ejs (EJS) or Pages/Error.vue with errorPage: 'Error' (Inertia). Refuse input with throw validationError({ name: 'Already taken' }) from @dokebi/web.
  • HTML forms come back (0.4.1, D-0074 addendum 1): with @dokebi/session registered, an HTML form post refused with 422 VALIDATION_FAILED flashes errors and old (without password-like fields) and answers 303 to the Referer of the same host, and every EJS page gets data.errors, data.old and data.flash (always objects, the flash read once per request), so the form shows <%= data.old.title ?? '' %> and data.errors.title. Without a session, or without a same-host Referer, the refused form gets errors/422.ejs (else errors/error.ejs) with fields, errors and old. A Referer whose path reads as another host (//other.host/…) is no way back (0.4.2). Another 4xx whose data.formErrors is a { field: message } record goes back the same way (0.4.3, D-0076 §2.5; _form is the whole-form key, which @dokebi/csrf uses for an expired token). Scripts (fetch, XHR, Bearer, JSON, Inertia) keep their JSON. Needs @dokebi/web ^0.4.3.
  • Mounting modules (D-0032): modules: { '/prefix': module }, where the module was made with defineModule.
    • The module's routes are registered with this app's route constraint and the prefix; handlers get page.module and page.base.
    • The module's sockets are registered under the prefix. Their tenant is <namespace>:<tenant>, as for app sockets.
    • Several apps can mount the same module under different prefixes. A mounted module's API is readable with app.get(module) without listing it in requires.
    • The prefix is / or /a/b (lower-case letters, digits, ., _, ~, -).
    • Module pages look for views in this order, so module pages render inside the app layout:
      1. <app>/<template>/<module>
      2. <app>/default/<module>
      3. the module's <template>
      4. the module's default
      5. the app's views
      6. shared views
  • shared(request, { app }): merged under the data of every EJS page this app renders (app routes and module routes). On overlapping keys the page data wins. Put app data the layout needs, such as the menu or the user, here. It corresponds to Inertia's shared props.
  • get and getDependency of the app context (app) allow only the plugins listed in requires. Controller factories have the same limit.
  • A plugin several apps require is checked against every app's version range (core gets their semver AND, which keeps each range's prerelease rule); ranges that cannot meet stop createPlugin with DEPENDENCY_CONFLICT, naming both apps.
  • App routes are registered with a Fastify route constraint (dokebiApp), so several apps can own the same path. Shared routes without the constraint (health checks, module APIs) answer on every domain.
  • The app's WebSocket routes (sockets) and SSE routes (events, @dokebi/sse) use the same constraint. The hub tenant is <namespace>:<tenant> and the default namespace is the app name, so without settings it stays <app>:<tenant>, one per app. The session's identity.tenant and stream.tenant hold this value.
  • Sockets and SSE routes take a route id like app routes (D-0140): config: { id: 'chat' } on defineSocket or the SseRoute. dokebi describe lists it, dokebi-web's route hooks and the refusal logs (routeId) see it, and an id used twice in one app (routes, sockets, streams and those of its modules together) fails the start with DUPLICATE_ROUTE_ID.

Realtime namespaces (0.2.0, D-0061)

Presence lists, broadcasts and SSE publishing are divided by namespace, not by app. apps.<app>.realtime.namespaces (1 to 32, default [<app>]) are the spaces the app may use. Apps with the same namespace share presence, broadcasts and chat (this is not the namespace of the hub settings).

  • One connection = one namespace. When a socket's or SSE route's authenticate returns namespace, the connection joins that space (it must be in the app's list), otherwise the first in the list. Outside the list: 401 and warn NAMESPACE_FORBIDDEN. namespace does not stay in the session identity.
  • Server code: app.realtime(namespace?) gives the handle of that space (NAMESPACE_FORBIDDEN outside the list). A target's tenant is the authenticated tenant without the prefix; leaving it out means every tenant of the space.
const front = app.realtime('front');
await front.list();                                        // connections on every server (tenant, member, rooms…)
await front.list({ tenant: 'shop-1', room: 'vip' });
await front.send({ tenant: 'shop-1', member: 'u42' }, 'chat.message', { text: 'Hello' });
await front.send({}, 'notice', { text: 'Maintenance' });   // the whole space
await front.kick({ tenant: 'shop-1', member: 'u42' });
const stop = front.onPresence(({ tenant, joined, left }) => {});
await front.publish({ tenant: 'shop-1' }, 'order.changed', { id: 7 });   // @dokebi/sse
  • send uses the same { type, data } frame as defineSocket broadcasts, at most once (D-0057), sealed per socket for sealed sockets. Without the needed plugin it throws REALTIME_UNAVAILABLE.
  • The admin app example (watching two front spaces and chatting with users) is in AI.md §4 "Realtime namespaces".

Seal hosts (0.2.0, D-0059 §4)

With domains.<host>.seal: true (default false), app sockets connected through that host seal every frame, and the host's HTTP data (REST JSON requests and responses, the Inertia first page and visits, SSE data) is sealed too (the dokebi-web seal slot, D-0062). Route seal: false and dokebi-web sealExclude are plain exceptions; EJS HTML stays as it is. app fills a socket's route.seal from the request host; the actual sealing and the static files under /dokebi/seal/* (browser loader, socket bundle, wasm) are the @dokebi/seal plugin's job. This public package looks that plugin up only by name (dokebi-seal).

  • Pages get sealed: page.sealed in handlers, sealed in EJS data (the app's shared and page data win), sealed in Inertia shared props. A sealed page preloads the seal loader (wasm) and connects with the seal bridge. A client given the bridge offers both dokebi.seal and dokebi.plain, so on a plain host dokebi.plain is picked and it runs in plain text.
  • Refused at start (fail closed): a seal: true domain without the dokebi-seal plugin is SEAL_PROVIDER_MISSING. An app on a sealed domain that has sockets with @dokebi/websocket older than 0.3 gets the same code. (The warn SEAL_WITHOUT_SOCKETS for sealed apps without sockets was removed by D-0062: HTTP is sealed too.)
  • The rules of sealed connections (4415, 4400, hello, broadcasts) follow "Sealing per host" in the @dokebi/websocket README.

Plugin routes on an app's hosts (D-0132)

Routes that other plugins add on dokebi-web answer on every host unless they are bound to apps. dokebi-app gives web its domain map, so a route with app and any plugin option <plugin>.app (dokebi-mcp.app: mcp, dokebi-oauth.app: studio, dokebi-oauth.resources.<id>.app) answers on exactly that app's hosts, dev hosts and unknownDomain included; other hosts get the app's 404 like an unknown route. See examples/53-app-hosts.

API

createPlugin returns the API { names, resolve(request) }. resolve returns the request's app, template and domain, or undefined for an unknown Host. APP_CONSTRAINT ('dokebiApp'), hostOf(request) and withoutPort(host) are public too.

Service composition

Use Controller -> Service -> Model for application data. Controllers handle HTTP input/output; services provide data and coordinate workflows through models; models own persistence. Generated app/module starters include this chain. Keep services free of request/reply and rendering objects, and reuse them from jobs. Compose dependencies once during setup. App-specific services belong in src/apps/<app>/services, cross-app services in src/shared/services, and module services in src/shared/modules/<module>/services. Shared services must not import app-specific code. These are ordinary application classes, not a new framework plugin or service registry. Existing projects are not moved automatically.