Building Applications with CloudFlare Workers and Hasura GraphQL Engine

Introduction

Modern applications (and the migrations of legacy applications) come in all shapes, sizes, and — importantly — deployment patterns. And, yet, the database tends toward a traditional monolithic infrastructure deployment. When the user experience, and code that powers it, have to scale a new challenge emerges.

  • Integrate it into Hasura’s GraphQL API using Hasura Actions
  • Deploy the auth service as a serverless function with Cloudlare Workers
  • Use Hasura’s permissions system to control access to records in your database
  • Write a small React frontend which allows users to sign up + log in, and see private data from Hasura
  • The fundamental Hasura knowledge to build and be productive
  • A sense of accomplishment and motivation that will inspire you to take these skills, and build something awesome out of them

Check out the Finished Product

You can view the code and live frontend we’ll be building, hooked up to Hasura and the CF Worker, here in your browser:

Initial CF Worker Scaffold

Register at https://dash.cloudflare.com/sign-up/workers

$ wrangler preview
Opened a link in your default browser: https://cloudflareworkers.com/?hide_editor#35cf785b73bcdc52e3a1aaf581867ecd:https://example.com/
Your Worker responded with: request method: GET

Initial Hasura Setup

Create an account at Hasura Cloud, and generate a new free-tier project

Implementing JWT Auth and Role-based Content Access via CF Workers + Hasura Actions

Now we’ll look at how to use Cloudflare Workers to authenticate users and gate/control access to content in our database.

  1. Implement some form of authentication (Hasura is unopinionated about how this is done — we just have to return a JWT token with the right claims shape)

Setting up Authorization in Hasura

To accomplish the first part, we simply need to change an ENV variable on our Cloud instance.
Let’s set HASURA_GRAPHQL_UNAUTHORIZED_ROLE to anonymous from the Cloud dashboard settings page:

{
"type": "HS256",
"key": "this-is-a-generic-HS256-secret-key-and-you-should-really-change-it"
}
CREATE TABLE user (
id int GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
email text NOT NULL UNIQUE,
password text NOT NULL
);

Setting up Authentication with Cloudflare Workers

Integrating Authentication with Hasura is straightforward. Hasura doesn’t have opinions on HOW you authenticate clients, only the response object your Auth endpoint sends back.

  • /login for authenticating users
import * as jwt from "jsonwebtoken"// You would use an ENV var for this
const HASURA_ENDPOINT = "https://<my-hasura-app>.hasura.app/v1/graphql"
// You can set up "Backend Only" mutations, or use a secret header or a service account for this
// Do not do this in a real application please
const HASURA_ADMIN_SECRET = "please-dont-actually-do-this"
const CORS_HEADERS = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET,HEAD,POST,OPTIONS",
"Access-Control-Max-Age": "86400",
}
interface User {
id: number
email: string
password: string
}
////////////////////////////////////////////////////////
// AUTH STUFF
////////////////////////////////////////////////////////
function generateHasuraJWT(user: User) {
// Really poor pseudo-example of AuthZ logic
const isAdmin = user.email == "admin@site.com"
const claims = {} as any
claims["https://hasura.io/jwt/claims"] = {
"x-hasura-allowed-roles": isAdmin ? ["admin", "user"] : ["user"],
"x-hasura-default-role": isAdmin ? "admin" : "user",
"x-hasura-user-id": String(user.id),
}
// Don't do this, read the key from an environment var via "process.env"
const secret =
"this-is-a-generic-HS256-secret-key-and-you-should-really-change-it"
return jwt.sign(claims, secret, { algorithm: "HS256" })
}
////////////////////////////////////////////////////////
// ROUTE HANDLER STUFF
////////////////////////////////////////////////////////
function makeHasuraError(code: string, message: string) {
return new Response(JSON.stringify({ message, code }), {
status: 400,
headers: CORS_HEADERS,
})
}
async function handleSignup(req: Request) {
const payload = await req.json()
const params = payload.input.args
// Here you would store the password hashed, you would hash-compare when logging a user in
// params.password = await bcrypt.hash(params.password)
const gqlRequest = await fetch(HASURA_ENDPOINT, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Hasura-Admin-Secret": HASURA_ADMIN_SECRET,
},
body: JSON.stringify({
query: `
mutation Signup($email: String!, $password: String!) {
insert_user_one(object: {
email: $email,
password: $password
}) {
id
email
password
}
}
`,
variables: {
email: params.email,
password: params.password,
},
}),
})
const gqlResponse = await gqlRequest.json()
const user = gqlResponse.data.insert_user_one
if (!user)
return makeHasuraError(
"auth/error-inserting-user",
"Failed to create new user"
)
const jwtToken = generateHasuraJWT(user as User)
return new Response(JSON.stringify({ token: jwtToken }), {
status: 200,
headers: CORS_HEADERS,
})
}
async function handleLogin(req: Request) {
const payload = await req.json()
const params = payload.input.args
const gqlRequest = await fetch(HASURA_ENDPOINT, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Hasura-Admin-Secret": HASURA_ADMIN_SECRET,
},
body: JSON.stringify({
query: `
query FindUserByEmail($email: String!) {
user(where: { email: { _eq: $email } }) {
id
email
password
}
}
`,
variables: {
email: params.email,
},
}),
})
const gqlResponse = await gqlRequest.json()
const user = gqlResponse.data.user[0]
// if (!user) <handle case of no user created and return an error here>
// check that user.password (hashed) successfully compares against plaintext password
if (params.password != user.password)
return makeHasuraError("auth/invalid-credentials", "Wrong credentials")
const jwtToken = generateHasuraJWT(user as User)
return new Response(JSON.stringify({ token: jwtToken }), {
status: 200,
headers: CORS_HEADERS,
})
}
////////////////////////////////////////////////////////
// MAIN
////////////////////////////////////////////////////////
export async function handleRequest(request: Request): Promise<Response> {
const url = new URL(request.url)
console.log("url.pathname=", url.pathname)
switch (url.pathname) {
case "/signup":
return handleSignup(request)
case "/login":
return handleLogin(request)
default:
return new Response(`request method: ${request.method}`, {
status: 200,
headers: CORS_HEADERS,
})
}
}
  • Run the following in your browser devtools
