Serverless Remix Sessions with Cloudflare Pages

K - Feb 1 '22 - - Dev Community

Using sessions with Remix is a pretty straightforward task. Usually, you put your session data into a cookie and are done with it. But cookies come with some downsides. For example, the client sends them with every request. This makes cookies a lousy place to store vast amounts of data.

But we're lucky! If we deploy our Remix app onto Cloudflare Pages, we get a globally replicated key-value store to store all our session data!

Workers KV can store all our session data on the backend, and we only need to send a session ID in the cookie to find that data on later requests.

Strangely, the way we access Workers KV on a Cloudflare Worker function is different from a Cloudflare Pages function. Because, why should things work as expected for once?! :D

I got the following error but only found examples online that access KVs via a global variable.

ReferenceError: KV is not defined.
Attempted to access binding using global in modules.
You must use the 2nd `env` parameter passed to exported
handlers/Durable Object constructors, or `context.env`
with Pages Functions.
Enter fullscreen mode Exit fullscreen mode

So, in this article, I'll explain how to set up a basic Remix session with KV and Pages.

Initializing a Remix Project

To start, we create a Remix project with the help of NPX.

$ npx create-remix@latest
Enter fullscreen mode Exit fullscreen mode

I answered the questions like this:

? Where would you like to create your app? example-remix-app
? Where do you want to deploy? Choose Remix if you're unsure; it's easy to change deployment targets. Cloudflare Pages
? TypeScript or JavaScript? TypeScript
? Do you want me to run `npm install`? Yes
Enter fullscreen mode Exit fullscreen mode

But the only meaningful answer here is to use "Cloudflare Pages" as a deployment target.

Adding a KV Storage to Our Scripts

Inside the package.json is a dev:wrangler script; we need to extend it with a KV parameter.

  "scripts": {
    "build": "cross-env NODE_ENV=production remix build",
    "dev": "cross-env NODE_ENV=development run-p dev:*",
    "postinstall": "remix setup cloudflare-pages",
    "dev:remix": "remix watch",
    "dev:wrangler": "wrangler pages dev ./public --watch ./build --kv sessionStorage",
    "start": "npm run dev:wrangler"
  },
Enter fullscreen mode Exit fullscreen mode

When we run the dev script, this will ensure that the local runtime environment Miniflare will bind a KV with the name sessionStorage to our Pages function.

Later, we can access our KV from context.env.sessionStorage.

Remix and Cloudflare's context Object

The next step is to create a session storage. In our case, it will be a Cloudflare KV based one.

And here we're already at the point where things differ between Cloudflare Pages and Workers.

The examples for Cloudflare Workers all use a global KV namespace variable, which doesn't exist.

So, for our example KV above, we would access a global sessionStorage variable. They create the storage before the request gets handled and then export it as a module for all other modules to use. But as explained, this doesn't work here.

Pages supplies our handler function inside functions/[[path]].js with a context object that has an env attribute. This means the KV reference isn't available before we handle a request.

Now, the problem here is this context object gets picked apart by Remix's handleRequest function, which, in turn, is created with the createPagesFunctionHandler function.

In the end, we don't get direct access to the context object, but only parts of it.

Creating a Session Storage

To create session storage anyway, we have to hook a callback between the Pages onRequest function and our Remix app.

To do so, we can use the getLoadContext callback createPagesFunctionHandler accepts as a parameter.

Simply update the code inside functions/[[path]].js as follows:

import { createPagesFunctionHandler } from "@remix-run/cloudflare-pages"
import { createCloudflareKVSessionStorage } from "@remix-run/cloudflare-pages"

import * as build from "../build"

const handleRequest = createPagesFunctionHandler({
  build,
  getLoadContext: (context) => {
    const sessionStorage = createCloudflareKVSessionStorage({
      cookie: {
        name: "SESSION_ID",
        secrets: ["YOUR_COOKIE_SECRET"],
        secure: true,
        sameSite: "strict",
      },
      kv: context.env.sessionStorage,
    })

    return { sessionStorage }
  },
})

export function onRequest(context) {
  return handleRequest(context)
}
Enter fullscreen mode Exit fullscreen mode

As we can see, the getLoadContext callback receives Cloudflare's context object, and we can use it to create our session storage.

Using the Session

The final question is, where does the object we returned from the callback end up?

Inside the context object of your Remix loader and action functions!

So, if you now write a loader, you can look into the session.

I wrote a simple example for an index route inside app/routes/index.ts:

import { json, LoaderFunction } from "remix"

export const loader: LoaderFunction = async ({ context, request }) => {
  const session = await context.sessionStorage.getSession(
    request.headers.get("Cookie")
  )

  const headers = {}

  if (!session.has("userId")) {
    session.set("userId", `user:${Math.random()}`)
    headers["Set-Cookie"] = await context.sessionStorage.commitSession(session)
  } else {
    console.log(session.get("userId))
  }
  return json(null, { headers })
}
Enter fullscreen mode Exit fullscreen mode

The context contains our sessionStorage, an abstraction around Workers KV.

This storage knows in which cookie the session ID is stored and uses the session ID to load the corresponding data from the KV.

In the first request, the cookie won't contain a session ID, so that we will end up with an empty session object.

We then use this session to check if it has a userId and, if not, add one to it.

Then the session gets saved to KV again, and the cookie gets the session ID.

Finally, to ensure our session ID gets sent to the client, we have to return a response with the Set-Cookie header.

Running the Example

To run the example, use the dev script, which calls the updated dev:wrangler script, which binds the KV.

$ npm run dev
Enter fullscreen mode Exit fullscreen mode

After one request, we will see a SESSION_ID cookie if we look into our cookies.

Looking into the log output after the second request, we see the randomly generated userId.

Conclusion

Setting up serverless session handling with Remix and Cloudflare Pages isn't too hard. Things are just a bit more dynamic than with Cloudflare Workers.

Remix offers a nice abstraction around session handling, and it works seamlessly with serverless KV storage.

Thanks to maverickdotdev for solving the mystery about the getLoaderContext!

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .