---
title: "Architecture philosophy"
description: "SotaAgents deliberately separates the platform control plane from the app data plane. The platform knows the user, organization, workspace, installation, selected environment, e…"
url: "https://sotaagents.ai/manual/developer-guide/architecture-philosophy"
generated_by: "sotaagents-ldp"
docs_index: "https://sotaagents.ai/manual/llms.txt"
locale: "en"
---

# Architecture philosophy

SotaAgents deliberately separates the **platform control plane** from the **app data plane**. The platform knows the user, organization, workspace, installation, selected environment, exact artifact, and granted capabilities. Your backend knows the domain: how to search documents, generate CAD, call a proprietary API, or apply business rules.

![Architecture diagram showing a request resolved through SotaAgents to an exact app environment](/manual/assets/developer/app-architecture.svg?v=2)
_One authority decision binds the request to an exact artifact, UI bundle, backend, and environment-scoped data._

### Why should an app have its own backend?

- **Independent ownership:** ship business logic on your cadence without adding domain code to the SotaAgents core.
- **Security boundary:** keep proprietary credentials and third-party integrations server-side; accept only signed platform context.
- **Data boundary:** choose the database, retention, residency, and scaling model the domain requires.
- **Operational isolation:** an app can scale, fail, and recover without coupling unrelated apps.

> [!WARNING]
> A separate backend is not a separate identity system.
>
> Do not ask the browser to send an arbitrary actor, workspace, or environment. Verify the signed Sota request and use the exact context selected by the platform.

### How the assistant calls your tool

Declaring a tool in the manifest is only half the story. This is what the platform does with that declaration at runtime — the part that makes an app more than a hosted web service.

1. #### The tool is offered to the model

   When a workspace resolves its app surface, every published tool becomes one model-callable function named `app_<appId>_<toolName>`. Development and Staging insert the environment (`app_my-app_stg_example`); Production omits it, so the name the model learns in Production stays clean. Characters the model-facing name cannot carry — including the dots in a name like `documents.query` — become underscores, names longer than 64 characters are truncated with a short hash suffix, and a collision between two apps gets a numeric suffix. The description the model reads is your manifest `description` followed by `App: <appId>. Runtime tool: <name>.`, and your `inputSchema` is handed to the model verbatim as the parameter schema. That is why a vague description or a loose schema degrades tool selection immediately: they are the entire basis on which the model decides.

2. #### The platform resolves and authorizes the call

   Before any request leaves SotaAgents, the arguments are validated against your input schema, and the platform resolves the organization, workspace, installation, environment, and the one exact artifact that applies. A tool that is not published in that artifact, or an app that is not installed for that workspace, is refused here — your backend is never reached.

3. #### A signed request reaches your backend

   The platform issues a server-to-server HTTP request to the `service.baseUrl` resolved for the active environment, at your declared path. In Development the same request travels through the `sota dev` tunnel to your laptop instead. The browser is never in this path, so your backend does not need to be reachable from the public internet by users.

4. #### Your backend verifies, works, and answers JSON

   Verify the token, do the domain work, return JSON. A non-2xx response, a body that is not valid JSON, or a timeout is treated as a tool failure.

5. #### The result re-enters the conversation

   The platform streams the tool result into the conversation as the tool's output part, records it, and — if a UI contribution declares `surface: tool-view` with this tool in `toolNames` — renders your React module in place of the plain result. The model sees a compact JSON projection of the same result.

### The request the platform sends

The body is an envelope. The model's arguments are nested at `body.input`, which is why the scaffolded route reads `request.body.body.input` rather than `request.body`.

HTTP

```
POST https://my-app.example.com/tools/example
content-type: application/json
authorization: Bearer <invocation JWT>
x-sota-core-token: <delegated Core token>   # only when the app declares Core tool grants

{
  "requestId": "01K...",
  "organizationId": "6a66...",
  "workspaceId": "6a66...",
  "appId": "my-first-project-demo",
  "hook": { "type": "tool", "name": "example" },
  "body": {
    "input":   { "message": "hello" },
    "context": { "organizationId": "…", "workspaceId": "…", "userId": "…",
                 "conversationId": "…", "agentId": "…", "toolCallId": "…", "config": {} }
  }
}
```

> [!WARNING]
> Trust the token, not the envelope.
>
> The `organizationId`, `workspaceId`, and `context` fields in the body are conveniences for logging and debugging. Every authorization decision must come from the verified JWT claims, because only those are signed.

### The invocation token

The `Authorization` header carries a short-lived, EdDSA-signed JWT minted for this one call. Your backend verifies it against the platform's public keys, published at `<core origin>/.well-known/jwks.json`. The app holds no Sota private key and never mints tokens itself.

| Property | Value |
| --- | --- |
| Algorithm / `typ` | `EdDSA` (Ed25519), header type `sota-invocation+jwt` |
| `iss` | `sota/invocation-token` |
| `aud` | Your `appId` — reject a token minted for another app |
| Lifetime | 60 seconds, with 30 seconds of accepted clock skew |
| `oid` / `wid` | Organization and workspace the call belongs to |
| `sub` | The acting user, when the call has an actor |
| `iid` | Compatibility install identity. Treat it as opaque; do not infer Development/Staging/Production from a prefix. |
| `ae` / `aei` / `ag` | Exact App Environment name, environment identity, and Backend Access Generation. The three claims arrive together and bind authorization to one resolved backend. |
| `aer` | Signed exact execution reference. Persisted/replayed work must keep this reference instead of resolving a newer artifact. |
| `scp` | Granted scopes. Current families include `tool:<name>`, `route:prompt:<name>`, `route:systemPrompt:<name>`, `event:<name>`, `route:artifact:<name>`, `route:resolver:<path>`, lifecycle route scopes, and `app:http`. Require the exact scope presented for that endpoint. |
| `rid` | The `requestId` from the envelope, for correlating logs |

Enforce the narrowest scope on each route: the `/tools/example` handler should require `tool:example` and nothing broader. The scaffolded `src/backend/sota-auth.ts` shows the complete check — issuer, audience, algorithm, type, expiry, tenant claims, and scope — in one reusable middleware.

### What the response has to look like

Return a normal JSON HTTP response. There is no Sota-specific wrapper to construct.

| Outcome | What the platform does |
| --- | --- |
| 2xx with a JSON body | Success. The body becomes the tool result. |
| Non-2xx | Failure. If the body is `{ "code": "…", "message": "…" }` — optionally with `details` and `hint` — those exact values are carried through instead of a generic message. Return a real status code rather than a success-shaped error body. |
| Body is not valid JSON | Failure with `VALIDATION_ERROR`. |
| No answer within the deadline | Failure with `TIMEOUT`. |

`outputSchema` is not enforced against the live response — it is checked when you publish, where it drives breaking-change detection between versions. Validate your own output at the boundary; the schema is the contract you promise consumers and renderers, not a runtime guard.

Two optional reserved keys let one result serve two audiences. Put the compact conclusion the model needs and the rich payload your renderer needs in the same response, then use `_sota.modelOutput` to give the model a completely different projection, or `_sota.modelProjection.omitKeys` to drop renderer-only keys from what the model reads. The model-visible projection is truncated at roughly 32,000 characters, so keep it to conclusions, ids, counts, and citations rather than raw documents.
