A Practical Architecture for Connected Applications

The subject of this series, Corsair, is an application-owned integration boundary for provider APIs, synchronized data, credentials, events, and AI tools. The final question is not whether Corsair can connect an app to GitHub, Slack, Gmail, or another service; it is when its common runtime creates real leverage and when application code must still own the difficult decisions. A defensible architecture follows identity, state, and authority from the user’s intent to the provider and back again.

The architecture is a set of ownership loops

A connected application has more than one direction of travel. A user may cause a live operation to leave the application. A provider may send an event back. A dashboard may read a local projection without contacting the provider. An agent may choose an operation at runtime. A workflow may turn one event into several actions. Corsair’s value is that these paths can share one plugin contract, one database boundary, one credential lifecycle, and one tenant scope instead of becoming unrelated feature implementations.

flowchart LR Identity[Authenticated application identity] --> Scope[Tenant-scoped Corsair client] Intent[User or system intent] --> Choice{Who chooses the operation?} Choice -->|Known operation| Direct[Use api or db] Choice -->|Open-ended request| Tools[Agent discovery and policy] Event[Provider event] --> Inbound[Verify and reconcile webhook] Direct --> Runtime[Corsair runtime and plugin contract] Tools --> Runtime Inbound --> Runtime Scope --> Runtime Controls[KEK, signing secret, host auth, idempotency] --> Runtime Runtime --> Providers[External providers] Runtime --> DB[(Application database)] DB --> App[Dashboard and knowledge reads] Runtime --> Reactions[Hooks, queues, and workflows] Hub[Hub or manual public surfaces] --> Connect[Connect and approval delivery] Connect --> Scope Limits[Provider semantics and delivery limits] -. constrain .-> Runtime

The center of the model is not a universal provider API. It is the runtime that carries context across interfaces. A plugin preserves a provider’s operations, schemas, authentication rules, event matchers, entity shapes, and risk metadata. Corsair’s core binds those definitions into a client and coordinates the shared lifecycle around them: account lookup, key construction, hooks, retries, connection recovery, and policy. The API and entity paths provide the persistence behavior, so the application gets one outer shape without losing provider identity inside it.

That arrangement is most useful when the application owns the relationship to the connected service. Suppose a support product has a ticket and a Slack message associated with it. The application decides that a notification should be posted. The Slack plugin knows how to perform that provider operation. Corsair resolves the account credential and can persist the resulting message entity. The application stores the ticket and private notes in its own tables. Slack remains authoritative for the live message, while the local entity gives the application a stable, queryable projection.

The same action crosses several ownership boundaries. The application owns the business reason and the user’s authorization. The tenant scope identifies which connected account may act. The plugin translates the operation into Slack’s request and response semantics. The credential manager selects an encrypted account value and refreshes it if the provider uses OAuth. Corsair updates the local projection after a successful call. A later Slack event can update that projection again. None of these responsibilities can be reduced safely to “send an HTTP request.”

The API/database split expresses this loop directly. Use .api when the application must change the provider or needs a live value now. Use .db for local lists, search, detail pages, and retrieval. An API response is upserted into the shared entity projection, and a supported webhook can update the same row. That gives a dashboard a stable read path and lets application tables reference provider data without maintaining a second feature-specific cache.

The projection remains a projection. Its freshness depends on the paths that update it: a successful API call, a delivered and processed webhook, initial backfill, or explicit polling. If a provider drops an event, an entity has never been backfilled, or the provider is unavailable during recovery, local state can lag. A feature that needs a current answer before an important decision should use a live provider read or a verification policy. A feature that mainly renders a list can use local state and offer an explicit refresh. Storage location does not determine freshness by itself.

Credentials form another loop inside the same architecture. Integration-level OAuth configuration can be shared by the application, while access tokens, refresh tokens, API keys, bot tokens, and provider identifiers belong to one account and tenant. Corsair uses a per-connection or per-integration DEK to protect secret values and a KEK supplied by the application to protect the DEK. Before an API call, the plugin’s key builder asks for the correct account key. If an OAuth token is fresh, the call proceeds; if it is expired, Corsair can refresh locally or through Hub according to where the client secret lives.

A successful refresh returns the request to a usable state, but a revoked refresh token does not. Corsair’s typed authentication errors can lead to reconnect rather than endless retries. That distinction matters to product design. “Connect your account” is a user-recovery state. “The provider rate-limited the request” is a retry state. “The local database failed” is a storage state. A common runtime is valuable because it gives those failures a consistent place to surface, not because it makes all of them the same error.

Tenant scope is what keeps the loops attached to one user or workspace. withTenant() does not merely add a condition to a local query. It selects the account used for the provider call, constrains entity access, carries identity into hooks and approvals, and gives workflow or background code a scope to reconstruct. A tenant is an application-chosen identifier; the provider may use a team, organization, or installation identifier. The account relationship bridges those namespaces.

That bridge does not authenticate the caller.