Table of Contents
What are Next.js and Appwrite, and why do we need them?
Next.js
Appwrite
Introduction to Multi-Tenant SaaS Authorization
Importance of tenant isolation and granular access control
What is Permit, and what are its benefits?
System Architecture
Backend Implementation with Permit
1. Setting up Permit
2. Installing dependencies
3. Setting up Appwrite
4. Create file structure and files
Reason for creating an extensive backend service using Appwrite
Conclusion
Home Web Front-end JS Tutorial Building a Multi-Tenant SaaS Application with Next.js (Backend Integration)

Building a Multi-Tenant SaaS Application with Next.js (Backend Integration)

Apr 11, 2025 am 08:23 AM

Building a Multi-Tenant SaaS Application with Next.js (Backend Integration)

I built a functional multi-tenant SaaS application (an EdTech app) with your everyday tech tool and you can do the same.

First, what’s a multi-tenant SaaS application?

Multi-tenant SaaS applications let you serve multiple customers from a single codebase. But to do this, you’ll need to manage secure and tenant-specific access, and this can be challenging when done manually. That’s why I decided to use Permit, a modern authorization tool that simplifies this process.

In this article, I’ll show you how to simplify authorization for your SaaS applications using Permit, with a step-by-step example of building a demo app featuring tenant isolation and role-based access control (RBAC) with Next.js and Appwrite.

What are Next.js and Appwrite, and why do we need them?

Next.js

Next.js is a React-based framework that provides server-side rendering (SSR), static site generation (SSG), API routes, and performance optimizations out of the box.

For this project, I used Next.js because:

  • It allows pre-rendering of pages, which improves performance and SEO.
  • Its built-in routing makes it easy to manage page transitions and dynamic content.
  • It integrates easily with backend services like Appwrite and Permit.io for authentication and authorization.

Appwrite

Appwrite is a backend-as-a-service (BaaS) platform that provides user authentication, databases, storage, and serverless functions. Using a service like Appwrite eliminates the need to build a backend from scratch, so you can focus on frontend development while having access to backend capabilities.

For this project, I used Appwrite:

  • To handle user registration, login, and session management.
  • To provide a structured NoSQL database to store tenant-specific data.

Using Next.js and Appwrite together allowed me to create a scalable, high-performance multi-tenant SaaS app while keeping the development process efficient.

Introduction to Multi-Tenant SaaS Authorization

A multi-tenant SaaS app is software that serves multiple users or groups of users, called tenants, using a single software instance of the application.

What it means is that in a multi-tenant SaaS architecture, multiple customers (tenants) share the same application infrastructure or use the same application but maintain data isolation.

A practical example of this is a project management tool like Trello.

  • It is a single infrastructure that runs on shared servers and has the same codebase for all its users.
  • Each company using Trello (e.g., Company A and Company B) is a tenant.
  • It isolates data:
    • Employees of Company A can only see their projects, tasks, and boards.
    • Employees of Company B cannot access or view Company A’s data, and vice versa.

This ensures that while resources are shared, each tenant’s data and activities are private and secure.

In a multi-tenant application, even within a tenant, some users will have higher access to some information, while some members will be restricted to certain resources.

Authorization in such applications must:

  • Ensure users can’t access other tenants’ or customers’ data or resources. This is called isolating tenants.
  • Ensure users within a tenant can access only resources their roles permit by providing granular access control.
  • Handle more users, tenants, and roles without slowing down or degrading performance.

Importance of tenant isolation and granular access control

Tenant isolation keeps data secure by ensuring that each customer’s information stays private. While granular access control ensures users within an organization only get the permissions they need.

Implementing authorization in your SaaS apps can be complex and tricky, but it doesn’t have to be when you have an authorization tool like Permit.

What is Permit, and what are its benefits?

Permit is an easy-to-use authorization tool for managing access in any application, including multi-tenant apps. Using Permit.io in your application allows you to easily define and assign roles with specific permissions for access control within your application. Aside from creating roles within the application, you can also add conditions and rules based on user or resource attributes to specify what each user can and cannot do.

Now that you know most of what you need to know about Permit and its benefits, let’s get into the main deal—building a SaaS application with Next.js and integrating Permit for authorization.

To demonstrate the power of Permit, we’ll be building a multi-tenant Edtech SaaS platform.

Building an EdTech SaaS platform involves several challenges, including user authentication, role-based access control (RBAC), and multi-tenancy. We’ll use Next.js for the frontend, Appwrite for authentication and database management, and Permit for fine-grained authorization.

Tech Stack Overview

TechnologyPurposeNext.jsFrontend frameworkShadCN TailwindcssUI Components and stylingZustandState managementAppwriteAuthentication & backendPermit.ioRole-based access control

System Architecture

The application follows a backend-first approach:

  1. Backend (Node.js Express)
    • Handles API requests and business logic.
    • Uses Appwrite for authentication and database management.
    • Implements Permit for authorization, defining roles and permissions.
    • Ensures every request is validated before data access.
  2. Frontend (Next.js)
    • Connects to the backend to fetch data securely.
    • Uses role-based UI rendering, meaning users only see what they’re authorized to access.
    • Restricts actions (like creating assignments) based on permissions.

By enforcing authorization at the API level, we ensure that users cannot bypass restrictions, even if they manipulate the frontend.

At the end of this guide, you’ll have a fully functional multi-tenant EdTech SaaS app, where:

  • Admins can add and view students.
  • Teachers can add and view students, as well as create assignments.
  • Students can only view their assigned coursework.

This article provides a step-by-step breakdown of how I implemented Permit to handle authorization to build this project, so follow along and build yours.

Backend Implementation with Permit

To enforce role-based access control (RBAC) and tenant isolation, we need to:

  1. Set up Permit and define roles, tenants, and policies.
  2. Integrate Permit in the backend (Node.js Express).
  3. Protect API routes using middleware that checks permissions before allowing requests.

Let’s go step by step.

1. Setting up Permit

