Skip to main content
Use storage to save plugin state. Use HTTP routes to turn a plugin into a simple handler. Route declarations live in manifest headers, and storage functions are imported directly from middleware.js.

Default storage

Default storage uses the global bucket otto and is suitable for small plugin state.
const { get, set, del } = require('./middleware.js')

await set('last_user', '10001')
const user = await get('last_user')
await del('last_user')
FunctionPurposeParametersReturns
get(key)Read from default storage.key: required.Promise<string>; returns an empty string when missing.
set(key, value)Write to default storage.key: required; value: required and saved as a string.Promise<boolean>
del(key)Delete a default storage key.key: required.Promise<boolean>
Storage values are saved as strings. To save objects, call JSON.stringify(...) before writing and JSON.parse(...) after reading.

Bucket naming rules

bucket is a storage namespace. A . in the name means a child bucket. For example, shop.orders means orders under shop. The runtime allows up to 3 bucket levels. Prefer at most 3 levels to avoid overly deep permission configuration, data migration, and troubleshooting.
bucket
string
required
Bucket name. It cannot be empty. Use . to separate levels. Each segment must be non-empty. Maximum 3 levels. Examples: my_plugin, my_plugin.users, my_plugin.orders.paid.
Use plugin_name.business_domain.state, such as card_claim.users or card_claim.inventory.active. For a small plugin, use a one-level bucket such as my_plugin.
Bucket nameLevelsRecommendationDescription
my_plugin1RecommendedGood for simple configuration and small state.
my_plugin.users2RecommendedGood for splitting users, orders, and cache by business domain.
my_plugin.orders.paid3UsableReaches the recommended limit; use only when you truly need this subdivision.
my_plugin.orders.paid.20264InvalidExceeds the runtime maximum level.
my_plugin..usersInvalidInvalidContains an empty segment.
Do not treat . as a normal character inside one level. a.b is interpreted as a two-level bucket, not one bucket named a.b.

Named buckets

Named buckets isolate plugin state by plugin name or business domain. The bucket isolates the namespace, and the key locates a specific record.
const { bucketGet, bucketSet, bucketAllKeys, bucketAll } = require('./middleware.js')

const config = { enabled: true, limit: 10 }
await bucketSet('my_plugin.config', 'runtime', JSON.stringify(config))
await bucketSet('my_plugin.users', '10001', JSON.stringify({ claimed: true }))

const raw = await bucketGet('my_plugin.config', 'runtime')
const parsed = raw ? JSON.parse(raw) : {}

const keys = await bucketAllKeys('my_plugin.users')
const all = await bucketAll('my_plugin.users')
Function / Sender methodPurposeParametersReturns
bucketGet(bucket, key)Read a key in the specified bucket.bucket: required, supports . child levels, maximum 3 levels; key: required.Promise<string>; returns an empty string when missing.
bucketSet(bucket, key, value)Write to the specified bucket.bucket: required, supports . child levels, maximum 3 levels; key: required; value: required and saved as a string.Promise<boolean>
bucketDel(bucket, key)Delete a key in the specified bucket.bucket: required, supports . child levels, maximum 3 levels; key: required.Promise<boolean>
bucketKeys(bucket?, value?)Get keys whose values equal value in the specified bucket. Without value, returns all keys.bucket: optional, defaults to otto; value: optional.Promise<string[]>
bucketAllKeys(bucket)Get all keys in the specified bucket.bucket: required.Promise<string[]>
bucketAll(bucket)Get all key-value pairs in the specified bucket.bucket: required.Promise<Record<string, string>>
Sender instances also provide bucket methods with the same names. They additionally carry the current senderid, which is useful when you need the permission system to check bucket access by the current run.
await sender.bucketSet('my_plugin.users', 'user:' + await sender.getUserID(), 'seen')

HTTP route requests

After declaring router and method in the manifest, scripts can read HTTP requests and return responses. See Plugin manifest headers for header declarations.
//[author: your-name]
//[runtime: [email protected]]
//[router: /demo/{id}]
//[method: post]
const { Sender, getSenderID } = require('./middleware.js')

async function main() {
  const sender = new Sender(getSenderID())
  const params = await sender.getRouterParams()
  const body = await sender.getRouterBody()
  await sender.response({ ok: true, id: params.id, body })
}

main().catch(console.error)
MethodPurposeParametersReturns
getRouterPath() / getRouter()Get the current route path.NonePromise<string>
getRouterParams()Get path parameters, such as { id: '42' }.NonePromise<Record<string, string>>
getRouterMethod() / getMethod()Get the request method.NonePromise<string>
getRouterHeaders()Get request headers.NonePromise<Record<string, string or string[]>>
getRouterCookies()Get cookies.NonePromise<Record<string, string>>
getRouterBody()Get the parsed request body. Returns {} when there is no body.NonePromise<any>
getRouterData()Get the request body as a JSON string.NonePromise<string>
response(data)Set HTTP response data.data: required, any JSON-like data.Promise<boolean>
data
unknown
required
Response body for response(data). It can be an object, array, string, number, or boolean.
Actual response(...) example:
{
  "ok": true,
  "id": "42",
  "body": {
    "name": "demo"
  }
}
Calling reply('text') or replyMarkdown('...') in a route run also writes response_data, which is useful for returning plain text. Use response(data) when you need to return an object.

Top-level route aliases

middleware.js also exports route-reading aliases bound to the default Sender:
FunctionEquivalent method
getRouter()new Sender(getSenderID()).getRouterPath()
getMethod()new Sender(getSenderID()).getRouterMethod()
getRouterData()new Sender(getSenderID()).getRouterData()

Next steps

Manifest headers

See router, method, param, and dependency declarations.

Tools and dependencies

See importModule(...) and system utility functions.
Last modified on June 3, 2026