A standalone App is a full-page experience that is not tied to any Agent or chat. You declare one by returning standalone: true (and a stable id) from your App's /meta route.
Where it renders
SAALT Core renders a standalone App's user surface at the top-level URL /app/{id}/user, outside any Space, Agent, or chat context, proxying to your App's own /app/{id}/user route. An optional admin/config surface is embedded through the Admin → Apps "Configure" dialog — shown when your meta sets hasAdminChatPage.
Bridge scope
The token forwarded to a standalone App is standalone-scoped: it carries no Agent ids. Create the bridge without an agentId:
Code
Because there is no Agent in scope, the agent-only namespaces (knowledge.*, conversation.findFirst, agent.getAgent, llm.getAvailableModels) throw on a standalone bridge, while the scope-agnostic ones (data.config, data.pluginData, files) work — scoped per-App, not per-Agent. See Agent-scoped vs standalone bridges for the full comparison.
Data
Standalone App data is stored per-App (Agent-optional), so pluginData and config persist without an Agent.
Access control
Standalone Apps are not gated by per-Agent or per-Space availability. Access is granted per-App through permission groups (an "App Permissions" / "Use App" scope). A user sees the sidebar Apps entry and the /apps gallery only for the Apps they have been granted.
Session-gated paths
Core decides per request whether a path under /app/<id>/ needs a signed-in SAALT user:
- Paths that start with
/admin,/user,/knowledgeor/standalonerequire a SAALT session, and the user must be allowed to use the App. Without a session the request is answered with401; without permission, with404for a standalone App (so its existence is not revealed) or403for an agent-scoped one. The match is a plain, case-insensitive prefix:/user.data,/user-syncand/administrationare all gated. - Every other path is passed through without a login or permission check. If the caller happens to be signed in, the token still carries their
userId— even when they are not allowed to use the App — and for a standalone App that token has full access to the App's data, files and model calls. AuserIdtherefore proves a login, not a permission. Your App must authenticate these requests itself — an API key, an OAuthstatenonce, a webhook signature. Keep machine-to-machine routes such as inbound webhooks and third-party APIs here, outside the four prefixes, or they become unreachable for callers without a SAALT session. - A path containing an ASCII control character, a backslash, a malformed escape or a
..segment is rejected with400before it reaches your App.
Static assets (/remoteEntry.js, /assets/*) are always public.
What this means for your App's routes:
- Put everything your own UI calls under
/user/…— loaders, actions, fetchers, form targets, for example/app/<id>/user/api/progress. Admin-only calls go under/admin/…. Only there does Core check the login and the App permission. - Use
/app/<id>/api/…only for routes that authenticate themselves. A fetcher pointed there works in the browser but is equally reachable by anyone else. - Reject requests without a
userIdon UI routes with401, as defence in depth. TheuserIdis in thesaalt_session_token(read it withdecodeToken); a request without a session has none. - Keep loaders and actions off the bare
/userand/adminroutes if your App must run on Core v1.4.1–v1.4.7. React Router's single-fetch URLs for them (/user.data,/admin.data) were not gated in those releases; v1.4.8 closed that. Put the data on a child route such as/user/overviewor/admin/settingsinstead.
The App permission check itself exists since Core v1.4.0; earlier releases only checked the login.
Headers
Requests to your App carry the same forwarded headers as agent-scoped views:
saalt_session_tokensaalt_server_urlsaalt_base_pathsaalt_instance_url
saalt_server_url is where the bridge reaches Core, which can be an internal address. saalt_instance_url is the public address of the SAALT instance — use it for anything a person will open, such as a link in an email or a generated document. Tool, cron and knowledge requests from Core carry it as well. Available since Core v1.4.6.