How To Manage Access and Refresh Tokens for an API in Next.js 13 With Supabase and Prisma
Tags: nextjs, supabase, prisma, api
Published: 2023-09-22 22:30:04

This article will show you how to use the Trackmania API in a Next.js 13 app using TypeScript. The API uses `access` and `refresh` tokens, so it is not as easily accessible as other APIs, like `jsonplaceholder` , or an API where you can include the same key(s) with every request.

So, we will use [**Supabase**](https://supabase.com/) as a Postgres database to store the tokens and [**Prisma**](https://www.prisma.io/) as our ORM of choice to communicate with our database.

The main focus of this article is to show you how to store / retrieve these tokens for API use, NOT for user authentication. If you’re looking for user authentication, I’d recommend looking at an **[example here](https://authjs.dev/guides/basics/refresh-token-rotation)** over on NextAuth’s website to get you started.

Let’s get started!

## Initial Setup

Quickly download and setup the latest Next.js TypeScript starter:

```bash
npx create-next-app@latest --ts .
```

If you are getting warnings in your CSS file complaining about unknown CSS rules, follow **[these steps here](https://github.com/tailwindlabs/tailwindcss/discussions/5258#discussioncomment-1979394)**

Still in `globals.css`, update the code with this reset from **[Josh Comeau](https://www.joshwcomeau.com/css/custom-css-reset/)**

```css
/* src/app/globals.css */

@tailwind base;
@tailwind components;
@tailwind utilities;

*, *::before, *::after {
  box-sizing: border-box;
}

* {
  margin: 0;
	padding: 0;
}

body {
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
}

img, picture, video, canvas, svg {
  display: block;
  max-width: 100%;
}

input, button, textarea, select {
  font: inherit;
}

p, h1, h2, h3, h4, h5, h6 {
  overflow-wrap: break-word;
}

#root, #__next {
  isolation: isolate;
}
```

Update `tsconfig.json` to this:

```json
{
  "compilerOptions": {
    "target": "es5",
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "Node",
    "isolatedModules": true,
    "jsx": "preserve",
    "incremental": true,
    "plugins": [
      {
        "name": "next"
      }
    ],
    "paths": {
      "@/*": ["./src/*"]
    }
  },
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
  "exclude": ["node_modules"]
}
```

Update `src/app/page.tsx` to this:

```tsx
// src/app/page.tsx

const HomePage = () => {
  return (
    <div>
      <h1>HomePage</h1>
    </div>
  );
};

export default HomePage;
```

Update `src/app/layout.tsx` to this:

```tsx
// src/app/layout.tsx

import { Inter } from "next/font/google";

import type { Metadata } from "next";
import type { ReactNode } from "react";

import "./globals.css";

const inter = Inter({ subsets: ["latin"] });

export const metadata: Metadata = {
  description: "Generated by create next app",
  title: "Trackmania API Demo"
};

type RootLayoutProps = {
  children: ReactNode;
};

const RootLayout = ({ children }: RootLayoutProps) => {
  return (
    <html lang="en">
      <body className={inter.className}>{children}</body>
    </html>
  );
};

export default RootLayout;
```

## GitHub & Vercel

I’m a fan of setting up a GitHub repo and deploying to Vercel early, and committing + pushing often in smaller chunks

This way, if the site should not build on Vercel, it’s a lot easier to figure when the issue started occurring.

I’m going to assume that since you’re doing this advanced tutorial that you:

1. Already know how to create a repo on GitHub and push your code up to it
2. Deploy a website from your Vercel dashboard by selecting the repo you’ve created

Please do both of these things before continuing.

## Getting API Access Token

In order to use Nadeo’s Trackmania API, we need to get an `access_token` (which also comes with a `refresh_token`)

There are 2 approaches to get this:

1. Use a dedicated Ubisoft account
2. Create Server Account on **[trackmania.com](https://www.trackmania.com/)**

While it is true that to login to [trackmania.com](http://trackmania.com/), you need a Ubisoft account anyways, the flow for refreshing the `access_token` is much simpler using a Server Account.

On [trackmania.com](http://trackmania.com/), once you are signed in, click on your user name in the top right corner

In the dropdown, select **My Server Accounts**

Enter a value for the **Server Login** (you can think of this is a project name)

I’ll call mine: `trackmania-api-demo`

Click **Submit**

Once you do, a message will appear to **Please copy the password**

Copy the Login (the name you just entered, ie `trackmania-api-demo`) and password into a text file for now

Let’s use Postman (or a similar platform of your choice) to send requests first to make sure things will work before coding it all up.

Create a new request with the following:

- Request type to `POST`
- URL to https://prod.trackmania.core.nadeo.online/v2/authentication/token/basic
- Under `Authorization`:
    - Set the `Type` to `Basic Auth`
    - For the `Username` and `Password`, copy / paste the Login and Password fields from the Server Account
- Under `Headers`, add
    - Content-Type: `application/json`
- Under `Body`
    - Set the type to `raw`, then select `JSON`
    - Add this:
    
    ```json
    {
      "audience": "NadeoLiveServices"
    }
    
    ```
    
    - **NOTE**: We have to include this JSON object in the body because we need a way of telling the API what to authenticate with. You can see a mapping of APIs and available audiences **[here](https://webservices.openplanet.dev/auth#base-urls-and-audiences)**.

Click **Send**, and you should see a response like this:

```json
{
  "accessToken":"eyJhbGciOiJIUzI1NiIsImVudiI6InRyYWNrbWFuaWEtcHJvZCIsInZlciI6IjEifQ.PAYLOAD.SIGNATURE",
  "refreshToken":"eyJhbGciOiJIUzI1NiIsImVudiI6InRyYWNrbWFuaWEtcHJvZCIsInZlciI6IjEifQ.PAYLOAD.SIGNATURE"
}
```

The start of the `access_tokens`, known as the `HEADER`, is always the same, so it’s safe to display them publicly like this. However, it’s the `PAYLOAD` and `SIGNATURE` parts of the tokens that are unique.

If you URL-base64-decode the `PAYLOAD` part of the `accessToken`, you get the following JSON object:

```json
{
  "jti": "UUID_HERE",
  "iss": "NadeoServices",
  "iat": 1595191016,
  "rat": 1595192816,
  "exp": 1595194616,
  "aud": "NadeoLiveServices",
  "usg": "Server",
  "sid": "UUID_HERE",
  "sub": "UUID_HERE",
  "aun": "mm",
  "rtk": false,
  "pce": false
}
```

Here, `exp` defines the expiration time, and `rat` defines the time after which you are able to refresh the token.

To URL-base64-decode something in TypeScript, you can use a function like this:

```tsx
const urlBase64Decode = (input: string) => {
  // decode the base64 string
  const base64Decoded = atob(input);

  // URL-decode the data
  const urlDecoded = decodeURIComponent(base64Decoded);

  // parse the JSON data
  return JSON.parse(urlDecoded);
};

```

## Using the Access Token

To keep things simple, let’s try and fetch the data for a single map for now to make sure our `access_token` is working

Let’s fetch data for the first map in the current campaign (which at the time of writing, is **Summer 2023-01**)

We will need send a **GET** request to the following URL:

```
<https://live-services.trackmania.nadeo.live/api/token/map/MAP_UID>

```

To get a `MAP_UID`, head back to the official Trackmania site: https://www.trackmania.com/

Once there, click on **Campaigns**

Then click **SUMMER 2023 - 01** on the next page

The URL will be this: https://www.trackmania.com/tracks/7hk8IflYsbMbpJv2gyYzx48Zvt7

The last part in the path, `7hk8IflYsbMbpJv2gyYzx48Zvt7`, is the `MAP_UID`

Back in Postman, create another request tab and set the following:

- Request type to `GET`
- URL to https://live-services.trackmania.nadeo.live/api/token/map/7hk8IflYsbMbpJv2gyYzx48Zvt7
- Under `Headers`, add
    - Content-Type: `application/json`
    - Authorization: `nadeo_v1 t=YOUR_FULL_ACCESS_TOKEN`
- Under `Body`
    - Set the type to `raw`, then select `JSON`
    - Add this:
    
    ```json
    {
      "audience": "NadeoLiveServices"
    }
    ```
    

Click **Send**, and you should see a response like this:

```json
{
    "uid": "7hk8IflYsbMbpJv2gyYzx48Zvt7",
    "mapId": "a93f902b-16a1-4448-ad34-65cf32d41425",
    "name": "Summer 2023 - 01",
    "author": "d2372a08-a8a1-46cb-97fb-23a161d85ad0",
    "submitter": "d2372a08-a8a1-46cb-97fb-23a161d85ad0",
    "authorTime": 23997,
    "goldTime": 26000,
    "silverTime": 29000,
    "bronzeTime": 36000,
    "nbLaps": 0,
    "valid": false,
    "downloadUrl": "https://prod.trackmania.core.nadeo.online/storageObjects/5e1cd15d-0b55-4db7-98ad-a82656ceb112",
    "thumbnailUrl": "https://prod.trackmania.core.nadeo.online/storageObjects/fa836805-f906-41c1-9ca2-e52a63c4b44f.jpg",
    "uploadTimestamp": 1687274471,
    "updateTimestamp": 1687274472,
    "fileSize": null,
    "public": false,
    "favorite": false,
    "playable": true,
    "mapStyle": "",
    "mapType": "TrackMania\\TM_Race",
    "collectionName": "Stadium",
    "gamepadEditor": false
}
```

Hazah! If you’ve made it this far, congrats! This is a very good milestone to hit.

## Planning Out the App

Ok, so now that we’ve tested some queries in Postman to ensure we get the responses we want, we should pause here and plan out how we want to do code wise:

- Store the `access_` and `refresh_token` in Supabase database
- Check whether or not the current `access_token` is expired
- If it is, make a request with the `refresh_token` to get a new one
- Store the new `access_token` in the Supabase database

Most resources online I’ve seen suggest using a JWT to store the `refresh_token`. However, using a JWT to store the `refresh_token` is less secure than saving it in a database.

Instead, we will use Supabase to store our tokens, and use Prisma as our ORM of choice to read and write data to / from the database.

## Install and Setup Prisma

First, let’s install Prisma as a `devDependency`

```bash
npm i -D prisma
```

Next, we can initialize Prisma by running:

```bash
npx prisma init
```

This will create an `.env` file as well as a prisma folder, both at the root level

**NOTES:**

- Do **NOT** change `.env` file name to `.env.local`, it **must** stay as `.env`
- The `DATABASE_URL` will need to be updated with the URL you get when creating a DB with Supabase later on

## Setup PostgreSQL on Supabase

Create an account if you do not have one already

Once logged in, go to your **Dashboard**

On the dashboard page, click **New project**

Give it a name, something in `kebal-case-like-this`

Click generate a password

Copy and paste the generated password into the **.env** file as a comment for now

Change your region accordingly

Click **Create new project**

Navigate to the project settings (gear icon, very bottom of sidebar nav)

Click on **Database**

Under **Connection string**, click **URI**

Copy this value

Back in the `.env` file, replace the default `DATABASE_URL` with the URI you just copied.

Also update the `[YOUR-PASSWORD]` placeholder with the generated password from before.

Back in Supabase, click on the Database icon.

Under tables, there should be nothing there. But not for long!

## Create Prisma Model Schema

Open the `schema.prisma` file generated earlier

We’ll add a simple `ApiToken` model:

```sql
model ApiToken {
  tokenId        String   @id @default(cuid())
  accessToken    String
  expirationDate DateTime
  refreshToken   String
}
```

This will be sufficient because we are not performing any auth, we are simply saving the keys in a secure location for when we want to reach out to the Trackmania API

While we’re at it, let’s also update the `.env` file with Trackmania Server Account login and password from earlier:

```
DATABASE_URL="..."

TM_SERVER_ACCOUNT_LOGIN="..."
TM_SERVER_ACCOUNT_PASSWORD="..."
```

We’ll need these whenever we make the initial POST request to get the tokens

Now we need to migrate

- **NOTE:** also any time the schema is changed, you’ll have to migrate

Run this command:

```bash
npx prisma migrate dev --name init
# "init" at the end is the name of the migration
```

Go back to Supabase, and click on the Table Editor icon

You should now see a User table!

## Install Prisma Client

Link to Prisma Client docs **[here](https://www.prisma.io/docs/getting-started/setup-prisma/start-from-scratch/relational-databases/install-prisma-client-typescript-postgresql)**

Install it:

```bash
npm install @prisma/client
```

Next, inside the `src` folder, create a folder called `lib`

In the `lib` folder, create a file called `prisma.ts`

There are some common / well known issues when working with Next.js and Prisma

There is a docs page about it **[here](https://www.prisma.io/docs/guides/other/troubleshooting-orm/help-articles/nextjs-prisma-client-dev-practices)**

Copy and paste this snippet (taken from the docs) into `prisma.ts`:

```tsx
import { PrismaClient } from '@prisma/client'

const prismaClientSingleton = () => {
  return new PrismaClient()
}

type PrismaClientSingleton = ReturnType<typeof prismaClientSingleton>

const globalForPrisma = globalThis as unknown as {
  prisma: PrismaClientSingleton | undefined
}

const prisma = globalForPrisma.prisma ?? prismaClientSingleton()

export default prisma

if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma
```

What this code does is when we are not in the production environment, and the Prisma client has already run, the PrismaClient won’t be initialized again and again every time we restart the server.

At this point, it’s a good idea to update our Environment Variables on Vercel with the values we’ve added.

From your dashboard, select the project you’ve created / deployed.

Click on **Settings**, then **Environment Variables.**

Add them here (you can actually copy / paste multiple at a time!).

## Adding a Row in Supabase

Head over to Supabase, and enter in a row manually.

Perform a `POST` request in Postman to get an up-to-date `access` and `refresh` token

You can then use the `urlBase64Decode` function shown earlier to get the `exp` value

You can then use this function to create a Date string from that value:

```tsx
const convertUnixTimestampToDate = (timestamp) => new Date(timestamp * 1000);

// example
convertUnixTimestampToDate(1695344641)
"Thu Sep 21 2023 18:04:01 GMT-0700 (Pacific Daylight Time)"
```

Use this value for the `expirationDate`

For the `tokenId`, you can also use this `generateCUID` function in a browser console to generate an id for you (thanks ChatGPT!):

```tsx
function generateCUID() {
  // Define a list of characters that can be used in the CUID
  const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';

  // Length of the CUID
  const length = 25;

  let cuid = 'c'; // Start with the letter 'c' to mark it as a CUID

  for (let i = 0; i < length; i++) {
    const randomIndex = Math.floor(Math.random() * characters.length);
    cuid += characters[randomIndex];
  }

  return cuid;
}

console.log(generateCUID());
// cVuBo6oMxjNbc7WKpMrT6lqQg3
```

## Using Next.js `middleware`

Since we need to always be checking whether or not the `access_token` is valid or not, we need some code to run on every page

A simple way to implement this is by using **[middleware](https://nextjs.org/docs/app/building-your-application/routing/middleware)**

Inside the `src` directory, create a file called `middleware.ts`

Add this code to it for now:

```tsx
// src/middleware.ts

import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";

// This function can be marked `async` if using `await` inside
const middleware = (request: NextRequest) => {
  console.log("middleware is running!");

  // return NextResponse.json(request)
};

export { middleware };
```

Then if you run `npm run dev`, you should see the `console.log` when you visit / refresh the home page.

Next, let’s update this code to fetch the example row from Supabase we added earlier:

```tsx
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import prisma from "./lib/prisma";

// This function can be marked `async` if using `await` inside
const middleware = async (request: NextRequest) => {
  // console.log("middleware running!!!");

  // return NextResponse.json(request)
  const apiTokenTest = await prisma.apiToken.findUnique({
    where: {
      tokenId: "c1Z1oSjpTT3REWxoeuK0xcWQBj"
    }
  });

  console.log("apiTokenTest: ", apiTokenTest);
};

export { middleware };
```

However, when we do this, we will get this error:

- **Error: PrismaClient is unable to run in Vercel Edge Functions. As an alternative, try Accelerate: https://pris.ly/d/accelerate.
If this is unexpected, please open an issue: [https://github.com/prisma/prisma/issues](https://github.com/prisma/prisma/issues**)**

At the time of writing, as the error says, `PrismaClient` is unable to run in Vercel Edge Functions.

Thankfully, it is being discussed and it looks like **[a solution is being worked on](https://github.com/prisma/prisma/issues/20566)**.

To get around this, we can use **[Prisma DataProxy](https://www.prisma.io/docs/data-platform/classic-projects/data-proxy)**.

To start, head over to https://cloud.prisma.io/ and create an account with your GitHub login.

Once you’ve authorized Prisma, go to your projects dashboard [here](https://cloud.prisma.io/projects) (you also might automatically be directed to the create a project flow).

Click on **Create classic project.**

Copy and paste your `DATABASE_URL` from the `.env` file into the **Connection string** field.

Click **Create project.**

Add your GitHub profile if not selectable.

Select your repo, and make sure the branch is set to `main`.

Create a Data Proxy connection string, give it a name.

Copy the value, and head back to the code and open the `.env` file.

Rename the existing `DATABASE_URL` variable to `MIGRATE_DATABASE_URL`.

Then create a `DATABASE_URL`, this time setting it to the proxy connection string you just created.

The `MIGRATE_DATABASE_URL` will be used to apply and pending migration to the database as the app will be deployed on Vercel.

Next, open up the terminal and run:

```bash
npx prisma generate --data-proxy
```

This will generate a Prisma client that will use Prisma data proxy

Next, go to `prisma.ts` and adjust the import path of the `PrismaClient`like so:

```tsx
// before
import { PrismaClient } from "@prisma/client";

// after
import { PrismaClient } from "@prisma/client/edge";
```

After that, run npm run dev in the terminal, and you should now see the response:

```tsx
apiTokenTest:  {
  tokenId: 'TOKEN_ID_HERE',
  accessToken: 'ACCESS_TOKEN_HERE',
  expirationDate: 2023-09-21T18:04:01.000Z,
  refreshToken: 'REFRESH_TOKEN_HERE'
}

```

## Updating the Prisma Schema

We should update the `ApiToken` model to also have a `addedOn` field

This will help us get the row in the database that was added most recently

```sql
model ApiToken {
  tokenId        String   @id @default(cuid())
  accessToken    String
  expirationDate DateTime
  refreshToken   String
  addedOn        DateTime @default(now())
}
```

We will also need to add a `directUrl` property to the `datasource db`:

```tsx
datasource db {
  provider  = "postgresql"
  url       = env("DATABASE_URL")
  directUrl = env("MIGRATE_DATABASE_URL") // <---
}
```

Again, the `MIGRATE_DATABASE_URL` is the old `DATABASE_URL` that was copied from Supabase.

Make sure to also add / update your Environment Variables on Vercel too!

Now we can run migrate command:

```bash
npx prisma migrate dev --name added_on_field
```

If you didn’t specify the `directUrl`, you would get an error. So that is why we added it.

Link to the docs page about using Prisma Migrate **[here](https://www.prisma.io/docs/getting-started/setup-prisma/start-from-scratch/relational-databases/using-prisma-migrate-typescript-sqlserver)**

Finally, we will need to add this `script` to our `package.json` (as per Prisma docs **[here](https://www.prisma.io/docs/guides/other/troubleshooting-orm/help-articles/vercel-caching-issue)**):

```json
"postinstall": "prisma generate"
```

We do this because you would run into the following error message on deployment otherwise:

```
Prisma has detected that this project was built on Vercel, which caches dependencies.
This leads to an outdated Prisma Client because Prisma's auto-generation isn't triggered.
To fix this, make sure to run the `prisma generate` command during the build process.

Learn how: <https://pris.ly/d/vercel-build>
```

## Updating the `middleware`

So now we can begin writing the necessary code to check our tokens in the database

But first, let’s setup a simple request on the home page knowing it *will* fail:

```tsx
const getMapInfo = async () => {
  const mapUId = "7hk8IflYsbMbpJv2gyYzx48Zvt7";
  const url = `https://live-services.trackmania.nadeo.live/api/token/map/${mapUId}`;

  const res = await fetch(url, {
    method: "GET",
    headers: {
      "Content-Type": "application/json"
    }
  });

  if (!res.ok) {
    const text = await res.text(); // get the response body for more information

    throw new Error(`
      Failed to fetch data
      Status: ${res.status}
      Response: ${text}
    `);
  }

  return res.json();
};

const HomePage = async () => {
  const mapInfo = await getMapInfo();

  console.log("mapInfo: ", mapInfo);

  return (
    <div>
      <h1>HomePage</h1>

      <p>Track name:</p>
    </div>
  );
};
```

As you can see, we are missing the `Authorization` along with the `accessToken`. This is where `middleware` can help us.

We can use it to set headers on requests. We will use it to set the `access_token` on each request to the Trackmania API. In the end, it looks like this:

```tsx
// src/middleware.ts

import { NextRequest, NextResponse } from "next/server";
import prisma from "./lib/prisma";
import { unixTimestampToDate, urlBase64Decode } from "./lib/utils";

import type { AuthTokenResp } from "./types";

const middleware = async (request: NextRequest) => {
  try {
    const mostRecentApiToken = await prisma.apiToken.findFirst({
      orderBy: {
        addedOn: "desc"
      }
    });

    if (mostRecentApiToken) {
      const { accessToken, expirationDate, refreshToken } = mostRecentApiToken;

      const tokenExpirationDate = new Date(expirationDate);
      const currentDate = new Date();

      // compare the current date to the expiration date
      if (currentDate > tokenExpirationDate) {
        console.log("the token is expired - get a new one");

        const url = "https://prod.trackmania.core.nadeo.online/v2/authentication/token/basic";
				const login = process.env.TM_SERVER_ACCOUNT_LOGIN;
        const password = process.env.TM_SERVER_ACCOUNT_PASSWORD;

        const res = await fetch(url, {
          method: "POST",
          headers: {
            "Content-Type": "application/json",
            Authorization: `Basic ${btoa(`${login}:${password}`)}`
          },
          body: JSON.stringify({
            audience: "NadeoLiveServices"
          })
        });

        if (!res.ok) {
          const text = await res.text(); // get the response body for more information

          throw new Error(`
            Failed to fetch data
            Status: ${res.status}
            Response: ${text}
          `);
        }

        const newTokens: AuthTokenResp = await res.json();
        const { accessToken: newAccessToken, refreshToken: newRefreshToken } = newTokens;

        const [header, payload, structure] = newAccessToken.split(".");
        const { exp, rat } = urlBase64Decode(payload);
        const newExpirationDate = unixTimestampToDate(rat);

        await prisma.apiToken.create({
          data: {
            accessToken: newAccessToken,
            expirationDate: newExpirationDate,
            refreshToken: newRefreshToken,
            addedOn: new Date()
          }
        });
      }

      console.log("the token is valid - you good!");

			// set a new response header "Authorization"
      const response = NextResponse.next();
      response.headers.set("Authorization", `nadeo_v1 t=${accessToken}`);
      return response;
    }
  } catch (error) {
    return NextResponse.json(
      {
        message: "Something went wrong!"
      },
      {
        status: 500
      }
    );
  }
};

export { middleware };
```

That’s quite a lot! Let’s break it down:

- First, we the most recent row in the database by using the `addedOn` field
- Then, we check we have something we can work with
- If we do, destructure the `accessToken`, `expirationDate`, and `refreshToken` variables out of `mostRecentApiToken`
- We then check if the `currentDate` is greater than `tokenExpirationDate`
- If it is, that means the `access_token` is expired and we need to get a new one
    - We then make the necessary `POST` request, using the refresh token for the `Authorization` header
    - We then add on a type annotation of `AuthTokenResp`, seen below, to the `newTokens`:
    
    ```tsx
    // src/types/index.ts
    
    type AuthTokenResp = {
      accessToken: string;
      refreshToken: string;
    };
    ```
    
    - When the destructure again, this time using aliases to avoid naming conflicts
    - We then split the `access_token` into 3 parts, mostly just for the `payload`
    - We then run the payload through our `urlBase64Decode` function, which itself now has an updated return type `DecodedPayload`. That type looks like this:
    
    ```tsx
    // src/types/index.ts
    
    type DecodedPayload = {
      aud: string;
      aun: string;
      exp: number;
      iat: number;
      iss: string;
      jti: string;
      pce: boolean;
      rat: number;
      rtk: boolean;
      sid: string;
      sub: string;
      usg: string;
    };
    ```
    
    - We can then destructure yet again, this time getting the value for `rat`
    - We then run the `rat` value through a `unixTimestampToDate` conversion function, which looks like this:
    
    ```tsx
    const unixTimestampToDate = (unixTimestamp: number): Date => {
      return new Date(unixTimestamp * 1000);
    };
    ```
    
    - Finally, after all that, we can properly setup the create call to add the new tokens to the database
- And outside this expiration check, we set new a `Authorization` header using the most recent access token

Whew!

Now we can go back to the home page and adjust our query:

```tsx
// src/app/page.tsx

import { headers } from "next/headers";
import type { TrackmaniaTrack } from "@/types";

const getMapInfo = async (): Promise<TrackmaniaTrack> => {
  const mapUId = "7hk8IflYsbMbpJv2gyYzx48Zvt7";
  const url = `https://live-services.trackmania.nadeo.live/api/token/map/${mapUId}`;

  const auth = headers().get("Authorization") || "";

  const res = await fetch(url, {
    method: "GET",
    headers: {
      "Content-Type": "application/json",
      Authorization: auth
    }
  });

  if (!res.ok) {
    const text = await res.text(); // get the response body for more information

    throw new Error(`
      Failed to fetch data
      Status: ${res.status}
      Response: ${text}
    `);
  }

  return res.json();
};

const HomePage = async () => {
  const mapInfo = await getMapInfo();

  console.log("mapInfo: ", mapInfo);

  return (
    <div>
      <h1>HomePage</h1>

      <p>Track name: {mapInfo.name}</p>
    </div>
  );
};
```

Here, we leverage the added on headers using the `headers` function from `next/headers`. It is read only, but that is perfectly fine since we just want to include it with our request anyways. If you don’t include this, the request fails.

Here is what the `TrackmaniaTrack` type looks like:

```tsx
type TrackmaniaTrack = {
  author: string;
  authorTime: number;
  bronzeTime: number;
  collectionName: string;
  downloadUrl: string;
  favorite: boolean;
  fileSize: null;
  gamepadEditor: boolean;
  goldTime: number;
  mapId: string;
  mapStyle: string;
  mapType: string;
  name: string;
  nbLaps: number;
  playable: boolean;
  public: boolean;
  silverTime: number;
  submitter: string;
  thumbnailUrl: string;
  uid: string;
  updateTimestamp: number;
  uploadTimestamp: number;
  valid: boolean;
};
```

And now, if you check the terminal, you should get the correct response, as well as the map name should appear on the screen!

## Minor Adjustments

One thing I noticed was that sometimes the site errors out instead of using the most recent `access_token`. The solution is to simply refresh the page. So, as a temporary fix, I’ve created some basic back up pages:

```tsx
// src/app/error.tsx

"use client";

import Link from "next/link";
import { useRouter } from "next/navigation";

const RootErrorPage = () => {
  const router = useRouter();

  return (
    <div>
      <h1 className="text-4xl">Oops!</h1>
      <p>Something went wrong there. You can try refreshing the page, or go back home.</p>

      <button
        className="bg-teal-600 text-white mr-6 py-2 px-4"
        onClick={() => router.refresh()}
        type="button"
      >
        Refresh
      </button>

      <Link className="border-teal-600 border-2 py-2 px-4" href="/">
        Go Home
      </Link>
    </div>
  );
};

export default RootErrorPage;
```

```tsx
// src/app/not-found.tsx

import Link from "next/link";

const RootNotFound = () => {
  return (
    <div>
      <h1 className="text-4xl">Oops!</h1>
      <p>Looks like you tried accessing a page that doesn't exist.</p>
      <Link className="border-teal-600 border-2 inline-block py-2 px-4" href="/">
        Go Home
      </Link>
    </div>
  );
};

export default RootNotFound;
```

I also created a root level `loading.tsx` so something is shown while the site is loading, which provides a better UX:

```tsx
// src/app/loading.tsx

const RootLoading = () => {
  return <p>Loading...</p>;
};

export default RootLoading;
```

## Outro

I cannot stress how helpful the **[documentation](https://webservices.openplanet.dev/)** over on Openplanet was in building this app. Big thanks to them! You can also view a list of the most common endpoints **[here](https://webservices.openplanet.dev/live)**.

And that’s it! As stated in the beginning, this was mainly to demonstrate how to use Supabase to store and retrieve tokens in a secure fashion instead of using JWT for API use. Most resources I found were revolving around authorization / user accounts, not using just an API. And I wanted to push myself and reinforce what I’ve been learning lately, and I’m very proud of myself for figuring this out.

The next steps are, well, up to you! If you want to continue building out your own Trackmania web app, you now have the proper foundation to do so. If not, you have the knowledge for storing tokens if the API you are using has a similar setup.

You could perhaps display maps from the current season on the home page, and create dynamic routes to each of them. Then, on an individual map page, display some information about that map.

The next ***major*** feature would be to include NextAuth using Ubisoft as a custom provider. Buuuttt I’m gonna leave that for another day ;)

I hope you found this article helpful. As always, you can view the **[full source here](https://github.com/andrews1022/demo-nextjs-13-trackmania-api)** in case you get stuck.

If you have any feedback for me on how I could improve the code, please let me know! This was only the second time I’ve built a project using this stack, so any suggestions you may have are greatly appreciated!

Cheers, and happy coding!