Before writing any code, you need to

  • Create an account on Permit.
Building a Multi-Tenant SaaS Application with Next.js (Backend Integration)

You will be presented with the onboarding, but once you enter your organization name, you can just skip the setup.

  • Create a resource and actions

Navigate to the policy section, where you’ll create a resource and actions that you can perform on that resource.

Building a Multi-Tenant SaaS Application with Next.js (Backend Integration)

Once you are done creating your resources, it should look like this:

Building a Multi-Tenant SaaS Application with Next.js (Backend Integration)
  • Creating a role

After creating the resources, navigate to the Roles page using the Roles tab. You’ll see that some roles have automatically been assigned.

Building a Multi-Tenant SaaS Application with Next.js (Backend Integration)

Delete those roles and create new roles. Each role will have specific rules associated with it, about what a user can and cannot do. Create the Admin role first, as it will later serve as a building block for the RBAC conditions. Click the Add Role button at the top and create the roles.

Building a Multi-Tenant SaaS Application with Next.js (Backend Integration)

When you are done creating your roles, it should look like this:

Building a Multi-Tenant SaaS Application with Next.js (Backend Integration)

Great!

Now that you have created your resources and roles, you can now configure permissions in the policy editor.

  • Configuring permissions in the policy editor

Go back to the Policy Editor and this is what the roles will look like now, with each individual resource defined and the actions that you can select. You’re now ready to give permissions to the roles to perform the selected actions on the resource.

Building a Multi-Tenant SaaS Application with Next.js (Backend Integration)

When you are done selecting the actions for each role, click the Save changes button at the bottom right of the page.

  • Copy API keys

Finally, to use the cloud PDP of Permit, you are going to need the API key of your current environment. For this project you are going to be using the development environment key. Proceed to Settings and click API Keys, scroll down to Environment API keys, click “Reveal Key,” then copy it.

Building a Multi-Tenant SaaS Application with Next.js (Backend Integration)

After setting up your Permit dashboard, you can now move on to your backend.

2. Installing dependencies

To get started, you’ll need to have Node.js installed on your computer. After ensuring Node.js is installed on your system, follow these steps:

  • Start by creating a new project using the following commands:
mkdir backend
cd backendNpm init -y
Copy after login
  • Then, install the following packages:
npm install express dotenv permitio cors appwwrite axios jsonwebtoken
Copy after login
  • Configure Permit in Express. In your .env file, store your API key:
PERMIT_API_KEY=your-permit-key-you-copied-earlier
Copy after login

3. Setting up Appwrite

  • Go to Appwrite and create a new project by inputting a project name and selecting a region. Note down your Project ID and API Endpoint; that’s what you’ll input as the values in your .env file. Your ENV file should be looking like this:
PERMIT_API_KEY=your-permit-key-you-copied-earlier
APPWRITE_ENDPOINT=https://cloud.appwrite.io/v1
APPWRITE_PROJECT_ID=your-project-id
Copy after login
  • Now proceed to databases to create your database, then copy your database ID to paste it into your ENV file.
Building a Multi-Tenant SaaS Application with Next.js (Backend Integration)

Your ENV file should now be looking like this:

PERMIT_API_KEY=your-permit-key-you-copied-earlier
APPWRITE_ENDPOINT=https://cloud.appwrite.io/v1
APPWRITE_PROJECT_ID=your-project-id
APPWRITE_DATABASE_ID=your-database-id
Copy after login

Now create the following collections in the Appwrite Database with the following attributes:

  • Profiles collection
Building a Multi-Tenant SaaS Application with Next.js (Backend Integration)
  • Students collection
Building a Multi-Tenant SaaS Application with Next.js (Backend Integration)
  • Assignments collection
Building a Multi-Tenant SaaS Application with Next.js (Backend Integration)

What your ENV file should be looking like at this point:

PERMIT_API_KEY=your-permit-key-you-copied-earlier
PERMIT_PROJECT_ID=copy-from-dashboard
PERMIT_ENV_ID=copy-from-dashboard
APPWRITE_ENDPOINT=https://cloud.appwrite.io/v1
APPWRITE_PROJECT_ID=your-project-id
APPWRITE_DATABASE_ID=your-database-id
APPWRITE_PROFILE_COLLECTION_ID=your-id
APPWRITE_ASSIGNMENTS_COLLECTION_ID=your-id
APPWRITE_STUDENTS_COLLECTION_ID=your-id
JWT_SECRET=generate-this-by-running//openssl rand -base64 16
PORT=8080
Copy after login

4. Create file structure and files

Now create a src folder in the root of the file. Then generate the tsconfig.json file in the root folder and paste the following code into it:

<span>{
</span>    <span>"compilerOptions": {
</span>      <span>"target": "ES6",
</span>      <span>"module": "commonjs",
</span>      <span>"outDir": "./dist",
</span>      <span>"esModuleInterop": true,
</span>      <span>"forceConsistentCasingInFileNames": true,
</span>      <span>"strict": true,
</span>      <span>"skipLibCheck": true,
</span>      <span>"resolveJsonModule": true,
</span>      <span>"baseUrl": "./",
</span>      <span>"paths": {
</span>        <span>"@/*": ["src/*"]
</span>      <span>}
</span>    <span>},
</span>    <span>"include": ["src/**/*"],
</span>    <span>"exclude": ["node_modules", "dist"]
</span>  <span>}</span>
Copy after login

This tsconfig.json configures the TypeScript compiler to target ES6, use CommonJS modules, and output files to ./dist. It enforces strict type-checking, enables JSON module resolution, sets up path aliases for src, and excludes node_modules and dist from compilation.

Inside of the src folder, create the following folders: api, config, controllers, middleware, models, and utils.

  • Utils folder
    • Now, create a new permit.ts file in the utils folder project to initialize Permit using the following code:
