DEV Community

Cover image for Authenticate Your React Site With Appwrite
Debajyati Dey
Debajyati Dey

Posted on • Originally published at devsunite.com

Authenticate Your React Site With Appwrite

It can feel overwhelming to add authentication to your React app, especially if you are a beginner.

Because, you know, in most cases you would absolutely need a backend and server that will have a database, to implement secure authentication for a website. Without these components, any authentication system you create is fundamentally insecure and easily bypassed.

Now, if you are new to building websites and have mostly built client side apps with React.js, it is natural for you to think why it is like that. So let me clarify these things for you.

WHY AUTHENTICATION NEEDS A BACKEND & DATABASE

If you need to verify a user's identity, you would need to go through a process that is called Authentication.

This involves two key steps:

  1. Storing user credentials: You need a secure place to store information like usernames and hashed passwords.

  2. Verifying credentials: When a user tries to log in, you need to check the provided credentials against the stored ones.

Now you would be like, "Okay, okay I know all these but still why would I need a separate server/backend for this stuff? Huh?"

WHY WOULD A BACKEND & DATABASE BE VITAL FOR SECURE AUTHENTICATION

Client-side (front-end) code, which runs in the user's browser, is completely exposed. Anyone with basic browser developer tools can see, modify, or disable the code. If you were to store passwords or authentication logic in the front end, it would be trivial for a malicious user to:

  1. Access all user credentials: Storing passwords (even if hashed) in the front end means they're all publicly available, making your site a massive security risk.

  2. Bypass the login process: An attacker could simply modify the front-end code to skip the authentication check and access protected pages.

A server-side backend is a trusted environment where your code and data are hidden from the user. It's the only place you can securely store sensitive information and perform logic that can't be tampered with.

Not only that, to remember a user's account information, you need to store it somewhere. A database is a dedicated system for storing, managing, and retrieving data. Without it, you'd have no way to create new user accounts or remember existing ones after the user closes their browser or navigates away.

Lastly, after a user logs in, the server needs to remember that they are authenticated for subsequent requests. This is called session management. The server creates a unique session token or a JSON Web Token (JWT) after a successful login and sends it to the client. The client then includes this token with every request for a protected resource. The backend server verifies the token to ensure the request is coming from a logged-in user.

A simple front-end-only approach can't maintain this secure, continuous state. Even if you stored a token in the browser's local storage, there would be no way to verify its authenticity without a server-side component.

I hope that makes enough sense now.

