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
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.
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);
}
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.
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.
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 />
"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 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.
Next: Build with AI — build the next one with an AI agent.