Building Multi‑Tenant Applications with Next.js

Hero image for Building Multi‑Tenant Applications with Next.js. Image by Pawel Czerwiński.
Hero image for 'Building Multi‑Tenant Applications with Next.js.' Image by Pawel Czerwiński.

Modern web applications often need to support multiple users, teams, or even entire organisations within a single system. Instead of running separate applications for each user or business, multitenant applications allow different tenants to share the same infrastructure whilst keeping their data, configurations, and customisations separate. The work I did with the Boohoo Group is a good example of this a single codebase and application powering multiple different ecommerce brand websites, all on separate domains.

Next.js, with its powerful routing system and flexible architecture, makes multitenancy relatively straightforward to implement, whether you're handling subdomains, paths, or entirely separate domains.


What is a Multi‑Tenant Application?

A multitenant application is a single web application which serves multiple users, businesses, or organisations whilst keeping their individual data and experiences separate. Instead of creating multiple deployments of the same application, a multitenant architecture allows one central system to handle different tenants from a single source.

Examples of Multi‑Tenant Applications

Many popular SaaS (SoftwareasaService) platforms follow a multitenant model. Here are a few examples:

  • Notion:

    Teams and individuals use the same platform but access different workspaces.
  • Slack:

    Each company has its own workspace with unique users, messages, and settings.
  • Shopify:

    Businesses run independent stores, but Shopify provides the same underlying application for all of their merchants.

Each of these platforms runs a single application that seamlessly serves multiple customers whilst keeping their data isolated.

Key Challenges in Multi‑Tenancy

Building a multitenant system isn't just about allowing different users, there are architectural challenges that must be addressed too, for example:

  • TenantBased Routing:

    How do we distinguish between different tenants? Should we use subdomains, URL paths, or completely separate domains?
  • Authentication & Authorisation:

    How do we ensure users only access the data and features they are entitled to?
  • Data Isolation:

    Should we store all tenant data in a single database with a tenantId column, or should each tenant have its own database?
  • Customisation & Theming:

    How can we allow tenants to have unique branding, settings, or even feature flags?
  • Scalability:

    How do we scale the system efficiently as the number of tenants grows?

Approaches to Multi‑Tenancy in Next.js

There is no single way to structure a multitenant application, and the best approach depends on the nature of the product, user experience requirements, and technical constraints. In Next.js, we have three primary ways to handle multitenancy: subdomainbased, pathbased, and domainbased tenancy. A hybrid approach combining these methods is even possible too..

Each of these strategies comes with its own advantages and tradeoffs...

Subdomain‑Based Tenancy

In this approach, each tenant is assigned a unique subdomain, such as:

  • tenant1.domain.com
  • tenant2.domain.com

The application detects the subdomain in the request and serves the appropriate tenantspecific content.

Pros:

  • Clean separation between tenants, making it easy to apply custom branding.
  • Subdomains feel natural for businessoriented applications like Slack (company.slack.com).
  • Easier to configure and enforce tenantspecific authentication.

Cons:

  • Requires wildcard domain setup (*.example.com) and DNS configuration, which may not be supported by all hosting providers or can involve extra setup steps.
  • Some thirdparty services may not support wildcard subdomains properly, particularly when integrating with services like OAuth, payment providers, or external APIs that expect a fixed domain.
  • Cookies and session management must consider subdomains carefully to avoid unintended crosstenant access. Some authentication mechanisms default to sharing cookies across subdomains unless explicitly scoped.

Subdomainbased multitenancy works well for B2B SaaS applications, where each tenant expects a distinct environment with its own URL (but perhaps not own domain).

Path‑Based Tenancy

With pathbased multitenancy, tenants are identified through a URL path:

  • domain.com/tenant1
  • domain.com/tenant2

Each request is routed based on the first segment of the path, allowing the system to determine which tenant is being accessed.

Pros:

  • No special DNS configuration is required; everything runs under a single domain, which makes deployment simpler.
  • Easier to deploy, especially for applications hosted on platforms like Vercel or Netlify where wildcard subdomains may not be practical.
  • Simpler to develop locally since all tenants share the same base URL, avoiding the need to manage multiple domains during development.

