From fd918b8f804d203a89b35c4c1d42249ae7a9ed37 Mon Sep 17 00:00:00 2001 From: Arpan Jokhakar Date: Mon, 21 Sep 2026 16:15:36 +0530 Subject: [PATCH 1/4] DEV-2032: document context.settings and the three app-settings tiers App Actions V3: context has payload and settings; add the context.settings reference (server-only, master values, fresh per run, 100 KB limit, CORS limit, leak rule) with a complete example. App settings: public / _private / __protected tiers, protected-settings behaviour, server-action section. Regenerated .well-known projections for the two pages. Co-Authored-By: Claude Fable 5.1 --- .../agent-skills/fliplet-js-api/SKILL.md | 2 +- docs/.well-known/agent-skills/index.json | 2 +- docs/.well-known/llms-full.txt | 158 ++++++++++++++++-- docs/.well-known/llms.txt | 2 +- docs/API/core/app-actions-v3.md | 72 +++++++- docs/API/v3/app-settings.md | 88 ++++++++-- 6 files changed, 290 insertions(+), 34 deletions(-) diff --git a/docs/.well-known/agent-skills/fliplet-js-api/SKILL.md b/docs/.well-known/agent-skills/fliplet-js-api/SKILL.md index a05debe2..fbcc62bb 100644 --- a/docs/.well-known/agent-skills/fliplet-js-api/SKILL.md +++ b/docs/.well-known/agent-skills/fliplet-js-api/SKILL.md @@ -101,7 +101,7 @@ The Fliplet client-side JavaScript API: every Fliplet.X namespace (Storage, User - [Link Action provider](https://developers.fliplet.com/API/providers/link-action.md): Configure link actions (navigate to screen, open URL, document, video, or run JS) in a reusable provider UI executable via `Fliplet.Navigate.to()`. - [V3 app analytics and event tracking](https://developers.fliplet.com/API/v3/analytics.md): V3 app analytics and event tracking. Page views are tracked automatically by the fliplet-analytics-spa runtime; this doc covers what you get for free and when to add event() calls for intent-bearing… - [V3 app bootstrap constraints](https://developers.fliplet.com/API/v3/app-bootstrap.md): The four constraints every V3 boot HTML must satisfy. Covers Fliplet.require.lazy for dependencies, Fliplet.Media.getContents for source files, the Fliplet().then(...) init sequence, and the locked v… -- [V3 App Settings Convention](https://developers.fliplet.com/API/v3/app-settings.md): V3 app settings convention for storing public and private configuration. Covers the underscore prefix convention for editor-private settings. +- [V3 App Settings Convention](https://developers.fliplet.com/API/v3/app-settings.md): V3 app settings convention: public, private (_) and protected (__) keys, who can read each, and how server app actions read them. - [V3 Authentication Patterns](https://developers.fliplet.com/API/v3/auth.md): V3 authentication patterns for email/password login, session management, logout, and protected routes. Use these patterns when building authentication flows in V3 apps. - [V3 barcodes](https://developers.fliplet.com/API/v3/barcode.md): Scan and generate QR codes and barcodes in V3 apps with Fliplet.Barcode — attachScanner() to scan (web + native) and encode() to render barcode images. - [V3 Alpine.js apps](https://developers.fliplet.com/API/v3/frameworks/alpine.md): Constraints for building V3 apps in Alpine.js. Alpine is attribute-driven HTML with no build step, so it maps cleanly to the V3 runtime. Covers x-init timing relative to Fliplet().then and platform-c… diff --git a/docs/.well-known/agent-skills/index.json b/docs/.well-known/agent-skills/index.json index 13cc1080..3cf6485c 100644 --- a/docs/.well-known/agent-skills/index.json +++ b/docs/.well-known/agent-skills/index.json @@ -130,7 +130,7 @@ "tags": [ "js-api" ], - "sha256": "351a1e4fcfc199cb2dea50ba70b293d33333026b8970937731be8d06baa1c529" + "sha256": "666092886f9387056475989c165b8c196fb114891d84c3360e9f695d6db120f0" }, { "name": "fliplet-docs-index", diff --git a/docs/.well-known/llms-full.txt b/docs/.well-known/llms-full.txt index 143f6b4e..cba3e41f 100644 --- a/docs/.well-known/llms-full.txt +++ b/docs/.well-known/llms-full.txt @@ -6545,7 +6545,7 @@ async function execute(context) { | Return value | Should return a value/object (available via `runWithResult`) | | Dependencies | Code using Fliplet APIs must include corresponding dependencies | -

The function must be named execute. Any other name causes a CODE_VALIDATION_FAILED error. The function does not receive any parameters beyond context — all input data is inside context.payload.

+

The function must be named execute. Any other name causes a CODE_VALIDATION_FAILED error. The function does not receive any parameters beyond context — input data is inside context.payload and, for server actions, private app settings are inside context.settings.

Do not use Handlebars {% raw %}{{ }}{% endraw %} syntax in action code. Handlebars expressions are not evaluated inside app actions and will cause unexpected behavior or errors. Use JavaScript template literals (${}) with backtick strings instead. For example: {% raw %}`Hello ${context.payload.name}`{% endraw %}

@@ -6575,7 +6575,14 @@ const execute = async function(context) { ### Context object -The `context` parameter is an object with a single property: `payload`. The `context` object does **not** contain any other properties — no `appId`, no `userId`, no `environment`. All input data comes through `context.payload`. +The `context` parameter is an object with exactly two properties: `payload` and `settings`. + +| Property | Type | Description | +|----------|------|-------------| +| `context.payload` | Object | Input data for this run. `{}` when no data is passed. The contents differ by trigger type — see the breakdown below. | +| `context.settings` | Object | The app's private settings (top-level app settings whose key starts with `_`). Populated **only** when the action's `environment` is `server`. Always `{}` for `client` and `any` actions. See [`context.settings`](#contextsettings-server-actions-only). | + +The `context` object does **not** contain any other properties — no `appId`, no `userId`, no `environment`. All input data comes through `context.payload`; `context.settings` carries configuration and credentials, never input data. The contents of `context.payload` differ by trigger type. See the detailed breakdown below. @@ -6697,6 +6704,67 @@ async function execute(context) { } ``` +#### `context.settings` (server actions only) + +`context.settings` gives a server action read access to the app's private settings, so a credential such as a third-party API key is stored on the app instead of being hardcoded in the action code. See [V3 app settings convention](../v3/app-settings) for how settings are named and saved. + +- **Server only.** Only actions with `environment: 'server'` receive values. `client` and `any` actions always get `context.settings = {}` — for `any` actions this applies both when they run on the server and when they run on the device. +- **Underscore keys only.** It contains every **top-level** app setting whose key starts with `_` — both `_private` and `__protected` keys. Public settings (no underscore) are not included. Nested keys starting with `_` are not special. +- **Master app values.** Values are always read from the **master** app, including when the published (production) app runs the action. There is no separate production copy of these values. +- **Fresh on every run.** Values are read from the database each time the action runs. A rotated value applies to the next run without republishing the app or the action. +- **100 KB limit.** If the app's underscore settings exceed 100 KB in total (serialized as JSON), **every** server action of that app fails with the error `Private app settings exceed the 100 KB limit for server actions` until the settings are reduced. +- **Read defensively.** Start with `const settings = context.settings || {};` and handle a missing value — the setting may not be configured yet. + +

Never let a setting value leave the action. Never return it, never log it (console.*), never put it in a thrown or returned error message, and never put it in a URL or query string — send it in a request header or body instead. Return values and error messages are stored in the app action logs and returned to the caller of runWithResult(); console output and every request URL are captured in the server runner logs.

+ +

Server actions run in a headless browser, so fetch from a server action is a cross-origin browser request. Provider APIs that do not allow cross-origin browser requests (CORS) reject it. Server-side HTTP requests without this restriction are not available yet.

+ +Store credentials under a `__` (protected) key. Protected keys are write-only over HTTP: nobody can read them back, not Studio editors and not API tokens. `_` (private) keys are readable by Studio editors. + +Example: read a credential, send it in a request header, return early when it is not configured, and return nothing that contains the value. + +```js +// Action environment must be 'server'. No dependencies required. +// App setting used (saved on the master app): __mailProvider = { apiKey: '...' } +async function execute(context) { + const settings = context.settings || {}; + const mailProvider = settings.__mailProvider || {}; + const apiKey = mailProvider.apiKey; + + if (!apiKey) { + // Not configured (or the action is not a server action): stop here + return { sent: false, error: 'MAIL_PROVIDER_NOT_CONFIGURED' }; + } + + let response; + + try { + response = await fetch('https://api.example.com/v1/messages', { + method: 'POST', + headers: { + // Credentials go in a header (or the body), never in the URL + Authorization: `Bearer ${apiKey}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + to: context.payload.email, + subject: 'Your booking is confirmed' + }) + }); + } catch (error) { + // Network or CORS failure. Return a fixed code — do not rethrow, + // log or return anything built from the request or the settings. + return { sent: false, error: 'MAIL_PROVIDER_REQUEST_FAILED' }; + } + + if (!response.ok) { + return { sent: false, error: 'MAIL_PROVIDER_REJECTED', status: response.status }; + } + + return { sent: true }; +} +``` + ### Example: simple action ```js @@ -27778,18 +27846,23 @@ URL: https://developers.fliplet.com/API/v3/app-settings.md # V3 App Settings Convention -App settings in V3 use `app.settings` to store configuration for features like authentication, push notifications, and analytics. This page describes the naming convention that controls which settings are visible to the app runtime (preview and published apps) vs only to Studio editors. +App settings in V3 use `app.settings` to store configuration for features like authentication, push notifications, and analytics. This page describes the naming convention that controls which settings are visible to the app runtime (preview and published apps), to Studio editors, or to server-side code only. ## The Underscore Convention +The prefix of a **top-level** settings key sets its tier: + +| Key pattern | Tier | Who can read it | Example | +|---|---|---|---| +| `settingName` | Public | App runtime + Studio + backend | `app.settings.saml2` (IdP URL, attribute mappings) | +| `_settingName` | Private | Studio editors + backend + server app actions. Never sent to the app runtime, preview or bundles. | `app.settings._saml2` (IdP certificate) | +| `__settingName` | Protected | Backend + server app actions only. Write-only over HTTP: nobody can read it back, not Studio editors and not API tokens. | `app.settings.__mailProvider` (third-party API key) | + Settings keys that start with `_` are **editor-private**. They are visible to Studio editors managing the app but are NOT included in the app runtime (preview iframe, published apps, bundled apps). -| Key pattern | Who can read it | Example | -|---|---|---| -| `settingName` | App runtime + Studio + backend | `app.settings.saml2` (IdP URL, attribute mappings) | -| `_settingName` | Studio editors + backend only | `app.settings._saml2` (IdP certificate) | +Settings keys that start with `__` are **protected**. See [Protected settings](#protected-settings). -The convention uses **top-level key namespacing**. All private settings for a feature go under a single `_feature` key, not mixed into the public feature key. +The convention uses **top-level key namespacing**. All private settings for a feature go under a single `_feature` (or `__feature`) key, not mixed into the public feature key. ## How It Works @@ -27804,11 +27877,16 @@ app.settings = { // Editor-private settings — visible to Studio editors, NOT to the running app _saml2: { idpCertificate: '-----BEGIN CERTIFICATE-----\nMIID...' + }, + + // Protected settings — NOT readable over HTTP by anyone, NOT in the running app + __mailProvider: { + apiKey: 'sk_live_...' } } ``` -When the app loads in the preview iframe or as a published app, `_saml2` is stripped from `window.ENV.appSettings`. The running app only sees: +When the app loads in the preview iframe or as a published app, every top-level key starting with `_` (`_saml2` and `__mailProvider` here) is stripped from `window.ENV.appSettings`. The running app only sees: ```js window.ENV.appSettings = { @@ -27816,11 +27894,49 @@ window.ENV.appSettings = { idpUrl: 'https://idp.company.com/sso', attributeMappings: { email: 'Email', name: 'Name' } } - // _saml2 is NOT here + // _saml2 and __mailProvider are NOT here +} +``` + +Backend code that runs inside the Fliplet API (server-side passports, hooks) reads the full `app.settings` from the model — no filtering is applied there. + +App action code does **not** run inside the API and never reads the model. A V3 app action with `environment: 'server'` receives the underscore keys as `context.settings` — see [Reading settings in a server action](#reading-settings-in-a-server-action). `client` and `any` actions get no private settings; on the device they only see what the app runtime sees. + +## Protected settings + +Use a `__` key for a secret that must never be read back, such as a third-party API key used by a server action. + +- Every HTTP read of app settings omits top-level keys starting with `__`. This applies to Studio editors, admins, API tokens and app action (task) tokens. +- Studio editors can write and delete `__` keys, but can never read them back. To change a value, overwrite it. +- App action (task) tokens can not write or delete `__` keys — the request fails with `403`. +- V3 app version snapshots do not store `__` keys. Restoring an app version keeps the app's current `__` values. +- Like `_` keys, `__` keys are never sent to the app runtime, preview or bundles. +- Server app actions receive `__` keys in `context.settings`, together with `_` keys. + +## Reading settings in a server action + +A V3 app action with `environment: 'server'` receives every top-level app setting whose key starts with `_` (both `_private` and `__protected`) as `context.settings`: + +```js +async function execute(context) { + const settings = context.settings || {}; + const mailProvider = settings.__mailProvider || {}; + + if (!mailProvider.apiKey) { + return { sent: false, error: 'MAIL_PROVIDER_NOT_CONFIGURED' }; + } + + // Send mailProvider.apiKey in a request header or body. + // Never return it, log it, throw it or put it in a URL. + return { configured: true }; } ``` -Backend code (server-side passports, hooks, app actions) reads the full `app.settings` from the model — no filtering is applied server-side. +- Values always come from the **master** app and are read fresh on every run. Published apps read the master's values; a rotated value applies to the next run without republishing. +- `client` and `any` actions always get `context.settings = {}`. +- If the underscore settings exceed 100 KB in total, every server action of the app fails until they are reduced. + +See [`context.settings` in App Actions V3](../core/app-actions-v3#contextsettings-server-actions-only) for the complete example, the limits and the rules for keeping values out of logs and responses. ## Usage Patterns @@ -27845,6 +27961,9 @@ var settings = response; // settings._saml2 is available here (editor context) var certificate = settings._saml2 && settings._saml2.idpCertificate; + +// __ keys are never returned, even to editors: +// settings.__mailProvider is undefined here ``` ### Saving Settings @@ -27892,9 +28011,15 @@ app.settings.push = { enabled: true }; // public app.settings._push = { apnsCertificate: '...' }; // editor-private // DON'T: Store secrets that should never leave the server in _ keys -// _ keys are visible to Studio editors. For truly server-only secrets, -// a future convention (__prefix) will be used. For now, use app widgets -// or environment variables for server-only secrets. +// _ keys are visible to Studio editors. + +// DO: Store server-only secrets in top-level __ keys +app.settings.__mailProvider = { apiKey: '...' }; +// __ keys are write-only over HTTP: nobody can read them back. +// Only server app actions (context.settings) and backend code read them. + +// DON'T: Read a __ key back to check or merge it — it is never returned. +// Always write the complete object for the key. // DO: Check for existence before reading settings var saml2 = app.settings.saml2 || {}; @@ -27907,9 +28032,11 @@ var url = app.settings.saml2.idpUrl; // Throws if saml2 is undefined Use `_` prefix for settings that: - Contain credentials, certificates, or keys that app users should not see -- Are only needed by Studio UI or backend processing, not by the running app +- Are only needed by Studio UI, backend processing or server app actions, not by the running app - Would be a security risk if exposed in client-side JavaScript +Use `__` prefix instead when Studio editors do not need to read the value back — for example an API key that only a server app action uses. + Examples: - `_saml2.idpCertificate` — X.509 certificate for SAML signature verification - `_push.apnsCertificate` — Apple Push Notification certificate (future) @@ -27917,6 +28044,7 @@ Examples: ## Related +- [App Actions V3](../core/app-actions-v3) — `context.settings` reference and full example - [Session JS APIs](../fliplet-session) — session management - [V3 Authentication Patterns](auth) — auth flows for V3 apps - [App Security](../../App-security) — app-level access control diff --git a/docs/.well-known/llms.txt b/docs/.well-known/llms.txt index 0b22dbd3..d244cfe4 100644 --- a/docs/.well-known/llms.txt +++ b/docs/.well-known/llms.txt @@ -147,7 +147,7 @@ - [LikeButton](https://developers.fliplet.com/API/like-buttons.md): Embed a one-tap like button on any screen element, backed by a Data Source that records likes per content ID. - [V3 app analytics and event tracking](https://developers.fliplet.com/API/v3/analytics.md): V3 app analytics and event tracking. Page views are tracked automatically by the fliplet-analytics-spa runtime; this doc covers what you get for free and when to add event() calls for intent-bearing… - [V3 app bootstrap constraints](https://developers.fliplet.com/API/v3/app-bootstrap.md): The four constraints every V3 boot HTML must satisfy. Covers Fliplet.require.lazy for dependencies, Fliplet.Media.getContents for source files, the Fliplet().then(...) init sequence, and the locked v… -- [V3 App Settings Convention](https://developers.fliplet.com/API/v3/app-settings.md): V3 app settings convention for storing public and private configuration. Covers the underscore prefix convention for editor-private settings. +- [V3 App Settings Convention](https://developers.fliplet.com/API/v3/app-settings.md): V3 app settings convention: public, private (_) and protected (__) keys, who can read each, and how server app actions read them. - [V3 Authentication Patterns](https://developers.fliplet.com/API/v3/auth.md): V3 authentication patterns for email/password login, session management, logout, and protected routes. Use these patterns when building authentication flows in V3 apps. - [V3 barcodes](https://developers.fliplet.com/API/v3/barcode.md): Scan and generate QR codes and barcodes in V3 apps with Fliplet.Barcode — attachScanner() to scan (web + native) and encode() to render barcode images. - [V3 Alpine.js apps](https://developers.fliplet.com/API/v3/frameworks/alpine.md): Constraints for building V3 apps in Alpine.js. Alpine is attribute-driven HTML with no build step, so it maps cleanly to the V3 runtime. Covers x-init timing relative to Fliplet().then and platform-c… diff --git a/docs/API/core/app-actions-v3.md b/docs/API/core/app-actions-v3.md index 48042e14..ad745adf 100644 --- a/docs/API/core/app-actions-v3.md +++ b/docs/API/core/app-actions-v3.md @@ -95,7 +95,7 @@ async function execute(context) { | Return value | Should return a value/object (available via `runWithResult`) | | Dependencies | Code using Fliplet APIs must include corresponding dependencies | -

The function must be named execute. Any other name causes a CODE_VALIDATION_FAILED error. The function does not receive any parameters beyond context — all input data is inside context.payload.

+

The function must be named execute. Any other name causes a CODE_VALIDATION_FAILED error. The function does not receive any parameters beyond context — input data is inside context.payload and, for server actions, private app settings are inside context.settings.

Do not use Handlebars {% raw %}{{ }}{% endraw %} syntax in action code. Handlebars expressions are not evaluated inside app actions and will cause unexpected behavior or errors. Use JavaScript template literals (${}) with backtick strings instead. For example: {% raw %}`Hello ${context.payload.name}`{% endraw %}

@@ -125,7 +125,14 @@ const execute = async function(context) { ### Context object -The `context` parameter is an object with a single property: `payload`. The `context` object does **not** contain any other properties — no `appId`, no `userId`, no `environment`. All input data comes through `context.payload`. +The `context` parameter is an object with exactly two properties: `payload` and `settings`. + +| Property | Type | Description | +|----------|------|-------------| +| `context.payload` | Object | Input data for this run. `{}` when no data is passed. The contents differ by trigger type — see the breakdown below. | +| `context.settings` | Object | The app's private settings (top-level app settings whose key starts with `_`). Populated **only** when the action's `environment` is `server`. Always `{}` for `client` and `any` actions. See [`context.settings`](#contextsettings-server-actions-only). | + +The `context` object does **not** contain any other properties — no `appId`, no `userId`, no `environment`. All input data comes through `context.payload`; `context.settings` carries configuration and credentials, never input data. The contents of `context.payload` differ by trigger type. See the detailed breakdown below. @@ -247,6 +254,67 @@ async function execute(context) { } ``` +#### `context.settings` (server actions only) + +`context.settings` gives a server action read access to the app's private settings, so a credential such as a third-party API key is stored on the app instead of being hardcoded in the action code. See [V3 app settings convention](../v3/app-settings) for how settings are named and saved. + +- **Server only.** Only actions with `environment: 'server'` receive values. `client` and `any` actions always get `context.settings = {}` — for `any` actions this applies both when they run on the server and when they run on the device. +- **Underscore keys only.** It contains every **top-level** app setting whose key starts with `_` — both `_private` and `__protected` keys. Public settings (no underscore) are not included. Nested keys starting with `_` are not special. +- **Master app values.** Values are always read from the **master** app, including when the published (production) app runs the action. There is no separate production copy of these values. +- **Fresh on every run.** Values are read from the database each time the action runs. A rotated value applies to the next run without republishing the app or the action. +- **100 KB limit.** If the app's underscore settings exceed 100 KB in total (serialized as JSON), **every** server action of that app fails with the error `Private app settings exceed the 100 KB limit for server actions` until the settings are reduced. +- **Read defensively.** Start with `const settings = context.settings || {};` and handle a missing value — the setting may not be configured yet. + +

Never let a setting value leave the action. Never return it, never log it (console.*), never put it in a thrown or returned error message, and never put it in a URL or query string — send it in a request header or body instead. Return values and error messages are stored in the app action logs and returned to the caller of runWithResult(); console output and every request URL are captured in the server runner logs.

+ +

Server actions run in a headless browser, so fetch from a server action is a cross-origin browser request. Provider APIs that do not allow cross-origin browser requests (CORS) reject it. Server-side HTTP requests without this restriction are not available yet.

+ +Store credentials under a `__` (protected) key. Protected keys are write-only over HTTP: nobody can read them back, not Studio editors and not API tokens. `_` (private) keys are readable by Studio editors. + +Example: read a credential, send it in a request header, return early when it is not configured, and return nothing that contains the value. + +```js +// Action environment must be 'server'. No dependencies required. +// App setting used (saved on the master app): __mailProvider = { apiKey: '...' } +async function execute(context) { + const settings = context.settings || {}; + const mailProvider = settings.__mailProvider || {}; + const apiKey = mailProvider.apiKey; + + if (!apiKey) { + // Not configured (or the action is not a server action): stop here + return { sent: false, error: 'MAIL_PROVIDER_NOT_CONFIGURED' }; + } + + let response; + + try { + response = await fetch('https://api.example.com/v1/messages', { + method: 'POST', + headers: { + // Credentials go in a header (or the body), never in the URL + Authorization: `Bearer ${apiKey}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + to: context.payload.email, + subject: 'Your booking is confirmed' + }) + }); + } catch (error) { + // Network or CORS failure. Return a fixed code — do not rethrow, + // log or return anything built from the request or the settings. + return { sent: false, error: 'MAIL_PROVIDER_REQUEST_FAILED' }; + } + + if (!response.ok) { + return { sent: false, error: 'MAIL_PROVIDER_REJECTED', status: response.status }; + } + + return { sent: true }; +} +``` + ### Example: simple action ```js diff --git a/docs/API/v3/app-settings.md b/docs/API/v3/app-settings.md index 5bf805fb..ba4ecc65 100644 --- a/docs/API/v3/app-settings.md +++ b/docs/API/v3/app-settings.md @@ -1,6 +1,6 @@ --- title: "V3 App Settings Convention" -description: V3 app settings convention for storing public and private configuration. Covers the underscore prefix convention for editor-private settings. +description: "V3 app settings convention: public, private (_) and protected (__) keys, who can read each, and how server app actions read them." type: guide tags: [js-api, v3, app-settings] v3_relevant: true @@ -9,18 +9,23 @@ deprecated: false # V3 App Settings Convention -App settings in V3 use `app.settings` to store configuration for features like authentication, push notifications, and analytics. This page describes the naming convention that controls which settings are visible to the app runtime (preview and published apps) vs only to Studio editors. +App settings in V3 use `app.settings` to store configuration for features like authentication, push notifications, and analytics. This page describes the naming convention that controls which settings are visible to the app runtime (preview and published apps), to Studio editors, or to server-side code only. ## The Underscore Convention +The prefix of a **top-level** settings key sets its tier: + +| Key pattern | Tier | Who can read it | Example | +|---|---|---|---| +| `settingName` | Public | App runtime + Studio + backend | `app.settings.saml2` (IdP URL, attribute mappings) | +| `_settingName` | Private | Studio editors + backend + server app actions. Never sent to the app runtime, preview or bundles. | `app.settings._saml2` (IdP certificate) | +| `__settingName` | Protected | Backend + server app actions only. Write-only over HTTP: nobody can read it back, not Studio editors and not API tokens. | `app.settings.__mailProvider` (third-party API key) | + Settings keys that start with `_` are **editor-private**. They are visible to Studio editors managing the app but are NOT included in the app runtime (preview iframe, published apps, bundled apps). -| Key pattern | Who can read it | Example | -|---|---|---| -| `settingName` | App runtime + Studio + backend | `app.settings.saml2` (IdP URL, attribute mappings) | -| `_settingName` | Studio editors + backend only | `app.settings._saml2` (IdP certificate) | +Settings keys that start with `__` are **protected**. See [Protected settings](#protected-settings). -The convention uses **top-level key namespacing**. All private settings for a feature go under a single `_feature` key, not mixed into the public feature key. +The convention uses **top-level key namespacing**. All private settings for a feature go under a single `_feature` (or `__feature`) key, not mixed into the public feature key. ## How It Works @@ -35,11 +40,16 @@ app.settings = { // Editor-private settings — visible to Studio editors, NOT to the running app _saml2: { idpCertificate: '-----BEGIN CERTIFICATE-----\nMIID...' + }, + + // Protected settings — NOT readable over HTTP by anyone, NOT in the running app + __mailProvider: { + apiKey: 'sk_live_...' } } ``` -When the app loads in the preview iframe or as a published app, `_saml2` is stripped from `window.ENV.appSettings`. The running app only sees: +When the app loads in the preview iframe or as a published app, every top-level key starting with `_` (`_saml2` and `__mailProvider` here) is stripped from `window.ENV.appSettings`. The running app only sees: ```js window.ENV.appSettings = { @@ -47,11 +57,49 @@ window.ENV.appSettings = { idpUrl: 'https://idp.company.com/sso', attributeMappings: { email: 'Email', name: 'Name' } } - // _saml2 is NOT here + // _saml2 and __mailProvider are NOT here } ``` -Backend code (server-side passports, hooks, app actions) reads the full `app.settings` from the model — no filtering is applied server-side. +Backend code that runs inside the Fliplet API (server-side passports, hooks) reads the full `app.settings` from the model — no filtering is applied there. + +App action code does **not** run inside the API and never reads the model. A V3 app action with `environment: 'server'` receives the underscore keys as `context.settings` — see [Reading settings in a server action](#reading-settings-in-a-server-action). `client` and `any` actions get no private settings; on the device they only see what the app runtime sees. + +## Protected settings + +Use a `__` key for a secret that must never be read back, such as a third-party API key used by a server action. + +- Every HTTP read of app settings omits top-level keys starting with `__`. This applies to Studio editors, admins, API tokens and app action (task) tokens. +- Studio editors can write and delete `__` keys, but can never read them back. To change a value, overwrite it. +- App action (task) tokens can not write or delete `__` keys — the request fails with `403`. +- V3 app version snapshots do not store `__` keys. Restoring an app version keeps the app's current `__` values. +- Like `_` keys, `__` keys are never sent to the app runtime, preview or bundles. +- Server app actions receive `__` keys in `context.settings`, together with `_` keys. + +## Reading settings in a server action + +A V3 app action with `environment: 'server'` receives every top-level app setting whose key starts with `_` (both `_private` and `__protected`) as `context.settings`: + +```js +async function execute(context) { + const settings = context.settings || {}; + const mailProvider = settings.__mailProvider || {}; + + if (!mailProvider.apiKey) { + return { sent: false, error: 'MAIL_PROVIDER_NOT_CONFIGURED' }; + } + + // Send mailProvider.apiKey in a request header or body. + // Never return it, log it, throw it or put it in a URL. + return { configured: true }; +} +``` + +- Values always come from the **master** app and are read fresh on every run. Published apps read the master's values; a rotated value applies to the next run without republishing. +- `client` and `any` actions always get `context.settings = {}`. +- If the underscore settings exceed 100 KB in total, every server action of the app fails until they are reduced. + +See [`context.settings` in App Actions V3](../core/app-actions-v3#contextsettings-server-actions-only) for the complete example, the limits and the rules for keeping values out of logs and responses. ## Usage Patterns @@ -76,6 +124,9 @@ var settings = response; // settings._saml2 is available here (editor context) var certificate = settings._saml2 && settings._saml2.idpCertificate; + +// __ keys are never returned, even to editors: +// settings.__mailProvider is undefined here ``` ### Saving Settings @@ -123,9 +174,15 @@ app.settings.push = { enabled: true }; // public app.settings._push = { apnsCertificate: '...' }; // editor-private // DON'T: Store secrets that should never leave the server in _ keys -// _ keys are visible to Studio editors. For truly server-only secrets, -// a future convention (__prefix) will be used. For now, use app widgets -// or environment variables for server-only secrets. +// _ keys are visible to Studio editors. + +// DO: Store server-only secrets in top-level __ keys +app.settings.__mailProvider = { apiKey: '...' }; +// __ keys are write-only over HTTP: nobody can read them back. +// Only server app actions (context.settings) and backend code read them. + +// DON'T: Read a __ key back to check or merge it — it is never returned. +// Always write the complete object for the key. // DO: Check for existence before reading settings var saml2 = app.settings.saml2 || {}; @@ -138,9 +195,11 @@ var url = app.settings.saml2.idpUrl; // Throws if saml2 is undefined Use `_` prefix for settings that: - Contain credentials, certificates, or keys that app users should not see -- Are only needed by Studio UI or backend processing, not by the running app +- Are only needed by Studio UI, backend processing or server app actions, not by the running app - Would be a security risk if exposed in client-side JavaScript +Use `__` prefix instead when Studio editors do not need to read the value back — for example an API key that only a server app action uses. + Examples: - `_saml2.idpCertificate` — X.509 certificate for SAML signature verification - `_push.apnsCertificate` — Apple Push Notification certificate (future) @@ -148,6 +207,7 @@ Examples: ## Related +- [App Actions V3](../core/app-actions-v3) — `context.settings` reference and full example - [Session JS APIs](../fliplet-session) — session management - [V3 Authentication Patterns](auth) — auth flows for V3 apps - [App Security](../../App-security) — app-level access control From 308c7589d353042161c06b6a928faba1b3c37ca1 Mon Sep 17 00:00:00 2001 From: Arpan Jokhakar Date: Mon, 21 Sep 2026 16:18:33 +0530 Subject: [PATCH 2/4] DEV-2032: document the AI Builder _aiartifact_ credential path for context.settings Present both credential sources (AI Builder secure panel field saved as _aiartifact_, private tier; REST-written __ protected key), switch the complete example to the _aiartifact_ form, and state the query-string-only provider limit. Regenerated llms-full.txt. Co-Authored-By: Claude Fable 5.1 --- docs/.well-known/llms-full.txt | 31 ++++++++++++++++++++++--------- docs/API/core/app-actions-v3.md | 15 ++++++++++----- docs/API/v3/app-settings.md | 16 ++++++++++++---- 3 files changed, 44 insertions(+), 18 deletions(-) diff --git a/docs/.well-known/llms-full.txt b/docs/.well-known/llms-full.txt index cba3e41f..21600e33 100644 --- a/docs/.well-known/llms-full.txt +++ b/docs/.well-known/llms-full.txt @@ -6715,21 +6715,26 @@ async function execute(context) { - **100 KB limit.** If the app's underscore settings exceed 100 KB in total (serialized as JSON), **every** server action of that app fails with the error `Private app settings exceed the 100 KB limit for server actions` until the settings are reduced. - **Read defensively.** Start with `const settings = context.settings || {};` and handle a missing value — the setting may not be configured yet. -

Never let a setting value leave the action. Never return it, never log it (console.*), never put it in a thrown or returned error message, and never put it in a URL or query string — send it in a request header or body instead. Return values and error messages are stored in the app action logs and returned to the caller of runWithResult(); console output and every request URL are captured in the server runner logs.

+

Never let a setting value leave the action. Never return it, never log it (console.*), never put it in a thrown or returned error message, and never put it in a URL or query string — send it in a request header or body instead. Return values and error messages are stored in the app action logs and returned to the caller of runWithResult(); console output and every request URL are captured in the server runner logs. A provider that only accepts its key in the URL query string can not be called without breaking this rule.

Server actions run in a headless browser, so fetch from a server action is a cross-origin browser request. Provider APIs that do not allow cross-origin browser requests (CORS) reject it. Server-side HTTP requests without this restriction are not available yet.

-Store credentials under a `__` (protected) key. Protected keys are write-only over HTTP: nobody can read them back, not Studio editors and not API tokens. `_` (private) keys are readable by Studio editors. +A credential reaches `context.settings` from one of two sources: + +- **Fliplet AI Builder secure panel field.** A panel field of type `secure` with a `secure` destination is saved by Studio as the app setting `_aiartifact_` and read as `context.settings._aiartifact_`. For example, `settingKey: 'mailApiKey'` is read as `context.settings._aiartifact_mailApiKey`. The setting holds the value the user entered in the field. These are `_` (private) keys: Studio editors of the app can read them over the API. +- **Setting written through the REST API.** A developer saving app settings directly can use any `_` (private) key, or a `__` (protected) key such as `__mailProvider`. + +Protected (`__`) keys are write-only over HTTP: nobody can read them back, not Studio editors and not API tokens. They are read the same way, for example `const apiKey = (settings.__mailProvider || {}).apiKey;`. Example: read a credential, send it in a request header, return early when it is not configured, and return nothing that contains the value. ```js // Action environment must be 'server'. No dependencies required. -// App setting used (saved on the master app): __mailProvider = { apiKey: '...' } +// App setting used (saved on the master app by an AI Builder secure panel +// field with settingKey 'mailApiKey'): _aiartifact_mailApiKey = '...' async function execute(context) { const settings = context.settings || {}; - const mailProvider = settings.__mailProvider || {}; - const apiKey = mailProvider.apiKey; + const apiKey = settings._aiartifact_mailApiKey; if (!apiKey) { // Not configured (or the action is not a server action): stop here @@ -27920,18 +27925,24 @@ A V3 app action with `environment: 'server'` receives every top-level app settin ```js async function execute(context) { const settings = context.settings || {}; - const mailProvider = settings.__mailProvider || {}; - if (!mailProvider.apiKey) { + // Saved by a Fliplet AI Builder secure panel field (settingKey: 'mailApiKey') + const apiKey = settings._aiartifact_mailApiKey; + + // Or, for a protected key saved through the REST API: + // const apiKey = (settings.__mailProvider || {}).apiKey; + + if (!apiKey) { return { sent: false, error: 'MAIL_PROVIDER_NOT_CONFIGURED' }; } - // Send mailProvider.apiKey in a request header or body. + // Send apiKey in a request header or body. // Never return it, log it, throw it or put it in a URL. return { configured: true }; } ``` +- A Fliplet AI Builder panel field of type `secure` with a `secure` destination is saved by Studio as `_aiartifact_` and read as `context.settings._aiartifact_`. These are `_` (private) keys: Studio editors of the app can read them over the API. - Values always come from the **master** app and are read fresh on every run. Published apps read the master's values; a rotated value applies to the next run without republishing. - `client` and `any` actions always get `context.settings = {}`. - If the underscore settings exceed 100 KB in total, every server action of the app fails until they are reduced. @@ -28035,7 +28046,9 @@ Use `_` prefix for settings that: - Are only needed by Studio UI, backend processing or server app actions, not by the running app - Would be a security risk if exposed in client-side JavaScript -Use `__` prefix instead when Studio editors do not need to read the value back — for example an API key that only a server app action uses. +Credentials collected by a Fliplet AI Builder secure panel field are always saved as `_aiartifact_` keys. They are private, not protected: Studio editors of the app can read them over the API, and server app actions read them via `context.settings`. + +When saving settings through the REST API, use `__` prefix instead when Studio editors do not need to read the value back — for example an API key that only a server app action uses. Examples: - `_saml2.idpCertificate` — X.509 certificate for SAML signature verification diff --git a/docs/API/core/app-actions-v3.md b/docs/API/core/app-actions-v3.md index ad745adf..5dd0572d 100644 --- a/docs/API/core/app-actions-v3.md +++ b/docs/API/core/app-actions-v3.md @@ -265,21 +265,26 @@ async function execute(context) { - **100 KB limit.** If the app's underscore settings exceed 100 KB in total (serialized as JSON), **every** server action of that app fails with the error `Private app settings exceed the 100 KB limit for server actions` until the settings are reduced. - **Read defensively.** Start with `const settings = context.settings || {};` and handle a missing value — the setting may not be configured yet. -

Never let a setting value leave the action. Never return it, never log it (console.*), never put it in a thrown or returned error message, and never put it in a URL or query string — send it in a request header or body instead. Return values and error messages are stored in the app action logs and returned to the caller of runWithResult(); console output and every request URL are captured in the server runner logs.

+

Never let a setting value leave the action. Never return it, never log it (console.*), never put it in a thrown or returned error message, and never put it in a URL or query string — send it in a request header or body instead. Return values and error messages are stored in the app action logs and returned to the caller of runWithResult(); console output and every request URL are captured in the server runner logs. A provider that only accepts its key in the URL query string can not be called without breaking this rule.

Server actions run in a headless browser, so fetch from a server action is a cross-origin browser request. Provider APIs that do not allow cross-origin browser requests (CORS) reject it. Server-side HTTP requests without this restriction are not available yet.

-Store credentials under a `__` (protected) key. Protected keys are write-only over HTTP: nobody can read them back, not Studio editors and not API tokens. `_` (private) keys are readable by Studio editors. +A credential reaches `context.settings` from one of two sources: + +- **Fliplet AI Builder secure panel field.** A panel field of type `secure` with a `secure` destination is saved by Studio as the app setting `_aiartifact_` and read as `context.settings._aiartifact_`. For example, `settingKey: 'mailApiKey'` is read as `context.settings._aiartifact_mailApiKey`. The setting holds the value the user entered in the field. These are `_` (private) keys: Studio editors of the app can read them over the API. +- **Setting written through the REST API.** A developer saving app settings directly can use any `_` (private) key, or a `__` (protected) key such as `__mailProvider`. + +Protected (`__`) keys are write-only over HTTP: nobody can read them back, not Studio editors and not API tokens. They are read the same way, for example `const apiKey = (settings.__mailProvider || {}).apiKey;`. Example: read a credential, send it in a request header, return early when it is not configured, and return nothing that contains the value. ```js // Action environment must be 'server'. No dependencies required. -// App setting used (saved on the master app): __mailProvider = { apiKey: '...' } +// App setting used (saved on the master app by an AI Builder secure panel +// field with settingKey 'mailApiKey'): _aiartifact_mailApiKey = '...' async function execute(context) { const settings = context.settings || {}; - const mailProvider = settings.__mailProvider || {}; - const apiKey = mailProvider.apiKey; + const apiKey = settings._aiartifact_mailApiKey; if (!apiKey) { // Not configured (or the action is not a server action): stop here diff --git a/docs/API/v3/app-settings.md b/docs/API/v3/app-settings.md index ba4ecc65..d5a6d9ae 100644 --- a/docs/API/v3/app-settings.md +++ b/docs/API/v3/app-settings.md @@ -83,18 +83,24 @@ A V3 app action with `environment: 'server'` receives every top-level app settin ```js async function execute(context) { const settings = context.settings || {}; - const mailProvider = settings.__mailProvider || {}; - if (!mailProvider.apiKey) { + // Saved by a Fliplet AI Builder secure panel field (settingKey: 'mailApiKey') + const apiKey = settings._aiartifact_mailApiKey; + + // Or, for a protected key saved through the REST API: + // const apiKey = (settings.__mailProvider || {}).apiKey; + + if (!apiKey) { return { sent: false, error: 'MAIL_PROVIDER_NOT_CONFIGURED' }; } - // Send mailProvider.apiKey in a request header or body. + // Send apiKey in a request header or body. // Never return it, log it, throw it or put it in a URL. return { configured: true }; } ``` +- A Fliplet AI Builder panel field of type `secure` with a `secure` destination is saved by Studio as `_aiartifact_` and read as `context.settings._aiartifact_`. These are `_` (private) keys: Studio editors of the app can read them over the API. - Values always come from the **master** app and are read fresh on every run. Published apps read the master's values; a rotated value applies to the next run without republishing. - `client` and `any` actions always get `context.settings = {}`. - If the underscore settings exceed 100 KB in total, every server action of the app fails until they are reduced. @@ -198,7 +204,9 @@ Use `_` prefix for settings that: - Are only needed by Studio UI, backend processing or server app actions, not by the running app - Would be a security risk if exposed in client-side JavaScript -Use `__` prefix instead when Studio editors do not need to read the value back — for example an API key that only a server app action uses. +Credentials collected by a Fliplet AI Builder secure panel field are always saved as `_aiartifact_` keys. They are private, not protected: Studio editors of the app can read them over the API, and server app actions read them via `context.settings`. + +When saving settings through the REST API, use `__` prefix instead when Studio editors do not need to read the value back — for example an API key that only a server app action uses. Examples: - `_saml2.idpCertificate` — X.509 certificate for SAML signature verification From fa926f76aec34694b19121afbda71eae92d3acec Mon Sep 17 00:00:00 2001 From: Arpan Jokhakar Date: Mon, 21 Sep 2026 17:11:01 +0530 Subject: [PATCH 3/4] DEV-2032: correct the app-settings save endpoint and the _aiartifact_ key caveat Saving/removing settings uses POST and DELETE /v1/apps/:id/settings with flat keys, not PUT /v1/apps/:id (which never writes settings). Document Studio's settingKey sanitizer, scope the editor-write and snapshot claims to the endpoints and the release that they hold for, and scope the "editor-private" sentence to single-underscore keys. Co-Authored-By: Claude Opus 5 (1M context) --- docs/.well-known/llms-full.txt | 61 +++++++++++++++++++++++---------- docs/API/core/app-actions-v3.md | 4 +-- docs/API/v3/app-settings.md | 57 +++++++++++++++++++++--------- 3 files changed, 86 insertions(+), 36 deletions(-) diff --git a/docs/.well-known/llms-full.txt b/docs/.well-known/llms-full.txt index 21600e33..3ebcef02 100644 --- a/docs/.well-known/llms-full.txt +++ b/docs/.well-known/llms-full.txt @@ -6721,8 +6721,8 @@ async function execute(context) { A credential reaches `context.settings` from one of two sources: -- **Fliplet AI Builder secure panel field.** A panel field of type `secure` with a `secure` destination is saved by Studio as the app setting `_aiartifact_` and read as `context.settings._aiartifact_`. For example, `settingKey: 'mailApiKey'` is read as `context.settings._aiartifact_mailApiKey`. The setting holds the value the user entered in the field. These are `_` (private) keys: Studio editors of the app can read them over the API. -- **Setting written through the REST API.** A developer saving app settings directly can use any `_` (private) key, or a `__` (protected) key such as `__mailProvider`. +- **Fliplet AI Builder secure panel field.** A panel field of type `secure` with a `secure` destination is saved by Studio as the app setting `_aiartifact_` and read as `context.settings._aiartifact_`. For example, `settingKey: 'mailApiKey'` is read as `context.settings._aiartifact_mailApiKey`. The setting holds the value the user entered in the field. These are `_` (private) keys: Studio editors of the app can read them over the API. Use a simple alphanumeric `settingKey`: Studio replaces every character outside `a-zA-Z0-9_.-` with `_`, so a key containing a space, `@` or `/` is stored under a different name, and a key containing `-` or `.` needs bracket access, for example `settings['_aiartifact_mail-api-key']`. +- **Setting written through the REST API.** A developer saving app settings with `POST /v1/apps/:id/settings` (keys flat in the body) can use any `_` (private) key, or a `__` (protected) key such as `__mailProvider`. See [Saving settings](../v3/app-settings#saving-settings). Protected (`__`) keys are write-only over HTTP: nobody can read them back, not Studio editors and not API tokens. They are read the same way, for example `const apiKey = (settings.__mailProvider || {}).apiKey;`. @@ -27863,9 +27863,9 @@ The prefix of a **top-level** settings key sets its tier: | `_settingName` | Private | Studio editors + backend + server app actions. Never sent to the app runtime, preview or bundles. | `app.settings._saml2` (IdP certificate) | | `__settingName` | Protected | Backend + server app actions only. Write-only over HTTP: nobody can read it back, not Studio editors and not API tokens. | `app.settings.__mailProvider` (third-party API key) | -Settings keys that start with `_` are **editor-private**. They are visible to Studio editors managing the app but are NOT included in the app runtime (preview iframe, published apps, bundled apps). +Settings keys that start with a **single** `_` are **editor-private**. They are visible to Studio editors managing the app but are NOT included in the app runtime (preview iframe, published apps, bundled apps). -Settings keys that start with `__` are **protected**. See [Protected settings](#protected-settings). +Settings keys that start with `__` are **protected**: they are not included in the app runtime either, and they are not visible to Studio editors. See [Protected settings](#protected-settings). The convention uses **top-level key namespacing**. All private settings for a feature go under a single `_feature` (or `__feature`) key, not mixed into the public feature key. @@ -27912,9 +27912,10 @@ App action code does **not** run inside the API and never reads the model. A V3 Use a `__` key for a secret that must never be read back, such as a third-party API key used by a server action. - Every HTTP read of app settings omits top-level keys starting with `__`. This applies to Studio editors, admins, API tokens and app action (task) tokens. -- Studio editors can write and delete `__` keys, but can never read them back. To change a value, overwrite it. +- Studio editors can write and delete `__` keys through `POST` and `DELETE /v1/apps/:id/settings`, but can never read them back. To change a value, overwrite it. +- `PUT /v1/admin/apps/:app` ignores `__` keys in the `settings` body and keeps the app's current values. Rotating a protected key there returns `200` and changes nothing — use `POST /v1/apps/:id/settings`. - App action (task) tokens can not write or delete `__` keys — the request fails with `403`. -- V3 app version snapshots do not store `__` keys. Restoring an app version keeps the app's current `__` values. +- V3 app version snapshots taken from this release onwards do not store `__` keys, and older snapshots that still hold them never return them. Restoring an app version keeps the app's current `__` values. - Like `_` keys, `__` keys are never sent to the app runtime, preview or bundles. - Server app actions receive `__` keys in `context.settings`, together with `_` keys. @@ -27943,6 +27944,7 @@ async function execute(context) { ``` - A Fliplet AI Builder panel field of type `secure` with a `secure` destination is saved by Studio as `_aiartifact_` and read as `context.settings._aiartifact_`. These are `_` (private) keys: Studio editors of the app can read them over the API. +- Keep `settingKey` simple and alphanumeric. Studio replaces every character outside `a-zA-Z0-9_.-` with `_`, so a key containing a space, `@` or `/` is stored under a different name than you wrote. `-` and `.` survive but break dot access — a key containing either needs bracket access, for example `settings['_aiartifact_mail-api-key']`. - Values always come from the **master** app and are read fresh on every run. Published apps read the master's values; a rotated value applies to the next run without republishing. - `client` and `any` actions always get `context.settings = {}`. - If the underscore settings exceed 100 KB in total, every server action of the app fails until they are reduced. @@ -27979,24 +27981,25 @@ var certificate = settings._saml2 && settings._saml2.idpCertificate; ### Saving Settings -The `PUT /v1/apps/:id` endpoint performs a **shallow merge** on `settings`. Top-level keys in your request overwrite existing keys with the same name. Keys you don't include are preserved. Nested objects are replaced entirely, not deep-merged. +Settings are saved with `POST /v1/apps/:id/settings`. The setting keys go **flat in the request body** — they are not nested under a `settings` property. + +The endpoint performs a **shallow merge**: top-level keys in your request overwrite existing keys with the same name, keys you don't include are preserved, and nested objects are replaced entirely rather than deep-merged. ```js // Save both public and private settings together. +// The keys are top-level in `data` — NOT wrapped in { settings: {...} }. // This MERGES at the top level: saml2 and _saml2 are set/replaced, // but other top-level keys (customCSS, etc.) are preserved. await Fliplet.API.request({ - url: 'v1/apps/' + appId, - method: 'PUT', + url: 'v1/apps/' + appId + '/settings', + method: 'POST', data: { - settings: { - saml2: { - idpUrl: 'https://idp.company.com/sso', - attributeMappings: { email: 'Email', name: 'Name' } - }, - _saml2: { - idpCertificate: certificateText - } + saml2: { + idpUrl: 'https://idp.company.com/sso', + attributeMappings: { email: 'Email', name: 'Name' } + }, + _saml2: { + idpCertificate: certificateText } } }); @@ -28007,6 +28010,28 @@ await Fliplet.API.request({ // Always send the complete object for each top-level key. ``` +

PUT /v1/apps/:id does not write app settings. It only updates name, startingPageId, hooks, isTemplate, dependencies and icon. Sending settings to it returns 200 and stores nothing, so a server action reading the value afterwards sees it as not configured, with no error anywhere.

+ +### Removing Settings + +Delete settings keys with `DELETE /v1/apps/:id/settings`, passing an array of top-level key names as `keys`. There is no way to delete a nested property — write the complete parent object instead. + +``` +DELETE /v1/apps/:id/settings + +{ "keys": ["_saml2", "__mailProvider"] } +``` + +```js +await Fliplet.API.request({ + url: 'v1/apps/' + appId + '/settings', + method: 'DELETE', + data: { + keys: ['_saml2', '__mailProvider'] + } +}); +``` + ## DO and DON'T ```js @@ -28046,7 +28071,7 @@ Use `_` prefix for settings that: - Are only needed by Studio UI, backend processing or server app actions, not by the running app - Would be a security risk if exposed in client-side JavaScript -Credentials collected by a Fliplet AI Builder secure panel field are always saved as `_aiartifact_` keys. They are private, not protected: Studio editors of the app can read them over the API, and server app actions read them via `context.settings`. +Credentials collected by a Fliplet AI Builder secure panel field are always saved as `_aiartifact_` keys, with every character outside `a-zA-Z0-9_.-` replaced by `_`. They are private, not protected: Studio editors of the app can read them over the API, and server app actions read them via `context.settings`. When saving settings through the REST API, use `__` prefix instead when Studio editors do not need to read the value back — for example an API key that only a server app action uses. diff --git a/docs/API/core/app-actions-v3.md b/docs/API/core/app-actions-v3.md index 5dd0572d..5063e22c 100644 --- a/docs/API/core/app-actions-v3.md +++ b/docs/API/core/app-actions-v3.md @@ -271,8 +271,8 @@ async function execute(context) { A credential reaches `context.settings` from one of two sources: -- **Fliplet AI Builder secure panel field.** A panel field of type `secure` with a `secure` destination is saved by Studio as the app setting `_aiartifact_` and read as `context.settings._aiartifact_`. For example, `settingKey: 'mailApiKey'` is read as `context.settings._aiartifact_mailApiKey`. The setting holds the value the user entered in the field. These are `_` (private) keys: Studio editors of the app can read them over the API. -- **Setting written through the REST API.** A developer saving app settings directly can use any `_` (private) key, or a `__` (protected) key such as `__mailProvider`. +- **Fliplet AI Builder secure panel field.** A panel field of type `secure` with a `secure` destination is saved by Studio as the app setting `_aiartifact_` and read as `context.settings._aiartifact_`. For example, `settingKey: 'mailApiKey'` is read as `context.settings._aiartifact_mailApiKey`. The setting holds the value the user entered in the field. These are `_` (private) keys: Studio editors of the app can read them over the API. Use a simple alphanumeric `settingKey`: Studio replaces every character outside `a-zA-Z0-9_.-` with `_`, so a key containing a space, `@` or `/` is stored under a different name, and a key containing `-` or `.` needs bracket access, for example `settings['_aiartifact_mail-api-key']`. +- **Setting written through the REST API.** A developer saving app settings with `POST /v1/apps/:id/settings` (keys flat in the body) can use any `_` (private) key, or a `__` (protected) key such as `__mailProvider`. See [Saving settings](../v3/app-settings#saving-settings). Protected (`__`) keys are write-only over HTTP: nobody can read them back, not Studio editors and not API tokens. They are read the same way, for example `const apiKey = (settings.__mailProvider || {}).apiKey;`. diff --git a/docs/API/v3/app-settings.md b/docs/API/v3/app-settings.md index d5a6d9ae..cb3bfaf5 100644 --- a/docs/API/v3/app-settings.md +++ b/docs/API/v3/app-settings.md @@ -21,9 +21,9 @@ The prefix of a **top-level** settings key sets its tier: | `_settingName` | Private | Studio editors + backend + server app actions. Never sent to the app runtime, preview or bundles. | `app.settings._saml2` (IdP certificate) | | `__settingName` | Protected | Backend + server app actions only. Write-only over HTTP: nobody can read it back, not Studio editors and not API tokens. | `app.settings.__mailProvider` (third-party API key) | -Settings keys that start with `_` are **editor-private**. They are visible to Studio editors managing the app but are NOT included in the app runtime (preview iframe, published apps, bundled apps). +Settings keys that start with a **single** `_` are **editor-private**. They are visible to Studio editors managing the app but are NOT included in the app runtime (preview iframe, published apps, bundled apps). -Settings keys that start with `__` are **protected**. See [Protected settings](#protected-settings). +Settings keys that start with `__` are **protected**: they are not included in the app runtime either, and they are not visible to Studio editors. See [Protected settings](#protected-settings). The convention uses **top-level key namespacing**. All private settings for a feature go under a single `_feature` (or `__feature`) key, not mixed into the public feature key. @@ -70,9 +70,10 @@ App action code does **not** run inside the API and never reads the model. A V3 Use a `__` key for a secret that must never be read back, such as a third-party API key used by a server action. - Every HTTP read of app settings omits top-level keys starting with `__`. This applies to Studio editors, admins, API tokens and app action (task) tokens. -- Studio editors can write and delete `__` keys, but can never read them back. To change a value, overwrite it. +- Studio editors can write and delete `__` keys through `POST` and `DELETE /v1/apps/:id/settings`, but can never read them back. To change a value, overwrite it. +- `PUT /v1/admin/apps/:app` ignores `__` keys in the `settings` body and keeps the app's current values. Rotating a protected key there returns `200` and changes nothing — use `POST /v1/apps/:id/settings`. - App action (task) tokens can not write or delete `__` keys — the request fails with `403`. -- V3 app version snapshots do not store `__` keys. Restoring an app version keeps the app's current `__` values. +- V3 app version snapshots taken from this release onwards do not store `__` keys, and older snapshots that still hold them never return them. Restoring an app version keeps the app's current `__` values. - Like `_` keys, `__` keys are never sent to the app runtime, preview or bundles. - Server app actions receive `__` keys in `context.settings`, together with `_` keys. @@ -101,6 +102,7 @@ async function execute(context) { ``` - A Fliplet AI Builder panel field of type `secure` with a `secure` destination is saved by Studio as `_aiartifact_` and read as `context.settings._aiartifact_`. These are `_` (private) keys: Studio editors of the app can read them over the API. +- Keep `settingKey` simple and alphanumeric. Studio replaces every character outside `a-zA-Z0-9_.-` with `_`, so a key containing a space, `@` or `/` is stored under a different name than you wrote. `-` and `.` survive but break dot access — a key containing either needs bracket access, for example `settings['_aiartifact_mail-api-key']`. - Values always come from the **master** app and are read fresh on every run. Published apps read the master's values; a rotated value applies to the next run without republishing. - `client` and `any` actions always get `context.settings = {}`. - If the underscore settings exceed 100 KB in total, every server action of the app fails until they are reduced. @@ -137,24 +139,25 @@ var certificate = settings._saml2 && settings._saml2.idpCertificate; ### Saving Settings -The `PUT /v1/apps/:id` endpoint performs a **shallow merge** on `settings`. Top-level keys in your request overwrite existing keys with the same name. Keys you don't include are preserved. Nested objects are replaced entirely, not deep-merged. +Settings are saved with `POST /v1/apps/:id/settings`. The setting keys go **flat in the request body** — they are not nested under a `settings` property. + +The endpoint performs a **shallow merge**: top-level keys in your request overwrite existing keys with the same name, keys you don't include are preserved, and nested objects are replaced entirely rather than deep-merged. ```js // Save both public and private settings together. +// The keys are top-level in `data` — NOT wrapped in { settings: {...} }. // This MERGES at the top level: saml2 and _saml2 are set/replaced, // but other top-level keys (customCSS, etc.) are preserved. await Fliplet.API.request({ - url: 'v1/apps/' + appId, - method: 'PUT', + url: 'v1/apps/' + appId + '/settings', + method: 'POST', data: { - settings: { - saml2: { - idpUrl: 'https://idp.company.com/sso', - attributeMappings: { email: 'Email', name: 'Name' } - }, - _saml2: { - idpCertificate: certificateText - } + saml2: { + idpUrl: 'https://idp.company.com/sso', + attributeMappings: { email: 'Email', name: 'Name' } + }, + _saml2: { + idpCertificate: certificateText } } }); @@ -165,6 +168,28 @@ await Fliplet.API.request({ // Always send the complete object for each top-level key. ``` +

PUT /v1/apps/:id does not write app settings. It only updates name, startingPageId, hooks, isTemplate, dependencies and icon. Sending settings to it returns 200 and stores nothing, so a server action reading the value afterwards sees it as not configured, with no error anywhere.

+ +### Removing Settings + +Delete settings keys with `DELETE /v1/apps/:id/settings`, passing an array of top-level key names as `keys`. There is no way to delete a nested property — write the complete parent object instead. + +``` +DELETE /v1/apps/:id/settings + +{ "keys": ["_saml2", "__mailProvider"] } +``` + +```js +await Fliplet.API.request({ + url: 'v1/apps/' + appId + '/settings', + method: 'DELETE', + data: { + keys: ['_saml2', '__mailProvider'] + } +}); +``` + ## DO and DON'T ```js @@ -204,7 +229,7 @@ Use `_` prefix for settings that: - Are only needed by Studio UI, backend processing or server app actions, not by the running app - Would be a security risk if exposed in client-side JavaScript -Credentials collected by a Fliplet AI Builder secure panel field are always saved as `_aiartifact_` keys. They are private, not protected: Studio editors of the app can read them over the API, and server app actions read them via `context.settings`. +Credentials collected by a Fliplet AI Builder secure panel field are always saved as `_aiartifact_` keys, with every character outside `a-zA-Z0-9_.-` replaced by `_`. They are private, not protected: Studio editors of the app can read them over the API, and server app actions read them via `context.settings`. When saving settings through the REST API, use `__` prefix instead when Studio editors do not need to read the value back — for example an API key that only a server app action uses. From 8202adb30bd9a100ac77e1d4f9b5ef323e35811e Mon Sep 17 00:00:00 2001 From: Arpan Jokhakar Date: Mon, 21 Sep 2026 17:18:21 +0530 Subject: [PATCH 4/4] DEV-2032: scope the app-settings endpoints to the master app and complete the PUT field list POST and DELETE /v1/apps/:id/settings are masterOnly; a production app id returns 403. The PUT /v1/apps/:id warning now also names the admin-only isSystemTemplate and isHidden fields it writes. Co-Authored-By: Claude Opus 5 (1M context) --- docs/.well-known/llms-full.txt | 6 +++--- docs/API/v3/app-settings.md | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/.well-known/llms-full.txt b/docs/.well-known/llms-full.txt index 3ebcef02..5691116d 100644 --- a/docs/.well-known/llms-full.txt +++ b/docs/.well-known/llms-full.txt @@ -27981,7 +27981,7 @@ var certificate = settings._saml2 && settings._saml2.idpCertificate; ### Saving Settings -Settings are saved with `POST /v1/apps/:id/settings`. The setting keys go **flat in the request body** — they are not nested under a `settings` property. +Settings are saved with `POST /v1/apps/:id/settings`, on the **master** app id. The setting keys go **flat in the request body** — they are not nested under a `settings` property. The endpoint performs a **shallow merge**: top-level keys in your request overwrite existing keys with the same name, keys you don't include are preserved, and nested objects are replaced entirely rather than deep-merged. @@ -28010,11 +28010,11 @@ await Fliplet.API.request({ // Always send the complete object for each top-level key. ``` -

PUT /v1/apps/:id does not write app settings. It only updates name, startingPageId, hooks, isTemplate, dependencies and icon. Sending settings to it returns 200 and stores nothing, so a server action reading the value afterwards sees it as not configured, with no error anywhere.

+

PUT /v1/apps/:id does not write app settings. It only updates name, startingPageId, hooks, isTemplate, dependencies and icon, plus isSystemTemplate and isHidden for admins. Sending settings to it returns 200 and stores nothing, so a server action reading the value afterwards sees it as not configured, with no error anywhere.

### Removing Settings -Delete settings keys with `DELETE /v1/apps/:id/settings`, passing an array of top-level key names as `keys`. There is no way to delete a nested property — write the complete parent object instead. +Delete settings keys with `DELETE /v1/apps/:id/settings`, on the **master** app id, passing an array of top-level key names as `keys`. There is no way to delete a nested property — write the complete parent object instead. ``` DELETE /v1/apps/:id/settings diff --git a/docs/API/v3/app-settings.md b/docs/API/v3/app-settings.md index cb3bfaf5..2ff97436 100644 --- a/docs/API/v3/app-settings.md +++ b/docs/API/v3/app-settings.md @@ -139,7 +139,7 @@ var certificate = settings._saml2 && settings._saml2.idpCertificate; ### Saving Settings -Settings are saved with `POST /v1/apps/:id/settings`. The setting keys go **flat in the request body** — they are not nested under a `settings` property. +Settings are saved with `POST /v1/apps/:id/settings`, on the **master** app id. The setting keys go **flat in the request body** — they are not nested under a `settings` property. The endpoint performs a **shallow merge**: top-level keys in your request overwrite existing keys with the same name, keys you don't include are preserved, and nested objects are replaced entirely rather than deep-merged. @@ -168,11 +168,11 @@ await Fliplet.API.request({ // Always send the complete object for each top-level key. ``` -

PUT /v1/apps/:id does not write app settings. It only updates name, startingPageId, hooks, isTemplate, dependencies and icon. Sending settings to it returns 200 and stores nothing, so a server action reading the value afterwards sees it as not configured, with no error anywhere.

+

PUT /v1/apps/:id does not write app settings. It only updates name, startingPageId, hooks, isTemplate, dependencies and icon, plus isSystemTemplate and isHidden for admins. Sending settings to it returns 200 and stores nothing, so a server action reading the value afterwards sees it as not configured, with no error anywhere.

### Removing Settings -Delete settings keys with `DELETE /v1/apps/:id/settings`, passing an array of top-level key names as `keys`. There is no way to delete a nested property — write the complete parent object instead. +Delete settings keys with `DELETE /v1/apps/:id/settings`, on the **master** app id, passing an array of top-level key names as `keys`. There is no way to delete a nested property — write the complete parent object instead. ``` DELETE /v1/apps/:id/settings