Connect Interstitials
Shared layout guidance for focused authorisation, invite, marketplace, CLI, and credit redemption flows.
Connect interstitials are focused, single-card flows that sit outside the main
Studio shell. Use the shared InterstitialLayout family instead of building
bespoke centered cards, logos, account rows, or organisation selectors.
import { Button } from 'ui'
import {
AccountRow,
InterstitialShell,
LogoPair,
SignOutButton,
StripeLogo,
SupabaseLogo,
} from './connect-interstitial-shared'
export function ConnectInterstitialDemo() {
return (
<InterstitialShell
logo={<LogoPair left={<StripeLogo />} right={<SupabaseLogo />} />}
title="Authorize Stripe Projects"
description="This will create an organization on your behalf in Supabase"
>
<div className="flex flex-col gap-4">
<AccountRow displayName="alex@example.com" action={<SignOutButton />} />
<Button variant="primary" block>
Authorize Stripe Projects
</Button>
<Button variant="text" block>
Cancel
</Button>
</div>
</InterstitialShell>
)
}Use this pattern for
This pattern fits short-lived connect flows: partner authorisation and consent (OAuth, MCP, Stripe Projects), organisation invites, marketplace and billing connections (AWS Marketplace, Vercel install, credit redemption), and CLI or device-code sign-in. Use the same shell for their loading, error, success, and wrong-account states.
Do not use it for normal authenticated Studio pages. Those should use the standard page layout patterns.
Source of truth
import { OrganizationSelector } from '@/components/interfaces/Connect/OrganizationSelector'
import {
InterstitialAccountRow,
InterstitialLayout,
LogoBox,
LogoPair,
PartnerLogo,
SupabaseLogo,
} from '@/components/layouts/InterstitialLayout'Basic shape
Use InterstitialLayout for the outer card, then put route-specific content in
px-6 pb-6. Widen the card only when the flow embeds a real tool, such as
project linking.
<InterstitialLayout
logo={
<LogoPair
left={<PartnerLogo src={`${BASE_PATH}/img/icons/stripe-icon.svg`} alt="Stripe" />}
right={<SupabaseLogo />}
/>
}
title="Authorize Stripe Projects"
description="This will create an organization on your behalf in Supabase"
>
<div className="px-6 pb-6">
<InterstitialAccountRow displayName={displayName} />
<Button variant="primary" block>
Continue
</Button>
</div>
</InterstitialLayout><InterstitialLayout
logo={<LogoPair left={<VercelLogo />} right={<SupabaseLogo />} />}
title="Connect Vercel project"
containerClassName="items-start"
cardClassName="max-w-[900px]"
>
<div className="px-6 pb-6">{projectLinker}</div>
</InterstitialLayout>Logos
Use LogoPair when the user is connecting two known services, and
SupabaseLogo alone for first-party flows or when the requester has no trusted
mark. PartnerLogo fills the 48px box edge-to-edge; LogoBox is for custom
inset marks or logos that need their own background. Store new partner icons in
apps/studio/public/img/icons.
Pairing
| Requester logo | Header treatment |
|---|---|
| Curated partner / MCP client, or a trusted uploaded icon | LogoPair with requester left, SupabaseLogo right |
| Unknown, missing, blocked, or failed-to-load icon | SupabaseLogo alone. Do not invent an initial tile. |
The user is usually arriving from the third-party app. The interstitial should confirm they are connecting to Supabase. Put the requester name in the title and description; do not manufacture a letter avatar to fill the left side of a pair.
Known services. When both sides are curated (or otherwise known), pair them. Theme-reactive tiles are fine when both marks have matching light/dark treatment.
import { Button } from 'ui'
import {
AccountRow,
InterstitialShell,
LogoPair,
SignOutButton,
StripeLogo,
SupabaseLogo,
} from './connect-interstitial-shared'
export function ConnectInterstitialLogoPair() {
return (
<InterstitialShell
logo={<LogoPair left={<StripeLogo />} right={<SupabaseLogo />} />}
title="Authorize Stripe Projects"
description="This will create an organization on your behalf in Supabase"
>
<div className="flex flex-col gap-4">
<AccountRow displayName="alex@example.com" action={<SignOutButton />} />
<Button variant="primary" block>
Authorize Stripe Projects
</Button>
</div>
</InterstitialShell>
)
}No trusted requester mark. If the icon is missing, blocked, or fails to
load, show SupabaseLogo alone. Do not invent an initial tile to fill the
pair.
import { Button } from 'ui'
import {
AccountRow,
InterstitialShell,
SignOutButton,
SupabaseLogo,
} from './connect-interstitial-shared'
export function ConnectInterstitialLogoUnknown() {
return (
<InterstitialShell
logo={<SupabaseLogo />}
title="Authorize Acme"
description="Acme is requesting access to your organization"
>
<div className="flex flex-col gap-4">
<AccountRow displayName="alex@example.com" action={<SignOutButton />} />
<Button variant="primary" block>
Authorize Acme
</Button>
</div>
</InterstitialShell>
)
}Uploaded organisation OAuth icons. Icons published via Studio’s OAuth app
builder are unclassified bitmaps — we do not know if they were authored for
light or dark. Treat the pair as light on both Studio themes: fixed light tile
chrome (border-black/10 bg-white, SupabaseLogo forceLight) on both sides.
Do not invent a dark variant for the upload. Toggle the docs theme to dark to
see the light tiles hold against the Studio chrome.
import { Button } from 'ui'
import {
AccountRow,
InterstitialShell,
LogoBox,
LogoPair,
SignOutButton,
SupabaseLogo,
} from './connect-interstitial-shared'
/** Stand-in uploaded OAuth icon: checked-in solid-colour bitmap (not a real brand). */
function UploadedAppLogo() {
return (
<LogoBox className="border-black/10 bg-white">
<img
alt="Acme"
src={`${process.env.NEXT_PUBLIC_BASE_PATH || '/design-system'}/img/icons/acme-oauth-icon.png`}
className="size-full object-cover"
/>
</LogoBox>
)
}
export function ConnectInterstitialLogoUploaded() {
return (
<InterstitialShell
logo={<LogoPair left={<UploadedAppLogo />} right={<SupabaseLogo forceLight />} />}
title="Authorize Acme"
description="Acme is requesting access to your organization"
>
<div className="flex flex-col gap-4">
<AccountRow displayName="alex@example.com" action={<SignOutButton />} />
<Button variant="primary" block>
Authorize Acme
</Button>
</div>
</InterstitialShell>
)
}Assets
Treat Connect logos as assets, not theme tokens.
Default to light. Prefer a single static light mark inside LogoBox.
Light assets read fine on both light and dark Studio themes. That is the
default for Connect tiles.
Keep pairs matched. In a LogoPair, both marks must use the same
treatment: both light, or both dark. Do not mix a light partner tile with a
dark-theme-only Supabase treatment, or the reverse. Theme-aware dark variants
are fine for curated partners that already have them, but then both sides of
the pair should use the dark set together.
What not to do
- Do not invent light/dark pairs for arbitrary remote OAuth icons.
- Do not recolour vendor SVGs with theme CSS. Monochrome identity-provider masks (for example GitHub on sign-in) stay a separate pattern.
Where logos come from on /authorize
- Curated partner logos resolve from allowlisted
redirect_urihosts, or from a trusted partner name whenredirect_uriis localhost / loopback (local MCP clients). Do not resolve curated logos from self-assertednameorwebsiteon a remote host. Those pairs may use theme tiles and dark assets when the partner has them. - Published organisation OAuth app icons uploaded in Studio remain trusted remote images, paired with forced-light tiles on both sides.
- Everything else falls back to
SupabaseLogoalone. - If the requester name looks like a known partner but
redirect_uriis a remote host outside that partner's allowlist, show a caution admonition. Localhost MCP redirects are excluded.
Account row
Use InterstitialAccountRow for signed-in context. Do not recreate it locally.
<InterstitialAccountRow avatarUrl={avatarUrl} displayName={displayName} action={signOutButton} />Organisation selection
Use OrganizationSelector when the flow needs an organisation pick. Extend it
for new states instead of inventing a parallel card style.
<OrganizationSelector
organizations={linkableOrganizations}
selectedSlug={selectedOrgSlug}
onSelect={setSelectedOrgSlug}
createLabel="Create new organization"
onCreate={() => setShowOrgCreationDialog(true)}
/>Actions
Prefer one full-width primary action. A full-width text button is fine for a secondary action that still belongs in the flow.
Action feedback
Match feedback to its scope:
- Use
FormMessageorFieldErrorbeside a field when that field needs to change. - Show a submission or action failure as simple destructive text below the actions. Separate it with a subtle divider when needed for composition. Keep the current account, selections, and actions visible so the user can retry.
- Use
Admonitionwhen the whole interstitial is blocked or has materially changed state, such as an invalid link, wrong account, or partially completed setup. - Use a toast only for non-blocking feedback or a completed action whose originating surface is no longer visible. A toast must not be the only feedback for a failure the user needs to resolve on the current card.
<InterstitialActionError error={actionError} />Clear stale action feedback when the user retries or changes a relevant selection. Error copy should say what failed and, when it is not obvious, what the user can do next. When passive supporting copy occupies the same footer region, replace it with the action error until the error is cleared instead of stacking both messages.
import { Button } from 'ui'
import {
AccountRow,
InterstitialActionError,
InterstitialShell,
LogoPair,
StripeLogo,
SupabaseLogo,
} from './connect-interstitial-shared'
export function ConnectInterstitialActionError() {
return (
<InterstitialShell
logo={<LogoPair left={<StripeLogo />} right={<SupabaseLogo />} />}
title="Authorize Stripe Projects"
description="This will create an organization on your behalf in Supabase"
>
<div className="flex flex-col gap-4">
<AccountRow displayName="alex@example.com" />
<div className="flex flex-col gap-2">
<Button variant="primary" block>
Authorize Stripe Projects
</Button>
<Button variant="text" block>
Cancel
</Button>
<InterstitialActionError error="Failed to authorize Stripe Projects. Please try again." />
</div>
</div>
</InterstitialShell>
)
}States
Keep loading, invalid, error, and success states inside the same card when the
route can explain them. Use ShimmeringLoader for loading, and Admonition
for warning, error, note, and success copy.
import { Button } from 'ui'
import { Admonition } from 'ui-patterns/Admonition'
import { AccountRow, InterstitialShell, SupabaseLogo } from './connect-interstitial-shared'
export function ConnectInterstitialLogoSingle() {
return (
<InterstitialShell
logo={<SupabaseLogo />}
title="Join organization"
description="You have been invited to Acme Labs"
>
<div className="flex flex-col gap-4">
<Admonition
type="warning"
title="Wrong account"
description="Sign in with the Supabase account that received this invite, then open the link again."
/>
<AccountRow displayName="alex@example.com" />
<Button variant="primary" block>
Sign out and continue
</Button>
</div>
</InterstitialShell>
)
}Copy
Use sentence case. Prefer sign in over login. Titles and primary actions
should follow Verb -> Thing, for example Authorize Stripe Projects or
Install Vercel.
Keep the layout title static across states and put state-specific copy in the body. Header descriptions should stay short and should not end with a full stop.