<span>import { Permit } from 'permitio';
</span><span>import { PERMIT_API_KEY } from '../config/environment';
</span><span>// This line initializes the SDK and connects your Node.js app
</span><span>// to the Permit.io PDP container you've set up in the previous step.
</span><span>const permit = new Permit({
</span>  <span>// your API Key
</span>  token<span>: PERMIT_API_KEY, // Store your API key in .env
</span>  <span>// in production, you might need to change this url to fit your deployment
</span>  pdp<span>: 'https://cloudpdp.api.permit.io', // Default Permit.io PDP URL
</span>  <span>// if you want the SDK to emit logs, uncomment this:
</span>  log<span>: {
</span>    level<span>: "debug",
</span>  <span>},
</span>  <span>// The SDK returns false if you get a timeout / network error
</span>  <span>// if you want it to throw an error instead, and let you handle this, uncomment this:
</span>  <span>// throwOnError: true,
</span><span>});
</span>
<span>export default permit;</span>
Copy after login

This file initializes Permit’s SDK for Node.js, connecting it to the Permit PDP container using an API key stored in the environment. It configures logging for debugging and sets up the SDK to handle errors silently unless explicitly configured to throw them.

  • Next, create a file called errorHandler.ts and paste the following code:
<span>// Utility functions (e.g., error handling)
</span><span>import { Request, Response, NextFunction } from 'express';
</span>
<span>export const errorHandler = (err: any, req: Request, res: Response, next: NextFunction) => {
</span>  <span>console.error('Error:', err.message || err);
</span>  res<span>.status(err.status || 500).json({
</span>    error<span>: err.message || 'Internal Server Error',
</span>  <span>});
</span><span>};</span>
Copy after login

This file defines an Express error-handling middleware that logs errors and sends a JSON response with the error message and status code. It defaults to a 500 status code if no specific status is provided.

  • Models folder
    • Create a file called profile.ts and paste the following code:
<span>export interface Profile {
</span>    name<span>: string;
</span>    email<span>: string;
</span>    role<span>: 'Admin' | 'Teacher' | 'Student';
</span>    userId<span>: string;
</span><span>}</span>
Copy after login

This file defines a TypeScript Profile interface with properties for name, email, role, and userId, where role is restricted to specific values: Admin, Teacher, or Student.

  • Create assignment.ts file and paste the following code:
<span>import { database, ID } from '../config/appwrite';
</span><span>import { DATABASE_ID, ASSIGNMENTS_COLLECTION_ID } from '../config/environment';
</span>
<span>export interface AssignmentData {
</span>  title<span>: string;
</span>  subject<span>: string;
</span>  className<span>: string;
</span>  teacher<span>: string;
</span>  dueDate<span>: string;
</span>  creatorEmail<span>: string;
</span><span>}
</span>
<span>// Create a new assignment
</span><span>export async function createAssignmentInDB(data: AssignmentData) {
</span>    <span>return await database.createDocument(
</span>      <span>DATABASE_ID,
</span>      <span>ASSIGNMENTS_COLLECTION_ID,
</span>      <span>ID.unique(),
</span>      data
    <span>);
</span><span>}
</span>
<span>// Fetch all assignments
</span><span>export async function fetchAssignmentsFromDB() {
</span>  <span>const response = await database.listDocuments(DATABASE_ID, ASSIGNMENTS_COLLECTION_ID);
</span>  <span>return response.documents;
</span><span>}</span>
Copy after login

This file provides functions to interact with an Appwrite database for managing assignments. It defines an AssignmentData interface and includes functions to create a new assignment and fetch all assignments from the database.

  • Create a student.ts file and paste the following code:
<span>import { database, ID, Permission, Role, Query } from '../config/appwrite';
</span><span>import { DATABASE_ID, STUDENTS_COLLECTION_ID } from '../config/environment';
</span>
<span>export interface StudentData {
</span>  firstName<span>: string;
</span>  lastName<span>: string;
</span>  gender<span>: 'girl' | 'boy' | 'Boy' | 'Girl';
</span>  className<span>: string;
</span>  age<span>: number;
</span>  creatorEmail<span>: string;
</span><span>}
</span>
<span>// Create a new student
</span><span>export async function createStudentInDB(data: StudentData) {
</span>    <span>return await database.createDocument(
</span>      <span>DATABASE_ID,
</span>      <span>STUDENTS_COLLECTION_ID,
</span>      <span>ID.unique(),
</span>      data<span>,
</span>      <span>[
</span>        Permission<span>.read(Role.any()),  // Public read permission
</span>      <span>]
</span>    <span>);
</span><span>}
</span>
<span>// Fetch all students
</span><span>export async function fetchStudentsFromDB() {
</span>  <span>const response = await database.listDocuments(DATABASE_ID, STUDENTS_COLLECTION_ID);
</span>  <span>return response.documents;
</span><span>}</span>
Copy after login

This file provides functions to manage student data in an Appwrite database. It defines a StudentData interface and includes functions to create a new student with public read permissions and fetch all students from the database.

  • Middleware folder
    • Create auth.ts file and paste the following code:
<span>import { Request, Response, NextFunction } from 'express';
</span><span>import jwt from 'jsonwebtoken';
</span>
<span>// Extend Request type to include 'user'
</span><span>interface AuthenticatedRequest extends Request {
</span>  user<span>?: {
</span>    id<span>: string;
</span>    role<span>: string;
</span>  <span>};
</span><span>}
</span>
<span>const authMiddleware = (req: AuthenticatedRequest, res: Response, next: NextFunction): void => {
</span>  <span>const token = req.headers.authorization?.split(' ')[1];
</span>
  <span>if (!token) {
</span>    res<span>.status(401).json({ error: 'Unauthorized. No token provided' });
</span>    <span>return
</span>  <span>}
</span>
  <span>try {
</span>    <span>const decoded = jwt.verify(token, process.env.JWT_SECRET!) as { id: string; role: string };
</span>    req<span>.user = decoded;
</span>    <span>next();
</span>  <span>} catch (error) {
</span>    res<span>.status(403).json({ error: 'Invalid token' });
</span>    <span>return
</span>  <span>}
</span><span>};
</span>
<span>export default authMiddleware;</span>
Copy after login


