This guide explains how to use SynthOS as an npm dependency and build your own white-labeled product on top of it.
npm install synthosCreate your entry point (e.g. src/index.ts):
import { Customizer, createConfig, init, server } from 'synthos';
class MyApp extends Customizer {
get productName() { return 'MyApp'; }
get localFolder() { return '.myapp'; }
}
const customizer = new MyApp();
async function main() {
const config = await createConfig(customizer.localFolder, {}, customizer);
await init(config);
server(config, customizer).listen(4242, () => {
console.log('MyApp running on http://localhost:4242');
});
}
main();Run it:
npx ts-node src/index.ts
# or compile with tsc and run the JS outputThat's it. You now have a fully working instance with your own branding.
Customizer is the single configuration surface. Subclass it and override getters to change behavior. Every getter has a sensible default so you only override what you need.
| Getter | Default | Purpose |
|---|---|---|
productName |
'SynthOS' |
Name used in LLM prompts, chat messages, and the brainstorm assistant. |
localFolder |
'.synthos' |
Name of the data folder created in the user's working directory. |
tabsListRoute |
'/pages' |
Route that outdated pages redirect to. |
class MyApp extends Customizer {
get productName() { return 'Acme Builder'; }
get localFolder() { return '.acme'; }
}productName is used everywhere: LLM prompts, chat message prefixes (<strong>Acme Builder:</strong>), the brainstorm assistant, first-run greeting, and error messages. All built-in required pages (builder, settings, pages gallery, etc.) have their "SynthOS" references automatically replaced at serve time when productName differs from the default.
Override these to supply your own default pages, themes, scripts, etc. Each getter returns a string[] array of folder paths. The sentinel value 'default' is resolved at config time to the built-in SynthOS folder for that getter. This lets forks layer custom folders alongside (or instead of) the defaults — first folder wins on name collisions.
| Getter | Default | Contents |
|---|---|---|
requiredPagesFolders |
['default'] |
Built-in system pages (builder, settings). |
defaultPagesFolders |
['default'] |
Starter page templates copied on first init. |
defaultThemesFolders |
['default'] |
Theme CSS/JSON files. |
defaultScriptsFolders |
['default'] |
Platform-specific terminal scripts. |
staticFilesFolders |
['default'] |
Versioned static files (page-v2.js, etc.). |
serviceConnectorsFolders |
['default'] |
Connector JSON definitions. |
import path from 'path';
class MyApp extends Customizer {
// Custom pages take priority, built-in pages used as fallback
get defaultPagesFolders() {
return [path.join(__dirname, '../my-pages'), 'default'];
}
// Replace built-in themes entirely
get defaultThemesFolders() {
return [path.join(__dirname, '../my-themes')];
}
}When you don't override a folder, the ['default'] array resolves to the built-in SynthOS assets from the npm package.
SynthOS has built-in feature groups that can be toggled on or off:
pages— Page serving and transformationapi— Core API routes (settings, images, completions)data— Per-page table storagebrainstorm— Brainstorm chat endpointsearch— Web search (Brave Search)scripts— User script executionconnectors— REST API connector proxyagents— A2A and OpenClaw agent routes
Disable groups you don't need:
const customizer = new MyApp();
customizer.disable('agents', 'connectors', 'search');Re-enable later if needed:
customizer.enable('search');Check at runtime:
if (customizer.isEnabled('brainstorm')) {
// brainstorm is active
}Add your own Express routes that the server will mount alongside the built-in ones:
customizer.addRoutes(
(config, app) => {
app.get('/api/my-endpoint', (req, res) => {
res.json({ hello: 'world' });
});
}
);To make the LLM aware of your routes (so pages can call them), pass route hints:
customizer.addRoutes({
installer: (config, app) => {
app.get('/api/weather/:city', async (req, res) => {
// ... fetch weather
res.json(result);
});
},
hints: `GET /api/weather/:city
description: Get current weather for a city
response: { temp: number, condition: string }`
});You can also add route hints without routes (useful if you mount routes elsewhere):
customizer.addRouteHints(
`POST /api/my-custom-action
description: Does something custom
request: { input: string }
response: { result: string }`
);Required pages (builder, settings, pages gallery, scripts, APIs) can be overridden by placing your own versions in a custom folder and listing it before 'default' in requiredPagesFolders. The first folder wins on name collisions:
class MyApp extends Customizer {
get requiredPagesFolders() {
return [path.join(__dirname, '../my-pages'), 'default'];
}
}To override the builder page, create my-pages/builder/page.html. For the settings page, create my-pages/settings/page.html, etc. Any page not found in your folder falls back to the built-in version.
Add custom context sections that are included in every builder LLM call. These appear alongside the built-in sections (SERVER_APIS, THEME, FLUENTLM_COMPONENTS, etc.):
customizer.addContextSections({
title: '<COMPANY_GUIDELINES>',
content: 'All pages must include the Acme Corp logo...',
instructions: 'Follow the guidelines in <COMPANY_GUIDELINES> when creating pages.',
});Append additional instructions to the LLM prompt that transforms pages. These are added after the built-in instructions on every page transformation call:
customizer.addTransformInstructions(
'Always include a footer with "Powered by Acme" at the bottom of the viewer panel.',
'Never use red as a primary color.'
);The three steps to start a SynthOS-based server:
// 1. Create config — resolves folder paths, discovers required pages
const config = await createConfig(
customizer.localFolder, // data folder name (e.g. '.myapp')
{ debug: false, debugPageUpdates: false },
customizer // your Customizer subclass
);
// 2. Init — creates the data folder, copies default pages/themes/scripts
// Returns true on first run, false if folder already exists.
const firstRun = await init(config);
// 3. Start the Express server
const app = server(config, customizer);
app.listen(4242);| Option | Type | Default | Purpose |
|---|---|---|---|
debug |
boolean |
false |
Log every HTTP request with timing. |
debugPageUpdates |
boolean |
false |
Log full LLM input/output for page transformations. |
A complete white-labeled app with custom pages, disabled features, and extra routes:
import path from 'path';
import { Customizer, createConfig, init, server } from 'synthos';
class AcmeBuilder extends Customizer {
get productName() { return 'Acme Builder'; }
get localFolder() { return '.acme'; }
get defaultPagesFolders() {
return [path.join(__dirname, '../acme-pages'), 'default'];
}
get defaultThemesFolders() {
return [path.join(__dirname, '../acme-themes')];
}
}
async function main() {
const customizer = new AcmeBuilder();
// Disable features we don't need
customizer.disable('agents', 'connectors');
// Add a custom API endpoint (with LLM-visible hints)
customizer.addRoutes({
installer: (config, app) => {
app.get('/api/company/info', (_req, res) => {
res.json({ name: 'Acme Corp', plan: 'enterprise' });
});
},
hints: `GET /api/company/info
description: Returns company information
response: { name: string, plan: string }`
});
// Tell the LLM to always use Acme branding
customizer.addTransformInstructions(
'All new pages should include "Acme Corp" in the header.'
);
const config = await createConfig(customizer.localFolder, { debug: true }, customizer);
await init(config);
const port = process.env.PORT ? parseInt(process.env.PORT) : 4242;
server(config, customizer).listen(port, () => {
console.log(`Acme Builder running on http://localhost:${port}`);
});
}
main();A typical extending project looks like this:
my-app/
package.json
tsconfig.json
src/
index.ts # Entry point (createConfig + init + server)
acme-pages/ # Custom default pages (optional)
dashboard.html
dashboard.json
acme-themes/ # Custom themes (optional)
acme-dark-v1.css
acme-dark-v1.json
Your package.json depends on synthos:
{
"name": "acme-builder",
"dependencies": {
"synthos": "^0.9.0"
},
"scripts": {
"start": "ts-node src/index.ts"
}
}These are handled internally and npm consumers won't encounter them:
synthos-cli.ts— The built-in CLI. You write your own entry point instead.migrations.ts— Legacy v1-to-v2 page migration. Only applies to pre-existing SynthOS installs.sshTunnelManager.ts— Internal temp file names. Not user-facing.
| Export | Type | Purpose |
|---|---|---|
Customizer |
Class | Base class to subclass for configuration. |
RouteInstaller |
Type | (config: SynthOSConfig, app: Application) => void |
createConfig |
Function | Builds the config object from customizer + options. |
init |
Function | Initializes the data folder (pages, themes, scripts). |
server |
Function | Creates and returns the Express app. |
SynthOSConfig |
Interface | The resolved config object passed throughout the system. |
| Getter | Returns | Default |
|---|---|---|
productName |
string |
'SynthOS' |
localFolder |
string |
'.synthos' |
requiredPagesFolders |
string[] |
['default'] → built-in required-pages |
defaultPagesFolders |
string[] |
['default'] → built-in default-pages |
defaultThemesFolders |
string[] |
['default'] → built-in default-themes |
defaultScriptsFolders |
string[] |
['default'] → built-in default-scripts |
staticFilesFolders |
string[] |
['default'] → built-in static-files |
serviceConnectorsFolders |
string[] |
['default'] → built-in service-connectors |
tabsListRoute |
string |
'/pages' |
| Method | Purpose |
|---|---|
disable(...groups) |
Turn off feature groups. |
enable(...groups) |
Turn feature groups back on. |
isEnabled(group) |
Check if a group is active. |
addRoutes(...installers) |
Register custom Express routes. |
addRouteHints(...hints) |
Add LLM-visible API documentation. |
addTransformInstructions(...instructions) |
Append rules to the page transform prompt. |
addContextSections(...sections) |
Add custom context sections to every builder LLM call. |