Skip to content

Backend and Build ​

Besides editor features, a plugin can extend the backend of the apps that use it, and change how they are compiled. The Users plugin is a complete example: it adds tables, guards, a request context, and server endpoints.

Route guards ​

A guard checks a request before a route runs. Users add guards in the Guards section of a route's Inspector.

ts
import { LogicType, makePlugin, type TRouteGuard } from '@luna-park/plugin';

const adminGuard: TRouteGuard = {
    id: 'admin',
    label: 'Admin only',
    description: 'Only admins can call this route.',
    config: LogicType.object({}),
    check: ({ config, context }) => assertAdmin(context.in_user),
    build: {
        generate: (config) => `async (request) => assertAdmin(request.context.in_user)`,
        imports: [{ name: 'assertAdmin', target: 'my-plugin/server' }]
    }
};

export default makePlugin({
    /* ... */
    editor: {
        guards: [adminGuard]
    }
});
PropertyDescription
id, label, descriptionIdentify the guard. Routes reference it as <plugin-id>/<guard-id>.
configOptional settings shown when the guard is added (a permission to check, for example).
checkRuns in the editor. Throw an error to reject the request.
build.generateReturns the code of the guard in the compiled backend: a function receiving the request.
build.importsFunctions to import in the generated route.

Hooks ​

Hooks let a plugin act on every request and every database query in the editor:

HookCalledParameters
backend/middlewareBefore each route runs.cookies, setContextVar(key, value) to add a value to the request context.
backend/input-nodeWhen the route input node is built.addInput(key, schema) to add an output to the route input node.
database/scopeBefore each query on a table.table, addConditions(conditions) to filter rows (row-level security).
database/changeAfter rows are inserted, updated, or deleted.operation, table, rows.

The Users plugin, for example, puts the connected user in the context with backend/middleware, and exposes it on every route with backend/input-node:

ts
makePlugin({
    hooks: {
        'backend/middleware': async ({ setContextVar }) => {
            setContextVar('in_user', await resolveUser());
        },
        'backend/input-node': ({ addInput }) => {
            addInput('in_user', userSchema);
        }
    }
});

In the compiled backend, reproduce the same behavior with code injections: a Fastify preHandler hook filling request.context, and the addDbScope and onDbChange functions of @/database/hooks.js.

Build ​

The build option changes the compiled app. Like other options, each entry can be a function of the environment.

Dependencies ​

frontImports and backImports add npm packages to the frontend and backend package.json:

ts
makePlugin({
    build: {
        frontImports: [{ name: 'my-design-system', version: '^1.2.0' }],
        backImports: [{ name: 'my-plugin', version: '1.0.0' }]
    }
});

A common pattern is to publish the runtime code of your plugin as a sub-path of its own package (e.g. my-plugin/server) and add the package to backImports.

Environment variables ​

env adds variables to the app's .env file. Use it for secrets, so they never appear in the generated code:

ts
makePlugin({
    build: {
        env: ({ config }) => ({ MY_PLUGIN_API_KEY: config.apiKey })
    }
});

Read them at runtime with process.env.MY_PLUGIN_API_KEY.

Code injections ​

injections inserts code at fixed places of the generated project:

Key (EInjectionKey)Inserted in
ViteImportImports of the frontend vite.config.ts.
VitePluginThe plugins array of the Vite config.
AppImportImports of the frontend main.ts.
AppBodymain.ts, after the app is created (e.g. app.use(...)).
AppSetupThe <script setup> of the root App.vue.
StyleThe global stylesheet.
ServerImportImports of the backend server.ts.
ServerBodyserver.ts, after the server is set up (register hooks and routes on server).
ts
import { EInjectionKey, makePlugin } from '@luna-park/plugin';

makePlugin({
    build: {
        injections: {
            [EInjectionKey.AppImport]: `import MyLib from "my-lib";`,
            [EInjectionKey.AppBody]: `app.use(MyLib);`
        }
    }
});

Instructions for AI ​

The llm property of a plugin (and of each component) is given to Sidekick and AI agents. Explain there how your plugin is meant to be used: which nodes to combine, which settings matter, common pitfalls.