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

# Member dining app

> Connect with Blackbird, list restaurants, and show a member passport — composed from the SDK.

# Member dining app

**Goal:** A small Next.js app that signs a member in with Blackbird, lists
restaurants, and shows their passport — built from `@flynetdev/core` and
`@flynetdev/react`.\
**Prep time:** \~15 minutes

## Mise en place

* `@flynetdev/core` — OAuth (with PKCE) and server-side Discovery
* `@flynetdev/react` — `FlynetProvider`, `ConnectWithBlackbird`, `RestaurantList`, `UserPassport`

```bash theme={null}
npm install @flynetdev/core @flynetdev/react
```

## First course — the connect link

Build the authorize URL on the server. `FlynetOAuth` adds the PKCE
`code_challenge`; stash the matching verifier and CSRF state in an
httpOnly cookie for the callback.

```typescript app/connect/route.ts theme={null}
import { FlynetOAuth } from "@flynetdev/core";
import { cookies } from "next/headers";

const oauth = new FlynetOAuth({
  clientId: process.env.CLIENT_ID!,
  clientSecret: process.env.CLIENT_SECRET!,
  redirectUri: process.env.REDIRECT_URI!,
  audience: process.env.AUDIENCE!,
  scopes: ["read:profile", "read:user_checkins", "read:wallets"],
});

export async function GET() {
  const { url, state, codeVerifier } = await oauth.getAuthorizeUrl();
  const jar = await cookies();
  jar.set("oauth_state", state, { httpOnly: true });
  jar.set("oauth_verifier", codeVerifier, { httpOnly: true });
  return Response.redirect(url);
}
```

```tsx app/page-connect.tsx theme={null}
import { ConnectWithBlackbird } from "@flynetdev/react";

export function Connect() {
  return <ConnectWithBlackbird href="/connect" />;
}
```

## Second course — the restaurant list

Discovery uses the API key, which stays on the server. `RestaurantList`
is presentational — hand it the data you fetched.

```tsx app/page.tsx theme={null}
import { FlynetDiscoveryClient } from "@flynetdev/core";
import { RestaurantList } from "@flynetdev/react";

export default async function Page() {
  const discovery = new FlynetDiscoveryClient({ apiKey: process.env.API_KEY! });
  const { restaurants } = await discovery.restaurants.listRestaurants({ pageSize: 12 });
  return <RestaurantList restaurants={restaurants} />;
}
```

## Main course — the passport

Exchange the code for tokens, then render the member component under
`FlynetProvider` with a member client.

```typescript app/callback/route.ts theme={null}
const code = new URL(req.url).searchParams.get("code")!;
const jar = await cookies();
const tokens = await oauth.exchangeCode({
  code,
  codeVerifier: jar.get("oauth_verifier")!.value,
});
// persist tokens.access_token in your session, then render <Passport />
```

```tsx app/passport.tsx theme={null}
"use client";
import { FlynetMemberClient } from "@flynetdev/core";
import { FlynetProvider, UserPassport } from "@flynetdev/react";

export function Passport({ accessToken }: { accessToken: string }) {
  const member = new FlynetMemberClient({ accessToken });
  return (
    <FlynetProvider member={member}>
      <UserPassport />
    </FlynetProvider>
  );
}
```

<Warning>
  **Chef's warning** — The API key and `CLIENT_SECRET` are server-only;
  never ship them to the browser. PKCE is required on authorize —
  `FlynetOAuth` adds the `code_challenge`, and the matching `code_verifier`
  must reach `exchangeCode` (so it has to survive the redirect, hence the
  cookie).
</Warning>

<Tip>
  **From the kitchen** — `FlynetProvider` owns the TanStack Query client;
  don't construct your own. Give it a `FlynetMemberClient` for member
  components, and a `FlynetDiscoveryClient` too if you render discovery
  components on the client.
</Tip>

**Next:** [Build with AI](/build-with-ai/overview) — build the next one with an AI agent.