Cons:

  • Can lead to messy routing logic as every request must be handled with tenantawareness, requiring extra checks in both frontend and backend logic.
  • Harder to enforce strict tenant isolation, as all tenants exist within the same domain, which can introduce risks in caching, session storage, and security.
  • Some thirdparty integrations assume a consistent base URL, making this setup trickier when working with payment processors or authentication providers that don't support dynamic paths.

Pathbased multitenancy is often used for consumerfacing applications where tenants don't require custom domains and users are comfortable accessing content via a shared domain, for example seller shops under the ebay.com domain.

Domain‑Based Tenancy (Separate Domains for Each Tenant)

In this approach, each tenant operates under its own fully independent domain:

  • shopA.com
  • shopB.com
  • shopC.com

Although the underlying application remains the same, it dynamically serves different configurations, styles, or data depending on the incoming domain.

Pros:

  • Clear separation between tenants, making them feel like independent applications, which is particularly useful for whitelabel solutions.
  • Full flexibility in branding, themes, and custom features per tenant, allowing for deeper customisation than subdomainbased or pathbased tenancy.
  • Avoids the need for subdomain or pathbased logic, simplifying URL structures and making each tenant's site feel like a standalone product.

Cons:

  • Requires domain mapping and configuration at the hosting level, which can introduce complexity in managing domain registrations, DNS records, and request routing.
  • SSL certificates must be handled dynamically for each domain, which means that automated certificate management (e.g., via Let's Encrypt) needs to be integrated into the deployment pipeline.
  • Managing tenantspecific settings requires careful handling in both frontend and backend logic, as features, styles, or data sources might need to be adjusted per tenant.

Domainbased multitenancy is particularly useful for whitelabel platforms and ecommerce solutions, where each tenant wants complete control over their branding and user experience.

A Hybrid Approach

For some applications, a single tenancy model might not be enough. A hybrid approach combines multiple strategies to offer greater flexibility.

For example:

  • Enterprise clients get a subdomain

    (enterprise.domain.com).
  • Smaller tenants use pathbased routing

    (domain.com/tenant1).
  • Large tenants with custom branding have their own domains

    (brandA.com).

This approach allows for greater scalability, balancing ease of deployment with the flexibility to meet different customer needs. However, it does introduce massive additional complexity, as the system needs to handle multiple routing mechanisms simultaneously.

Each of these approaches has its strengths and weaknesses, and the best choice depends on the specific needs of the application.


Setting up Multi‑Tenant Routing in Next.js

Once we have decided on a multitenancy approach, the next step is implementing routing to ensure each request is mapped to the appropriate tenant. Next.js provides flexible tools to handle this, whether using subdomains or pathbased tenancy.

This section covers how to set up subdomainbased routing using middleware and pathbased routing using Next.js' dynamic routing system.

Subdomain‑Based Routing

In a subdomainbased multitenant setup, each tenant has their own subdomain, such as:

  • tenant1.domain.com
  • tenant2.domain.com

The application needs to detect the subdomain from the request and serve the correct tenantspecific content.

Detecting Subdomains Using Next.js Middleware

Next.js Middleware runs before a request reaches a page, which makes it the ideal place to detect subdomains dynamically. With this, we can extract the tenant from the request and apply it accordingly. For example:

import { NextRequest, NextResponse } from 'next/server';export function middleware(req: NextRequest) {  const url = new URL(req.url);  const host = req.headers.get('host') || '';  const subdomain = host.split('.')[0];  // Extract the subdomain  if (subdomain && subdomain !== 'www' && subdomain !== 'domain') {    // Attach tenant info to request headers or cookies for later use    req.headers.set('x-tenant', subdomain);  }  return NextResponse.next();}

Applying the Tenant Data in Pages

Once the subdomain is detected, we can then access it in our pages through headers or context:

import { GetServerSideProps } from 'next';export const getServerSideProps: GetServerSideProps = async ({ req }) => {  const tenant = req.headers['x-tenant'] || 'default';  return {    props: { tenant },  };};const TenantPage = ({ tenant }: { tenant: string }) => {  return <h1>Welcome to {tenant}’s dashboard</h1>;};export default TenantPage;

This setup means that each subdomain correctly maps to a different tenant, making subdomainbased multitenancy relatively seamless.

Path‑Based Routing

For pathbased multitenancy, tenants are identified through a URL path, such as:

  • domain.com/tenant1
  • domain.com/tenant2

Setting up Dynamic Routes in Next.js

Next.js makes pathbased tenancy straightforward with dynamic route segments. A dynamic folder in pages/ handles tenantspecific pages. Your folder structure might look something like this:

pages/ ├── [tenant]/ │   ├── index.tsx │   ├── dashboard.tsx │   ├── settings.tsx

Each [tenant] segment captures the value from the URL and makes it available in the page component.

Handling Tenant‑Based Access in Pages

Inside pages/[tenant]/index.tsx, we can extract the tenant name from the URL and load the correct data.

import { useRouter } from 'next/router';const TenantDashboard = () => {  const router = useRouter();  const { tenant } = router.query;  return <h1>Welcome to {tenant}’s dashboard</h1>;};export default TenantDashboard;

In this way we can ensure that requests to domain.com/tenant1 and domain.com/tenant2 dynamically serve different tenantspecific content.

Enforcing Tenant‑Based Page Access

One concern with pathbased tenancy is ensuring users can only access data belonging to their tenant. To enforce access, a serverside check can be implemented.

import { GetServerSideProps } from 'next';export const getServerSideProps: GetServerSideProps = async ({ params }) => {  const tenant = params?.tenant as string;  if (!isValidTenant(tenant)) {    return { notFound: true };  // Return a 404 if tenant is invalid  }  return {    props: { tenant },  };};const TenantDashboard = ({ tenant }: { tenant: string }) => {  return <h1>Welcome to {tenant}’s dashboard</h1>;};export default TenantDashboard;

This can also be combined with authentication checks in your middleware to ensure that only the correct tenant can access their specific data.


Handling Multiple Domains in Next.js

In a domainbased multitenant setup, each tenant has a completely separate domain, such as:

  • shopA.com
  • shopB.com
  • shopC.com

The core application remains the same, but it dynamically serves different themes, configurations, or even feature sets depending on which domain the request comes from.

This approach is common in whitelabel platforms, ecommerce solutions, and SaaS applications where each tenant operates as a unique brand.

Detecting the Incoming Domain

To serve the correct tenant configuration, the application needs to determine which domain the request is coming from. This can again be done using Next.js Middleware, which allows us to inspect the request before it reaches the page.

import { NextRequest, NextResponse } from 'next/server';const tenantConfig = {  'shopA.com': { theme: 'dark', logo: '/logos/shopA.png' },  'shopB.com': { theme: 'light', logo: '/logos/shopB.png' },};export function middleware(req: NextRequest) {  const host = req.headers.get('host') || '';  const config = tenantConfig[host] || { theme: 'default', logo: '/logos/default.png' };  // Attach tenant-specific config to request headers  req.headers.set('x-tenant-theme', config.theme);  req.headers.set('x-tenant-logo', config.logo);  return NextResponse.next();}

This middleware checks the incoming request's domain and attaches the correct configuration to the request headers, allowing pages to access tenantspecific settings.

Applying Different Styles and Themes

Once the middleware attaches tenantspecific settings to the request, we can retrieve and apply them in our Next.js pages or components.

import { GetServerSideProps } from 'next';export const getServerSideProps: GetServerSideProps = async ({ req }) => {  const theme = req.headers['x-tenant-theme'] || 'default';  const logo = req.headers['x-tenant-logo'] || '/logos/default.png';  return {    props: { theme, logo },  };};const HomePage = ({ theme, logo }: { theme: string; logo: string }) => {  return (    <div className={`theme-${theme}`}>      <img src={logo} alt="Tenant Logo" />      <h1>Welcome to your store</h1>    </div>  );};export default HomePage;

Here, styles and branding elements adjust dynamically based on the detected domain.

Configuring Middleware for Multi‑Domain Handling

When handling multiple domains, middleware is a useful way to enforce tenantspecific rules. Beyond theme selection, middleware can:

  • Redirect unknown domains to a default landing page.
  • Enforce authentication rules per tenant.
  • Apply tenantspecific caching or ratelimiting policies.

For example, if a request comes from an unregistered domain, we can redirect it to a fallback page like this:

export function middleware(req: NextRequest) {  const host = req.headers.get('host') || '';  if (!tenantConfig[host]) {    return NextResponse.redirect(new URL('/not-found', req.url));  }  return NextResponse.next();}

Deployment Considerations

Handling multiple domains requires additional setup at the hosting level.

Mapping Multiple Domains

For platforms like Vercel and Netlify, you need to configure custom domain mappings:

  • In Vercel, add each tenant's domain under project settings.
  • In Netlify, use domain aliasing to map multiple domains to the same application.
  • For custom hosting solutions, configure Nginx or Apache to route requests correctly.

Handling Ssl Certificates and Domain Routing

Each tenant domain is going to need a valid SSL certificate for secure access. Hosting providers like Vercel and Netlify handle this automatically, but if using a custom server, Let's Encrypt or Cloudflare can manage certificates dynamically.

For Next.js deployments on custom servers, dynamic domain handling can be implemented at the proxy level, ensuring that requests are routed correctly based on the incoming hostname.


Handling Tenant‑Specific Authentication

Authentication in a multitenant application is more complex than in a standard singletenant system. Each tenant must have its own isolated user base, ensuring that authentication and session management correctly separate users from different tenants.

There are several ways to handle tenantaware authentication in Next.js, including using authentication providers like NextAuth.js or Firebase.

Authenticating Users Separately for Each Tenant

When handling authentication in a multitenant application, the main challenge is ensuring that users log in under the correct tenant and cannot access other tenants' data. The authentication process needs to be aware of the current tenant and enforce restrictions accordingly.

There are two common ways to associate authentication with a tenant:

  • Subdomainbased tenants:

    The subdomain (tenant1.domain.com, tenant2.domain.com) determines which tenant the user is logging into.
  • Pathbased tenants:

    The tenant is part of the URL (domain.com/tenant1, domain.com/tenant2), and authentication flows are scoped accordingly.

For domainbased tenancy (shopA.com, shopB.com), authentication is naturally isolated, as users always log in via the correct domain.

Storing and Managing Tenant‑Specific User Sessions

Session management must ensure that user sessions remain valid only for the tenant they authenticated with.

Using Cookies for Tenant‑Specific Sessions

For subdomainbased tenants, session cookies should be scoped to the subdomain:

import { NextApiRequest, NextApiResponse } from 'next';import { serialize } from 'cookie';export default function handler(req: NextApiRequest, res: NextApiResponse) {  const { tenant, token } = req.body;  res.setHeader('Set-Cookie', serialize('session', token, {    path: '/',    domain: `.${tenant}.domain.com`,  // Restrict session to tenant subdomain    httpOnly: true,    secure: process.env.NODE_ENV === 'production',  }));  res.status(200).json({ message: 'Session set' });}

This ensures that authentication cookies are not shared between tenants.

Storing Sessions in a Database

For pathbased tenancy, user sessions can be stored in a shared authentication database, with each session linked to a tenantId. For example:

const sessionSchema = new Schema({  userId: String,  tenantId: String,  token: String,  expiresAt: Date,});

When retrieving sessions, always filter by the authenticated tenant:

const session = await Session.findOne({ token, tenantId });if (!session) {  throw new Error('Invalid session');}

In this way we can ensure that even if a user attempts to reuse a session token across multiple tenants, access is correctly restricted.

Using NextAuth.js for Tenant‑Aware Authentication

NextAuth.js is a flexible authentication library for Next.js that supports multitenant applications. To make it tenantaware, we need to:

  • Detect the tenant from the request (subdomain, path, or domain).
  • Store and retrieve authentication data scoped to that tenant.
  • Configure separate authentication providers per tenant if needed.

Example: Customising NextAuth.js for Multi‑Tenant Authentication

import NextAuth from 'next-auth';import CredentialsProvider from 'next-auth/providers/credentials';export default NextAuth({  providers: [    CredentialsProvider({      async authorize(credentials, req) {        const tenant = req.headers['x-tenant'] || 'default';                const user = await fetchUserFromTenantDB(credentials.email, tenant);        if (!user) throw new Error('No user found');        return { id: user.id, name: user.name, email: user.email, tenant };      },    }),  ],  callbacks: {    async session({ session, token }) {      session.tenant = token.tenant;  // Store tenant info in session      return session;    },    async jwt({ token, user }) {      if (user) {        token.tenant = user.tenant;      }      return token;    },  },});

This setup ensures that authentication logic respects the current tenant throughout the session lifecycle.

Using Firebase for Multi‑Tenant Authentication

Firebase Authentication does not natively support multitenancy, but we can implement tenantaware authentication by:

  • Creating a separate Firebase project per tenant (best for strong isolation).
  • Using one Firebase project with Firestore rules enforcing tenant access (simpler to manage).

For a shared Firebase project, Firestore rules ensure users can only access their own tenant's data:

rules_version = '2';service cloud.firestore {  match /databases/{database}/documents {    match /users/{userId} {      allow read, write: if request.auth.token.tenantId == resource.data.tenantId;    }  }}

This ensures that authentication is tenantaware and prevents crosstenant data access.


Managing Tenant‑Specific Data and Databases

Handling data correctly in a multitenant application is critical to ensuring security, performance, and maintainability. The approach to database management depends on how much isolation is required between tenants. There are two common strategies: using separate databases per tenant or storing all tenant data in a shared database with tenant identifiers.

Each approach has tradeoffs in complexity, scalability, and cost...

Separate Databases per Tenant

With this approach, each tenant has its own independent database instance. When a new tenant is created, a new database is provisioned specifically for them.

Advantages:

  • Ensures complete data isolation, reducing the risk of crosstenant data leaks.
  • Allows tenants to have independent database configurations, optimising performance as needed.
  • Easier to comply with strict security and compliance regulations, as tenant data is fully separate.

Challenges:

  • Adds management overhead, as new tenants require database provisioning, migrations, and backups.
  • Scaling can become difficult as the number of tenants increases, requiring automation for database management.
  • Connection pooling may be limited if using a serverless database, as each tenant needs a separate connection.

Example of Connecting to a Separate Database per Tenant:

import { PrismaClient } from '@prisma/client';const getDatabaseForTenant = (tenant: string) => {  return new PrismaClient({    datasources: {      db: {        url: `postgresql://user:password@host/${tenant}_database`,      },    },  });};export const getTenantData = async (tenant: string) => {  const db = getDatabaseForTenant(tenant);  return db.user.findMany();};

In this setup, each request is routed to the correct database based on the tenant identifier.

Shared Database with Tenant Identifiers

Instead of provisioning a new database for each tenant, this approach keeps all tenant data in a single database but ensures each record is tagged with a tenant identifier.

Advantages:

  • Easier to manage, as all tenant data resides in one place, reducing operational complexity.
  • More efficient for applications with a large number of small tenants, where separate databases would be overkill.
  • Simplifies analytics and reporting, as all tenant data is accessible from one location.

Challenges:

  • Requires careful query structuring to prevent crosstenant data leaks.
  • Query performance can degrade as the number of tenants and data volume increase, requiring indexing strategies.
  • Backup and restore operations must be designed to allow tenantspecific data recovery if needed.

Example of Enforcing Tenant Isolation in Queries:

import { PrismaClient } from '@prisma/client';const db = new PrismaClient();export const getTenantUsers = async (tenantId: string) => {  return db.user.findMany({    where: { tenantId },  });};

Every query must explicitly filter by tenantId to ensure users only access their own data.

Connecting to the Right Database in Next.js

When using separate databases per tenant, the application must dynamically select the correct database connection based on the incoming request.

Example Using Middleware to Set the Tenant Database Connection:

import { NextRequest, NextResponse } from 'next/server';export function middleware(req: NextRequest) {  const host = req.headers.get('host') || '';  const tenant = host.split('.')[0];  // Extract the subdomain  req.headers.set('x-tenant-db', `postgresql://user:password@host/${tenant}_database`);  return NextResponse.next();}

Applying the Correct Database Connection in API Routes:

import { PrismaClient } from '@prisma/client';export default async function handler(req, res) {  const databaseUrl = req.headers['x-tenant-db'];  if (!databaseUrl) {    return res.status(400).json({ error: 'Tenant database not found' });  }  const db = new PrismaClient({    datasources: { db: { url: databaseUrl } },  });  const users = await db.user.findMany();  res.status(200).json(users);}

Deployment and Scaling Considerations

Deploying a multitenant Next.js application requires careful planning to ensure it remains performant, scalable, and costefficient. The best deployment strategy depends on the chosen tenancy model, as subdomainbased, pathbased, and domainbased approaches each have different infrastructure requirements.

Deploying a Multi‑Tenant Application on Vercel, Aws, and Digitalocean

Deploying on Vercel

Vercel is one of the easiest platforms for deploying a Next.js application, but multitenancy requires additional configuration.

  • Subdomainbased tenancy
    : Vercel supports wildcard subdomains, allowing dynamic tenantbased routing. However, wildcard subdomains require manual configuration in Vercel's project settings.
  • Domainbased tenancy
    : Each tenant's domain must be added as a custom domain in Vercel. A script or admin panel may be needed to automate domain registration.
  • Pathbased tenancy
    : This works with no special configuration, as all routes are handled within the same deployment.

Here is an example configuration for handling wildcard subdomains in Vercel:

{  "rewrites": [    { "source": "/(.*)", "destination": "/api/middleware" }  ]}

Deploying on Aws

AWS provides more flexibility but requires more setup compared to Vercel.

  • Subdomainbased tenancy
    : Use AWS Route 53 for DNS management and Application Load Balancer to route requests based on subdomain.
  • Domainbased tenancy
    : Configure separate domain mappings using AWS Certificate Manager for SSL and API Gateway for request handling.
  • Pathbased tenancy
    : Can be handled using a single Next.js deployment on an EC2 instance or with AWS Lambda for a serverless approach.

Deploying on Digitalocean

DigitalOcean is a good middle ground between simplicity and control.

  • Subdomainbased tenancy
    : Configure wildcard DNS using DigitalOcean's networking features.
  • Domainbased tenancy
    : Custom domains require manual configuration unless automated via API.
  • Pathbased tenancy
    : Can be deployed as a standard Next.js application using the App Platform or a managed Kubernetes cluster.

Using Serverless Functions to Optimise Performance

Serverless functions allow our application to scale dynamically based on incoming traffic. Next.js API routes can be deployed as serverless functions on Vercel, AWS Lambda, or DigitalOcean Functions.

Advantages of Using Serverless Functions:

  • Automatically scale based on load, reducing infrastructure costs.
  • Improve performance by running tenantspecific logic at the edge.
  • Reduce maintenance by eliminating the need for alwayson backend servers.

Here's an example of a multitenant API route optimised for serverless deployment:

import { NextApiRequest, NextApiResponse } from 'next';export default function handler(req: NextApiRequest, res: NextApiResponse) {  const tenant = req.headers['x-tenant'] || 'default';  res.json({ message: `Data for tenant: ${tenant}` });}

Serverless functions are particularly useful for subdomainbased and domainbased tenancy, as requests can be processed per tenant without relying on a centralised backend.

Scaling Strategies Based on the Tenancy Model

Scaling a multitenant application depends on how tenants are structured.

Scaling Subdomain‑Based and Domain‑Based Tenancy

  • Use caching layers
    : Reduce database queries by caching tenantspecific data with Redis or Cloudflare.
  • Load balancing
    : Distribute traffic across multiple instances with AWS Elastic Load Balancer or DigitalOcean Load Balancer.
  • Edge computing
    : Deploy middleware and API logic closer to users with Vercel Edge Functions or AWS CloudFront.

Scaling Path‑Based Tenancy

  • Efficient database indexing
    : Ensure queries are optimised to filter by tenant ID.
  • Sharding strategy
    : Split tenants across multiple database instances if the number of tenants grows significantly.
  • Rate limiting
    : Protect against tenant abuse by implementing rate limiting at the API level.

Each of these strategies helps ensure that the application remains responsive and scalable, even as the number of tenants grows.


Wrapping up

Building a multitenant application in Next.js requires careful planning across several key areas, including routing, authentication, data management, and deployment. The right approach depends on the needs of the application and its tenants, balancing isolation, scalability, and maintainability.

Next.js provides the flexibility needed to implement multitenancy effectively, whether using subdomains, paths, or entirely separate domains. With proper middleware configuration, tenantaware authentication, and scalable database strategies, a multitenant architecture can be both powerful and efficient.

Key Takeaways

  • Multitenancy in Next.js can be done with subdomains, paths, or separate domains.
  • Proper authentication and database design are crucial for tenant separation.
  • Middleware and dynamic routing make handling tenants easier.
  • Deployment strategies should align with the chosen tenancy model.

By structuring the application correctly from the start, managing tenants becomes much more straightforward, allowing for better scalability and security as the platform grows.


Categories:

  1. Adaptive Development
  2. Architecture
  3. Development
  4. Front‑End Development
  5. Guides
  6. Next.js
  7. Shopify