oconr
Appwrite SSR auth with Next.js
March 10, 2024

Appwrite SSR auth with Next.js

With the release of Appwrite 1.5, the team introduced a much easier approach to handling authentication using server-side rendering. This post will walk you through getting a basic Next.js project setup to use SSR and handling auth using Appwrite.

Before we get started with all of the Appwrite side of things, we'll need to make sure we've got a basic Next.js project ready to go. The example I'm going to be using is very basic and is available on GitHub if you want to follow along.

Just before we get started, we want to make sure that we have installed node-appwrite in order to be able to use SSR. To do so, install it using npm or your preferred package manager.

npm install node-appwrite

Creating clients

Before we look at creating clients, we are going to need to make sure that we have all of the necessary information and credentials to get things started. For this example, we will need the endpoint of the Appwrite instance, the ID of the Appwrite project and an API key that you can find in your Appwrite console. I will be keeping these all stored in an .env file as shown below.

APPWRITE_API_KEY=
APPWRITE_ENDPOINT=https://cloud.appwrite.io/v1
APPWRITE_PROJECT_ID=

.env

When it comes to handling SSR with Appwrite, you will need two different clients to handle the two different types of requests that you will come across. We will be keeping all of these clients inside of a single file where they can be accessed across the project. This file will also contain a shared value which will be the name of the cookie where the session data is going to be stored.

export const SESSION_COOKIE = "appwrite-session";

Appwrite.ts

The first type of request you will encounter is for any requests where there is no user session or if you want to circumvent rate limiting. For this client, we will be using the API key that we setup earlier.

import { Account, Client } from "node-appwrite";

export function createAdminClient() {
	const endpoint = process.env.APPWRITE_ENDPOINT;
	const apiKey = process.env.APPWRITE_API_KEY;
	const projectId = process.env.APPWRITE_PROJECT_ID;

	if (!endpoint) {
		throw new Error("APPWRITE_ENDPOINT is not set");
	}

	if (!apiKey) {
		throw new Error("APPWRITE_API_KEY is not set");
	}

	if (!projectId) {
		throw new Error("APPWRITE_PROJECT_ID is not set");
	}

	const client = new Client()
		.setEndpoint(endpoint)
		.setProject(projectId)
		.setKey(apiKey);

	return {
		get account() {
			return new Account(client);
		}
	}
}

Appwrite.ts

As you might be able to tell, this is essentially identical to how you would previously setup the client for server-side environments like Node.js. At the end of the method, we are returning an object which contains getters for the Account. In this example, we are only going to need Account but you should add any other Appwrite products here as well.

Now that we have the client set up to be used for requests without sessions, the change with 1.5 comes with the support of sessions in server side requests. To handle this, we are going to create another method that will allow us to pass the session to the client.

import { ReadonlyRequestCookies } from 'next/dist/server/web/spec-extension/adapters/request-cookies';

export function createSessionClient(cookies: ReadonlyRequestCookies) {
	const endpoint = process.env.APPWRITE_ENDPOINT;
	const projectId = process.env.APPWRITE_PROJECT_ID;

	if (!endpoint) {
		throw new Error("APPWRITE_ENDPOINT is not set");
	}

	if (!projectId) {
		throw new Error("APPWRITE_PROJECT_ID is not set");
	}

	const client = new Client()
		.setEndpoint(endpoint)
		.setProject(projectId);

	const session = cookies.get(SESSION_COOKIE);

	if (session) {
		client.setSession(session);
	}

	return {
		get account() {
			return new Account(client);
		}
	}
}

Appwrite.ts

For the session client, we will be passing the headers from Next.js to this method in order for us to fetch the session secret from the cookies and passing that into the client. We will be making use of the cookies() method and ReadonlyRequestCookies type from Next.js to help with this.

This is all we are going to need to do in order to create the clients, now it's time to move on to using the clients.

Sign up & login

We will be using server actions when it comes to signing up and logging in, allowing us to call the methods from the server component. First up, we will create basic sign up and login forms.

export default async function LoginPage() {
	return (
		<div>
			<form action={signInWithEmail}>
				<h1>Login</h1>
				<input
					type="email"
					name="email"
					placeholder="Email address"
				/>
				<input
					type="password"
					name="password"
					placeholder="Password"
				/>
				<button type="submit">Login</button>
			</form>

			<form action={signUpWithEmail}>
				<h1>Sign up</h1>
				<input
					type="email"
					name="email"
					placeholder="Email address"
				/>
				<input
					type="password"
					name="password"
					placeholder="Password"
				/>
				<button type="submit">Sign up</button>
			</form>
		</div>
	)
}

