Integrating Cloudflare Pages, Hono, and Supabase
As a web developer looking for a stack for greenfield projects, you might be overwhelmed by the amount of platforms you can choose when it comes to infrastructure. These platforms as a service (PaaS) can have cutthroat prices that only trigger when your app starts to attract more traffic, or don’t offer a free tier with the features needed for a complete full-stack development experience.
In a chaotic world of aggressive marketing and sales people, Cloudflare is truly a light in the darkness. They have a 100% free tier with static file hosting, CDN, DNS, SSL, and many more services - perfect to spin up a quick and effective prototype and eventually scale to cater to production-grade loads.
Using Cloudflare Workers
A Cloudflare Worker is a JavaScript edge computing service that is deployed to Cloudflare’s CDN. As of April 2024, the Cloudflare Worker free tier allows 100,000 requests per day with each request having a limit of 10ms of CPU time. This means that when you call your worker API that renders a statically-served index.html page that further fetches scripts/main.js, styles/main.css, /favicon.ico, etc. that is needed to run the page, you’re invoking N requests where N is the number of files you’ve fetched.
# serves all files in dist/
wrangler deploy --minify src/worker.ts --assets dist/Although convenient, this solution to serving your static files that are required on every page can quickly spike your Cloudflare Worker’s free-tier requests and become a problem down a line.
Similarly, using the wrangler.toml to configure a Cloudflare Worker Site bucket to serve static files will also consume your free requests due to being hosted on the Cloudflare Worker infrastructure.
Solving the Request Costs with Cloudflare Pages
To combat this predicament, we’ll have to use Cloudflare Pages, another edge runtime platform like Cloudflare Workers that was created to serve static files in tandem with enabling backend functionality with Cloudflare Worker endpoints. You can think of these Worker endpoints as serverless functions, similar to Vercel’s Next.js Serverless Functions.
The pricing and services of Cloudflare Pages differ from Cloudflare Workers quite a bit. I’ll be outlining some key ones below:
- Unlimited static file serving which solves our previous issue of consuming requests to serve scripts, stylesheets, and assets
- Git integration, similar to Vercel where you can connect a GitHub repository to automatically build on repository updates
- by extension of that, it also supports different deployment environments based on the branch you’re in
# serves all files in dist/ and uses dist/_worker.js for the server
wrangler pages deploy distDeveloping Your App with Hono
Hono is a framework for using JSX, TypeScript, ES6 features for a seamless developer experience while being web standards-first - allowing it to run in ANY JavaScript runtime like Node, Bun, Deno, and various edge runtimes (Cloudflare Workers being one of them). This allows us to simplify our development with features like routing, JWT, Web Sockets, CORS, and many more. For a full list, you can check the Hono Documentation.
# Runs the wizard to create a Hono app
$ bun create hono@latest
create-hono version 0.6.2
? Target directory my-app
? Which template do you want to use? bun
? Directory not empty. Continue? yes
✔ Cloning the template
? Do you want to install project dependencies? yes
? Which package manager do you want to use? bun
✔ Installing project dependencies
🎉 Copied project files
Get started with: cd my-appOnce you have it setup, you can start defining your route handlers, using JSX, and using its various other helpers.
Supabase Integration
One issue I had while developing with Hono was when I was trying to integrate @supabase/supabase-js to no avail while using the Cloudflare Pages Hono Vite Plugin. It seems that the bundling of my _worker.js did not succeed despite my application following proper ES Module imports.
To solve this, I had to ditch the use of the Vite plugin and resort to my own scripts to do the development server watching and building.
// package.json
"scripts": {
"prebuild": "shx cp -ur 'public' 'dist/static'",
"build:scripts": "esbuild ./app/client/scripts/*.ts --outdir=./dist/static/scripts --minify",
"build:server": "esbuild --bundle app/server/index.tsx --format=esm --outfile=dist/_worker.js",
"dev:server": "esbuild --bundle app/server/index.tsx --format=esm --watch --outfile=dist/_worker.js",
"dev:scripts": "bun run build:scripts -- --watch",
"dev:wrangler": "wrangler pages dev dist --live-reload",
"dev": "bun run prebuild && concurrently \"bun:dev:*\"",
"build": "concurrently \"bun:build:*\"",
"deploy": "bun run prebuild && bun run build && wrangler pages deploy dist"
},Using esbuild allowed me to bundle my own _worker.js file to be used for the Cloudflare Worker Server Functions in my Cloudflare Pages deployment.
The prebuild script copies my public/ files into dist/static/ for Cloudflare Page’s unlimited static file serving together with this route handler in my Hono app:
// Static File Serving for Cloudflare Pages
app.get('/static/*', async (c) => {
// @ts-ignore
return await c.env.ASSETS.fetch(c.req.raw);
});
app.get('/favicon.ico', async (c) => c.redirect('/static/favicon.ico'));
Configuring Environment Variable Bindings
You’ll have to go to your Cloudflare Dashboard to set your project’s environment bindings under Workers & Pages > YOUR_PROJECT_NAME > Settings > Environment Variables.
This was the wrangler.toml file I used for my project:
name = "YOUR_PROJECT_NAME"
compatibility_date = "2023-12-01"
compatibility_flags = [ "nodejs_compat" ]
[vars]
SUPABASE_URL = "your-supabase-url"
SUPABASE_ANON_KEY = "your-supabase-anon-key"
[env.dev.vars]
SUPABASE_URL = "your-supabase-url"
SUPABASE_ANON_KEY = "your-supabase-anon-key"Creating a Supabase Middleware Function
I created a Supabase Middleware function based on kosei28’s hono-supabase-auth project to allow us to use cookies to pass our Supabase Client to our Hono Context to use it in our handlers and other middleware.
First, we add the variable types to global.d.ts.
import {} from 'hono';
import { SupabaseClient } from '@supabase/supabase-js';
declare module 'hono' {
interface ContextRenderer {
(
content: string | Promise<string>,
props?: { title?: string; path?: string }
): Response;
}
interface Env {
// c.var types
Variables: {
supabase: SupabaseClient;
};
// c.env types
Bindings: {
SUPABASE_URL: string;
SUPABASE_ANON_KEY: string;
};
}
}Then, we create the middleware handler to use used with app.use(supabaseMiddleware).
// middleware/supabase.ts
import { createServerClient } from '@supabase/ssr';
import type { MiddlewareHandler } from 'hono';
import { getCookie, setCookie, deleteCookie } from 'hono/cookie';
import { env } from 'hono/adapter';
import { Context, Env } from 'hono';
export const supabaseMiddleware: MiddlewareHandler = async (
c: Context<Env>,
next
) => {
const { SUPABASE_URL, SUPABASE_ANON_KEY } = env(c);
const client = createServerClient(
SUPABASE_URL ?? '',
SUPABASE_ANON_KEY ?? '',
{
cookies: {
get: (key: string) => {
return getCookie(c, key);
},
set: (key: string, value: any, options: object) => {
setCookie(c, key, value, options);
},
remove: (key: string, options: object) => {
deleteCookie(c, key, options);
},
},
cookieOptions: {
httpOnly: true,
secure: true,
},
}
);
c.set('supabase', client);
await next();
};
Now we can use it via c.var.supabase.
const tasksTable = async (c: Context) => {
const { data: tasks, error } = await c.var.supabase.from('tasks').select();
if (error) {
throw error;
}
return c.render(<Table tasks={tasks} />);
};Conclusion
This setup allows me to build a full stack application with Supabase & Cloudflare Services which is globally-distributed on a CDN for free. And as long as Cloudflare continues to abide by its statement that they “believe every website should have free access to foundational security and performance.”, then it will continue to be a good technology stack for quick projects.


