Skip to content

Deploying Multiple Apps From a Monorepo to GitHub Pages

Deploying Multiple Apps From a Monorepo to GitHub Pages

This article was written over 18 months ago and may contain information that is out of date. Some content may be relevant but please refer to the relevant official documentation or available resources for the latest information.

Deploying Multiple Apps from a Monorepo to GitHub Pages

When it comes to deploying static sites, GitHub Pages is a popular solution thanks to being free and easy to set up in CI. The thing is, however, while it's perfectly suited for hosting a single application, such as a demo of your library, it does not support hosting multiple applications out of the box. It kind of just expects you to have a single app in your repository.

It just so happened I ended up with a project that originally had a single app deployed to GitHub Pages via a GitHub Actions workflow, and I had to extend it to be a monorepo with multiple apps. Once a second app was deploy-worthy, I had to figure out how to deploy it to GitHub Pages as well. As I found myself struggling a little bit while figuring out the best way to do it, I decided to write this post to share my experience and hopefully help someone else with a similar problem.

The Initial Setup

Initially, the project had a GitHub Actions workflow to test, build, and deploy the single app to GitHub Pages. The configuration looked something like this:

name: Build and deploy static content to Pages

on:
  # Runs on pushes targeting the default branch
  push:
    branches: ['main']

  # Allows you to run this workflow manually from the Actions tab
  workflow_dispatch:

# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
permissions:
  contents: read
  pages: write
  id-token: write

# Allow one concurrent deployment
concurrency:
  group: 'pages'
  cancel-in-progress: true

jobs:
  unit-tests:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v3
      - name: Instal deps and run unit tests
        run: |
          npm ci
          npm run test

  deploy:
    environment:
      name: github-pages
      url: ${{ steps.deployment.outputs.page_url }}
    runs-on: ubuntu-latest
    needs: [unit-tests]
    steps:
      - name: Checkout
        uses: actions/checkout@v3
      - name: Build
        run: |
          npm ci
          npm run build
      - name: Setup Pages
        uses: actions/configure-pages@v2
      - name: Upload artifact
        uses: actions/upload-pages-artifact@v1
        with:
          path: 'dist/my-app'
      - name: Deploy to GitHub Pages
        id: deployment
        uses: actions/deploy-pages@v1

The URL structure for GitHub Pages is [your-organization-name].github.io/[your-repo-name], which means on a merge to the main branch, this action deployed my app to thisdot.github.io/my-repo.

Accommodating Multiple Apps

As I converted the repository to an Nx monorepo and eventually developed the second application, I needed to deploy it to GitHub Pages too.

I researched some options and found a solution to deploy the apps as subdirectories. In the end, the changes to the workflow were not very drastic. As Nx was now building my apps into the dist/apps folder alongside each other, I just had to update the build step to build both apps and the upload step to upload the dist/apps directory instead of the dist/my-app directory. The final workflow at this point looked like this:

name: Build and deploy static content to Pages

on:
  # Runs on pushes targeting the default branch
  push:
    branches: ['main']

  # Allows you to run this workflow manually from the Actions tab
  workflow_dispatch:

# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
permissions:
  contents: read
  pages: write
  id-token: write

# Allow one concurrent deployment
concurrency:
  group: 'pages'
  cancel-in-progress: true

jobs:
  unit-tests:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v3
      - name: Install dependencies and run unit tests
        run: |
          npm ci
          nx run app1:test
          nx run app2:test

  deploy:
    environment:
      name: github-pages
      url: ${{ steps.deployment.outputs.page_url }}
    runs-on: ubuntu-latest
    needs: [unit-tests]
    steps:
      - name: Checkout
        uses: actions/checkout@v3
      - name: Install dependencies
        run: npm ci
      - name: Build app1
        run: |
          nx run app1:build
      - name: Build app2
        run: |
          nx run app2:build
      - name: Upload artifact
        uses: actions/upload-pages-artifact@v1
        with:
          path: 'dist/apps'
      - name: Setup Pages
        uses: actions/configure-pages@v2
      - name: Deploy to GitHub Pages
        id: deployment
        uses: actions/deploy-pages@v1

And that seemed to work fine. The apps were deployed to thisdot.github.io/my-repo/app1 and thisdot.github.io/my-repo/app2 respectively. But, then I noticed something was off...

