> ## Documentation Index
> Fetch the complete documentation index at: https://docs.flexslot.gg/llms.txt
> Use this file to discover all available pages before exploring further.

# Connected Apps UX

> Build the "Connected to Flexslot" experience users expect

OAuth gives you a connection. UX guidance on what to do **with** that connection — how to surface it, how to handle revocation, when to silently refresh vs prompt — is what separates a polished integration from a fragile one.

<Note>
  Two surfaces share the name "apps" — don't conflate them. **This page** is about the "Connected to Flexslot" card *you* build inside *your* product, where your end users see and disconnect their Flexslot connection. Separately, *you* (the partner) manage *your own* OAuth clients — `client_id`, secret, redirect URIs — from **Account → API access** on flexslot.gg. See [Managing OAuth Clients](/oauth/client-management) for that.
</Note>

## What good looks like

A well-built OAuth integration:

<CardGroup cols={2}>
  <Card title="Names the connection clearly" icon="signature">
    "Connected to Flexslot as **@username**", not "Token saved".
  </Card>

  <Card title="Shows status, not just a button" icon="circle-check">
    Green check when healthy, yellow warning when reconnect needed, red when broken.
  </Card>

  <Card title="Handles 401 invisibly when possible" icon="rotate">
    Silent refresh before the user notices. Prompt re-consent only when the refresh fails.
  </Card>

  <Card title="Provides a clear disconnect" icon="link-slash">
    One click. Calls `/revoke`. Confirms with the user. No leftover state.
  </Card>
</CardGroup>

## The connection card

Most apps put the Flexslot connection on a Settings → Integrations page. Here's a baseline pattern:

```
┌──────────────────────────────────────────────────────────┐
│  [Flexslot logo]   Flexslot                              │
│                                                          │
│  ✓ Connected as @george                                  │
│    Permissions: Read decks, Read sideboards              │
│    Last sync: 2 minutes ago                              │
│                                                          │
│            [Manage on Flexslot]    [Disconnect]          │
└──────────────────────────────────────────────────────────┘
```

When disconnected:

```
┌──────────────────────────────────────────────────────────┐
│  [Flexslot logo]   Flexslot                              │
│                                                          │
│  Connect your Flexslot account to sync decks and         │
│  sideboard guides.                                       │
│                                                          │
│                              [Connect Flexslot]          │
└──────────────────────────────────────────────────────────┘
```

When the grant was revoked or the refresh expired:

```
┌──────────────────────────────────────────────────────────┐
│  [Flexslot logo]   Flexslot                              │
│                                                          │
│  ⚠ Connection expired. Please reconnect.                 │
│    Your last sync was 47 days ago.                       │
│                                                          │
│                              [Reconnect Flexslot]        │
└──────────────────────────────────────────────────────────┘
```

## Handling 401 from the API

A `401` on a normal API call means **the access token failed**. Three cases, three different responses:

<Steps>
  <Step title="Token expired (most common)">
    Refresh silently. The user sees nothing. If you proactively refresh 5 minutes before expiry, this should be rare.
  </Step>

  <Step title="Refresh succeeds, retry succeeds">
    Replace stored tokens, retry the original request. The user still sees nothing.
  </Step>

  <Step title="Refresh fails with invalid_grant">
    The grant was revoked — either by the user from `/account/connected-apps`, by Flexslot for suspected token abuse, or by your own bug (refresh token replay). Mark the user as disconnected and prompt them to reconnect.
  </Step>
</Steps>

### Don't loop

If a refresh succeeds but the retried call still 401s (e.g. `FLEXSLOT_OAUTH2_TOKEN_EXPIRED` or `FLEXSLOT_AUTHENTICATION_FAILED`), **don't** refresh again. That's a bug pointing at your code, not at the user. Surface the error to your monitoring and show a polite error to the user — don't infinite-loop the AS.

### Token-bus pattern for parallel requests

A common source of `invalid_grant` storms: 10 in-flight requests all 401 at once, each independently tries to refresh, only one wins, the other 9 use the old refresh token, grant gets revoked.

Pattern: a single token coordinator per user.

```javascript theme={null}
class FlexslotTokenStore {
  constructor() {
    this.refreshInFlight = new Map()  // userId -> Promise<tokens>
  }

  async getValidToken(userId) {
    const stored = await this.load(userId)
    if (Date.now() < stored.expiresAt - 5 * 60 * 1000) {
      return stored.accessToken
    }
    return this.refresh(userId, stored)
  }

  async refresh(userId, stored) {
    if (this.refreshInFlight.has(userId)) {
      return this.refreshInFlight.get(userId)
    }
    const p = this.doRefresh(stored).finally(() => this.refreshInFlight.delete(userId))
    this.refreshInFlight.set(userId, p)
    return p
  }
}
```

Across processes (a horizontally scaled service), use Redis with `SET NX` or a database row lock.

## The user revoked you

A user can revoke your access at any time from `flexslot.gg/account/connected-apps`. You won't know until the next API call returns 401 and your refresh returns `invalid_grant`. **Don't treat this as an error.**

* Mark the user as disconnected (clear the tokens).
* Don't email them about it (they revoked you on purpose).
* Don't try to reconnect silently.
* The next time they use a Flexslot-dependent feature, show "Reconnect Flexslot".