Now you are going to question me, "What is Appwrite? What is its relation with the article? Is it a database? Are you going to give a tutorial on setting up a separate backend server with database for adding & managing secure user Authentication in my client side React App?" (Based on the article title you've read, likely ...)

So yeah, at the end of the article, if you read thoroughly, all of your questions will be answered!

WHAT IS APPWRITE?

Appwrite is a powerful, open-source backend-as-a-service (BaaS) platform, designed to simplify and accelerate the development of web, mobile, and native applications. It provides a suite of pre-built, ready-to-use APIs and tools that handle common backend tasks, allowing developers to focus on the frontend and core logic of their applications.

That's a lot to take in. Now what does it mean? Why is Appwrite useful to you?

In other fullstack apps, like the ones you generally build with Django or Spring Boot, you would manage most of the auth system on the backend side.

Appwrite's main goal is to replace your backend logic with entirely client-side requests managed by Appwrite’s fully managed server system.

That means we can build entirely client side yet secure websites without needing to build our own web servers and database. Appwrite services manage all user data and authentication for us.

How, cool, is that!

Now let's see how to use Appwrite to set up user authentication, in our CSR React App.

GUIDING WITH CODE

Assuming you have an existing typescript react project -

First, Install necessary dependencies -

npm install appwrite --save
npm install --save-dev @types/node
Enter fullscreen mode Exit fullscreen mode

Now you need to set up your Appwrite project:-

Creating Appwrite Project

First, you need to create a project in the Appwrite Console.

  1. Create a New Project: On the Appwrite Console dashboard, select "Create project," provide a name, and a unique project ID will be automatically generated.

  2. Add a Web Platform: In your project's dashboard, click "Add a platform" and choose "Web App." Enter a name and set the hostname to localhost to allow requests from your local development server.

Create a lib/constants.ts file, to import from env vars -

export const appwriteEndpoint = import.meta.env.VITE_APPWRITE_ENDPOINT;
export const appwriteProjectID = import.meta.env.VITE_APPWRITE_PROJECT_ID;
Enter fullscreen mode Exit fullscreen mode

Create a configuration file for appwrite. Name it - appwrite.ts -

import { Client, Account, TablesDB, ID } from "appwrite";
import { appwriteEndpoint, appwriteProjectID } from "@/lib/constants";

const client = new Client()
    .setEndpoint(appwriteEndpoint)
    .setProject(appwriteProjectID);

const account = new Account(client);
const database = new TablesDB(client);

export { client, account, database, ID };
Enter fullscreen mode Exit fullscreen mode

Find your project IDs in settings page of Appwrite console and create an .env.local file to store them with the exact names (VITE_APPWRITE_ENDPOINT, VITE_APPWRITE_PROJECT_ID) we specified them in the lib/constants.ts file.

Appwrite Project Settings

Now, backup your existing App.tsx file, by renaming it into MainApp.tsx and rename the component defined inside into MainApp.

Now create a new App.tsx file, and paste the following code into the file -

import React, { useState, useEffect } from 'react';
import { account } from './lib/appwrite';
import { Models, ID } from 'appwrite';
import MainApp from './MainApp';

const App: React.FC = () => {
  const [loggedInUser, setLoggedInUser] = useState<Models.User<Models.Preferences> | null>(null);
  const [email, setEmail] = useState<string>('');
  const [password, setPassword] = useState<string>('');
  const [name, setName] = useState<string>('');

  useEffect(() => {
    const checkUserSession = async () => {
      try {
        const user = await account.get();
        setLoggedInUser(user);
      } catch {
        setLoggedInUser(null);
      }
    };

    checkUserSession();
  }, []);

  const login = async () => {
    try {
      await account.createEmailPasswordSession(email, password);
      const user = await account.get();
      setLoggedInUser(user);
      alert('Logged in successfully!');
    } catch (error: any) {
      alert(`Login failed: ${error.message}`);
    }
  };

  const register = async () => {
    try {
      await account.create(ID.unique(), email, password, name);
      await login();
      alert('Registered and logged in successfully!');
    } catch (error: any) {
      alert(`Registration failed: ${error.message}`);
    }
  };

  const logout = async () => {
    try {
      await account.deleteSession('current');
      setLoggedInUser(null);
      alert('Logged out successfully!');
    } catch (error: any) {
      alert(`Logout failed: ${error.message}`);
    }
  };
  if (loggedInUser) {
    console.log(loggedInUser.name, loggedInUser.email);
  }

  return (
    <div style={{ padding: "20px", fontFamily: "sans-serif" }}>
      <h2>Appwrite React Authentication</h2>
      {loggedInUser ? (
        <>
          <MainApp />
        </>
      ) : (
        <p>Not logged in</p>
      )}

      <div
        style={{
          display: "flex",
          flexDirection: "column",
          gap: "10px",
          maxWidth: "300px",
        }}
      >
        <input
          type="email"
          placeholder="Email"
          value={email}
          onChange={(e) => setEmail(e.target.value)}
          style={{ padding: "8px" }}
        />
        <input
          type="password"
          placeholder="Password"
          value={password}
          onChange={(e) => setPassword(e.target.value)}
          style={{ padding: "8px" }}
        />
        <input
          type="text"
          placeholder="Name (optional)"
          value={name}
          onChange={(e) => setName(e.target.value)}
          style={{ padding: "8px" }}
        />
      </div>

      <div style={{ marginTop: "20px", display: "flex", gap: "10px" }}>
        <button onClick={login} style={{ padding: "10px", cursor: "pointer" }}>
          Login
        </button>
        <button
          onClick={register}
          style={{ padding: "10px", cursor: "pointer" }}
        >
          Register
        </button>
        <button onClick={logout} style={{ padding: "10px", cursor: "pointer" }}>
          Logout
        </button>
      </div>
    </div>
  );
};

export default App;
Enter fullscreen mode Exit fullscreen mode

The useEffect hook ensures that the user's session is checked when the app loads. It calls account.get() from the Appwrite SDK. If the call is successful, it means a user is already logged in, and their data is stored in the loggedInUser state. If it fails (due to no active session), the state is set to null. This prevents the user from having to log in every time they refresh the page.

The component contains three main asynchronous functions that interact with the Appwrite backend:

  • login(): This function is called when the login button is clicked. It uses account.createEmailPasswordSession() to authenticate the user with their email and password. If successful, it then calls account.get() to retrieve the user's details and updates the loggedInUser state.

  • register(): Called when the register button is clicked. It first uses account.create() to create a new user account in Appwrite. After a successful registration, it automatically calls the login() function to create a new session for the newly created user, providing a seamless experience.

  • logout(): This function uses account.deleteSession('current') to terminate the current user's session. It then sets the loggedInUser state to null to update the UI and reflect that the user has been logged out.

Finally, run your app in development mode to see the site with authentication in action.

npm run dev -- --open
Enter fullscreen mode Exit fullscreen mode

This command starts your local development server and automatically opens the app in your browser at http://localhost:5173. You can now register a new user, log in, and log out using the buttons on the page.

Now for more security -

You can always enable features like -

  • Password dictionary: Blocks common or weak passwords.

  • Password Recovery: Submitting a request to the confirmation endpoint. The verification link sent to the user's email address is valid for 1 hour.

  • Password history: Prevents reuse of old passwords.

  • Disallow personal data: Restricts use of personal information in passwords.

Purpose

  • Protects user data.

  • Encourages stronger password habits.

  • Contributes to a safer internet.

SECURITY BEST PRACTICES WITH APPWRITE

  1. Environment Variables Always use environment variables (like .env.local) to store sensitive information, just like your Appwrite endpoint and project ID. This prevents hard-coding secrets into your source code, which could accidentally be exposed in version control.
  2. Password Policies Appwrite allows you to enforce strong password policies. Enable these in your Appwrite console under Auth > Settings:
  3. Minimum password length (e.g., 8+ characters)
  4. Require a mix of uppercase, lowercase, numbers, and symbols
  5. Block common passwords and personal data.
  6. Email Verification Enable email verification to ensure users provide valid email addresses. This adds an extra layer of security and reduces fake accounts. How to enable:
  7. In the Appwrite console, go to Auth > Settings > Email Verification.
  8. Toggle the switch to require email verification for new users.

Troubleshooting Common Issues

Even with Appwrite’s almost perfect setup, a few hiccups can occur during development. Here’s how to tackle the most common ones:
CORS Errors

  • Symptom: You see errors like CORS policy: No 'Access-Control-Allow-Origin' header.

  • Fix:

    • Go to your Appwrite Console → Project → Platform Settings.
    • Ensure your development URL (e.g., http://localhost:5173) is correctly listed under allowed origins.
    • For production, add your live domain (e.g., https://yourdomain.com) as a platform.

Invalid Project ID or Endpoint

  • Symptom: SDK calls fail silently or throw “Invalid project” errors.

  • Fix:

    • Double-check your .env.local file for correct values.
    • Ensure VITE_APPWRITE_ENDPOINT and VITE_APPWRITE_PROJECT_ID match the values in your Appwrite Console.

Session Not Persisting

  • Symptom: User gets logged out on page refresh.

  • Fix:

    • Make sure account.get() is called inside a useEffect on app load.
    • Avoid clearing cookies or local storage unless explicitly intended.

SDK Version Conflicts

  • Symptom: Type errors or missing methods in Appwrite SDK.

  • Fix:

    • Run npm ls appwrite to check the installed version.
    • Update using npm install appwrite@latest to ensure compatibility with TablesDB and other new features.

DEPLOYING YOUR APP

When deploying your React app:

  • Ensure your Appwrite project is configured for your production domain.

  • Update environment variables for the production endpoint and project ID.

  • Use HTTPS to encrypt all traffic.

  • If suitable, add row level security to your Database, to get more security on user data also.

WHY APPWRITE IS A GAME-CHANGER

Appwrite eliminates the need to build and maintain a custom backend for authentication. It’s:

  • Open-source: Transparent and community-driven.

  • Scalable: Handles millions of users out of the box.

  • Secure: Built-in protections against common vulnerabilities.

FINAL THOUGHTS

Adding authentication to your React app doesn’t have to be daunting. With Appwrite, you can focus on building a great user experience while leaving the heavy lifting of security and infrastructure to a trusted platform.

It was just a brief overview.

You must explore Appwrite’s documentation for more advanced features like roles and teams; and maybe the Appwrite Tables DB and Appwrite sites hosting if you are interested.

Thank you for reading this article so far. Hope this article was helpful for you. Have a nice day ahead, and most importantly, happy coding!

Top comments (0)