Skip to content

Plugin Cookbook

github-actions[bot] edited this page Jul 5, 2026 · 1 revision

Plugin Cookbook

Short, copy-paste recipes for the things plugins can do. Each one names the permission it needs (declare it in trek-plugin.jsonpermissions) and the capability it uses. For the full API see Plugin Development; for the permission catalogue see Plugin Permissions.

Every trip/place/day operation is membership-checked by the host against the user bound to the invocation — your plugin never passes a user id. Writes also need that user's *_edit permission. A read/write you're not allowed to do fails loudly; it never silently escalates.

The complete, runnable version of these recipes is the trip-doctor example plugin.


Read a trip's places and bookings

Needs: db:read:trips

async handler(req, ctx) {
  const places = await ctx.trips.getPlaces(Number(req.query.tripId))
  const bookings = await ctx.trips.getReservations(Number(req.query.tripId))
  // …use them; the host already checked req.user can see this trip
}

Read the packing list or files of a trip

Needs: db:read:packing · db:read:files (declare only what you use)

const packing = await ctx.packing.list(tripId)   // hydrated bags/assignees
const files   = await ctx.files.list(tripId)      // trash excluded

Both are membership-checked against the current user — same gate as ctx.trips.*.

Add / move something on the itinerary

Needs: db:write:places · db:write:days · db:write:itinerary (declare only what you use)

const place = await ctx.places.create(tripId, { name: 'Teamlab', lat: 35.62, lng: 139.78 })
const day   = await ctx.days.create(tripId, { date: '2027-04-02', notes: 'Odaiba' })
await ctx.itinerary.assign(tripId, day.id, place.id, 'buy tickets first')
// days.create accepts { date?, notes?, position? }; set a day title later with ctx.days.update(tripId, day.id, { title: 'Odaiba' }).

Updates and deletes mirror the REST app exactly (ctx.places.update/delete, ctx.days.update/delete, ctx.itinerary.unassign). They broadcast the same live event, so open web sessions update instantly.

Tag a core entity — no schema fork

Needs: db:meta

Store your own JSON-serialisable data on a trip, place or day. Rows are namespaced to your plugin id — no other plugin can read or overwrite them.

await ctx.meta.set('place', placeId, 'lastCheckedAt', Date.now())
const when = await ctx.meta.get('place', placeId, 'lastCheckedAt')
const all  = await ctx.meta.list('place', placeId)   // { lastCheckedAt: 172… }
await ctx.meta.delete('place', placeId, 'lastCheckedAt')

Reads need trip access; writes additionally need the entity's edit permission.

Contribute extra info to a place (rendered natively)

Needs: hook:place-detail-provider

Return rows and TREK draws them at the foot of the place panel — no iframe.

module.exports = {
  hooks: {
    placeDetailProvider: {
      async getDetails(placeId, ctx) {
        return [
          { label: 'Crowd', value: 'Quiet right now' },
          { label: 'Official site', url: 'https://…' },
        ]
      },
    },
  },
}

Raise validation warnings on a trip

Needs: hook:trip-warning-provider

Return problems and they show as a non-blocking banner in the planner.

hooks: {
  warningProvider: {
    async getWarnings(tripId, ctx) {
      const places = await ctx.trips.getPlaces(tripId)
      return places
        .filter((p) => p.lat == null)
        .map((p) => ({ level: 'warning', message: `"${p.name}" has no location`, placeId: p.id }))
    },
  },
}

level is 'info' | 'warning' | 'error'; dayId/placeId are optional anchors.

Push a live update to a trip / a user

Needs: ws:broadcast:trip and/or ws:broadcast:user

ctx.ws.broadcastToTrip(tripId, 'doctor:rechecked', { count })   // → plugin:<id>:doctor:rechecked
ctx.ws.broadcastToUser(userId, 'nudge', { text: '…' })          // (userId, event, data) — only that user

Events are automatically namespaced to plugin:<your-id>:… so they can't collide with core events.

React to core activity

Needs: events:subscribe

Handlers run with no user and get the event name + tripId only (never the payload).

events: [
  { on: 'file:created', async handler({ tripId }, ctx) {
      await notifySlack(`New file on trip ${tripId}`)   // needs http:outbound
  } },
]

Fire-and-forget on a short timeout — never blocks a core write. Trip reads are refused (no user); use ctx.db, ctx.ws.*, or an outbound call. Your own plugin:* broadcasts are never re-delivered, so handlers can't loop.

Depend on another plugin — call it and hear its events

Needs: a pluginDependencies entry for the other plugin (no permission).

Expose a contract from the dependency (declare the names in capabilities.provides / capabilities.emits):

// plugin "koffi"  ·  manifest: "capabilities": { "provides": ["convert"], "emits": ["rate.updated"] }
exports: {
  async convert({ amount, from, to }) { return { amount: amount * rate(from, to), to } },
},
async onLoad(ctx) { ctx.events.emit('rate.updated', { pair: 'USD/EUR' }) },

Consume it from the dependent (declare koffi as a dependency):

// manifest: "pluginDependencies": [{ "id": "koffi", "version": ">=1.0.0 <2.0.0" }]
routes: [
  { method: 'GET', path: '/price', async handler(_req, ctx) {
      const out = await ctx.plugins.call('koffi', 'convert', { amount: 10, from: 'USD', to: 'EUR' })
      return { status: 200, body: JSON.stringify(out) }
  } },
],
subscriptions: [
  { plugin: 'koffi', event: 'rate.updated', async handler(payload, ctx) { ctx.log.info('rates changed', payload) } },
],

TREK auto-enables koffi before your plugin, routes the call (as your acting user), and refuses it if koffi isn't a satisfied dependency or doesn't export convert. See Plugin Development#talking-to-other-plugins.

Match the TREK look

Add <!-- trek:ui --> to your widget's <head>. The dev server and pack inline TREK's token-driven kit (glass surfaces, buttons, inputs, dark-mode) and a window.trek bridge with the live theme + tokens. See Plugin Development#the-design-kit-recommended. window.trek.ui gives you bundler-free, kit-styled DOM builders (ui.el/button/card/chip/input/mount).


Where things run

Surface Runs Gets
routes forked server child ctx bound to the HTTP request's user
jobs forked server child, on a schedule ctx with no user (can't read user-scoped data)
hooks forked server child, when core asks ctx bound to the user who triggered the read, short timeout
widget / page sandboxed iframe (no same-origin) postMessage bridge; calls its own routes via trek:invoke

Getting Started

Account & Auth

Planning Trips

Travel Management

Photos & Media

Collaboration

Addons

Plugins

AI / MCP

Admin Panel

Operations

Help

Contributing

Clone this wiki locally