|
| 1 | +--- |
| 2 | +date: 2026-04-02 |
| 3 | +author: Onur Solmaz <onur@textcortex.com> |
| 4 | +title: Channel Install Result Surface |
| 5 | +tags: [spritz, channel-gateway, slack, oauth, error-handling, architecture] |
| 6 | +--- |
| 7 | + |
| 8 | +## Overview |
| 9 | + |
| 10 | +Shared channel installs must always end on a Spritz-controlled result surface. |
| 11 | + |
| 12 | +In concrete terms, the browser should never fall through from a provider |
| 13 | +callback to an infrastructure-generated error page such as a CDN or proxy |
| 14 | +`502`. If the install fails for an expected product reason, Spritz should show |
| 15 | +that failure as product UI. |
| 16 | + |
| 17 | +This document defines the generic Spritz-side contract for: |
| 18 | + |
| 19 | +- install success |
| 20 | +- install failure |
| 21 | +- typed browser-facing install errors |
| 22 | +- the split between Spritz-owned UX and deployment-owned install policy |
| 23 | + |
| 24 | +The immediate motivation is Slack workspace install UX, but the same model |
| 25 | +should apply to any shared channel gateway. |
| 26 | + |
| 27 | +Related docs: |
| 28 | + |
| 29 | +- [Shared App Tenant Routing Architecture](2026-03-23-shared-app-tenant-routing-architecture.md) |
| 30 | +- [Slack Channel Gateway Implementation Plan](2026-03-24-slack-channel-gateway-implementation-plan.md) |
| 31 | +- [External Identity Resolution API Architecture](2026-03-12-external-identity-resolution-api-architecture.md) |
| 32 | + |
| 33 | +## Problem |
| 34 | + |
| 35 | +The callback flow currently has an important UX gap: |
| 36 | + |
| 37 | +1. the user starts a provider install flow |
| 38 | +2. the provider redirects back to the shared channel gateway callback |
| 39 | +3. the gateway exchanges the callback code and finalizes install state |
| 40 | +4. a deployment-owned backend returns an expected business failure |
| 41 | +5. the browser sees a generic proxy or upstream error page |
| 42 | + |
| 43 | +That is the wrong contract. |
| 44 | + |
| 45 | +Expected install failures such as unresolved identity, denied authorization, or |
| 46 | +installation conflict are not infrastructure failures. They are product |
| 47 | +outcomes and must be rendered as product UI. |
| 48 | + |
| 49 | +## Goals |
| 50 | + |
| 51 | +- always show a clear success or failure surface after install callback |
| 52 | +- keep the install result flow provider-agnostic inside Spritz |
| 53 | +- preserve typed error information without leaking backend internals |
| 54 | +- keep request correlation so operators can map a visible failure back to logs |
| 55 | + |
| 56 | +## Non-Goals |
| 57 | + |
| 58 | +- changing deployment-specific owner or account-linking policy |
| 59 | +- defining provider-specific copy inside Spritz core |
| 60 | +- moving deployment-specific installation storage into Spritz |
| 61 | +- replacing typed API errors with free-form HTML from external backends |
| 62 | + |
| 63 | +## Core Decision |
| 64 | + |
| 65 | +The provider callback is app-controlled. |
| 66 | + |
| 67 | +Here, "app-controlled" means the result is owned by the shared channel gateway |
| 68 | +or the Spritz UI, not by a reverse proxy, ingress, CDN, or provider-generated |
| 69 | +error page. |
| 70 | + |
| 71 | +The callback must always terminate in one of two Spritz-owned outcomes: |
| 72 | + |
| 73 | +- install success |
| 74 | +- install failure |
| 75 | + |
| 76 | +Recommended high-level flow: |
| 77 | + |
| 78 | +1. gateway receives the provider callback |
| 79 | +2. gateway validates callback state |
| 80 | +3. gateway exchanges the provider code |
| 81 | +4. gateway calls the deployment-owned install finalizer |
| 82 | +5. gateway normalizes the outcome into a stable Spritz result |
| 83 | +6. gateway redirects to or renders a Spritz-owned result surface |
| 84 | + |
| 85 | +## Result Surface |
| 86 | + |
| 87 | +Spritz should expose one stable install-result surface, for example: |
| 88 | + |
| 89 | +- a UI route such as `/install/result` |
| 90 | +- or a minimal HTML page rendered directly by the gateway when no richer UI is |
| 91 | + available |
| 92 | + |
| 93 | +The long-term preferred shape is a dedicated Spritz UI route, but either option |
| 94 | +is acceptable as long as the surface is app-controlled. |
| 95 | + |
| 96 | +The normalized result payload should carry at least: |
| 97 | + |
| 98 | +- `status`: `success | error` |
| 99 | +- `provider` |
| 100 | +- `code` |
| 101 | +- `requestId` |
| 102 | +- `retryable` |
| 103 | +- optional safe display metadata such as next-step hints |
| 104 | + |
| 105 | +Rules: |
| 106 | + |
| 107 | +- browser-facing copy must come from the normalized result code |
| 108 | +- raw backend payloads must not be shown directly to the user |
| 109 | +- expected failures must not render as raw `5xx` infrastructure pages |
| 110 | + |
| 111 | +## Error Taxonomy |
| 112 | + |
| 113 | +Spritz should define a stable set of install error codes. |
| 114 | + |
| 115 | +Recommended initial set: |
| 116 | + |
| 117 | +- `install_state_invalid` |
| 118 | +- `install_state_expired` |
| 119 | +- `provider_authorization_denied` |
| 120 | +- `provider_authorization_failed` |
| 121 | +- `external_identity_unresolved` |
| 122 | +- `external_identity_forbidden` |
| 123 | +- `external_identity_ambiguous` |
| 124 | +- `installation_conflict` |
| 125 | +- `installation_registry_unavailable` |
| 126 | +- `runtime_binding_unavailable` |
| 127 | +- `internal_error` |
| 128 | + |
| 129 | +Notes: |
| 130 | + |
| 131 | +- the `external_identity_*` codes should align with the typed error model from |
| 132 | + [External Identity Resolution API Architecture](2026-03-12-external-identity-resolution-api-architecture.md) |
| 133 | +- retryable upstream failures should map to typed availability codes, not a raw |
| 134 | + browser-visible `502` |
| 135 | +- unknown failures may collapse to `internal_error` |
| 136 | + |
| 137 | +## Scope Split |
| 138 | + |
| 139 | +### Spritz owns |
| 140 | + |
| 141 | +- callback termination behavior |
| 142 | +- the install result route or result page |
| 143 | +- the install error taxonomy |
| 144 | +- safe default result-page copy |
| 145 | +- request ID propagation into logs and user-visible result pages |
| 146 | + |
| 147 | +### Deployment-owned integration owns |
| 148 | + |
| 149 | +- install finalization policy |
| 150 | +- identity and account-linking policy |
| 151 | +- installation registry implementation |
| 152 | +- branding and deployment-specific copy overrides |
| 153 | +- any fallback policy involving an existing first-party browser session |
| 154 | + |
| 155 | +This keeps Spritz reusable while still letting each deployment enforce its own |
| 156 | +ownership and identity rules. |
| 157 | + |
| 158 | +## Backend And Adapter Contract |
| 159 | + |
| 160 | +The deployment-owned install finalizer should not force the gateway to infer |
| 161 | +product meaning from arbitrary upstream failures. |
| 162 | + |
| 163 | +Preferred contract: |
| 164 | + |
| 165 | +- success returns a normalized install success payload |
| 166 | +- expected failures return typed machine-readable codes |
| 167 | +- temporary failures return typed availability errors |
| 168 | + |
| 169 | +If an existing deployment backend does not yet expose the normalized shape, the |
| 170 | +deployment adapter in the gateway must translate backend-specific responses into |
| 171 | +the Spritz install-result taxonomy before the browser sees them. |
| 172 | + |
| 173 | +## User-Facing Behavior |
| 174 | + |
| 175 | +The result surface should stay simple and production-oriented: |
| 176 | + |
| 177 | +- always show success or failure explicitly |
| 178 | +- always explain the next action when one exists |
| 179 | +- always show a request ID for support and debugging |
| 180 | +- never show stack traces or raw upstream payloads |
| 181 | + |
| 182 | +Examples: |
| 183 | + |
| 184 | +- `external_identity_unresolved`: tell the user the install could not be linked |
| 185 | + to a product account yet |
| 186 | +- `install_state_expired`: tell the user the install link expired and should be |
| 187 | + started again |
| 188 | +- `installation_registry_unavailable`: tell the user the install could not be |
| 189 | + completed right now and can be retried |
| 190 | + |
| 191 | +## Validation |
| 192 | + |
| 193 | +Minimum validation for this flow: |
| 194 | + |
| 195 | +- invalid or expired state shows a controlled error surface |
| 196 | +- unresolved external identity shows a controlled error surface |
| 197 | +- temporary install-finalizer outage shows a controlled retryable error surface |
| 198 | +- successful install shows a controlled confirmation surface |
| 199 | +- every result logs the same `requestId` visible to the user |
| 200 | + |
| 201 | +## Follow-Ups |
| 202 | + |
| 203 | +- add the install-result route or gateway-rendered fallback |
| 204 | +- wire the shared Slack gateway callback to the normalized result contract |
| 205 | +- reuse the same result surface for future Discord and Teams shared installs |
0 commit comments