I Built a Serverless Live Chat App with Next.js, Fauna, and WunderGraph for GraphQL Live Queries

A step-by-step guide to creating a scalable, real-time chat app using Serverless technologies, with a little help from NextAuth.js for GitHub sign-ins. Who needs WebSockets when you’ve got Live Queries?

Prithwish Nath
JavaScript in Plain English

--

If you’re building apps that work with real-time data, you’re probably using WebSockets. These allow a web browser and a web server to communicate in real-time by keeping a persistent connection open between the two — data is pushed to clients as soon as it becomes available, rather than having the client constantly poll the server to check for new data.

But what if your app is serverless — running on infrastructure managed by a cloud provider?

To facilitate high elasticity and fault tolerance, these environments are designed to be stateless and ephemeral by their very nature, meaning there’s no guarantee that your code will be running on the same physical server from one request to the next — and thus no persistent connection between the client and the server.

So what’s the secret sauce to real-time apps without WebSockets? Let’s find out by building this Slack/Discord clone using Next.js as our JS framework, Fauna (using GraphQL) as our database, and WunderGraph as a backend-for-frontend (BFF) that facilitates the link between the two. Our app will also use GitHub sign-ins, and we’ll use NextAuth.js for our auth needs.

You’ll find the GitHub repo for this tutorial here.

Live Queries on the Server — The Secret Sauce

The GraphQL specification defines Subscriptions — you set up a stateful WebSocket connection between the client and server, and then subscribe to an event on the server. When the server sees an event that matches a defined Subscription, it sends requested data over the WebSocket connection to the client — and we have our data.

💡 This explanation is a little handwave-y, but bear with me. Going over the differences between the transports graphql-ws (GraphQL over WebSocket), graphql-helix (GraphQL over SEE) and @n1ru4l/socket-io-graphql-server (GraphQL over Socket.io) are a little beyond the scope of a tutorial.

When you can’t use WebSockets (e.g., on serverless platforms, as mentioned before), you can’t use Subscriptions…but that’s where Live Queries come in!

They aren’t part of the GraphQL spec, so definitions differ by client library; but essentially, unlike Subscriptions, Live Queries:

  1. Aim to subscribe to the current state of server data — not events (so, new payloads whenever the query would yield different data)
  2. Can use plain old HTTP polling at an interval, instead, when WebSocket connections aren’t available.

I can see readers picking up their pitchforks, right on cue.

“Client-side HTTP polling for real-time data?! That’s insanely expensive! What’s going to happen with multiple users in the same chat?”

All valid concerns! But…that’s where WunderGraph comes in. You see, we’re not going to be polling this data client-side. Instead, we’re pushing these Live Querying responsibilities onto WunderGraph, our Backend-for-Frontend server.

💡 WunderGraph is a dev tool that lets you define your data dependencies — GraphQL, REST APIs, Postgres and MySQL databases, Apollo Federations, and anything else you might think of — as config-as-code, and then it introspects them, and turns all of it into a single virtual graph that you can then query and mutate via GraphQL, and then expose as JSON-over-RPC for your client.

When you’re making Live Queries from the WunderGraph layer, no matter how many users are subscribed to your live chat on the frontend, you’ll only ever have a single polling instance active at any given time, for your entire app.

Much better than raw client-side polling; but is this perfect? No — the polling interval still adds some latency and isn’t ideal for apps that need truly instant feedback, and without a JSON patch, the entire result of a query will be sent over the wire to the client every single time, even the unchanged data — but for our use-case, this is more than acceptable.

Architecture

Let’s talk about the flow of this app.

Our users sign in with their GitHub account (via OAuth) for the chatroom. We’re building a workgroup/internal team chat, so using GitHub as the provider makes sense.

Using NextAuth greatly streamlines our auth story — and it even has an official integration for Fauna — a serverless database! This makes the latter a really good pick as a database for both auth and business logic (storing chat messages).

Our Next.js frontend is relatively simple thanks to WunderGraph — it’ll provide us with auto-generated (and typesafe!) querying and mutation hooks built on top of Vercel’s SWR, and that’s what our components will use to fetch (chat messages, and the list of online users) and write (new chat messages) data.

Let’s get started!

Step 0: The Setup

Next.js + WunderGraph

WunderGraph’s create-wunder-graph CLI is the best way to set up both our BFF server and the Next.js frontend in one go, so let’s do just that. Just make sure you have the latest Node.js LTS installed, first.

npx create-wundergraph-app my-project -E nextjs

Then, cd into the project directory, and:

npm install && npm start

NextAuth

