Serverless Remix Sessions with Cloudflare Pages
K
Posted on February 1, 2022
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.
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
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
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"
},
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)
}
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 })
}
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
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
!
Posted on February 1, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.