If your app sends scheduled or background sync jobs to Flexslot, **stop them immediately** when the grant is revoked. Repeated 401s on a revoked grant can trip Flexslot's rate-limit and abuse heuristics.

## When to start the flow

| Trigger                                                | Good idea?                                        |
| ------------------------------------------------------ | ------------------------------------------------- |
| User clicks "Connect Flexslot"                         | Yes                                               |
| First time the user lands on a Flexslot-dependent page | Yes, with explanation                             |
| In response to a refresh failure                       | Yes — show a banner, don't auto-redirect          |
| As soon as the user signs up for your product          | **No.** They haven't earned the trust.            |
| On every login                                         | **No.** Tokens persist; only re-auth when broken. |

## Naming the connection

In your UI, refer to it consistently:

* "Flexslot" — the platform
* "Connected to Flexslot" — past tense, the state
* "Connect Flexslot" — the call to action
* "Flexslot account" — the user's identity on flexslot.gg

Avoid "your Flexslot token" or "Flexslot API key" in user-facing copy. Those leak implementation details.

## Showing what you can see

Users trust apps that are upfront about what they're doing. On the connection card, list the scopes in plain language:

```
✓ Connected as @george
  This app can:
  • Read your decks
  • Read your sideboards
```

You can fetch the current granted scopes from your stored token (you got them back on the `/token` response) or by calling `/introspect`.

## The Disconnect flow

<Steps>
  <Step title="User clicks Disconnect">
    Confirm with a modal: "Disconnect from Flexslot? Your synced data will stay; future syncs will stop until you reconnect."
  </Step>

  <Step title="On confirm, call /revoke">
    POST the refresh token to `https://api.flexslot.gg/api/public/v1/oauth/revoke`. RFC 7009 always returns 200 — don't worry about the response.
  </Step>

  <Step title="Clear your tokens">
    Delete `access_token`, `refresh_token`, `expires_at` from your store. **Don't** delete the user's synced Flexslot data unless they ask — they may reconnect later.
  </Step>

  <Step title="Update the UI">
    Switch the card to "Not connected" state.
  </Step>
</Steps>

## Edge cases

### Identifying the user across sessions

Two endpoints, two jobs:

* **`GET /_probe/user`** is what you call to render the connection card. With the user's access token it returns `user.id` (the bare, stable Firebase id — identical to `author.id` on that user's public decks) and `user.username` (the handle shown on the consent screen, e.g. `george-cardeio`). No extra scope is required; identity here is exactly what the user approved at consent. Treat `username` as a **mutable** display field and `user.id` as the **stable** correlation key. The user's **email is not returned** — it isn't consistently shown at consent, so it would be undisclosed PII; if you need to contact the user, ask them in your own product. See the [quickstart](/oauth/quickstart#step-7-identify-the-connected-user) for the full response shape.
* **`POST /introspect`** is a token-metadata endpoint (RFC 7662), not an identity endpoint. Its `username` field carries the **stable** Firebase id (the same value as `_probe/user`'s `user.id`), **not** a display name. There is **no** `sub` claim — this is a plain OAuth 2.0 authorization server, not OIDC. Use `/introspect` to check whether a token is live and what it's scoped to; use `_probe/user` to show who's connected.

The `caller` field on `_probe/user` (`partner:<slug>+user:<firebaseId>`) is an opaque composite principal for rate-limiting and audit — stable, but don't display it. Key your own profile store on `user.id`, which is the same firebase id `/introspect` returns as `username`.

### User's refresh token approaches expiry

Refresh tokens live 30 days from issue **or last use**. If the user opens your app once a week, their token will essentially never expire because every use extends it. If your app is used infrequently, expect cold-start 401s — handle them gracefully with a re-auth prompt.

### Multiple browser tabs

If your app is a web SPA and the user has it open in two tabs, both tabs may try to refresh. See the token-bus pattern above, or use a [`BroadcastChannel`](https://developer.mozilla.org/en-US/docs/Web/API/BroadcastChannel) to coordinate.

### Background sync vs interactive use

Background workers using a user's tokens are subject to the same lifetimes as foreground requests. If a background sync fails with `invalid_grant`, **don't retry** — wait for the user to reconnect interactively. Background retries on a revoked grant can be flagged as abuse.

## Patterns to avoid

| Anti-pattern                                                | Why it's bad                                                   |
| ----------------------------------------------------------- | -------------------------------------------------------------- |
| Showing the access\_token to the user                       | It's an internal credential; users don't need to see it        |
| Storing tokens in localStorage                              | Readable by any XSS; use httpOnly cookies or in-memory         |
| Polling /introspect on every API call                       | Wasteful and slow. Trust the access\_token until it 401s.      |
| Auto-reconnecting after revocation                          | The user revoked you on purpose. Respect that.                 |
| One huge "Sync everything" button that opens the OAuth flow | Surprising. Users want to know what they're agreeing to first. |
| Hiding the disconnect button                                | OAuth requires an obvious way to disconnect.                   |

<Tip>
  Good OAuth UX is invisible when everything's working and helpful when something breaks. If a user can't tell whether they're connected, you have a UX bug. If they can't tell **why** they're disconnected, you have another.
</Tip>