Addressing Client-Side Routing

My apps were both written with React and used react-router-dom. And as GitHub Pages doesn't support client-side routing out of the box, the routing wasn't working properly and I've been getting 404 errors.

One of the apps had a workaround using a custom 404.html from spa-github-pages. The script in that file redirects all 404s to the index.html, preserving the path and query string. But that workaround wasn't working anymore at this point, and adding it to the second app didn't work either.

The reason why it wasn't working was that the 404.html wasn't in the root directory of the GitHub pages for that repository, as the apps were now deployed to subdirectories. So, the 404.html was not being picked up by the server. I needed to move the 404.html to the root directory of the apps.

I moved the 404.html to a shared folder next to the apps and updated the build script to copy it to the dist/apps directory alongside the two app subdirectories:

      - name: Move 404.html
        run: |
          mv dist/apps/app1/404.html dist/apps/404.html

So the whole workflow definition now looked like this:

name: Build and deploy static content to Pages

on:
  # Runs on pushes targeting the default branch
  push:
    branches: ['main']

  # Allows you to run this workflow manually from the Actions tab
  workflow_dispatch:

# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
permissions:
  contents: read
  pages: write
  id-token: write

# Allow one concurrent deployment
concurrency:
  group: 'pages'
  cancel-in-progress: true

jobs:
  unit-tests:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v3
      - name: Install dependencies and run unit tests
        run: |
          npm ci
          nx run app1:test
          nx run app2:test

  deploy:
    environment:
      name: github-pages
      url: ${{ steps.deployment.outputs.page_url }}
    runs-on: ubuntu-latest
    needs: [unit-tests]
    steps:
      - name: Checkout
        uses: actions/checkout@v3
      - name: Install dependencies
        run: npm ci
      - name: Build app1
        run: |
          nx run app1:build
      - name: Build app2
        run: |
          nx run app2:build
      - name: Upload artifact
        uses: actions/upload-pages-artifact@v1
        with:
          path: 'dist/apps'
      - name: Setup Pages
        uses: actions/configure-pages@v2
      - name: Deploy to GitHub Pages
        id: deployment
        uses: actions/deploy-pages@v1

Another thing to do was to increase the segmentsToKeep variable in the 404.html script to accommodate the app subdirectories:

var pathSegmentsToKeep = 2;

Handling Truly Missing URLs

At this point, the routing was working fine for the apps and I thought I was done with this ordeal. But then someone mistyped the URL and the page just kept redirecting to itself and I was getting an infinite loop of redirects. It just kept adding ?/&/~and~/~and~/~and~/~and~/~and~/~and~ over and over again to the URL. I had to fix this.

So I dug into the 404.html page and figured out, that I'll just check the path segment corresponding to the app name and only execute the redirect logic for known app subdirectories. So I added a allowedPathSegments array and check if the path segment matches one of the allowed ones:

// allowed subdirectories
var allowedPathSegments = ['app1', 'app2'];

// if the current URL is for an allowed subdirectory, redirect to it
var shouldRedirect =
    pathSegments.length > pathSegmentsToKeep &&
    allowedPathSegments.indexOf(pathSegments[pathSegmentsToKeep]) !== -1;
if (shouldRedirect) {
    // existing redirect code
}

At that point, the infinite redirect loop was gone. But the 404 page was still not very helpful. It was just blank.

So I also took this opportunity to enhance the 404.html to list the available apps and provide some helpful information to the user in case of a truly missing page.

I just had to add a bit of HTML code into the body:

  <div id="not-found-content" style="display: none">
   <h1>404</h1>
   <p>This page not found. Looks like such an app doesn't exist.</p>
   <p>Here is a list of apps we have:</p>
   <ul id="app-list"></ul>
  </div>

And a bit of javascript to populate the list of apps and show the content:

  if (shouldRedirect) {
    // existing redirect code
   } else {
    // populate the app list and show the not-found content
    document.addEventListener('DOMContentLoaded', function () {
     var appList = document.getElementById('app-list');
     allowedPathSegments.forEach(function (segment) {
      var listItem = document.createElement('li');
      var link = document.createElement('a');
      link.href = `/my-repo/${segment}`;
      link.textContent = segment.charAt(0).toUpperCase() + segment.slice(1);
      listItem.appendChild(link);
      appList.appendChild(listItem);
     });
     document.getElementById('not-found-content').style.display = 'block';
    });
   }