This file defines an Express middleware for JWT-based authentication. It checks for a valid token in the request header, verifies it using a secret key, and attaches the decoded user information (ID and role) to the request object. If the token is missing or invalid, it returns an appropriate error response.

  • Create permit.ts and paste the following code:
<span>import permit from '../utils/permit';
</span>
<span>export const checkUsertoPermitStudents = async (email: string, action: string, resource: string): Promise<boolean> => {
</boolean></span>  <span>try {
</span>    <span>const permitted = await permit.check(email, action, resource);
</span>    <span>console.log("Permitted", permitted);
</span>    <span>return permitted;
</span>  <span>} catch (error) {
</span>    <span>console.error(<span>`Error syncing user <span>${email}</span> to Permit.io:`</span>, error);
</span>    <span>return false;
</span>  <span>}
</span><span>};
</span>
<span>export const checkUserToPermitAssignment = async (email: string, action: string, resource: string): Promise<boolean> => {
</boolean></span>  <span>try {
</span>    <span>const permitted = await permit.check(email, action, resource);
</span>    <span>console.log("Permitted", permitted);
</span>    <span>return permitted;
</span>  <span>} catch (error) {
</span>    <span>console.error(<span>`Error syncing user <span>${email}</span> to Permit.io:`</span>, error);
</span>    <span>return false;
</span>  <span>}
</span><span>};</span>
Copy after login

This file defines utility functions, checkUsertoPermitStudents and checkUserToPermitAssignment, to check user permissions in Permit for specific actions and resources. Both functions handle errors gracefully, logging issues and returning false if the permission check fails. They are used to enforce authorization in the application.

  • Controllers folder
    • Create auth.ts file and paste the following code:
<span>import { account, ID } from '../config/appwrite';
</span><span>import { Request, Response } from 'express';
</span><span>import jwt from 'jsonwebtoken';
</span>
<span>const JWT_SECRET = process.env.JWT_SECRET as string; // Ensure this is set in your .env file
</span>
<span>// Sign-up Controller
</span><span>export const signUp = async (req: Request, res: Response) => {
</span>  <span>const { email, password, name } = req.body;
</span>
  <span>if (!email || !password || !name) {
</span>    <span>return res.status(400).json({ error: 'Name, email, and password are required.' });
</span>  <span>}
</span>
  <span>try {
</span>    <span>const user = await account.create(ID.unique(), email, password, name);
</span>    <span>// Generate JWT
</span>    <span>const token = jwt.sign({ email }, JWT_SECRET, { expiresIn: '8h' });
</span>      res<span>.cookie('token', token, {
</span>        httpOnly<span>: true,
</span>        sameSite<span>: 'strict',
</span>        secure<span>: true,
</span>      <span>});
</span>
    res<span>.status(201).json({ success: true, user, token });
</span>  <span>} catch (error: any) {
</span>    <span>console.error('Sign-up Error:', error);
</span>    res<span>.status(500).json({ success: false, message: error.message });
</span>  <span>}
</span><span>};
</span>
<span>// Login Controller
</span><span>export const login = async (req: Request, res: Response) => {
</span>  <span>const { email, password } = req.body;
</span>
  <span>if (!email || !password) {
</span>    <span>return res.status(400).json({ error: 'Email and password are required.' });
</span>  <span>}
</span>
  <span>try {
</span>    <span>const session = await account.createEmailPasswordSession(email, password);
</span>
    <span>// Generate JWT without role
</span>    <span>const token = jwt.sign(
</span>      <span>{ userId: session.userId, email }, // No role included
</span>      <span>JWT_SECRET,
</span>      <span>{ expiresIn: '8h' }
</span>    <span>);
</span>
    res<span>.cookie('token', token, {
</span>      httpOnly<span>: true,
</span>      sameSite<span>: 'strict',
</span>      secure<span>: true,
</span>    <span>});
</span>
    res<span>.status(200).json({ success: true, token, session });
</span>  <span>} catch (error: any) {
</span>    <span>console.error('Login Error:', error);
</span>    res<span>.status(401).json({ success: false, message: error.message });
</span>  <span>}
</span><span>};
</span>
<span>// Logout Controller
</span><span>export const logout = async (req: Request, res: Response) => {
</span>  <span>try {
</span>    <span>await account.deleteSession('Current Session ID');
</span>    res<span>.clearCookie('token');
</span>    res<span>.status(200).json({ success: true, message: 'Logged out successfully' });
</span>  <span>} catch (error: any) {
</span>    <span>console.error('Logout Error:', error);
</span>    res<span>.status(500).json({ success: false, message: error.message });
</span>  <span>}
</span><span>};</span>
Copy after login

This file defines authentication controllers for sign-up, login, and logout, integrating with Appwrite for user management and JWT for session handling. The signUp and login controllers validate input, create user sessions, and generate JWTs, while the logout controller clears the session and token. All controllers handle errors and return appropriate responses.

  • Create assignment.ts file and paste the following code:
<span>import { Request, Response } from 'express';
</span><span>import { createAssignmentInDB, AssignmentData, fetchAssignmentsFromDB } from '../models/assignment';
</span><span>import { checkUserToPermitAssignment } from '../middleware/permit';
</span>
<span>// Create a new assignment
</span><span>export async function createAssignment(req: Request, res: Response): Promise<void> {
</void></span>    <span>try {
</span>        <span>const { title, subject, teacher, className, dueDate, creatorEmail }: AssignmentData = req.body;
</span>
        <span>const isPermitted = await checkUserToPermitAssignment(creatorEmail, "create", "assignments");
</span>        <span>if (!isPermitted) {
</span>            res<span>.status(403).json({ error: 'Not authorized' });
</span>            <span>return;
</span>        <span>}
</span>
        <span>const newAssignment = await createAssignmentInDB({
</span>            title<span>,
</span>            subject<span>,
</span>            teacher<span>,
</span>            className<span>,
</span>            dueDate<span>,
</span>            creatorEmail
        <span>});
</span>
        <span>console.log('New assignment created:', newAssignment);
</span>
        res<span>.status(201).json(newAssignment);
</span>    <span>} catch (error) {
</span>        <span>console.error('Error creating assignment:', error);
</span>        res<span>.status(500).json({ error: (error as any).message });
</span>    <span>}  
</span><span>}
</span>
<span>// Fetch all assignments
</span><span>export async function fetchAssignments(req: Request, res: Response): Promise<void> {
</void></span>    <span>try {
</span>        <span>const { email } = req.params;
</span>       
        <span>const isPermitted = await checkUserToPermitAssignment(email, "read", "assignments");
</span>        <span>if (!isPermitted) {
</span>            res<span>.status(403).json({ message: 'Not authorized' });
</span>            <span>return;
</span>        <span>}
</span>
        <span>const assignments = await fetchAssignmentsFromDB();
</span>        res<span>.status(200).json(assignments);
</span>    <span>} catch (error) {
</span>        res<span>.status(500).json({ error: (error as any).message });
</span>    <span>}
</span><span>}</span>
Copy after login

This file defines controllers for creating and fetching assignments to integrate with a database and Permit for authorization checks. The createAssignment controller validates input, checks permissions, and creates a new assignment, while the fetchAssignments controller retrieves all assignments after verifying access. Both controllers handle errors and return appropriate responses.

  • Create a student.ts file and paste the following code:
<span>import {
</span>    createStudentInDB<span>,
</span>    fetchStudentsFromDB<span>,
</span>    StudentData
<span>} from '../models/student';
</span><span>import { Request, Response } from 'express';
</span><span>import { checkUsertoPermitStudents } from '../middleware/permit';
</span>
<span>export async function createStudent(req: Request, res: Response): Promise<void> {
</void></span>    <span>try {
</span>        <span>const { firstName, lastName, gender, className, age, creatorEmail }: StudentData = req.body;
</span>
        <span>if (!['girl', 'boy'].includes(gender)) {
</span>            res<span>.status(400).json({ error: 'Invalid gender type' });
</span>            <span>return;
</span>        <span>}
</span>
        <span>const isPermitted = await checkUsertoPermitStudents(creatorEmail, "create", "students");
</span>        <span>if (!isPermitted) {
</span>            res<span>.status(403).json({ message: 'Not authorized' });
</span>            <span>return;
</span>        <span>}
</span>
        <span>const newStudent = await createStudentInDB({
</span>            firstName<span>,
</span>            lastName<span>,
</span>            gender<span>,
</span>            className<span>,
</span>            age<span>,
</span>            creatorEmail
        <span>});
</span>        res<span>.status(201).json(newStudent);
</span>    <span>} catch (error) {
</span>        res<span>.status(500).json({ error: (error as any).message });
</span>    <span>}  
</span><span>}
</span>
<span>// Fetch all students
</span><span>export async function fetchStudents(req: Request, res: Response): Promise<void> {
</void></span>    <span>try {
</span>        <span>const { email } = req.params;
</span>
        <span>const isPermitted = await checkUsertoPermitStudents(email, "read", "students");
</span>        <span>if (!isPermitted) {
</span>            res<span>.status(403).json({ message: 'Not authorized' });
</span>            <span>return;
</span>        <span>}
</span>
        <span>const students = await fetchStudentsFromDB();
</span>        res<span>.status(200).json(students);
</span>    <span>} catch (error) {
</span>        res<span>.status(500).json({ error: (error as any).message });
</span>    <span>}
</span><span>}</span>
Copy after login

This file defines controllers for creating and fetching students, integrating with a database and Permit for authorization checks. The createStudent controller validates input, checks permissions, and creates a new student, while the fetchStudents controller retrieves all students after verifying access. Both controllers handle errors and return appropriate responses.

  • Create a profile.ts file and paste the following code:
<span>import { Profile } from '@/models/profile';
</span><span>import axios from 'axios';
</span><span>import { database, ID, Query } from '../config/appwrite';
</span><span>import { Request, Response, NextFunction, RequestHandler } from 'express';
</span><span>import { PERMIT_API_KEY } from '../config/environment';
</span>
<span>const profileId = process.env.APPWRITE_PROFILE_COLLECTION_ID as string; // Ensure this is in .env
</span><span>const databaseId = process.env.APPWRITE_DATABASE_ID as string; // Ensure this is in .env
</span><span>const projectId = process.env.PERMIT_PROJECT_ID as string
</span><span>const environmentId = process.env.PERMIT_ENV_ID as string
</span>
<span>const PERMIT_API_URL = <span>`https://api.permit.io/v2/facts/<span>${projectId}</span>/<span>${environmentId}</span>/users`</span>;
</span><span>const PERMIT_AUTH_HEADER = {
</span>  Authorization<span>: <span>`Bearer <span>${PERMIT_API_KEY}</span>`</span>,
</span>  <span>"Content-Type": "application/json",
</span><span>};
</span>
<span>// Create Profile Controller
</span><span>export const createProfile: RequestHandler = async (req: Request, res: Response, next: NextFunction): Promise<void> => {
</void></span>  <span>const { firstName, lastName, email, role, userId } = req.body;
</span>  <span>console.log(req.body);
</span>
  <span>if (!email || !role || !userId) {
</span>    res<span>.status(400).json({ error: 'FirstName, lastName, email, role, and userId are required.' });
</span>    <span>return;
</span>  <span>}
</span>
  <span>// Validate role
</span>  <span>const allowedRoles: Profile['role'][] = ['Admin', 'Teacher', 'Student'];
</span>  <span>if (!allowedRoles.includes(role)) {
</span>    res<span>.status(400).json({ error: 'Invalid role. Allowed roles: admin, teacher, student' });
</span>    <span>return;
</span>  <span>}
</span>
  <span>try {
</span>    <span>const newUser = await database.createDocument(
</span>      databaseId<span>,
</span>      profileId<span>,
</span>      <span>ID.unique(),
</span>      <span>{ firstName, lastName, email, role, userId }
</span>    <span>);
</span>    <span>// Step 2: Sync user to Permit.io
</span>    <span>const permitPayload = {
</span>      key<span>: email,
</span>      email<span>,
</span>      first_name<span>: firstName,
</span>      last_name<span>: lastName,
</span>      role_assignments<span>: [{ role, tenant: "default" }],
</span>    <span>};
</span>
    <span>let permitResponse;
</span>    <span>try {
</span>      <span>const response = await axios.post(PERMIT_API_URL, permitPayload, { headers: PERMIT_AUTH_HEADER });
</span>      permitResponse <span>= response.data;
</span>      <span>console.log("User synced to Permit.io:", permitResponse);
</span>    <span>} catch (permitError) {
</span>      <span>if (axios.isAxiosError(permitError)) {
</span>        <span>console.error("Failed to sync user to Permit.io:", permitError.response?.data || permitError.message);
</span>      <span>} else {
</span>        <span>console.error("Failed to sync user to Permit.io:", permitError);
</span>      <span>}
</span>      permitResponse <span>= { error: "Failed to sync with Permit.io" };
</span>    <span>}
</span>
    <span>// Step 3: Return both responses
</span>    res<span>.status(201).json({
</span>      message<span>: "User profile created successfully",
</span>      user<span>: newUser,
</span>      permit<span>: permitResponse,
</span>    <span>});
</span>    <span>return;
</span>  <span>} catch (error: any) {
</span>    res<span>.status(500).json({ success: false, message: error.message });
</span>    <span>return;
</span>  <span>}
</span><span>};
</span>
<span>// Fetch Profile by Email
</span><span>export const getProfileByEmail = async (req: Request, res: Response, next: NextFunction): Promise<void> => {
</void></span>  <span>const { email } = req.params;
</span>   
  <span>if (!email) {
</span>    res<span>.status(400).json({ error: 'Email is required.' });
</span>    <span>return;
</span>  <span>}
</span>
  <span>try {
</span>    <span>const profile = await database.listDocuments(
</span>      databaseId<span>,
</span>      profileId<span>,
</span>      <span>[Query.equal("email", email)]
</span>    <span>);
</span>
    <span>if (profile.documents.length === 0) {
</span>      res<span>.status(404).json({ error: 'Profile not found' });
</span>      <span>return;
</span>    <span>}
</span>
    res<span>.status(200).json({ success: true, profile: profile.documents[0] });
</span>  <span>} catch (error: any) {
</span>    <span>console.error('Error fetching profile:', error);
</span>    res<span>.status(500).json({ success: false, message: error.message });
</span>  <span>}
</span><span>};</span>
Copy after login

This file defines controllers for creating and fetching user profiles, integrating with Appwrite for database operations and Permit for role synchronization. The createProfile controller validates input, creates a profile, and syncs the user to Permit, while the getProfileByEmail controller retrieves a profile by email. Both controllers handle errors and return appropriate responses.

  • Config Folder
    • Create appwrite.ts file and paste the following code:
<span>import { Client, Account, Databases, Storage, ID, Permission, Role, Query } from 'appwrite';
</span><span>import { APPWRITE_ENDPOINT, APPWRITE_PROJECT_ID, APPWRITE_API_KEY } from './environment';
</span>
<span>// Initialize the Appwrite client
</span><span>const client = new Client()
</span>  <span>.setEndpoint(APPWRITE_ENDPOINT) // Appwrite endpoint
</span>  <span>.setProject(APPWRITE_PROJECT_ID); // Appwrite project ID
</span>
<span>// Add API key if available (for server-side operations)
</span><span>if (APPWRITE_API_KEY) {
</span>  <span>(client as any).config.key = APPWRITE_API_KEY;  // Workaround to set API key
</span><span>}
</span>
<span>// Initialize Appwrite services
</span><span>const account = new Account(client);
</span><span>const database = new Databases(client);
</span><span>const storage = new Storage(client);
</span>
<span>// Export Appwrite client and services
</span><span>export { client, account, database, storage, ID, Permission, Role, Query };</span>
Copy after login

This file initializes and configures the Appwrite client with the project endpoint, ID, and optional API key. It also sets up and exports Appwrite services like Account, Databases, and Storage, along with utility constants like ID, Permission, Role, and Query.

  • Create environment.ts file and paste the following code:
<span>import dotenv from 'dotenv';
</span>dotenv<span>.config();  // Load environment variables from .env
</span>
<span>export const APPWRITE_ENDPOINT = process.env.APPWRITE_ENDPOINT || '';
</span><span>export const PERMIT_API_KEY = process.env.PERMIT_API_KEY || '';
</span><span>export const PERMIT_PROJECT_ID = process.env.PERMIT_PROJECT_ID || '';
</span><span>export const PERMIT_ENV_ID = process.env.PERMIT_ENV_ID || '';
</span><span>export const APPWRITE_PROJECT_ID = process.env.APPWRITE_PROJECT_ID || '';
</span><span>export const DATABASE_ID = process.env.APPWRITE_DATABASE_ID || '';
</span><span>export const STUDENTS_COLLECTION_ID = process.env.APPWRITE_STUDENTS_COLLECTION_ID || '';
</span><span>export const ASSIGNMENTS_COLLECTION_ID = process.env.APPWRITE_ASSIGNMENTS_COLLECTION_ID || '';
</span>
<span>export const PROFILE_COLLECTION_ID = process.env.APPWRITE_PROFILE_COLLECTION_ID || '';</span>
Copy after login

This file loads environment variables from a .env file and exports them as constants for use in the application, such as Appwrite and Permit configurations, database IDs, and collection IDs. Default values are provided as fallbacks if the environment variables are not set.

  • API folder
    • Create student.ts and paste the following code:
<span>import express from 'express';
</span><span>import { createStudent, fetchStudents } from '../controllers/student';
</span><span>import authMiddleware from '../middleware/auth';
</span>
<span>const router = express.Router();
</span>
<span>// Define student-related endpoints
</span>router<span>.post('/students', authMiddleware, createStudent); // Create a new student
</span>router<span>.get('/students/:email', authMiddleware, fetchStudents); // Fetch all students
</span><span>export default router; // Export the router instance</span>
Copy after login

This file sets up an Express router with endpoints for managing student data. It includes routes for creating a new student and fetching students, both protected by an authentication middleware (authMiddleware). The router is then exported for use in the application.

  • Create auth.ts file and paste the following code:
<span>// src/routes/authRoutes.ts
</span><span>import express from 'express';
</span><span>import { signUp, login, logout } from '../controllers/auth';
</span>
<span>const router = express.Router();
</span>
<span>// Define auth-related endpoints
</span>router<span>.post('/signup', (req, res, next) => { // Signup route
</span>    <span>signUp(req, res).then(() => {
</span>      <span>next();
</span>    <span>}).catch((err) => {
</span>      <span>next(err);
</span>    <span>});
</span><span>});
</span>router<span>.post('/login', (req, res, next) => { // Login route
</span>    <span>login(req, res).then(() => {
</span>      <span>next();
</span>    <span>}).catch((err) => {
</span>      <span>next(err);
</span>    <span>});
</span><span>});
</span>router<span>.post('/logout', logout); // Logout route
</span><span>export default router; // Export the router instance</span>
Copy after login

This file sets up an Express router with endpoints for authentication-related actions, including user signup, login, and logout. The signup and login routes handle asynchronous operations with error handling, while the logout route is straightforward. The router is exported for use in the application.

  • Create assignment.ts file and paste the following code:
<span>import express from "express"
</span><span>import { createAssignment, fetchAssignments } from "../controllers/assignment"
</span><span>import authMiddleware from "../middleware/auth"
</span>
<span>const router = express.Router()
</span>
router<span>.post("/create", authMiddleware, createAssignment)
</span>router<span>.get("/:email", authMiddleware, fetchAssignments)
</span><span>export default router</span>
Copy after login

This file sets up an Express router with endpoints for managing assignments. It includes routes for creating an assignment and fetching assignments, both protected by an authentication middleware (authMiddleware). The router is exported for use in the application.

  • Create profile.ts file and paste the following code:
<span>import express from 'express';
</span><span>import { createProfile, getProfileByEmail } from '../controllers/profile';
</span><span>import authMiddleware from '../middleware/auth';
</span>
<span>const router = express.Router();
</span>
<span>// Route for creating a profile
</span>router<span>.post('/profile', authMiddleware, createProfile);
</span>
<span>// Route for getting a profile by email
</span>router<span>.get('/profile/:email', authMiddleware, getProfileByEmail);
</span><span>export default router;</span>
Copy after login

This file sets up an Express router with endpoints for managing user profiles. It includes routes for creating a profile and fetching a profile by email, both protected by an authentication middleware (authMiddleware). The router is exported for use in the application.

  • Create index.ts file and paste the following code:
<span>import express, { Request, Response } from 'express';
</span><span>import dotenv from 'dotenv';
</span><span>import cors from 'cors';  // CORS middleware
</span><span>import authRoutes from './auth';  // Import auth routes
</span><span>import profileRoutes from './profile';
</span><span>import studentRoutes from './student';
</span><span>import assignmentRoutes from './assignment';
</span><span>import { errorHandler } from '../utils/errorHandler';  // Custom error handler middleware
</span>
dotenv<span>.config();  // Load environment variables from .env file
</span>
<span>const app = express();
</span><span>const PORT = process.env.PORT || 8080;
</span>
<span>// Middleware
</span>app<span>.use(cors());  // Handle CORS
</span>app<span>.use(express.json());  /// Parse incoming JSON requests
</span>
<span>// Routes
</span>app<span>.use('/api/auth', authRoutes);  // Authentication routes
</span>app<span>.use('/api', profileRoutes); // Profile routes mounted
</span>app<span>.use('/api', studentRoutes); // Student routes mounted
</span>app<span>.use('/api/assignments', assignmentRoutes); // Assignment routes mounted
</span>
<span>// Global Error Handling Middleware
</span>app<span>.use(errorHandler);  // Handle errors globally
</span>
<span>// Default Route
</span>app<span>.get('/', (req: Request, res: Response) => {
</span>  res<span>.send('Appwrite Express API');
</span><span>});
</span>
<span>// Start Server
</span>app<span>.listen(PORT, () => {
</span>  <span>console.log(<span>`Server is running on port <span>${PORT}</span>`</span>);
</span><span>});
</span><span>export default app;</span>
Copy after login

This file sets up an Express server, configuring middleware like CORS and JSON parsing, and mounts routes for authentication, profiles, students, and assignments. It includes a global error handler and a default route to confirm the server is running. The server listens on a specified port, logs its status, and exports the app instance for further use.

  • Finally, to run this project, change a part of package.json and install the following packages below so when you run npm run dev, it works.
    • Install packages:
npm install concurrently ts-node nodemon --save-dev
Copy after login
  • By updating the scripts in the package.json, when you start the server, the typescript files are compiled to JavaScript in a new folder that is automatically created called dist
"scripts": {
    "dev": "concurrently \"tsc --watch\" \"nodemon -q --watch src --ext ts --exec ts-node src/api/index.ts\"",
    "build": "tsc",
    "start": "node ./dist/api/index.js"
},
Copy after login

Now run npm run dev to start your server. When you see this message, it means that you have successfully implemented the backend.

Building a Multi-Tenant SaaS Application with Next.js (Backend Integration)

Congratulations, your backend is ready for requests.

Now that our backend is set up, move on to frontend integration, where you’ll:

  • Secure API requests from Next.js
  • Dynamically show/hide UI elements based on user permissions.

Reason for creating an extensive backend service using Appwrite