Our NextAuth setup involves a little busywork. Let’s get the base package out of the way first.

npm install next-auth

Next, get the NextAuth adapter for FaunaDB. An “adapter” in NextAuth.js connects your application to whatever database or backend system you want to use to store data for users, their accounts, sessions, etc. Adapters are technically optional, but since we want to persist user sessions for our app, we’ll need one.

npm install @next-auth/fauna-adapter faunadb

Finally, we’ll need a “provider” — trusted services that can be used to sign in a user. You could define your own OAuth Provider if you wanted to, but here, we can just use the built-in GitHub provider.

Read the GitHub OAuth docs for how to register your app, configure it, and get the URL and Secret Key from your GitHub account (NOT optional). For the callback URL in your GitHub settings, use http://localhost:3000/api/auth/callback/github

Finally, once you have the GitHub client ID and secret key, put them in your Next.js ENV file (as GITHUB_ID and GITHUB_SECRET) respectively.

Step 1: The Data

Fauna is a geographically distributed (a perfect fit for the Serverless/Edge era of Vercel and Netlify) document-relational database that aims to offer the best of both SQL (schema-based modeling) and NoSQL (flexibility, speed) worlds.

It offers GraphQL out of the box, but the most interesting feature about Fauna is that it makes it trivially easy to define stored procedures and expose them as GraphQL queries via the schema. (They call these User Defined Functions or UDFs). Very cool stuff! We’ll make use of this liberally.

Sign up at Fauna, create a database (without sample data), note down your URL and Secret in your Next.js ENV (as FAUNADB_GRAPHQL_URL and FAUNADB_TOKEN respectively) and you can move on to the next step.

Auth

To use Fauna as our auth database, we’ll need to define its Collections (think tables) and Indexes (all searching in Fauna is done using these) in a certain way. NextAuth walks us through this procedure here, so it’s just a matter of copy-pasting these commands into your Fauna Shell.

CreateCollection({ name: "accounts" })
CreateCollection({ name: "sessions" })
CreateCollection({ name: "users" })
CreateCollection({ name: "verification_tokens" })


CreateIndex({
name: "account_by_provider_and_provider_account_id",
source: Collection("accounts"),
unique: true,
terms: [
{ field: ["data", "provider"] },
{ field: ["data", "providerAccountId"] },
],
})
CreateIndex({
name: "session_by_session_token",
source: Collection("sessions"),
unique: true,
terms: [{ field: ["data", "sessionToken"] }],
})
CreateIndex({
name: "user_by_email",
source: Collection("users"),
unique: true,
terms: [{ field: ["data", "email"] }],
})
CreateIndex({
name: "verification_token_by_identifier_and_token",
source: Collection("verification_tokens"),
unique: true,
terms: [{ field: ["data", "identifier"] }, { field: ["data", "token"] }],
})

Now that we have Fauna set up to accommodate our auth needs, go back to your project directory, and create a ./pages/api/auth/[…nextauth.ts] file with these contents:

That’s it, we have some basic auth set up! We’ll test this out in just a minute. But first…

Business Logic

It’s time to define the GraphQL schema needed for our business logic, i.e users, chats, and sessions (with slight modifications to account for the existing NextAuth schema). Go to the GraphQL tab in your Fauna dashboard, and import this schema.

Why does chat have a proper relation to users, but not sessions? This is a limitation of the way NextAuth currently interacts with Fauna for storing user sessions — but we’ll get around it, as you’ll soon see.

See that last query marked with a @resolver directive? This is a Fauna stored procedure/User Defined Function we’ll be creating! Go to the Functions tab and add it.

getUserIDByEmail

Query(
Lambda(
["email"],
Select(["ref", "id"], Get(Match(Index("user_by_email"), Var("email"))))
)
)

Being familiar with the FQL syntax helps, but these functions should be self-explanatory — they do exactly what their names suggest — take in an argument, and look up value(s) that match it using the indexes defined earlier. When used with the @resolver directive in our schema, they are now exposed as GraphQL queries — incredibly useful. You could do almost anything you want with Fauna UDFs, and return whatever data you want.

Step 2: Data Dependencies and Operations

WunderGraph works by introspecting all data sources you define in a dependency array and building data models for them.

First, make sure your ENV file is configured properly:

GITHUB_ID="XXXXXXXXXXXXXXXX"
GITHUB_SECRET="XXXXXXXXXXXXXXXX"
FAUNA_SECRET="XXXXXXXXXXXXXXXX"
FAUNADB_GRAPHQL_URL="https://graphql.us.fauna.com/graphql"
FAUNADB_TOKEN="Bearer XXXXXXXXXXXXXXXX"