Now, when a user mistypes the URL, they get a helpful message and a list of available apps to choose from. If they use one of the available apps, the routing works as expected. This is the final version of the 404.html page:

<!DOCTYPE html>
<html>
 <head>
  <meta charset="utf-8" />
  <title>Single Page Apps for GitHub Pages</title>
  <script type="text/javascript">
   // Single Page Apps for GitHub Pages
   // MIT License
   // https://github.com/rafgraph/spa-github-pages
   // This script takes the current url and converts the path and query
   // string into just a query string, and then redirects the browser
   // to the new url with only a query string and hash fragment,
   // e.g. https://www.foo.tld/one/two?a=b&c=d#qwe, becomes
   // https://www.foo.tld/?/one/two&a=b~and~c=d#qwe
   // Note: this 404.html file must be at least 512 bytes for it to work
   // with Internet Explorer (it is currently > 512 bytes)

   // If you're creating a Project Pages site and NOT using a custom domain,
   // then set pathSegmentsToKeep to 1 (enterprise users may need to set it to > 1).
   // This way the code will only replace the route part of the path, and not
   // the real directory in which the app resides, for example:
   // https://username.github.io/repo-name/one/two?a=b&c=d#qwe becomes
   // https://username.github.io/repo-name/?/one/two&a=b~and~c=d#qwe
   // Otherwise, leave pathSegmentsToKeep as 0.
   var pathSegmentsToKeep = 2;

   var allowedPathSegments = ['app1', 'app2'];

   var l = window.location;

   var pathSegments = l.pathname.split('/');

   var shouldRedirect =
    pathSegments.length > pathSegmentsToKeep &&
    allowedPathSegments.indexOf(pathSegments[pathSegmentsToKeep]) !== -1;

   if (shouldRedirect) {
    l.replace(
     l.protocol +
      '//' +
      l.hostname +
      (l.port ? ':' + l.port : '') +
      l.pathname
       .split('/')
       .slice(0, 1 + pathSegmentsToKeep)
       .join('/') +
      '/?/' +
      l.pathname
       .slice(1)
       .split('/')
       .slice(pathSegmentsToKeep)
       .join('/')
       .replace(/&/g, '~and~') +
      (l.search ? '&' + l.search.slice(1).replace(/&/g, '~and~') : '') +
      l.hash,
    );
   } else {
    document.addEventListener('DOMContentLoaded', function () {
     var appList = document.getElementById('app-list');
     allowedPathSegments.forEach(function (segment) {
      var listItem = document.createElement('li');
      var link = document.createElement('a');
      link.href = `/my-repo/${segment}`;
      link.textContent = segment.charAt(0).toUpperCase() + segment.slice(1);
      listItem.appendChild(link);
      appList.appendChild(listItem);
     });
     document.getElementById('not-found-content').style.display = 'block';
    });
   }
  </script>
 </head>
 <body>
  <div id="not-found-content" style="display: none">
   <h1>404</h1>
   <p>This page was not found. Looks like such an app doesn't exist.</p>
   <p>Here is a list of apps we have:</p>
   <ul id="app-list"></ul>
  </div>
 </body>
</html>

Conclusion

Deploying multiple apps from an Nx monorepo to GitHub Pages required some adjustments, both in the GitHub Actions workflow and in handling client-side routing. With these changes, I was able to deploy and manage two apps effectively and I should be able to deploy even more apps in the future if they get added to the monorepo.

And, while the changes were not very drastic, it wasn't easy to find information on the topic and figure out what to do. That's why I decided to write this post. and I hope it will help someone else with a similar problem.

This Dot is a consultancy dedicated to guiding companies through their modernization and digital transformation journeys. Specializing in replatforming, modernizing, and launching new initiatives, we stand out by taking true ownership of your engineering projects.

We love helping teams with projects that have missed their deadlines or helping keep your strategic digital initiatives on course. Check out our case studies and our clients that trust us with their engineering.

You might also like

Communication Between Client Components in Next.js cover image

Communication Between Client Components in Next.js

