Skip to content

Commit 9048fbd

Browse files
authored
fix(slack-gateway): render controlled install results (#196)
* docs: define channel install result surface * fix(slack-gateway): render controlled install results * fix(slack-gateway): preserve typed install result mapping
1 parent 5751e3d commit 9048fbd

6 files changed

Lines changed: 827 additions & 27 deletions

File tree

Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
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

integrations/slack-gateway/gateway.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ func (g *slackGateway) routes() http.Handler {
7474
mux := http.NewServeMux()
7575
g.registerRoute(mux, "/healthz", g.handleHealthz)
7676
g.registerRoute(mux, "/slack/install", g.handleInstallRedirect)
77+
g.registerRoute(mux, "/slack/install/result", g.handleInstallResult)
7778
g.registerRoute(mux, "/slack/oauth/callback", g.handleOAuthCallback)
7879
g.registerRoute(mux, "/slack/events", g.handleSlackEvents)
7980
return mux

0 commit comments

Comments
 (0)