Call the API from Vantage_
Read tasks with the granted access token and add a task composer that lives or dies by its scope.
2 min read
Vantage already holds the access token in its session after the token exchange. Reading tasks is one authenticated fetch away, and a small composer will exercise the write path.
Point Vantage at the API
Add TaskFlow's API base to consumer/.env:
# consumer/.env# TaskFlow's API, called with the granted access token.TASKFLOW_API_URL=http://localhost:4000The API client
Create consumer/src/lib/taskflow.ts. Every request carries the access token as a Bearer header, and TaskFlow's guard does the rest:
// consumer/src/lib/taskflow.ts// Vantage's client for TaskFlow's API. Every request carries the access// token the user granted, and TaskFlow enforces its scopes.
const taskflowApi = process.env.TASKFLOW_API_URL!
export type TaskFlowTask = { id: string title: string project: string due: string done: boolean}
/** Read the user's tasks. Requires the tasks.read scope. */export async function fetchTasks(accessToken: string): Promise<TaskFlowTask[]> { const res = await fetch(`${taskflowApi}/api/tasks`, { headers: { Authorization: `Bearer ${accessToken}` }, }) if (!res.ok) throw new Error(`tasks failed: ${res.status}`) const { tasks } = await res.json() return tasks}
export type WriteResult = | { created: true; task: TaskFlowTask } | { created: false; status: number; error: string }
/** Create a task on TaskFlow. Succeeds only when the access token * carries the tasks.write scope; otherwise TaskFlow answers with * 403 insufficient_scope. */export async function createTask( accessToken: string, title: string,): Promise<WriteResult> { const res = await fetch(`${taskflowApi}/api/tasks`, { method: 'POST', headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ title }), }) if (res.ok) { const { task } = await res.json() return { created: true, task } } const body = await res.json().catch(() => ({ error: 'unknown' })) return { created: false, status: res.status, error: body.error }}createTask does not check any scope itself. Vantage cannot know what the user granted until it tries; the refusal comes from TaskFlow, which is the only side that can be trusted to enforce it.
Keep the granted scope
The token response reports which scopes were granted, which matters now that the user picks them individually on the consent screen. Store it in the session in consumer/src/routes/oauth.callback.tsx, next to the access token:
// consumer/src/routes/oauth.callback.tsxawait session.update({ accessToken: tokens.access_token, grantedScope: tokens.scope, user, state: undefined,})Add the matching grantedScope?: string field to SessionData in consumer/src/lib/oauth.ts.
Load tasks and add the composer
Replace the dashboard loader in consumer/src/routes/dashboard.tsx and add a server function for the write:
// consumer/src/routes/dashboard.tsx (loader and write)const loadDashboard = createServerFn().handler( async (): Promise<DashboardData> => { const session = await vantageSession() const { user, accessToken, grantedScope } = session.data if (!user || !accessToken) throw redirect({ to: '/' })
// Reads the user's tasks from TaskFlow's API with the granted // access token. Works because the user granted tasks.read. const tasks = await fetchTasks(accessToken)
return { user, grantedScope: grantedScope ?? '', tasks } },)
const addTask = createServerFn({ method: 'POST' }) .validator((d: { title: string }) => d) .handler(async ({ data }): Promise<WriteResult> => { const session = await vantageSession() const { accessToken } = session.data if (!accessToken) throw redirect({ to: '/' })
// TaskFlow only accepts this if the token carries tasks.write. return createTask(accessToken, data.title) })The component renders the granted scopes as chips, the task list, and an Add task composer under it. On submit it calls addTask; when the result is created it refreshes the list with router.invalidate(), and when it is not, it shows TaskFlow's refusal next to the composer. The full component is in the tutorial repository.
The payoff

The dashboard shows the user's tasks, fetched live from TaskFlow's API with the granted token, with the composer ready below them. What happens when you use it depends entirely on what the user granted on the consent screen, which is exactly what the final step walks through.
Continue to run the whole flow.
Was this page helpful?
Share what worked or what we should fix. Once approved, our agents automatically apply suggested updates to the docs.