Soln Studio
Soln Studio is used to build tenant-side scripts and custom pages. Scripts run on the server with StudioScriptContext. Page assets run in the browser and can declare server actions for work that needs member data, form APIs, files, approvals, or external HTTP calls.
Asset Types
| Type | Runtime | Entry | Use case |
|---|---|---|---|
| Script | server_js | index.ts | Server-side automation, form writes, approval service calls, and integration logic |
| Page | client_page | src/App.tsx | Custom user pages, dashboards, buttons, and pages that call server actions |
New page assets use the standard project layout:
soln.extension.json
src
├── App.tsx
└── server
└── main.ts
Script Asset Starter
Script assets start with a default index.ts file:
export default async function main(
payload: StudioScriptRuntimeContextPayload,
context: StudioScriptContext
) {
const created = await context.form.create({
formApiName: 'orders',
title: 'SO-1001',
data: {
orderNo: 'SO-1001',
amount: 1200,
},
});
return {
ok: true,
recordId: created.id,
};
}
payload always uses soln.runtime-context/v1. Caller-provided business values are available only under payload.input; source, form, record, draft, selection, child-row, previous-result, and runtime metadata keep their canonical top-level fields.
Page Asset Manifest
Every standard page asset includes soln.extension.json.
{
"entryFilePath": "src/App.tsx",
"serverActions": [
{
"name": "main",
"entryFilePath": "src/server/main.ts"
}
]
}
entryFilePath points to the browser page entry. serverActions declares server-side TypeScript files inside the same page asset.
Only declared action files are executed on the server. Other .ts files remain normal page helper files.
Page Entry
src/App.tsx is the browser page:
export default function App() {
return (
<main
style={{
minHeight: '100vh',
display: 'grid',
placeItems: 'center',
padding: 24,
boxSizing: 'border-box',
fontFamily: 'Arial, sans-serif',
}}
>
Soln Studio Page
</main>
);
}
Local Imports
Studio assets are compiled as controlled asset-local modules. Use relative imports for files inside the same asset:
import { formatMoney } from './shared/money';
import { OrderCard } from './client/components/OrderCard';
Page client files can import client and shared files, but must not import server action files directly. Call server actions through @soln/studio instead. Server actions can import server and shared helpers with relative paths.
Page client files can also import the built-in Ant Design 6 package from the project runtime:
import { Button, Card, Table } from 'antd';
Use the root antd import for Ant Design components. Other npm packages are still not available unless the platform adds them as built-ins.
When building Studio page UI, prefer Ant Design 6 components for forms, tables, cards, buttons, modals, drawers, tabs, alerts, empty/loading states, and common layout controls.
Built-in Dependencies and Compilation Failures
The project currently locks Ant Design to 6.5.2. The ^6.5.2 dependency range is not a claim that this is the latest npm release; the Runtime release lockfile determines the installed version.
antd is available only to page client code, not server scripts or server actions. Import components from the root package; subpaths such as antd/es/*, antd/lib/*, and antd/dist/reset.css are not supported.
If compilation reports Cannot find module 'antd' with compiler-core.js in the require stack for a root import, the deployed Runtime cannot resolve its built-in dependency. The Runtime image must install antd as a production dependency and be rebuilt and upgraded. Updating only the frontend does not fix it. A successful compile in a full local repository does not verify the deployed container.
This is a blocking compilation error. Preserve the requested component-based page and report the Runtime repair needed; do not silently replace it with native elements and describe the dependency problem as fixed. An alternative UI can be used when the user requests or approves it. Runtime image builds check compilation of a page using Card, Table, and Tag; existing installations still require the corrected Runtime image.
Organize Pages and Scripts by Responsibility
Prefer multiple cohesive files instead of putting an entire feature into one entry. Read the existing asset tree before editing and reuse appropriate modules.
- Pages: keep
src/App.tsxfocused on composition. Put substantial UI sections insrc/client/components/, data/state hooks insrc/client/hooks/, server actions and business services insrc/server/, and browser-safe types or pure helpers insrc/shared/. - Scripts: keep the configured entry focused on orchestration. Separate validation, business rules, external API adapters, and reusable helpers into local modules when those responsibilities grow.
- Use descriptive English file names and relative imports. Keep each file focused; split growing files before roughly 500–600 lines. This is a maintenance guideline, not a compiler limit. Small cohesive features do not need artificial fragmentation.
- Client code must not import server modules. Shared modules must be safe in every runtime that imports them; call declared server actions through
callAction. - Apply related file and import changes together, compile the complete asset with its active binding, inspect all diagnostics, then save and publish separately. A successful file write is not successful compilation or publication.
Server Actions
Server actions are declared in soln.extension.json and run with the same StudioScriptContext as script assets.
export default async function main(
payload: StudioScriptRuntimeContextPayload,
context: StudioScriptContext
) {
const orderNo = String(payload.input.orderNo || '').trim();
return {
ok: true,
orderNo,
executionId: context.runtime.executionId,
};
}
Call an action from the page:
import { callAction } from '@soln/studio';
async function runAction() {
const result = await callAction<
{ orderNo: string },
{ ok: boolean; orderNo: string }
>('main', {
orderNo: 'SO-1001',
});
console.log(result);
}
The page never receives the user token directly. It sends a message to the host page, and the host calls the server action with the current session.
Read the page runtime context or theme from the same built-in SDK:
import { getRuntimeContext, getTheme } from '@soln/studio';
const context = getRuntimeContext();
const theme = getTheme();
const isDark = theme?.resolvedScheme === 'dark';
When a Studio page is opened by an interaction button on a form view or edit page, read the host record from the runtime context:
import { getRuntimeContext } from '@soln/studio';
const context = getRuntimeContext();
const recordId = context?.record?.id;
const draftData = context?.draft?.data || {};
const formApiName = context?.form?.apiName;
const selectedRecords = context?.selection?.records || [];
For pages opened by form buttons, current form values are in context.draft.data. context.record is null on create pages, and its data is not preloaded on view or edit pages. A server action that needs persisted values must call context.form.get({ recordId: payload.record.id }). Use context.selection.records or context.selection.childRows for selections and context.child.row for the current child row. Do not read the current record from URL query parameters unless the page is explicitly opened by URL navigation.
Preview And Run
Use the debug panel to compile, preview, or run the current asset.
- For script assets, Run executes
index.ts - For page assets, Run previews the page when a normal page file is active
- If the active file is declared in
serverActions, Run executes that action directly and shows logs, errors, andreturnValue
For example, opening src/server/main.ts and clicking Run executes only the main action. Opening src/App.tsx and clicking Run previews the page.
External HTTP And Member Data
Server actions can read the current member and call external HTTP APIs:
export default async function createLoginUrl(
payload: StudioScriptRuntimeContextPayload,
context: StudioScriptContext
) {
const member = await context.member.current();
const data = (member?.data || {}) as Record<string, unknown>;
const phone = String(data.phone || '').trim();
const response = await context.http.request({
url: 'https://example.com/session',
method: 'POST',
json: { phone },
timeoutMs: 10000,
expect: 'json',
});
return {
ok: response.ok,
data: response.bodyJson,
};
}
Keep temporary logs while checking real member fields, then remove them after the field mapping is confirmed.