var req = await fetch("http://127.0.0.1:8787/signup", {
method: "POST",
headers: {
contentType: "application/json",
},
body: JSON.stringify({
input: {
args: {
email: "newuser@site.com",
password: "mypassword",
},
},
}),
})
var res = await req.json()
console.log(res)
var req = await fetch("http://127.0.0.1:8787/login", {
method: "POST",
headers: {
contentType: "application/json",
},
body: JSON.stringify({
input: {
args: {
email: "newuser@site.com",
password: "mypassword",
},
},
}),
})
var res = await req.json()
console.log(res)
var req = await fetch("http://127.0.0.1:8787/login", {
method: "POST",
headers: {
contentType: "application/json",
},
body: JSON.stringify({
input: {
args: {
email: "newuser@site.com",
password: "WRONGPASSWORD",
},
},
}),
})
var res = await req.json()
console.log(res)

Deploying to Cloudflare Workers

To deploy your working API and turn it into a live, production endpoint, we just need to use the Wrangler CLI to run the publish command.
That looks like this (run from inside of the directory containing your Worker code and TOML config):

$ wrangler publish

Writing a frontend, testing it all out

Below is an overly-simplified example of frontend code that consumes this CF Worker auth API and uses the JWT bearer token to authenticate the client to Hasura, to fetch private data.

import React, { useEffect, useRef, useState } from "react"
import "./App.css"
const HASURA_ENDPOINT = "https://<your-cloud-app>.hasura.app/v1/graphql"function App() {
const form = useRef(null)
const [jwt, setJWT] = useState("")
const [isLoggingIn, setIsLoggingIn] = useState(false)
const [isSigningUp, setIsSigningUp] = useState(false)
const [privateStuff, setPrivateStuff] = useState<any>([])
useEffect(() => {
if (!jwt) return
fetchPrivateStuff().then(setPrivateStuff)
}, [jwt])
async function signup(email: string, password: string) {
const req = await fetch(HASURA_ENDPOINT, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
query: `
mutation Signup($email: String!, $password: String!) {
signup(args: { email: $email, password: $password }) {
token
}
}
`,
variables: {
email,
password,
},
}),
})
const res = await req.json()
const token = res?.data?.signup?.token
if (!token) alert("Signup failed")
setJWT(token)
}
async function login(email: string, password: string) {
const req = await fetch(HASURA_ENDPOINT, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
query: `
mutation Login($email: String!, $password: String!) {
login(args: { email: $email, password: $password }) {
token
}
}
`,
variables: {
email,
password,
},
}),
})
const res = await req.json()
const token = res?.data?.login?.token
if (!token) alert("Login failed")
setJWT(token)
}
async function fetchPrivateStuff() {
const req = await fetch(HASURA_ENDPOINT, {
method: "POST",
headers: {
Authorization: `Bearer ${jwt}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
query: `
query AllPrivateStuff {
private_table_of_awesome_stuff {
id
something
}
}
`,
}),
})
const res = await req.json()
const data = res?.data?.private_table_of_awesome_stuff
if (!data) alert("Couldn't retrieve any records")
return data
}
if (!jwt) {
if (isSigningUp || isLoggingIn)
return (
<form
ref={form}
onSubmit={(e) => {
e.preventDefault()
const data = new FormData(form.current!)
const email = data.get("email") as string
const password = data.get("password") as string
if (isLoggingIn) return login(email, password)
if (isSigningUp) return signup(email, password)
}}
>
<input name="email" type="email" placeholder="email" />
<input name="password" type="password" placeholder="password" />
<button type="submit">Submit</button>
</form>
)
else
return (
<div>
<p>Please sign up or log in</p>
<button onClick={() => setIsSigningUp(true)}>
Click here to sign up
</button>
<button onClick={() => setIsLoggingIn(true)}>
Click here to log in
</button>
</div>
)
}
return (
<div>
<p>Here is a list of private stuff only authenticated users can see:</p>
<ul>
{privateStuff?.map((it: any) => (
<li>
{it.id}: {it.something}
</li>
))}
</ul>
</div>
)
}
export default App

⚡️ Instant realtime GraphQL APIs! Connect Hasura to your database & data sources (GraphQL, REST & 3rd party API) and get a unified data access layer instantly.