app/login/page.tsx

Now that we've got our forms created, you can see that we are referencing the server actions I mentioned earlier, all that's left to get these forms working is to create those server actions.

import { SESSION_COOKIE, createAdminClient } from "@/Appwrite";
import { cookies } from "next/headers";
import { redirect } from "next/navigation";
import { ID } from "node-appwrite";

async function signInWithEmail(formData: FormData) {
	"use server";

	const email = formData.get("email");
	const password = formData.get("password");

	if (email === null) {
		throw new Error("Email is required");
	}

	if (password === null) {
		throw new Error("Password is required");
	}

	const { account } = createAdminClient();

	const session = await account.createEmailPasswordSession(
		email.toString(),
		password.toString()
	);

	cookies().set(SESSION_COOKIE, session.secret, {
		path: "/",
		httpOnly: true,
		sameSite: "strict",
		secure: true
	});

	redirect("/");
}

app/login/page.tsx

Here we are getting the email and password values from formData and checking to make sure that we do in fact have values for them. Next we are creating an admin client using the method we created earlier and destructuring the object so we can access account. This will all seem very similar to how Appwrite typically works on the client-side so hopefully this won't look too unfamiliar to you.

Once we are ready to login and create the session, we use account.createEmailPasswordSession() to send our credentials to the Appwrite instance and in return we will be given a session. Within the session, you will find session.secret which is what we are going to be using to allow us to handle auth with SSR. Now that we have been returned the session object, we want to store the session.secret as a cookie so we are able to access and use it later on, which we can do using cookies() from Next.js. Finally, we redirect to the homepage once we have successfully logged in.

The process for creating a new account is almost identical with the only difference being that we must first create the account before attempting to create a session with those credentials.

async function signUpWithEmail(formData: FormData) {
	"use server";

	const email = formData.get("email");
	const password = formData.get("password");

	if (email === null) {
		throw new Error("Email is required");
	}

	if (password === null) {
		throw new Error("Password is required");
	}

	const { account } = createAdminClient();

	await account.create(
		ID.unique(),
		email.toString(),
		password.toString()
	);

	const session = await account.createEmailPasswordSession(
		email.toString(),
		password.toString()
	);

	cookies().set(SESSION_COOKIE, session.secret, {
		path: "/",
		httpOnly: true,
		sameSite: "strict",
		secure: true
	});

	redirect("/");
}

app/login/page.tsx

And that's everything that we need in order to create and log in to accounts using Appwrite and SSR.

Fetching data

Now that we have created the session, we should be able to make requests using the session client we created earlier. In this example, I'm just going to show you how to retrieve the user data for the currently logged in user and display the user ID. We will also be treating this as a protected route so that if no session or user is found, it will redirect to the login page.

import { createSessionClient } from "@/Appwrite";
import { redirect } from "next/navigation";
import { cookies } from "next/headers";

export default async function Home() {
	try {
		const { account } = createSessionClient(cookies());
		const user = await account.get();

		return (
			<div>
				<h1>Logged in as</h1>
				<h2>{user.$id}</h2>
				<form action={logout}>
					<button type="submit">Logout</button>
				<form>
			</div>
		)
	} catch {
		redirect("/login");
	}
}

app/page.tsx

As mentioned earlier, we will need to pass cookies() from Next.js into the createSessionClient() method in order for it to correctly find the session cookie that we set during the login stage.

You may have noticed that I have also included a logout button, so lets quickly create the server action for logging out, it's a really straightforward process.

import { cookies } from "next/headers";
import { SESSION_COOKIE } from "@/Appwrite";

async function logout() {
	"use server";

	const { account } = createSessionClient(cookies());

	cookies().delete(SESSION_COOKIE);
	await account.deleteSession("current");

	redirect("/login");
}

app/page.tsx

The process to logout is really simple, all we need to do is delete the session cookie locally using cookies() and then delete the current session from the Appwrite instance as well using account.deleteSession().

Conclusion

And that's everything. All of this code is available in both a completed and starter form on my GitHub.

If you have any questions, please feel free to reach out on X, Threads, YouTube or my Discord.