Skip to main content

Server Setup

Unchained Engine supports Express, Fastify and plain Node.js, with identical functionality through the connect() function from @unchainedshop/api/express, @unchainedshop/api/fastify or @unchainedshop/api/node. Fastify is used by the default kitchensink example and recommended for new projects; the kitchensink-express example shows the Express equivalent.

import Fastify from 'fastify';
import { startPlatform } from '@unchainedshop/platform';
import { connect, unchainedLogger } from '@unchainedshop/api/fastify';
import { registerAllPlugins } from '@unchainedshop/plugins/presets/all';

registerAllPlugins();

const fastify = Fastify({
loggerInstance: unchainedLogger('fastify'),
disableRequestLogging: true,
trustProxy: true,
});

const platform = await startPlatform({});

await connect(fastify, platform, {
allowRemoteToLocalhostSecureCookies: process.env.NODE_ENV !== 'production',
adminUI: true,
});

await fastify.listen({
host: '::',
port: process.env.PORT ? parseInt(process.env.PORT) : 3000,
});

Express Setup​

import express from 'express';
import http from 'node:http';
import { startPlatform } from '@unchainedshop/platform';
import { connect } from '@unchainedshop/api/express';
import { registerAllPlugins } from '@unchainedshop/plugins/presets/all';

registerAllPlugins();

const app = express();
const httpServer = http.createServer(app);

const platform = await startPlatform({});

await connect(app, platform, {
allowRemoteToLocalhostSecureCookies: process.env.NODE_ENV !== 'production',
adminUI: true,
});

await httpServer.listen({ port: process.env.PORT || 3000 });
note

Import the preset without a file extension (@unchainedshop/plugins/presets/all) — the package exports map appends .js itself. startPlatform({}) needs no modules argument for the built-in core modules.

Plain Node.js, Bun and Deno​

connect(createServer(), platform, options) from @unchainedshop/api/node serves a node:http server. Bun and Deno use the fetch handler, which answers undefined for requests that are not the engine's:

import { createFetchHandler } from '@unchainedshop/api/fetch';

const handler = createFetchHandler(platform, { adminUI: true });
Bun.serve({ fetch: async (request) => (await handler(request)) ?? new Response(null, { status: 404 }) });

Connect Options​

All adapters accept the same options in the third argument:

await connect(server, platform, {
// Allow secure cookies over HTTP for development;
// connect() throws if this is enabled with NODE_ENV=production
allowRemoteToLocalhostSecureCookies: boolean,

// Serve the Admin UI: true, or an options object
adminUI: boolean | { prefix?: string; theme?: AdminUIThemeConfig; plugins?: AdminUIPluginConfig[] },

// AI chat endpoint (Vercel AI SDK model instances)
chat: { model: LanguageModel; imageGenerationTool?: { model: ImageModel; uploadUrl?: string }; allowedOrigins?: string[] },

// OIDC/OAuth authentication configuration
authConfig: AuthConfig,

// Trust X-Forwarded-For / X-Real-IP headers from a reverse proxy
// (implied automatically when allowRemoteToLocalhostSecureCookies is set)
trustProxy: boolean,
});

Key Differences​

HTTP Server Creation​

Express requires manually creating an HTTP server, while Fastify handles it internally:

// Express - manual HTTP server
const app = express();
const httpServer = http.createServer(app);
await httpServer.listen({ port: 3000 });

// Fastify - built-in server
const fastify = Fastify({ trustProxy: true });
await fastify.listen({ host: '::', port: 3000 });

Logger Integration​

// Express - use createLogger from @unchainedshop/logger
import { createLogger } from '@unchainedshop/logger';
const logger = createLogger('app');

// Fastify - use unchainedLogger wrapper
import { unchainedLogger } from '@unchainedshop/api/fastify';
const fastify = Fastify({
loggerInstance: unchainedLogger('fastify'),
});

Adding Custom Routes​

// Express - standard middleware
app.use('/api/custom', (req, res) => {
res.json({ hello: 'world' });
});

// Fastify - route registration
fastify.route({
method: 'GET',
url: '/api/custom',
handler: async (request, reply) => {
return { hello: 'world' };
},
});

Your routes get the request context as req.unchainedContext (Express: routes added after connect()) or request.unchainedContext (Fastify, skipped with config: { unchainedContext: false }). The engine's endpoints are routes of your app too: Express middleware added before connect() and Fastify hooks, including those of @fastify/rate-limit, apply to them. Add Express body parsers after connect(), or keep req.rawBody (express.json({ verify })), so webhooks get the body as sent.

Admin UI​

Enable the Admin UI via the adminUI option:

// Serve at the root path
await connect(server, platform, { adminUI: true });

// Serve under a prefix
await connect(server, platform, { adminUI: { prefix: '/admin' } });

The options object also accepts theme (design token overrides) and plugins (custom Admin UI extensions) — see the kitchensink examples for a full configuration.

OIDC Authentication​

For authentication with external identity providers, pass the authConfig option. See the OIDC example for a complete implementation.