The SDK ships around forty React components. Listing them alphabetically is
what a reference does, and it is not much help when the question is *"what do
I actually put on the page?"*

So this section is arranged the way you build: the shell that wraps
everything, the gates that decide whether a screen renders at all, then the
screens themselves.

## The three layers

Every partner application ends up with the same shape, whether or not it was
planned:

| Layer | What it does | Components |
|---|---|---|
| **Shell** | Chrome that persists across every route — the rail, the workspace switcher, the account row | `AppShell` |
| **Gate** | Decides whether a screen may render, and what to show instead | `ConnectAccessGate`, `ConnectFeatureGate`, `ConnectSubscriptionGate`, `ConnectWorkspaceTypeGate` |
| **Screen** | The page itself | `ConnectPageShell` + your own components + SDK widgets |

The layers only ever look downward. A gate may live inside the shell; the shell
never lives inside a gate. Getting that backwards is the single most common
integration mistake, and it shows up as the sidebar disappearing whenever a
subscription lapses — the user loses their navigation at exactly the moment
they need it to go and fix their billing.

<Callout type="warn" title="Never gate the chrome">
  `AppShell` renders its chrome unconditionally and gates only the page
  body. If you wrap `<AppShell>` in a gate instead of the other way
  round, a user whose plan expired sees a blank page with no way out.
</Callout>

## The whole thing, once

This is a complete application layout. Everything else in this section is a
detail of one of these lines.

```tsx title="app/providers.tsx"
"use client";

import { ConnectProvider, AppShell } from "@cosmoops/connect-sdk/client";
import Link from "next/link";
import { usePathname } from "next/navigation";

const NAV = [
  { label: "Dashboard", href: "/", icon: <LayoutDashboard className="size-5" /> },
  { label: "Campaigns", href: "/campaigns", icon: <Send className="size-5" /> },
];

// The routes you go to in order to change how the job works, not to do it.
const SETTINGS_NAV = [
  { label: "Settings", href: "/settings", icon: <Settings className="size-5" /> },
];

// `connectProviderProps()` is server-only, so the origin arrives as a prop.
export function Shell({
  children,
  connect,
}: {
  children: React.ReactNode;
  connect: { appUrl: string };
}) {
  const pathname = usePathname();

  return (
    <ConnectProvider {...connect}>
      <AppShell
        name="Pulse"
        nav={NAV}
        secondaryNav={SETTINGS_NAV}
        activePath={pathname}
        renderLink={({ href, children, ...props }) => (
          <Link href={href} {...props}>{children}</Link>
        )}
      >
        {children}
      </AppShell>
    </ConnectProvider>
  );
}
```

```tsx title="app/layout.tsx"
import { connectProviderProps } from "@cosmoops/connect-sdk/next";
import { Shell } from "./providers";
import "@cosmoops/connect-sdk/theme.css";

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <Shell connect={connectProviderProps()}>{children}</Shell>
      </body>
    </html>
  );
}
```

`usePathname` is a client hook, so the shell lives in its own `"use client"`
file and the root layout stays a Server Component — everything inside
`{children}` still renders on the server.

<Callout type="warn" title="Call connectProviderProps() in the layout, not the shell">
It reads `CONNECT_APP_URL`, which is server-side only, so it ships from
`@cosmoops/connect-sdk/next` — an entry a Client Component cannot import. Try it
inside `"use client"` and the build fails with *"no matching export ...
this-entry-is-server-only"*, which is the error doing its job rather than a bug.
Call it in the Server Component and pass the result down, as above. Threading
one plain object is also cheaper than it looks: it is serializable, so it
crosses the boundary with no client bundle cost.
</Callout>

Five things are worth pulling out of that.

**`ConnectProvider` is the only thing that talks to Connect.** Every hook and
every component reads from it. It calls *your* origin — the bridge route from
[Getting started](/docs/getting-started) — never Connect directly, so no
credential is ever in the browser.

**Routing is a prop, not a dependency.** `renderLink` and `activePath` are how
the SDK stays framework-agnostic. Pass `next/link` and `usePathname()` in Next,
your router's equivalents elsewhere.

**The access gate is on by default.** `AppShell` runs `ConnectAccessGate`
around its children — suspended workspace, no plan, onboarding pending,
payment overdue. You do not wire it up; you opt *out* per route if you need to.

**The workspace switcher is on by default too**, and hides itself for accounts
with one workspace. See [Workspaces](/docs/ui/workspaces).

**`appUrl` is the one prop a non-production setup must supply**, which is what
the `connect` prop above carries. The rail's account menu and app launcher
navigate to `{appUrl}/...`, and the SDK cannot read your server's
`CONNECT_APP_URL` from the browser — so a shell without it keeps working and
sends every one of those links to production Connect. See
[Environments](/docs/environments#the-browser-half-points-somewhere-too).

## Where to go next

- **[Shell and navigation](/docs/ui/shell)** — the two navs, collapsing, the account row, exempting routes from the gate
- **[Gates](/docs/ui/gates)** — the four gates, and which question each one answers
- **[Screens](/docs/ui/screens)** — assembling a page from `ConnectPageShell`, `ConnectSection` and the widgets
- **[Workspaces](/docs/ui/workspaces)** — the switcher, switching semantics, and rendering a team

## Theme

Every component paints with **semantic tokens only** — `accent`, `muted`,
`surface`, `separator`, `danger`. None of them contain a hardcoded colour, so
they inherit whatever theme your application defines.

```tsx
import "@cosmoops/connect-sdk/theme.css";
```

That stylesheet is a complete, sensible default. Override the tokens in your
own CSS and every SDK component follows — that is the whole contract, and it is
why a component that hardcoded a colour would be a bug rather than a
preference.