Skip to main content

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/reactFlynetProvider, ConnectWithBlackbird, RestaurantList, UserPassport
npm install @flynetdev/core @flynetdev/react
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.
app/connect/route.ts
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: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);
}
app/page-connect.tsx
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.
app/page.tsx
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.
app/callback/route.ts
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 />
app/passport.tsx
"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>
  );
}
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).
From the kitchenFlynetProvider 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.
Next: Build with AI — build the next one with an AI agent.