Communication Between Client Components in Next.js In recent years, Next.js has become one of the most popular React frameworks for building server-rendered applications. With the introduction of the App Router in Next.js 13, the framework has taken a major leap forward by embracing a new approach to building web applications: the concept of server components and client components. This separation of concerns allows developers to strategically decide which parts of their application should be rendered on the server and which are then hydrated in the browser for interactivity. The Challenge: Communicating Between Client Components While server components offer numerous benefits, they also introduce a new challenge: how can client components within different boundaries communicate with each other? For instance, let's consider a scenario where you have a button (a client component) and a separate client component that displays the number of times the button has been clicked. In a regular React application, this would typically be accomplished by lifting the state to a common ancestor component, allowing the button to update the state, which is then passed down to the counter display component. However, the traditional approach may not be as straightforward in a Next.js application that heavily relies on server components, with client components scattered across the page. This blog post will explore three ways to facilitate communication between client components in Next.js, depending on where you want to store the state. Lifting State to a Common Client Component One approach to communicating between client components is to lift the state to a common client component. This could be a React context provider, a state management system like Zustand, or any other solution that allows you to share state across components. The key aspect is that this wrapper component should be higher up in the tree (perhaps even in the layout) and accept server components as children. Next.js allows you to interleave client and server components as much as you want, as long as server components are passed to client components as props or as component children. Here's how this approach might look in practice. First of all, we'd create a wrapper client component that holds the state: ` This wrapper component can be included in the layout: ` Any server components can be rendered within the layout, potentially nesting them several levels deep. Finally, we'd create two client components, one for the button and one for the counter display: ` ` The entire client and server component tree is rendered on the server, and the client components are then hydrated in the browser and initialized. From then on, the communication between the client components works just like in any regular React application. Check out the page using this pattern in the embedded Stackblitz window below: Using Query Params for State Management Another approach is to use query params instead of a wrapper client component and store the state in the URL. In this scenario, you have two client components: the button and the counter display. The counter value (the state) is stored in a query param, such as counterValue. The client components can read the current counter value using the useSearchParams hook. Once read, the useRouter hook can update the query param, effectively updating the counter value. However, there's one gotcha to this approach. If a route is statically rendered, calling useSearchParams will cause the client component tree up to the closest Suspense boundary to be client-side rendered. Next.js recommends wrapping the client component that uses useSearchParams in a boundary. Here's an example of how this approach might look. The button reads the current counter value and updates it on click by using the router's replace function: ` The counter display component is relatively simple, only reading the counter value: ` And here is the page that is a server component, hosting both of the above client components: ` Feel free to check out the above page in the embedded Stackblitz below: Storing State on the Server The third approach is to store the state on the server. In this case, the counter display component accepts the counter value as a prop, where the counter value is passed by a parent server component that reads the counter value from the database. The button component, when clicked, calls a server action that updates the counter value and calls revalidatePath() so that the counter value is refreshed, and consequently, the counter display component is re-rendered. It's worth noting that in this approach, unless you need some interactivity in the counter display component, it doesn't need to be a client component – it can be purely server-rendered. However, if both components need to be client components, here's an example of how this approach might look. First, we'll implement a server action that updates the counter value. We won't get into the mechanics of updating it, which in a real app would require a call to the database or an external API - so we're only commenting that part. After that, we revalidate the path so that the Next.js caches are purged, and the counter value is retrieved again in server components that read it. ` The button is a simple client component that calls the above server action when clicked. ` The counter display component reads the counter value from the parent: ` While the parent is a server component that reads the counter value from the database or an external API. ` This pattern can be seen in the embedded Stackblitz below: Conclusion Next.js is a powerful framework that offers various choices for implementing communication patterns between client components. Whether you lift state to a common client component, use query params for state management, or store the state on the server, Next.js provides all the tools you need to add a communication path between two separate client component boundaries. We hope this blog post has been useful in demonstrating how to facilitate communication between client components in Next.js. Check out other Next.js blog posts we've written for more insights and best practices. You can also view the entire codebase for the above snippets on StackBlitz....

Why is My React Reducer Called Twice and What the Heck is a Pure Function? cover image

Why is My React Reducer Called Twice and What the Heck is a Pure Function?

