Skip to content

Platforms ​

An App answers fetch(request, env, platform), so anything that can call a Web fetch handler can run it. The App is the same on every platform; there are no adapter packages.

Cloudflare Workers ​

ts
import { 
app
} from "./app.ts"
export default
app
// Workers calls fetch(request, env, ctx): it fits as is

Secrets and bindings are c.env. Background Work uses ctx.waitUntil. Build with @cloudflare/vite-plugin (Deploying).

Node and Bun ​

server() next to composure() builds the Default Server around your App (src/app.tsx, exported as app, or composure({ app })): no entry file to write.

ts
export default 
defineConfig
({
plugins
: [
composure
(),
server
()], // vite build → dist/index.js: node (or bun) dist/index.js
})

It listens on PORT (or 3000), passes process.env as the env, answers GET /health and caps bodies at 1 MiB. Background Work runs on tracked promises. On SIGTERM it stops accepting requests and waits up to 30 seconds for that work. It runs on Node and on Bun.

The Default Server ships in @composurejs/vite/server, not in core: core uses no Node APIs, so it runs unchanged on Workers, and the server is built into your bundle by server() either way, so it adds no runtime dependency.

Your own server ​

To change any of that, write the entry yourself and point server({ entry }) at it. @composurejs/vite/server exports the pieces of the Default Server: serve with options, and toRequest/send to mount the App in a node:http server you already have.

ts
import { 
serve
} from "@composurejs/vite/server"
import {
app
} from "./app.ts"
const
server
= await
serve
(
app
, {
port
: 8080,
healthPath
: "/healthz",
shutdownGrace
: 10_000 })
console
.
info
({
msg
: "Listening",
url
:
server
.
url
})
ts
export default 
defineConfig
({
plugins
: [
composure
(),
server
({
entry
: "src/server.ts" })],
})

The entry can skip @composurejs/vite/server entirely and call app.fetch from anything that speaks Web Request/Response: Bun.serve, Deno, Hono, a serverless handler. Pass the env, and a platform with waitUntil if the host keeps running after the response; without one, Background Work finishes before the response is sent (Deferring).

ts
import { 
createTracker
} from "composurejs"
import {
app
} from "./app.ts"
const
platform
=
createTracker
() // waitUntil; platform.settle() on shutdown
Bun
.
serve
({
port
: 3000,
fetch
: (
request
) =>
app
.
fetch
(
request
,
process
.
env
,
platform
) })

In vite dev, server() serves the App itself, so a custom entry applies to the build only. Drop server() altogether to build with another plugin.

Credentials and Bot Data ​

Core needs only the public key, which by default it reads as DISCORD_PUBLIC_KEY from the env (env on Workers, process.env elsewhere). DISCORD_TOKEN enables c.bot. To load them elsewhere, or to build your own configuration once:

tsx
type 
BotEnv
= {
Bindings
: {
CONFIG_URL
: string };
Data
: {
greeting
: string } }
export const
app
= new
App
<
BotEnv
>({
credentials
: async (
env
) => ({
publicKey
: await
secrets
.
get
("discord-public-key"),
token
: await
secrets
.
get
("discord-token") }),
data
: async (
env
) => (await
fetch
(
env
.
CONFIG_URL
)).
json
() as
Promise
<{
greeting
: string }>,
})

Loaders run once per isolate (on Workers, at the first request) and are cached. A failing loader answers 500, is logged, and is retried on the next request.