Build Your First Plugin

In this tutorial you'll build a Weather plugin that exposes a REST endpoint to fetch current weather data for any location. Estimated time: 15 minutes.

1

Scaffold the project

Use the official generator to create a typed plugin skeleton.

2

Configure TypeScript

Review package.json and tsconfig.json generated by the scaffolder.

3

Implement the plugin

Write the plugin entry point with lifecycle hooks and a custom route.

4

Write tests

Use the built-in test utilities to unit-test your routes.

5

Run locally

Hot-reload your plugin into a running UniCore dev instance.

6

Publish

Build and publish your plugin to npm for others to install.

1

Scaffold the project

Run the official scaffold generator. It creates a ready-to-use TypeScript project with all boilerplate pre-configured:

terminalbash
# Scaffold a new plugin project
npx @bemindlabs/unicore-create-plugin my-weather-plugin
cd my-weather-plugin

Plugin package names must start with unicore-plugin- so UniCore can discover them automatically.

2

Review the configuration

The scaffolder generates these files. Review and adjust as needed:

package.jsonjson
{
  "name": "unicore-plugin-weather",
  "version": "1.0.0",
  "description": "Show weather data inside UniCore",
  "main": "dist/index.js",
  "unicorePlugin": true,
  "scripts": {
    "build": "tsc",
    "dev":   "tsc --watch",
    "test":  "jest"
  },
  "dependencies": {
    "@bemindlabs/unicore-plugin-sdk": "^1.0.0"
  },
  "devDependencies": {
    "typescript": "^5.5.0",
    "@types/node": "^20.0.0",
    "jest": "^29.0.0"
  }
}
tsconfig.jsonjson
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "CommonJS",
    "lib": ["ES2022"],
    "outDir": "dist",
    "rootDir": "src",
    "strict": true,
    "esModuleInterop": true,
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true
  },
  "include": ["src"],
  "exclude": ["node_modules", "dist"]
}

The "unicorePlugin": truefield in package.json is required for UniCore's plugin discovery to recognise your package.

3

Implement the plugin

Open src/index.ts and replace the generated boilerplate:

src/index.tstypescript
import { definePlugin } from '@bemindlabs/unicore-plugin-sdk'

const WEATHER_API = 'https://api.open-meteo.com/v1/forecast'

export default definePlugin({
  id: 'my-weather-plugin',
  name: 'Weather',
  version: '1.0.0',
  description: 'Fetch current weather for any city.',

  async onActivate(ctx) {
    ctx.log.info('Weather plugin activated')

    // Register a REST route: GET /api/plugins/my-weather-plugin/weather?lat=&lon=
    ctx.router.get('/weather', async (req, res) => {
      const { lat, lon } = req.query as { lat: string; lon: string }

      if (!lat || !lon) {
        return res.status(400).json({ error: 'lat and lon are required' })
      }

      const url = new URL(WEATHER_API)
      url.searchParams.set('latitude', lat)
      url.searchParams.set('longitude', lon)
      url.searchParams.set('current_weather', 'true')

      const response = await fetch(url.toString())
      const data = await response.json()

      return res.json({
        temperature: data.current_weather.temperature,
        windspeed:   data.current_weather.windspeed,
        weathercode: data.current_weather.weathercode,
      })
    })
  },

  async onDeactivate(ctx) {
    ctx.log.info('Weather plugin deactivated')
  },
})

Key points:

  • ctx.router.get() registers a route at /api/plugins/my-weather-plugin/weather
  • Route handlers receive standard Express-compatible req/res objects
  • The platform JWT middleware protects all plugin routes automatically
  • Use ctx.log instead of console.log — logs appear in the admin UI
4

Write tests

The SDK ships with a testing sub-package that provides an in-memory PluginContext so you can unit-test plugin logic without a running UniCore instance:

src/__tests__/plugin.test.tstypescript
import { createTestContext } from '@bemindlabs/unicore-plugin-sdk/testing'
import plugin from '../src/index'

describe('Weather plugin', () => {
  it('registers GET /weather route', async () => {
    const ctx = createTestContext({ pluginId: plugin.id })
    await plugin.onActivate!(ctx)

    const routes = ctx.router.getRegisteredRoutes()
    expect(routes).toContainEqual({ method: 'GET', path: '/weather' })
  })

  it('returns 400 when lat/lon missing', async () => {
    const ctx = createTestContext({ pluginId: plugin.id })
    await plugin.onActivate!(ctx)

    const { status, body } = await ctx.router.inject('GET', '/weather')
    expect(status).toBe(400)
    expect(body.error).toBe('lat and lon are required')
  })
})
5

Run locally

Point your local UniCore dev instance to your plugin directory using theUNICORE_LOCAL_PLUGINS env var. Changes are hot-reloaded automatically:

terminalbash
# In your plugin directory
pnpm dev

# In another terminal, tell UniCore to load your local plugin
export UNICORE_LOCAL_PLUGINS=/path/to/my-weather-plugin
docker compose --profile apps restart unicore-api-gateway

Then test your route:

terminalbash
# Test the route via the API gateway (requires JWT)
curl http://localhost:4000/api/plugins/my-weather-plugin/weather?lat=51.5&lon=-0.12 \
  -H "Authorization: Bearer ${TOKEN}"
6

Publish

Build your plugin and publish it to npm. Any UniCore instance can then install it by running npm install unicore-plugin-weather:

terminalbash
# Build for production
pnpm build

# Publish to npm (must match unicore-plugin-* naming)
npm publish --access public
Marketplace listing: Once the Plugin Marketplace launches (Q3 2026), you can submit your plugin for listing by opening a PR to the marketplace registry. Details will be published in the developer docs.