Web developers who need fast, secure, and scalable applications, often turn to Cloudflare Workers to achieve these goals. But integrating Workers with Remix, the framework that focuses on building server-rendered React applications, can be challenging. Fortunately, Superflare is here to help.
Superflare is an experimental full-stack toolkit designed to work with Cloudflare Workers, providing features like a relational ORM for D1 databases (Cloudflare queryable databases built on SQLite) and utilities for R2 storage (Cloudflare object storage).
In this article, we’ll discuss how Superflare improves the integration of Remix and Cloudflare Workers. Then, we’ll demonstrate why Superflare is a must-have for web developers looking to maximize the power of Cloudflare Workers with Remix.
For our tutorial, we’ll create a simple link shortener, similar to Bit.ly. We’ll also add an authentication system and a database to store the link.
Jump ahead:
- What is Superflare?
- Why use Cloudflare Workers with Remix and Superflare?
- Working with Superflare
- Creating a Superflare demo project
- Working with Cloudflare Workers and Remix
What is Superflare?
Cloudflare Workers provides serverless computing for the edge, meaning developers can write and deploy code that runs at Cloudflare’s edge locations anywhere in the world. This allows for fast and efficient execution of code and reduces latency for end users.
Superflare is built on top of Cloudflare’s primitives, which means you cannot use Superflare without using Cloudflare. Cloudflare acts as a reverse proxy that sits in front of any type of cloud infrastructure, making internet properties faster, more secure, and more reliable.
Superflare aims to provide a simpler solution by embracing vendor lock-in with Cloudflare and focusing on its core features, like D1 databases and R2 storage. It is designed to work like a “glue” between Cloudflare and your framework of choice, without dictating routing, controllers, or views.
Despite these advantages, there are a few tradeoffs to consider when using Superflare:
- Required use of Cloudflare: This requirement means that Superflare may not be suitable for all projects
- Dedicated ORM for D1 databases: This feature may not be appealing to those who prefer to use existing ORMs, like Prisma
- Tension between Superflare and Cloudflare’s official roadmap: It’s possible that Cloudflare is working on its own full-stack suite, which could make parts of Superflare obsolete or cause compatibility issues in the future
Why use Cloudflare Workers with Remix and Superflare?
Using Cloudflare Workers with Remix and Superflare offers several benefits:
- Performance: Cloudflare Workers run on Cloudflare’s edge network, reducing latency and increasing application performance by executing code close to the end user. This results in faster load times and better overall performance
- Native compatibility: Remix is designed to work seamlessly with Cloudflare Workers, allowing us to write apps that leverage modern Fetch API and native web APIs. This compatibility simplifies development and enhances performance by removing the need for server-side Node.js processes
- Scalability: Cloudflare Workers automatically scales with traffic, ensuring the application remains responsive even during high-traffic periods
- Data storage and integration: Cloudflare offers multiple data storage options, like KV and Durable Objects, that can be easily integrated with Remix applications. Superflare simplifies this integration by providing a package that works with Remix and Cloudflare Workers, making it easy to set up and manage data storage and session handling
- Platform agnostic: Remix is designed to be transportable and adaptable to different deployment platforms, allowing us to easily switch between providers if needed
By combining Cloudflare Workers, Remix, and Superflare, developers can build fast, scalable, and efficient web applications that leverage the power of Cloudflare’s edge network and the modern features of Remix and Superflare.
Working with Superflare
Superflare is a versatile and powerful toolkit for building applications on Cloudflare Workers, providing essential utilities and seamless integration with popular frameworks. Here are some of Superflare’s most important features:
- Authentication: Simplifies the process of adding authentication to a Remix app
- Relational ORM for D1 databases: Provides an ORM to interact with D1 databases, offering a simple and intuitive way to manage data models in an application
- Utilities for R2 storage: Includes utilities for working with R2 storage, simplifying file uploads and management in an application
- Sessions: Provides a way to store information about users across requests; Superflare provides a SuperflareSession class that acts as a wrapper around a simple Session object
- Queues: Allows jobs to be created and processed asynchronously in an application using a simple interface for working with Cloudflare Queues
- Events: Offers a powerful way to communicate changes across different parts of our application
- Broadcasting: Simplifies the process of implementing real-time communication in an web application using WebSockets
- New ORM: Introduces a new ORM that follows the
ActiveRecord
style for data model interactions, making it easier to work with our data models
Creating a Superflare demo project
To demonstrate using Cloudflare Workers with Remix and Superflare, we’ll create a simple link shortener, similar to Bit.ly. For our example, we’ll add an authentication system and a database to store the shortened link.
You can access the full source code for the project on GitHub and see the demo app that we’ll build here:
Let’s start by running the following command to create a new Superflare project:
npx superflare@latest new tok
This command will ask you about bindings for D1, R2, and KV, and will offer to create them if necessary:
Setting up a Superflare local development server
Next, run the following command to start a Superflare local development server:
npx superflare dev
Superflare uses Wrangler, the Cloudflare Workers CLI, to spin up the development server. Superflare also uses Wrangler for tasks like modifying the D1 database through migrations. Wrangler is used for generating, configuring, building, previewing, and publishing Cloudflare Workers projects from our development environment. It accesses Cloudflare OAuth token to manage Workers’ resources on our behalf.
To confirm that the local dev server is set up, open http://127.0.0.1:8788 in your browser.
Running the database migration
Storing data in a database is one of the most critical aspects of developing a full-stack application. Fortunately, Superflare offers support for the D1 Document database from Cloudflare, which is built on top of SQLite and provides an efficient and scalable solution.
Run the Superflare database migration using the following command:
npx superflare migrate
To check if everything is working, go to: http://127.0.0.1:8788/register and try to register a new user. After successful registration, you’ll see a dashboard page like this:
To create a model and migration for the D1 Document database, we can use the CLI provided by Superflare:
npx superflare generate model Path --migration
This will create a new file:
Generating model Path Generated model Path at ~/tok/app/models/Path.ts Migration generated at ~/tok/db/migrations/0001_create_paths.ts
To define the column we can edit the ~/tok/db/migrations/0001_create_paths.ts
file:
import { Schema } from "superflare"; export default function () { return Schema.create("paths", (table) => { table.increments("id"); table.integer("user_id"); table.string("route").unique(); table.string("url"); table.string("link"); table.timestamps(); }); }
Now, run the migration:
npx superflare migrate
Next, let’s create one more model to store unique visitors:
npx superflare generate model Visitor --migration
Edit the schema in the ~/tok/db/migrations/0002_create_visitors.ts
file, like so:
import { Schema } from "superflare"; export default function () { return Schema.create("paths", (table) => { table.increments("id"); table.integer("user_id"); table.integer("path_id"); table.string("ip_address"); table.timestamps(); }); }
Now we can run the migration to create the tables:
npx superflare migrate
This command will generate the superflare.env.d.ts
file, which will be used as our database schema type. This will enable us to access the table properties from our model.
Here’s an example of the schema properties of the User
model:
... interface UserRow { id: number; email: string; password: string; createdAt: string; updatedAt: string; }
Now, let’s try out Superflare’s inbuilt authentication system.
Implementing the Superflare authentication
When leveraging the Superflare CLI to create a new project, the authentication system is conveniently preconfigured.
Here are the steps necessary to successfully implement registration logic within the system:
export async function action({ request, context: { auth } }: ActionArgs) { if (await auth.check(User)) { return redirect("/dashboard"); } const formData = new URLSearchParams(await request.text()); const email = formData.get("email") as string; const password = formData.get("password") as string; if (await User.where("email", email).count()) { return json({ error: "Email already exists" }, { status: 400 }); } const user = await User.create({ email, password: await hash().make(password), }); auth.login(user); return redirect("/dashboard"); }
Use the following code for the login logic:
export async function action({ request, context: { auth } }: ActionArgs) { if (await auth.check(User)) { return redirect("/dashboard"); } const formData = new URLSearchParams(await request.text()); const email = formData.get("email") as string; const password = formData.get("password") as string; if (await auth.attempt(User, { email, password })) { return redirect("/dashboard"); } return json({ error: "Invalid credentials" }, { status: 400 }); }
And, use this code for the logout logic:
import { type ActionArgs, redirect } from "@remix-run/server-runtime"; export async function action({ context: { auth } }: ActionArgs) { auth.logout(); return redirect("/"); }
Storing data with the Superflare ORM
Next, let’s create an app/routes/path.ts
file to create, edit, and read the shortened links data:
import { type ActionArgs, redirect, json } from "@remix-run/server-runtime"; import { nanoid } from "nanoid"; import { Path } from "~/models/Path"; import { User } from "~/models/User"; type UpdateRequest = { path: Path; url: string; title?: string; route: string; }; async function handleUpdate({ path, url, title, route }: UpdateRequest) { path.url = url; path.title = title || ""; path.route = route; path.save(); return redirect("/histories"); } export async function action({ request, context: { auth } }: ActionArgs) { if (!(await auth.check(User))) { return redirect("/login"); } const user = (await auth.user(User)) as User; const formData = new URLSearchParams(await request.text()); let url = formData.get("url") as string; let title = formData.get("title") as string; let route = formData.get("route") as string; let id = formData.get("id") as unknown as number; if (id) { let path = await Path.find(id); if (!path) { return json({ error: "Path not found!" }); } return handleUpdate({ path, url, title, route }); } if (!route) { route = nanoid(6); } await Path.create({ user_id: user.id, title, url, route, }); return redirect("/histories"); }
The Superflare database ORM draws inspiration from Laravel; if you’ve worked with that framework before, you may already be familiar with how the ORM interface looks. When a request is captured via a Cloudflare Workers endpoint useful data, such as request headers and database instances, are provided in the action({ request, context: { auth, env } }: ActionArgs)
code.
Retrieving user data from authentication is also straightforward using const user = (await auth.user(User)) as User;
. The action
function runs on the server when a user makes a POST
request, enabling access to both user request information and the database. This is where the backend work happens.
Redirecting user requests
Now, let’s create a redirect route, app/routes/$route.ts
:
import { LoaderArgs, redirect } from "@remix-run/server-runtime"; import { Path } from "~/models/Path"; import { Visitor } from "~/models/Visitor"; export async function loader({ request, params }: LoaderArgs) { const path = await Path.where("route", params.route || "").first(); if (!path) return redirect("/404"); await Visitor.create({ user_id: path.user_id, path_id: path.id, ip_address: request.headers.get("cf-connecting-ip"), }); return redirect(path.url); }
This route enables access to all URLs under the root, /
, using a GET
request, except for routes created within the routes
folder. This implementation also grants the ability to retrieve user-requested data, including the user’s IP address, by using the request.headers.get("cf-connecting-ip")
header. This feature can prove particularly advantageous in establishing unique request data.
Writing custom queries in the Superflare D1 database
The Superflare ORM provides a user-friendly interface for interacting with our database; however, there are certain limitations on the types of queries that can be used in the ORM. For instance, the ORM does not support the distinct
query, which is essential for displaying analytical data.
Fortunately, we have the ability to access the database connection and create our own raw queries to overcome these limitations. Here’s an example of how to run custom raw queries to the D1 database:
export async function loader({ context: { auth, env } }: LoaderArgs) { if (!(await auth.check(User))) { return redirect("/login"); } const user = (await auth.user(User)) as User; const totalPaths = await Path.where("user_id", user.id).count(); const totalVisitor = await Visitor.where("user_id", user.id).count(); let totalUniqueVisitor = 0; try { const query = `select COUNT(DISTINCT ip_address) AS totalUniqueVisitor from visitors where user_id = '${user.id}';`; console.log("query", query); const res = await env.DB.prepare(query).all(); if (res.success) { totalUniqueVisitor = (res.results as any[])[0].totalUniqueVisitor; console.log("totalUniqueVisitor", totalUniqueVisitor); console.log("res.results", res.results); } } catch (e: any) { console.log("error", e); } const analytic = { links: { number: totalPaths, description: `${totalPaths} Total Links`, }, visitor: { number: totalVisitor, description: `${totalVisitor} Visitors`, }, uniqueVisitor: { number: totalUniqueVisitor, description: `${totalUniqueVisitor} Unique Visitors`, }, }; return json({ user, analytic, }); }
Using this data, we can display analytic data:
Checking the authenticated request
Checking that a user is authenticated is also easy with Superflare. Here’s an example:
import { Link, useLoaderData } from "@remix-run/react"; import { LoaderArgs } from "@remix-run/server-runtime"; import { Button } from "~/components/button"; import GenerateLink from "~/components/generate-link"; import { User } from "~/models/User"; export async function loader({ context: { auth } }: LoaderArgs) { return { isAuthenticated: await auth.check(User), }; } export default function Index() { const { isAuthenticated } = useLoaderData<typeof loader>(); return ( <div className="w-full h-screen grid place-content-center relative"> <div className="absolute top-0 left-0 right-0 p-8"> <div className="flex gap-2 container mx-auto"> <div className="w-full"> <a href="/" className="font-semibold text-lg"> Tok.link </a> </div> <div className="flex gap-2"> {isAuthenticated ? ( <Link to="/dashboard"> <Button variant="link">Dashboard</Button> </Link> ) : ( <> <Link to="/login"> <Button variant="link">Login</Button> </Link> <Link to="/register"> <Button>Register</Button> </Link> </> )} </div> </div> </div> <GenerateLink /> </div> ); }
Working with Cloudflare Workers and Remix
To integrate Superflare with an existing Remix app on the Cloudflare Workers adapter, we first need to install the @superflare/remix
package:
npm install @superflare/remix
Setting up Cloudflare Workers in a Remix project
Next, create a worker.ts
file. We’ll use this worker to handle any incoming requests to Remix and also to serve static assets using the @awwong1/kv-asset-handler package.
Import the handleFetch
function from @superflare/remix
, and import your local superflare.config.ts
file as config
:
import { createRequestHandler } from "@remix-run/cloudflare"; import config from "./superflare.config"; import * as build from "./build"; import { getAssetFromKV, NotFoundError, MethodNotAllowedError, } from "@cloudflare/kv-asset-handler"; import manifestJSON from "__STATIC_CONTENT_MANIFEST"; import { handleQueue, handleScheduled } from "superflare"; import { handleFetch } from "@superflare/remix"; let remixHandler: ReturnType<typeof createRequestHandler>; const assetManifest = JSON.parse(manifestJSON); export default { async fetch(request: Request, env: Env, ctx: ExecutionContext) { try { return await getAssetFromKV( { request, waitUntil(promise) { return ctx.waitUntil(promise); }, }, { ASSET_NAMESPACE: env.__STATIC_CONTENT, ASSET_MANIFEST: assetManifest, } ); } catch (e) { if (e instanceof NotFoundError || e instanceof MethodNotAllowedError) { // fall through to the remix handler } else { return new Response("An unexpected error occurred", { status: 500 }); } } if (!remixHandler) { remixHandler = createRequestHandler(build as any, process.env.NODE_ENV); } try { return handleFetch(request, env, ctx, config, remixHandler); } catch (reason) { console.error(reason); return new Response("Internal Server Error", { status: 500 }); } } };
Now, create the D1 database by selecting D1 under “workers” and clicking Create database:
Next, create the wrangler.json
file to store the deployment configuration:
{ "name": "tok", "compatibility_flags": [ "nodejs_compat" ], "compatibility_date": "2023-05-01", "site": { "bucket": "public" }, "main": "worker.ts", "define": { "process.env.REMIX_DEV_SERVER_WS_PORT": "8002" }, "d1_databases": [ { "binding": "DB", "name": "tok-db", // create from "database_id": "ddcd847d-...-57bd8391bffe" } ] }
Next, add the Cloudflare .env file, cloudflare.env.d.ts
:
/// <reference types="@cloudflare/workers-types" /> /** * This is only used in Workers mode. */ declare module "__STATIC_CONTENT_MANIFEST" { const value: string; export default value; } declare const process: { env: { NODE_ENV: "development" | "production"; }; }; interface Env { /** * Only used in Workers mode. */ __STATIC_CONTENT: string; DB: D1Database; BUCKET: R2Bucket; APP_KEY: string; QUEUE: Queue; CHANNELS: DurableObjectNamespace; }
Then, create the Superflare config file, superflare.config.ts
:
import { defineConfig } from "superflare"; export default defineConfig<Env>((ctx) => { return { appKey: ctx.env.APP_KEY, database: { default: ctx.env.DB, }, }; });
Now, edit the remix.env.d.ts
file, like so:
/// <reference types="@remix-run/dev" /> /// <reference types="@remix-run/cloudflare" /> /// <reference types="@cloudflare/workers-types" /> import type { SuperflareAppLoadContext } from "@superflare/remix"; declare module "@remix-run/server-runtime" { export interface AppLoadContext extends SuperflareAppLoadContext<Env> {} }
Deploying a Remix app with Superflare
To deploy the Superflare project using the Remix app with Superflare, follow these steps:
Step 1: Add the APP_KEY
environment variable
Open your terminal and run the following command to add the APP_KEY
environment variable to your Superflare project:
npx wrangler secret put APP_KEY -j
Step 2: Run the migration
To apply the database changes to your Superflare project, run the following command in your terminal:
npx wrangler d1 migrations apply DB -j
Step 3: Publish the site
Publish your Superflare project with the Remix app using the Wrangler CLI by running the following command in your terminal:
npx wrangler publish -j
Conclusion
In this tutorial, we built a link shortener app with authentication and a database to demonstrate the benefits of using Cloudflare Workers with Remix and Superflare — namely, improved performance, scalability, data storage, and data integration.
The features offered by Superflare, such as authentication, ORM, and session handling, can make the development process smoother and more streamlined. Superflare is a promising toolkit for developers who are looking to build fast, efficient, and secure applications and are interested in working with Cloudflare Workers and Remix. It will be interesting to see how Superflare develops in the future.
Get set up with LogRocket's modern error tracking in minutes:
- Visit https://logrocket.com/signup/ to get an app ID
- Install LogRocket via npm or script tag.
LogRocket.init()
must be called client-side, not server-side - (Optional) Install plugins for deeper integrations with your stack:
- Redux middleware
- NgRx middleware
- Vuex plugin
$ npm i --save logrocket
// Code:
import LogRocket from 'logrocket';
LogRocket.init('app/id');
Add to your HTML:
<script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script>
<script>window.LogRocket && window.LogRocket.init('app/id');</script>