Why is My React Reducer Called Twice and What the Heck is a Pure Function? In a recent project, we encountered an interesting issue: our React reducer was dispatching twice, producing incorrect values, such as incrementing a number in increments of two. We hopped on a pairing session and started debugging. Eventually, we got to the root of the problem and learned the importance of pure functions in functional programming. This article will explain why our reducer was being dispatched twice, what pure functions are, and how React's strict mode helped us identify a bug in our code. The Issue We noticed that our useReducer hook was causing the reducer function to be called twice for every action dispatched. Initially, we were confused about this behavior and thought it might be a bug in React. Additionally, we had one of the dispatches inside a useEffect, which caused it to be called twice due to React strict mode, effectively firing the reducer four times and further complicating our debugging process. However, we knew that React's strict mode caused useEffect to be called twice, so it didn't take very long to realize that the issue was not with React but with how we had implemented our reducer function. React Strict Mode React's strict mode is a tool for highlighting potential problems in an application. It intentionally double-invokes specific lifecycle methods and hooks (like useReducer and useEffect) to help developers identify side effects. This behavior exposed our issue, as we had reducers that were not pure functions. What is a Pure Function? A pure function is a function that: - Is deterministic: Given the same input, always returns the same output. - Does Not Have Side Effects: Does not alter any external state or have observable interactions with the outside world. In the context of a reducer, this means the function should not: - Modify its arguments - Perform any I/O operations (like network requests or logging) - Generate random numbers - Depend on any external state Pure functions are predictable and testable. They help prevent bugs and make code easier to reason about. In the context of React, pure functions are essential for reducers because they ensure that the state transitions are predictable and consistent. The Root Cause: Impure Reducers Our reducers were not pure functions. They were altering external state and had side effects, which caused inconsistent behavior when React's strict mode double-invoked them. This led to unexpected results and made debugging more difficult. The Solution: Make Reducers Pure To resolve this issue, we refactored our reducers to ensure they were pure functions. Here's an extended example of how we transformed an impure reducer into a pure one in a more complex scenario involving a task management application. Let's start with the initial state and action types: ` And here's the impure reducer similar to what we had initially: ` This reducer is impure because it directly modifies the state object, which is a side effect. To make it pure, we must create a new state object for every action and return it without modifying the original state. Here's the refactored pure reducer: ` Key Changes: - Direct State Modification: In the impure reducer, the state is directly modified (e.g., state.tasks.push(action.payload)). This causes side effects and violates the principles of pure functions. - Side Effects: The impure reducer included side effects such as logging and direct state changes. The pure reducer eliminates these side effects, ensuring consistent and predictable behavior. I've created an interactive example to demonstrate the difference between impure and pure reducers in a React application. Despite the RESET_TASKS action being implemented similarly in both reducers, you'll notice that the impure reducer does not reset the tasks correctly. This problem happens because the impure reducer directly modifies the state, leading to unexpected behavior. Check out the embedded StackBlitz example below: Conclusion Our experience with the reducer dispatching twice was a valuable lesson in the importance of pure functions in React. Thanks to React's strict mode, we identified and fixed impure reducers, leading to more predictable and maintainable code. If you encounter similar issues, ensure your reducers are pure functions and leverage React strict mode to catch potential problems early in development. By embracing functional programming principles, you can write cleaner, more reliable code that is easier to debug and maintain....

Vercel & React Native - A New Era of Mobile Development? cover image

Vercel & React Native - A New Era of Mobile Development?

