Follow a Message Through NextChat
In NextChat, sending a message is a handoff across several owners, not a single browser operation. This chapter follows that handoff through a normal web deployment and an exported desktop build so readers can identify which layer handles the endpoint, credentials, request forwarding, and streamed return path. The practical result is a way to predict where a deployment decision will change the behavior of an otherwise identical conversation.
In a web deployment, the route is part of the request
The web path begins in the client, where the current session supplies the messages and the selected model configuration identifies a provider. NextChat’s provider classes share a common shape, but each class knows how to construct its provider’s URL and payload. For a normal web build, the default URL is usually an internal API path rather than the provider’s public address. The browser therefore talks to the NextChat deployment, and the deployment talks onward.
That intermediate step is not just a workaround for cross-origin restrictions. It creates a place where the server can own part of the operating policy. The request carries the selected messages, model, and generation settings into a provider-specific route. The route authenticates the request before forwarding it, chooses a configured upstream base URL, and passes the request body onward. For OpenAI-compatible traffic, the forwarding layer can also inspect the requested model and reject one disabled by the deployment’s custom-model policy. Other provider routes apply their own allowed paths and authentication rules.
The client’s headers determine how this authorization begins. A user may supply a provider key, or the client may send a deployment access code. If the server has a system key and the request did not already contain a provider key, the authentication layer can insert the server-managed key before forwarding. This gives a self-hosted operator a choice: keep the provider credential in deployment configuration, let users bring their own credential, or combine an access password with a server-managed key. The choice belongs to the deployment, not to the abstract idea of “using NextChat.”
The route also normalizes differences that are easy to miss from the browser. An Azure request uses a deployment-specific path and API version. An OpenAI request uses a different path from an image-generation request. Anthropic and Google have separate handlers and headers. The client can present a single Send action, but the web server still has to know which upstream contract it is forwarding. This is why the route is part of the request’s meaning: it decides how the client’s intent is interpreted before the provider sees it.
After forwarding, the response takes the reverse path. The upstream response body is returned rather than replaced by a new application-level result, and headers are adjusted for browser behavior and proxy buffering. For a streamed answer, the important property is preservation of the response stream. If an intermediate reverse proxy buffers that body, the provider may be streaming correctly while the user sees nothing until the response is nearly complete. The visible symptom appears in the chat window, but the cause can sit between the NextChat edge route and the browser.
A web deployment therefore has at least three distinct responsibilities. The client selects context, model, and provider. The NextChat server authenticates, applies deployment policy, and forwards the request. The upstream provider performs model processing and decides what response format it returns. Keeping those responsibilities separate helps explain both the value and the cost of the web path. The server can centralize keys and policy, but it also becomes another system whose configuration, logs, timeouts, and network route must be trusted.
The boundary is especially visible when the request is rejected. A missing or incorrect access code can stop the request before it reaches the provider. A deployment may hide user keys and require its own configured credential, or it may accept a user’s key instead. A provider route can refuse a path that is outside its allowed set, while a model policy can refuse a model that the interface still knows about. These are different kinds of failure, even though the user may see each one as an error after pressing the same button.
The server’s forwarding role also explains why a custom endpoint is more than a text-field preference. The deployment can point the request at an alternative base URL, such as a compatible gateway or a provider-specific endpoint. That changes the organization receiving the prompt and the system responsible for the response, while the browser may still display the same NextChat conversation. A route that preserves the body but changes the upstream is therefore preserving the user interface, not necessarily preserving the data relationship.
The response path has a similar division of responsibility. The upstream decides whether it can answer and which event format it emits. The forwarding route must preserve the body and expose headers that let the client recognize a stream. The client then decides how to parse the response and when to update the assistant message. If any layer changes the content type, buffers the body, or returns a plain error response, the client has to stop treating the result as a normal stream. The visible product remains one chat, but success depends on a chain of compatible contracts.
The diagram’s two branches share a provider but not a transport owner. In the web branch, the edge route sits between client and provider. In the desktop branch, the native transport occupies that position. The distinction is architectural even when both branches eventually deliver the same assistant text.
In a desktop build, the boundary moves
NextChat’s desktop application uses Tauri to wrap the client in a native shell. The client-side stream helper checks whether it is running in that environment. If so, it invokes a native command instead of using the browser’s ordinary fetch. The Rust side creates an HTTP client, sends the method, URL, headers, and body, and returns response metadata immediately. It then reads the response body as chunks and emits those chunks as window events.
The webview turns those events back into a browser-readable response. It registers a listener for the stream event, waits for the native request identifier, writes matching chunks into a TransformStream, and closes that stream when the native side emits its end signal. The higher-level chat code can therefore keep using the same response-processing contract. It does not need one message parser for browsers and another for the desktop shell merely because the transport underneath is different.
The direct path changes endpoint ownership. In the exported app, provider classes can use public provider base URLs instead of internal NextChat API routes. That means the desktop configuration can place the provider endpoint and key closer to the user’s environment. It may be useful when a user wants a standalone application or when a web deployment cannot reach a provider, but it also removes some server-side mediation. The native transport is a transport mechanism, not an identity provider, secret vault, or policy engine.
The desktop path has its own success conditions. The URL must parse, the HTTP method and headers must be accepted by the native client, the provider must be reachable, and the event bridge must associate chunks with the correct request identifier. The client also needs to close its event listener and response stream when the request is aborted or fails. A native HTTP response with a valid status is not enough if the chunks never reach the webview in the form the browser-side stream expects.
There are also differences that the shared interface deliberately hides. The browser normally applies web security and routing rules to a request. The Tauri command can follow limited redirects and send headers directly through a native HTTP library. The application’s build mode decides which path is selected, while the user’s configured provider and credentials decide where the request ultimately goes. A desktop build may reduce dependence on a server route, but it does not make the provider or the local machine irrelevant.
The native bridge is designed to preserve the shape of the browser contract, not to duplicate the whole chat system. The Rust command returns status, status text, and response headers, then emits body chunks tagged with a request identifier. The webview filters events by that identifier before writing them into its response stream. This matters when several requests exist at once: a chunk belongs to the request whose identifier accompanies it, rather than simply to whichever conversation happens to be visible.
The bridge also defines a clear termination boundary. A successful native response ends after its byte stream is exhausted; a transport error is represented as an error response and emitted payload; and an aborted client request closes the listener and readable stream. The higher-level client can then handle completion or failure in one place. This is a practical example of an interface absorbing implementation differences while leaving transport-specific conditions visible when they affect reliability.
Build mode is consequently an architectural switch. It selects not only how assets are packaged but also which request path the provider class constructs. A standalone web server, a static export, a desktop shell, and a deployment behind a proxy can expose the same screen while creating different owners for routing, credentials, connectivity, and response delivery. Reading the product without this distinction makes a deployment decision look cosmetic when it actually changes the request boundary.
The same message, different control points
Suppose two users select the same model and enter the same prompt. One runs a web deployment with a server-managed key; the other runs the desktop app with a personal key. The model may receive nearly identical content, but the control points differ. In the first case, the deployment operator can decide which models are visible or allowed, where the upstream base URL points, and how the server handles access codes. In the second, the user’s application configuration is more directly responsible for endpoint and credential selection.
This is not a simple ranking in which one path is always safer. Centralization can make policy and credential rotation easier for a person operating a shared service, but it concentrates responsibility in the server and its logs. Direct desktop requests can reduce the number of forwarding layers and suit a single user, but they put more security responsibility into the local application environment. The source shows these control locations; it does not establish a universal security outcome for every host, webview, or network.
The difference also affects troubleshooting. A failed web request may be caused by an access code, a provider key, a route allowlist, a model restriction, an upstream URL, a reverse proxy, or the provider itself. A failed desktop request removes some web-route causes but introduces native URL parsing, event delivery, and direct-connectivity conditions. Asking “Which layer owns this step?” is more useful than asking only whether the model is available.
The boundary map also explains why streaming is a system property. The client asks for streaming, the provider emits an event stream, the web route or native transport carries the bytes, and the client reconstructs a readable response. Any layer can change the observed result without changing the prompt. The next chapter focuses on the provider adapter itself, where another kind of boundary appears: the shared conversation must be translated into a model-specific dialect before it can be processed.