Replace with your own values, and double-check to make sure the Fauna URL is set to the region you’re hosting your instance in!

…and then, add our Fauna database as a dependency in WunderGraph, and let it do its thing.

You can then write GraphQL queries/mutations to define operations on this data (these go in the .wundergraph/operations directory), and WunderGraph will generate typesafe Next.js client hooks for accessing them.

Getting all messages here, along with their related Users.

The way NextAuth works with GitHub is, it stores currently active sessions in the database, with an expires field. You can read the userId from the active sessions table, and whoever that userId belongs to can be considered to be currently online.

So, this GraphQL query fetches all users who currently have an active session — we’ll use this in our UI to indicate the ones who are online. WunderGraph makes it trivial to perform JOINs using multiple queries — and using its _join field (and the @transform directive to cut down on unnecessary nesting), we can overcome NextAuth not adding the proper relations in Fauna — seen here while fetching the user linked with a session by their userId.

With a brief enough time-to-live for GitHub OAuth tokens, you will not have to worry about stale data when it comes to online users, and NextAuth is smart enough to invalidate stale tokens anyway when these users try to login with an expired token.

And finally, this is our only mutation, triggered when the currently signed-in user sends a new message. Here you see how Fauna handles relations in Mutations — the connect field (read more here) is used to connect the current document being created(a chat message), with an existing document (a user)

Step 3: On To The Frontend

You’ll have to wrap your app in <SessionProvider> to be able to use NextAuth’s useSession hooks, by exposing the session context at the top level of your app.

Also, I’m using TailwindCSS for styling — instructions for getting it set up with Next.js here.

NextAuth’s hooks make it trivially easy to implement authorization — the second part of auth. Use useSession to check if someone is signed in (this returns a user object with GitHub username, email, and avatar URL — nifty!), and the signIn and signOut functions redirect users to those pages automatically.

You can style custom NextAuth signIn/signOut pages yourself if you want (instructions here) but the default, unbranded styles work just fine for our needs.

Step 4: Showing Online Users

Nothing much to see here; our strategy for determining online users was mentioned in the GraphQL query for this already.

Step 5: The Chat Window (Feed and Input)

WunderGraph’s generated hooks for querying and mutation are built on top of SWR, so if you want to learn more about how manually-initiated mutations work via the trigger function, read up on it here.

Here, we see how WunderGraph can turn any standard query into a Live Query with just one added option — liveQuery: true. You don’t even have to be using a GraphQL-native API like Fauna’s for this, WunderGraph’s Live Queries work with any datasource.

To fine-tune polling intervals for Live Queries, though, check out ./wundergraph/wundergraph.operations.ts, and adjust this value in seconds.

For the timestamp, we’ll use a simple utility function to get the current time in epoch — the number of seconds that have elapsed since January 1, 1970 (midnight UTC/GMT) — and converting it into a human-readable string to be stored in our database.

Step 6: The Navbar

The NavBar component uses NextAuth’s useSession hook again, to get the current user’s GitHub name and avatar URL and render them. It also uses signOut to, well, sign out.

You’re done! Head on over tolocalhost:3000, and try it out. For screenshots here, I’m using two GitHub accounts on two different browsers, but I’ve tested this with up to five users and it works great.

That’s All, Folks!

Bringing together NextAuth, Fauna, and WunderGraph for GraphQL Live Queries is a powerful combination that will serve you well in creating real-time, interactive chat experiences — whether you’re building something simple for a small community, or complex, enterprise-level platforms.

For Serverless applications, using WunderGraph as a backend-for-frontend with GraphQL Live Queries will give you far better latency than client-side polling — only one polling instance for all clients subscribed to the chat app, equals reduced server load and network traffic.

Here are some things you should note, though, going forward:

For Quality of Life

  • Leave liveQuery: true commented out while you’re building the UI, and only turn it on while testing the chat features. You don’t want to thrash the Fauna server with calls — especially if on the limited free tier!
  • If you make changes to your Fauna GraphQL Schema during development, you’ll probably find that WunderGraph’s introspection doesn’t catch the new changes. This is intentional — WunderGraph builds a cache after the first introspection, and works on that instead to cut down on resources wasted on full introspections every single time.

To get around this, run npx wunderctl generate –-clear-cache if you’ve made changes to your database schema during development and want the models and hooks regenerated.

For Deployments

If you need something clarified re: WunderGraph as a BFF/API Gateway, head on over to their Discord community, here. Happy coding!

--

--