Appwrite is often described as a backend-as-a-service (BaaS) solution, meaning it provides ready-made backend functionality like authentication, database management, and storage without requiring developers to build a traditional backend.

However, for this project, I needed more flexibility and control over how data was processed, secured, and structured, which led me to create an extensive custom backend using Node.js and Express while still leveraging Appwrite’s services.

Instead of relying solely on Appwrite’s built-in API calls from the frontend, I designed a Node.js backend that acted as an intermediary between the frontend and Appwrite. This allowed me to:

  • Implement fine-grained access control with Permit.io before forwarding requests to Appwrite.
  • Structure API endpoints for multi-tenancy to ensure tenant-specific data isolation.
  • Create custom business logic, such as processing role-based actions before committing them to the Appwrite database.
  • Maintain a centralized API layer, making it easier to enforce security policies, log activities, and scale the application.

Appwrite provided the core authentication and database functionality of this application, but this additional backend layer enhanced security, flexibility, and maintainability, to ensure strict access control before any action reached Appwrite.

Conclusion

That’s it for part one of this article series. In part 2, we’ll handle the frontend integration by setting up API calls with authorization, initializing and installing necessary dependencies, writing out the component file codes, and handling state management & routes.

The above is the detailed content of Building a Multi-Tenant SaaS Application with Next.js (Backend Integration). For more information, please follow other related articles on the PHP Chinese website!

Statement of this Website
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn

Hot AI Tools

Undresser.AI Undress

Undresser.AI Undress

AI-powered app for creating realistic nude photos

AI Clothes Remover

AI Clothes Remover

Online AI tool for removing clothes from photos.

Undress AI Tool

Undress AI Tool

Undress images for free

Clothoff.io

Clothoff.io

AI clothes remover

AI Hentai Generator

AI Hentai Generator

Generate AI Hentai for free.

Hot Article

R.E.P.O. Energy Crystals Explained and What They Do (Yellow Crystal)
3 weeks ago By 尊渡假赌尊渡假赌尊渡假赌
R.E.P.O. Best Graphic Settings
3 weeks ago By 尊渡假赌尊渡假赌尊渡假赌
R.E.P.O. How to Fix Audio if You Can't Hear Anyone
4 weeks ago By 尊渡假赌尊渡假赌尊渡假赌
WWE 2K25: How To Unlock Everything In MyRise
1 months ago By 尊渡假赌尊渡假赌尊渡假赌

Hot Tools

Notepad++7.3.1

Notepad++7.3.1

Easy-to-use and free code editor

SublimeText3 Chinese version

SublimeText3 Chinese version

Chinese version, very easy to use

Zend Studio 13.0.1

Zend Studio 13.0.1

Powerful PHP integrated development environment

Dreamweaver CS6

Dreamweaver CS6

Visual web development tools

SublimeText3 Mac version

SublimeText3 Mac version

God-level code editing software (SublimeText3)

How do I create and publish my own JavaScript libraries? How do I create and publish my own JavaScript libraries? Mar 18, 2025 pm 03:12 PM

Article discusses creating, publishing, and maintaining JavaScript libraries, focusing on planning, development, testing, documentation, and promotion strategies.

How do I optimize JavaScript code for performance in the browser? How do I optimize JavaScript code for performance in the browser? Mar 18, 2025 pm 03:14 PM

The article discusses strategies for optimizing JavaScript performance in browsers, focusing on reducing execution time and minimizing impact on page load speed.

What should I do if I encounter garbled code printing for front-end thermal paper receipts? What should I do if I encounter garbled code printing for front-end thermal paper receipts? Apr 04, 2025 pm 02:42 PM

Frequently Asked Questions and Solutions for Front-end Thermal Paper Ticket Printing In Front-end Development, Ticket Printing is a common requirement. However, many developers are implementing...

How do I debug JavaScript code effectively using browser developer tools? How do I debug JavaScript code effectively using browser developer tools? Mar 18, 2025 pm 03:16 PM

The article discusses effective JavaScript debugging using browser developer tools, focusing on setting breakpoints, using the console, and analyzing performance.

How do I use source maps to debug minified JavaScript code? How do I use source maps to debug minified JavaScript code? Mar 18, 2025 pm 03:17 PM

The article explains how to use source maps to debug minified JavaScript by mapping it back to the original code. It discusses enabling source maps, setting breakpoints, and using tools like Chrome DevTools and Webpack.

Who gets paid more Python or JavaScript? Who gets paid more Python or JavaScript? Apr 04, 2025 am 12:09 AM

There is no absolute salary for Python and JavaScript developers, depending on skills and industry needs. 1. Python may be paid more in data science and machine learning. 2. JavaScript has great demand in front-end and full-stack development, and its salary is also considerable. 3. Influencing factors include experience, geographical location, company size and specific skills.

Getting Started With Chart.js: Pie, Doughnut, and Bubble Charts Getting Started With Chart.js: Pie, Doughnut, and Bubble Charts Mar 15, 2025 am 09:19 AM

This tutorial will explain how to create pie, ring, and bubble charts using Chart.js. Previously, we have learned four chart types of Chart.js: line chart and bar chart (tutorial 2), as well as radar chart and polar region chart (tutorial 3). Create pie and ring charts Pie charts and ring charts are ideal for showing the proportions of a whole that is divided into different parts. For example, a pie chart can be used to show the percentage of male lions, female lions and young lions in a safari, or the percentage of votes that different candidates receive in the election. Pie charts are only suitable for comparing single parameters or datasets. It should be noted that the pie chart cannot draw entities with zero value because the angle of the fan in the pie chart depends on the numerical size of the data point. This means any entity with zero proportion

The difference in console.log output result: Why are the two calls different? The difference in console.log output result: Why are the two calls different? Apr 04, 2025 pm 05:12 PM

In-depth discussion of the root causes of the difference in console.log output. This article will analyze the differences in the output results of console.log function in a piece of code and explain the reasons behind it. �...

See all articles