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:
Storing user credentials: You need a secure place to store information like usernames and hashed passwords.
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:
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.
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
Now you need to set up your Appwrite project:-
First, you need to create a project in the Appwrite Console.
Create a New Project: On the Appwrite Console dashboard, select "Create project," provide a name, and a unique project ID will be automatically generated.
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
localhostto 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;
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 };
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.
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;
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 usesaccount.createEmailPasswordSession()to authenticate the user with their email and password. If successful, it then callsaccount.get()to retrieve the user's details and updates theloggedInUserstate.register(): Called when the register button is clicked. It first usesaccount.create()to create a new user account in Appwrite. After a successful registration, it automatically calls thelogin()function to create a new session for the newly created user, providing a seamless experience.logout(): This function usesaccount.deleteSession('current')to terminate the current user's session. It then sets theloggedInUserstate 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
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
-
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. - Password Policies Appwrite allows you to enforce strong password policies. Enable these in your Appwrite console under Auth > Settings:
- Minimum password length (e.g., 8+ characters)
- Require a mix of uppercase, lowercase, numbers, and symbols
- Block common passwords and personal data.
- 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:
- In the Appwrite console, go to Auth > Settings > Email Verification.
- 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.localfile for correct values. - Ensure
VITE_APPWRITE_ENDPOINTandVITE_APPWRITE_PROJECT_IDmatch the values in your Appwrite Console.
- Double-check your
Session Not Persisting
Symptom: User gets logged out on page refresh.
-
Fix:
- Make sure
account.get()is called inside auseEffecton app load. - Avoid clearing cookies or local storage unless explicitly intended.
- Make sure
SDK Version Conflicts
Symptom: Type errors or missing methods in Appwrite SDK.
-
Fix:
- Run
npm ls appwriteto check the installed version. - Update using
npm install appwrite@latestto ensure compatibility withTablesDBand other new features.
- Run
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)