REST chart storage adapter
createRestChartStorageAdapter maps the public storage contract to a fixed HTTP
convention. Use it when your backend can expose these routes. Use a
custom chart storage adapter for different routes, GraphQL,
RPC, or pushed drawing updates.
This adapter persists chart workspaces and related chart-owned state. It does
not persist the outer widget panel arrangement used by WidgetLayoutContainer
or Trading Terminal. Use the separate
Widget layout storage guide for that
WidgetLayoutState integration.
Configure the client
import { createRestChartStorageAdapter } from '@tradescript/pro/sdk';
declare function getAccessToken(): Promise<string>;
export const storage = createRestChartStorageAdapter({
baseUrl: 'https://api.example.com/chart-storage',
apiVersion: 'v1',
clientId: 'pro-terminal',
userId: 'user-123',
workspaceId: 'primary',
headers: async () => ({
Authorization: `Bearer ${await getAccessToken()}`,
}),
});
The factory makes no request during construction. It resolves headers() for
every operation, so short-lived credentials can be refreshed without replacing
the adapter.
Configured identity becomes query context:
client=pro-terminal&user=user-123&workspace=primary
Per-request userId and workspaceId override configured defaults. The
adapter may also send chartId, symbol, and interval. Treat all browser
query values as routing context; derive authorization from the credential on
the server.
Response rules
Successful JSON may be returned directly or inside this envelope:
{ "status": "ok", "data": {} }
The adapter also accepts an empty successful response, including 204, for
writes and deletes. A chart workspace layout create is the exception: it must
return a server-assigned id.
Load routes map 404 to null. Other non-success responses reject. Drawing
routes map 409 to DrawingRevisionConflictError and 401 or 403 to
DrawingPermissionError; non-drawing HTTP failures are generic storage errors.
The adapter runtime-parses JSON syntax but does not replace backend schema validation. Validate incoming payloads and preserve unknown documented metadata on the server.
Chart layout routes
| Operation | Request |
|---|---|
| List | GET /v1/charts |
| Create | POST /v1/charts without chart |
| Update | POST /v1/charts?chart={storageId} |
| Load | GET /v1/charts?chart={storageId} |
| Delete | DELETE /v1/charts?chart={storageId} |
Create and update use the same body:
{
"name": "Opening drive",
"symbol": "AAPL",
"resolution": "1D",
"content": {
"id": "opening-drive",
"version": 1,
"name": "Opening drive",
"activeChartId": "chart-1",
"charts": [
{
"id": "chart-1",
"symbol": { "ticker": "AAPL" },
"interval": "1D"
}
]
}
}
Create must return an object with id, either directly or inside the standard
response envelope:
{ "status": "ok", "data": { "id": "layout-record-481" } }
The SDK retains opening-drive → layout-record-481 and sends the backend id as
chart on update, load, and delete. The widget-level chart layout controller
keeps this mapping in memory; hosts that drive it directly must persist
getStorageIds() and restore it with restoreStorageIds() across sessions.
Chart layout tabs persist the mapping through user settings. Do not treat the
name as identity.
List returns summary records whose id is the backend storage id. Use Unix
milliseconds for timestamp:
{
"status": "ok",
"data": [
{
"id": "layout-record-481",
"name": "Opening drive",
"symbol": "AAPL",
"resolution": "1D",
"timestamp": 1720000000123
}
]
}
Chart workspace layout load may return the ChartLayout directly or as
content or layout.
content may be an object or a JSON string.
Separate drawing routes
| Operation | Request |
|---|---|
| Save | POST /v1/drawings plus drawing bucket query |
| Load | GET /v1/drawings plus drawing bucket query |
| Permissions | GET /v1/drawings/permissions |
Drawing calls carry the applicable general context and these wire keys:
chart={chartId}&layout={layoutId}&sharing={sharingMode}
The general query may also include chartId, symbol, and interval. Loads
for indicator-owned drawings add requestType, ownerSource, and
seriesSourceId when supplied.
Save body:
{
"state": {
"chartId": "chart-1",
"layoutId": "opening-drive",
"sharingMode": "not-shared",
"drawings": [],
"removed": [],
"removedGroups": []
},
"baseRevision": "revision-8",
"force": false
}
A production save returns the new revision and millisecond timestamp:
{ "revision": "revision-9", "savedAt": 1720000000123 }
On stale baseRevision, return 409 with the current server state:
{
"status": "error",
"error": {
"remoteRevision": "revision-9",
"remoteState": {
"sharingMode": "not-shared",
"drawings": [],
"revision": "revision-9"
}
}
}
Load returns DrawingState directly or under state; 404 means no saved
bucket. Permissions return DrawingSharingPermissions directly or under
permissions.
The REST adapter does not implement subscribeDrawings. Use a custom adapter
when remote drawing changes must be pushed to a mounted chart.
Template routes
| Kind | Route | Identity query |
|---|---|---|
| Indicator | /v1/indicator_templates | template={templateId} |
| Chart | /v1/chart_templates | template={templateId} |
| Drawing | /v1/drawing_templates | name={templateId}&template={templateId}&tool={toolId} |
indicator_templates is the wire route even though the public SDK calls the kind
indicator. List omits the template id; drawing-template list retains tool.
Save uses POST, load and list use GET, and delete uses DELETE.
Template save body:
{
"name": "Momentum stack",
"content": {
"id": "momentum-stack",
"kind": "indicator",
"version": 1,
"name": "Momentum stack"
}
}
Load returns a ChartTemplate directly or as content or template; 404
maps to null. Template writes are last-write-wins in this adapter.
User-setting routes
| Operation | Request and body |
|---|---|
| Load | GET /v1/settings |
| Save | POST /v1/settings with { "settings": UserSettingsState } |
| Delete keys or all | DELETE /v1/settings with { "keys": string[] } or no keys |
Load returns UserSettingsState directly or under settings; 404 maps to
null. Serialize overlapping writes on the backend if request order matters.
The adapter has no user-setting revision contract.
Replay-state routes
| Operation | Request and body |
|---|---|
| Save | POST /v1/replay?layout={layoutId} with { "state": ReplayState } |
| Load | GET /v1/replay?layout={layoutId} |
The layout query is omitted when the request has no layoutId. Replay-state
load returns ReplayState directly or under state; 404 maps to null.
There is no replay-state delete method in ChartStorageAdapter.
What the REST adapter does not cover
- widget panel arrangements (
WidgetLayoutState); - pushed drawing subscriptions;
- retries, timeouts, cancellation, or offline queues;
- image file upload;
- chart workspace layout, template, user-setting, or replay-state compare-and-swap revisions.
Use a custom adapter or wrapped fetch where those requirements apply. Pass a
separate image storage adapter for inline image drawings.
Production requirements
- Configure CORS for the application origin, methods, and authorization headers.
- Authenticate and authorize every operation; never authorize from
userorworkspacequery values alone. - Keep tenant keys for chart workspace layouts, drawing buckets, template kinds, settings, and replay state explicit in the database.
- Return a stable backend id from chart workspace layout create and preserve it on update.
- Version the HTTP path independently from
ChartLayout.versionandChartTemplate.version. Drawing, settings, and replay records have no built-in schema-version field. - Treat uncertain writes as unknown until reconciled; the adapter does not automatically retry them.
- Redact credentials and stored payloads from logs.
Next steps
- Chart storage — compare the local, REST, and custom integration paths.
- Test chart storage — verify every route, tenant boundary, and failure outcome.
- Chart workspace layouts — understand logical and backend chart workspace layout identity.
- Drawing storage — implement bucket keys, revisions, and permissions.
- Custom chart storage adapter — own a different transport or add drawing streams.