# Ethyx: Lock-in Is an Architecture Problem

How Ethyx keeps chat portable across providers with a queued turn pipeline and DB-backed SSE—not a single vendor socket.


**Published:** 2026-08-19
**Tags:** Ethyx, Architecture, AI


First-party AI apps are ecosystems. Claude Code, ChatGPT, Gemini—each one owns your history, your habits, and often treats your prompts as training fuel. Switching means relearning the product. Your threads don't move. Your muscle memory doesn't either.

I've been building [Ethyx](https://ethyx.ai) to invert that. Ethyx is a vendor-agnostic AI client: one workspace, many models. Your tools and history stay put when you switch providers. Pick a model at prompt time; if a vendor is down or wrong for the task, change providers without migrating your life.

This post is the first in a series. I'm focusing on one thesis: **lock-in is an architecture problem.** Ethyx's answer is a queued, provider-agnostic chat turn—HTTP persists work, a worker talks to whatever adapter the catalog picks, and the browser streams progress from Ethyx's own store—not from a single vendor's socket.

## The problem

Portable work and vendor silos don't mix. When your conversation history, search tools, and project context live inside one company's product, "trying another model" means starting over somewhere else. Outages and pricing changes hit harder for the same reason: the UI and the upstream provider are the same product.

Vendors have incentives around retention and training data. That's fine—it's their business. Ethyx's product is the client. We encrypt chat at rest; the server decrypts to call providers. We are not end-to-end encrypted, and we don't claim zero data retention. Upstream providers still see your prompts for inference. I'll write more honestly about that surface later; for this post, the architecture is the point.

## Ethyx's stance

Ethyx is a web AI chat client (Laravel + Inertia/React): streaming text chat, history, projects, search tools, usage, and subscription access. Marketing taglines you'll see—"The AI client that fits your life," "One Platform, Every Model"—point at the same idea: stay in one workspace while the model behind a turn changes.

Access is gated by `hasAiAccess()` on operator-managed keys (`ETHYX_APP_*`). This is a subscription / granted-access product, not a free proxy and not user-supplied keys.

Vendor-agnosticism is a shared turn pipeline plus a `ChatProviderAdapter` per provider. `ChatModelCatalog` lists models; optional `model=auto` picks a concrete model per turn. Same conversation, same skills surface, same history—only the adapter and model id change.

## Architecture: one chat turn

The design idea is simple: **decouple provider HTTP from client SSE.** The queue worker owns the upstream stream and writes progress to the database. The browser tails Ethyx via SSE. That keeps the UI provider-blind and lets turns survive reconnects.

### Lifecycle

1. **Enqueue** — `POST …/chat/turns` hits `StoreChatTurnController`, which calls `PersistedChatTurnService::enqueueTurn`. That persists encrypted user and pending assistant messages, then dispatches `ProcessChatTurnJob` on the `chat` queue. Auto model resolution happens in request validation (`StreamChatRequest` / `ChatModelAutoSelector`) before the job is queued.

2. **Worker** — The job runs `processQueuedTurn()`, optionally through `ChatPromptRouter` (rewrite outbound prompt only—stored user text stays raw), then into `ChatStreamManager::streamMessages`.

3. **Provider** — `ChatApiKeyResolver` and `ProviderRouter` select a concrete `ChatProviderAdapter`: Anthropic, Gemini, OpenAI Completions/Responses, or OpenAI-compatible providers (xAI, DeepSeek, and so on).

4. **Tools (optional)** — Mid-stream rounds via `ChatToolDispatcher` / `ChatToolRoundExecutor` (web search, Wikipedia, parallel research in Expert, etc.).

5. **Progress** — `DatabaseTurnProgressWriter` updates message stream state and revision in our store.

6. **Client** — `GET …/turns/{id}/stream` hits `StreamChatTurnController`, which SSE-streams from the database via `TurnProgressReader`—not from the vendor connection.

### Happy path (and optional tool round)

```mermaid
flowchart TD
  clientPost[Client_POST_turn] --> store[StoreChatTurnController]
  store --> enqueue[PersistedChatTurnService_enqueueTurn]
  enqueue --> persist[Persist_encrypted_messages]
  persist --> job[ProcessChatTurnJob]
  clientSse[Client_SSE] --> streamCtrl[StreamChatTurnController]
  streamCtrl --> reader[TurnProgressReader]
  reader --> sse[SSE_to_browser]
  job --> process[processQueuedTurn]
  process --> writer[DatabaseTurnProgressWriter]
  process --> router[ChatPromptRouter_optional]
  router --> mgr[ChatStreamManager]
  mgr --> keys[ChatApiKeyResolver]
  mgr --> provRouter[ProviderRouter]
  provRouter --> adapter[ChatProviderAdapter]
  adapter --> upstream[Upstream_provider_API]
  mgr --> tools{Tool_calls}
  tools -->|yes| exec[ChatToolRoundExecutor]
  exec --> mgr
  tools -->|no| done[writer_complete]
  writer --> db[(messages_stream_progress)]
  done --> db
  db --> reader
```

Provider streaming dies with the worker's HTTP client to that vendor. The browser never held that socket. If you refresh mid-turn, you reconnect to Ethyx's progress store and catch up from the latest revision.

### Why this fights lock-in

```mermaid
flowchart LR
  ui[Ethyx_UI_history_tools] --> turn[Shared_turn_pipeline]
  turn --> catalog[ChatModelCatalog]
  catalog --> a[Anthropic_adapter]
  catalog --> b[OpenAI_adapter]
  catalog --> c[Gemini_adapter]
  catalog --> d[Other_OpenAI_compatible]
  a --> vendors[Many_upstream_APIs]
  b --> vendors
  c --> vendors
  d --> vendors
```

Same conversation, skills, and history. Only the adapter and model id change. That's the whole point of treating the turn as Ethyx's unit of work instead of a vendor chat session.

### Mid-turn details (kept short)

A few pieces sit on the same spine; each deserves its own post later:

- **Prompt router:** a nano rewrite for the provider copy; stored user text stays raw.
- **Tools:** mode-gated search/knowledge tools inside the same turn loop.
- **Parallel research:** Expert tool fans out concurrent tracks; the UI gets whole-state `research` events.
- **Think harder:** escalate a non-Expert turn to Expert without leaving the thread.
- **Auto model:** `model=auto` resolves to a concrete catalog id from intent buckets before enqueue.

## What's next

This post is the turn spine—the thing everything else hangs on. Later I'll cover Auto routing, tools and research modes, and a clearer privacy/encryption write-up that matches what we say on [ethyx.ai](https://ethyx.ai): encrypted at rest, honest about what leaves the server for inference.

If you're curious about the product itself, Ethyx is in closed testing at [ethyx.ai](https://ethyx.ai).

