Overview
What Moddable is, how a project is laid out, and the pieces a module is built from.
Moddable is a full-stack TypeScript framework that compiles a server, a website and a service worker out of one source tree and ships them as a single Cloudflare Worker. There is no separate frontend project, no API project, and no build server — there is a tree of modules and a CLI that turns it into a deployment.
The shape of a project
A project is moddable/. Everything inside it is either a module you compose your
platform from, or a target the CLI writes into.
moddable/
├── core/ # the base module — auth, storage, notification, site
├── modules/ # everything else you register
├── cli/ # the `moddable` command, compiled from source
├── common/ # code shared between core and the CLI
├── scripts/ # Bun macros and Vite plugins
├── server/ # build target: the Cloudflare Worker
├── site/ # build target: the browser bundles
├── settings/ # module load order and project settings
├── tests/ # one directory per module
└── types/ # generated type unions
server/, site/ and types/ are generated. You do not edit
them; moddable config composes them from the modules that are actually on disk, so
adding a module is a matter of creating a directory and re-running a generator.
Modules and submodules
A module is a folder with up to three submodules, and the submodule a file lives in decides where its code ends up:
| Submodule | Compiled into |
|---|---|
api/ |
The Worker — signals, views, durable objects, routines |
client/ |
The browser bundle and the service worker |
shared/ |
Both, merged into each |
Inside a submodule the directory names are the contract. signals/ holds request
handlers, views/ holds URL-routed pages, elements/ holds custom
elements, objects/ holds durable objects, routines/ holds scheduled
work, helpers/ holds the use* clients, and
registration/ holds the lifecycle hooks that run when the module is loaded.
Module order is registration order, and later modules merge over earlier ones. That is the extension mechanism: a module does not patch core, it registers something at the same key and wins.
Signals
A signal is a route plus a handler. It is declared with the pathname it answers to, the middleware it runs behind, and a handler that switches on the HTTP method.
export const authAPISignInSignal = makeSignal('api', '/auth/signin', {
middleware: [useRequestTelemetry],
handler: async (moddable, request, params) => {
const { useRequestSwitch, onRequestException } = moddable.helpers
return useRequestSwitch({ POST }, onRequestException).handle(moddable, request, params)
}
})
Signals registered by a module under api/signals are served beneath
/api, so the signal above answers at /api/auth/signin.
Views and elements
A view is a page. It is a folder — index.ts declaring what the page is,
template.html, style.css and logic.ts — and the CLI
compiles it twice: the definition is read on the server so the page can be rendered into HTML,
and the logic is bundled into a custom element the browser upgrades the same markup into.
export default makeView({
name: 'DocsView',
tag: 'docs-view',
pathname: '/docs',
layouts: ['docs-layout'],
template,
style,
logic,
hook: (moddable, props, view) => props
})
The hook runs on the server for every request, before the template is
interpolated. Whatever it returns becomes the page's props — rendered into the HTML and handed
to the browser as JSON, so the client hydrates the exact tree the server sent.
An element is the same folder shape without a pathname. Elements are addressed by tag, listed
in their module's elements/index.ts, and served as
/elements/<tag>.js. A view names elements in
layouts to be wrapped in them.
One runtime, three outputs
moddable build produces the worker bundle, the browser app, and one module per
view, element and asset. The worker serves the site it was compiled with, so a deployment is a
single wrangler deploy — there is nothing else to put anywhere.
Read Quickstart next to get one running.