Vercel & React Native - A New Era of Mobile Development? Jared Palmer of Vercel recently announced an acquisition that spiked our interest. Having worked extensively with both Next.js and Vercel, as well as React Native, we were curious to see what the appointment of Fernando Rojo, the creator of Solito, as Vercel's Head of Mobile, would mean for the future of React Native and Vercel. While we can only speculate on what the future holds, we can look closer at Solito and its current integration with Vercel. Based on the information available, we can also make some educated guesses about what the future might hold for React Native and Vercel. What is Solito? Based on a recent tweet by Guillermo Rauch, one might assume that Solito allows you to build mobile apps with Next.js. While that might become a reality in the future, Jamon Holmgren, the CTO of Infinite Red, added some context to the conversation. According to Jamon, Solito is a cross-platform framework built on top of two existing technologies: - For the web, Solito leverages Next.js. - For mobile, Solito takes advantage of Expo. That means that, at the moment, you can't build mobile apps using Next.js & Solito only - you still need Expo and React Native. Even Jamon, however, admits that even the current integration of Solito with Vercel is exciting. Let's take a closer look at what Solito is according to its official website: > This library is two things: > > 1. A tiny wrapper around React Navigation and Next.js that lets you share navigation code across platforms. > > 2. A set of patterns and examples for building cross-platform apps with React Native + Next.js. We can see that Jamon was right - Solito allows you to share navigation code between Next.js and React Native and provides some patterns and components that you can use to build cross-platform apps, but it doesn't replace React Native or Expo. The Cross-Platformness of Solito So, we know Solito provides a way to share navigation and some patterns between Next.js and React Native. But what precisely does that entail? Cross-Platform Hooks and Components If you look at Solito's documentation, you'll see that it's not only navigation you can share between Next.js and React Native. There are a few components that wrap Next.js components and make them available in React Native: - Link - a component that wraps Next.js' Link component and allows you to navigate between screens in React Native. - TextLink - a component that also wraps Next.js' Link component but accepts text nodes as children. - MotiLink - a component that wraps Next.js' Link component and allows you to animate the link using moti, a popular animation library for React Native. - SolitoImage - a component that wraps Next.js' Image component and allows you to display images in React Native. On top of that, Solito provides a few hooks that you can use for shared routing and navigation: - useRouter() - a hook that lets you navigate between screens across platforms using URLs and Next.js Url objects. - useLink() - a hook that lets you create Link components across the two platforms. - createParam() - a function that returns the useParam() and useParams() hooks which allow you to access and update URL parameters across platforms. Shared Logic The Solito starter project is structured as a monorepo containing: - apps/next - the Next.js application. - apps/expo or apps/native - the React Native application. - packages/app - shared packages across the two applications: - features - providers - navigation The shared packages contain the shared logic and components you can use across the two platforms. For example, the features package contains the shared components organized by feature, the providers package contains the shared context providers, and the navigation package includes the shared navigation logic. One of the key principles of Solito is gradual adoption, meaning that if you use Solito and follow the recommended structure and patterns, you can start with a Next.js application only and eventually add a React Native application to the mix. Deployments Deploying the Next.js application built on Solito is as easy as deploying any other Next.js application. You can deploy it to Vercel like any other Next.js application, e.g., by linking your GitHub repository to Vercel and setting up automatic deployments. Deploying the React Native application built on top of Solito to Expo is a little bit more involved - you cannot directly use the Github Action recommended by Expo without some modification as Solito uses a monorepo structure. The adjustment, however, is luckily just a one-liner. You just need to add the working-directory parameter to the eas update --auto command in the Github Action. Here's what the modified part of the Expo Github Action would look like: ` What Does the Future Hold? While we can't predict the future, we can make some educated guesses about what the future might hold for Solito, React Native, Expo, and Vercel, given what we know about the current state of Solito and the recent acquisition of Fernando Rojo by Vercel. A Competitor to Expo? One question that comes to mind is whether Vercel will work towards creating a competitor to Expo. While it's too early to tell, it's not entirely out of the question. Vercel has been expanding its offering beyond Next.js and static sites, and it's not hard to imagine that it might want to provide a more integrated, frictionless solution for building mobile apps, further bridging the gap between web and mobile development. However, Expo is a mature and well-established platform, and building a mobile app toolchain from scratch is no trivial task. It would be easier for Vercel to build on top of Expo and partner with them to provide a more integrated solution for building mobile apps with Next.js. Furthermore, we need to consider Vercel's target audience. Most of Vercel's customers are focused on web development with Next.js, and switching to a mobile-first approach might not be in their best interest. That being said, Vercel has been expanding its offering to cater to a broader audience, and providing a more integrated solution for building mobile apps might be a step in that direction. A Cross-Platform Framework for Mobile Apps with Next.js? Imagine a future where you write your entire application in Next.js — using its routing, file structure, and dev tools — and still produce native mobile apps for iOS and Android. It's unlikely such functionality would be built from scratch. It would likely still rely on React Native + Expo to handle the actual native modules, build processes, and distribution. From the developer’s point of view, however, it would still feel like writing Next.js. While this idea sounds exciting, it's not likely to happen in the near future. Building a cross-platform framework that allows you to build mobile apps with Next.js only would require a lot of work and coordination between Vercel, Expo, and the React Native community. Furthermore, there are some conceptual differences between Next.js and React Native that would need to be addressed, such as Next.js being primarily SSR-oriented and native mobile apps running on the client. Vercel Building on Top of Solito? One of the more likely scenarios is that Vercel will build on top of Solito to provide a more integrated solution for building mobile apps with Next.js. This could involve providing more components, hooks, and patterns for building cross-platform apps, as well as improving the deployment process for React Native applications built on top of Solito. A potential partnership between Vercel and Expo, or at least some kind of closer integration, could also be in the cards in this scenario. While Expo already provides a robust infrastructure for building mobile apps, Vercel could provide complementary services or features that make it easier to build mobile apps on top of Solito. Conclusion Some news regarding Vercel and mobile development is very likely on the horizon. After all, Guillermo Rauch, the CEO of Vercel, has himself stated that Vercel will keep raising the quality bar of the mobile and web ecosystems. While it's unlikely we'll see a full-fledged mobile app framework built on top of Next.js or a direct competitor to Expo in the near future, it's not hard to imagine that Vercel will provide more tools and services for building mobile apps with Next.js. Solito is a step in that direction, and it's exciting to see what the future holds for mobile development with Vercel....

The simplicity of deploying an MCP server on Vercel cover image

The simplicity of deploying an MCP server on Vercel

The current Model Context Protocol (MCP) spec is shifting developers toward lightweight, stateless servers that serve as tool providers for LLM agents. These MCP servers communicate over HTTP, with OAuth handled clientside. Vercel’s infrastructure makes it easy to iterate quickly and ship agentic AI tools without overhead. Example of Lightweight MCP Server Design At This Dot Labs, we built an MCP server that leverages the DocuSign Navigator API. The tools, like `get_agreements`, make a request to the DocuSign API to fetch data and then respond in an LLM-friendly way. ` Before the MCP can request anything, it needs to guide the client on how to kick off OAuth. This involves providing some MCP spec metadata API endpoints that include necessary information about where to obtain authorization tokens and what resources it can access. By understanding these details, the client can seamlessly initiate the OAuth process, ensuring secure and efficient data access. The Oauth flow begins when the user's LLM client makes a request without a valid auth token. In this case they’ll get a 401 response from our server with a WWW-Authenticate header, and then the client will leverage the metadata we exposed to discover the authorization server. Next, the OAuth flow kicks off directly with Docusign as directed by the metadata. Once the client has the token, it passes it in the Authorization header for tool requests to the API. ` This minimal set of API routes enables me to fetch Docusign Navigator data using natural language in my agent chat interface. Deployment Options I deployed this MCP server two different ways: as a Fastify backend and then by Vercel functions. Seeing how simple my Fastify MCP server was, and not really having a plan for deployment yet, I was eager to rewrite it for Vercel. The case for Vercel: * My own familiarity with Next.js API deployment * Fit for architecture * The extremely simple deployment process * Deploy previews (the eternal Vercel customer conversion feature, IMO) Previews of unfamiliar territory Did you know that the MCP spec doesn’t “just work” for use as ChatGPT tooling? Neither did I, and I had to experiment to prove out requirements that I was unfamiliar with. Part of moving fast for me was just deploying Vercel previews right out of the CLI so I could test my API as a Connector in ChatGPT. This was a great workflow for me, and invaluable for the team in code review. Stuff I’m Not Worried About Vercel’s mcp-handler package made setup effortless by abstracting away some of the complexity of implementing the MCP server. It gives you a drop-in way to define tools, setup https-streaming, and handle Oauth. By building on Vercel’s ecosystem, I can focus entirely on shipping my product without worrying about deployment, scaling, or server management. Everything just works. ` A Brief Case for MCP on Next.js Building an API without Next.js on Vercel is straightforward. Though, I’d be happy deploying this as a Next.js app, with the frontend features serving as the documentation, or the tools being a part of your website's agentic capabilities. Overall, this lowers the barrier to building any MCP you want for yourself, and I think that’s cool. Conclusion I'll avoid quoting Vercel documentation in this post. AI tooling is a critical component of this natural language UI, and we just want to ship. I declare Vercel is excellent for stateless MCP servers served over http....

Let's innovate together!

We're ready to be your trusted technical partners in your digital innovation journey.

Whether it's modernization or custom software solutions, our team of experts can guide you through best practices and how to build scalable, performant software that lasts.

Prefer email? hi@thisdot.co