Building a Github Repo Template Part 5: Next.js Environment Variables

Building a Github Repo Template Part 5: Next.js Environment Variables

ยท

10 min read

In this ongoing series, I am putting together a Github Repository Template for my "Go To" front-end tech stack of Next.js, React, TypeScript etc. In this installment of the series, I will get custom environment variables into our Next.js application using .env files. I will also explore how Next.js handles runtime vs build time environment variables.

Just want the Code? View the full changeset covered in this post or check out the 0.0.5 release of the repository.

Next.js Environment Variables Background

One of the powerful things about Next.js is that it renders your React components on the server as well as in the browser. This allows you to leverage the servers' processing power and caching to serve up the React content (mostly) the same as if it was rendered only in the browser. Server Rendering also helps with SEO and a host of other things. (See this helpful graphic about the tradeoffs and benefits of server rendering with respect to rendering time).

Additionally, certain backend code can also isolated to only the server - getServerProps, Api Routes, etc. This allows us, to for example, query a database. However, we wouldn't want to expose our database credentials to the browser client. That would be bad. Very bad. The only way to accomplish this was with Next.js was utilizing Runtime Configuration to inject environment variables into your app. However, with the release of Next 9.4, we have a far easier and more universal approach leveraging .env files. In this post, I'll explore the new environment variables support and add some very basic environment variables to my Github Repository Template.

Introducing .env files

First, I'll create a new file in the project root named .env and add the following content:

# Base Environment Variables
NEXT_PUBLIC_THEME_BACKGROUND="#f6f7f9"
NEXT_PUBLIC_THEME_FONT_COLOR="rgba(0, 0, 0, 0.87)"
NEXT_PUBLIC_THEME_GREETING_EMOJI="๐Ÿ”ฅ"
MY_SECRET="This is top secret"

Restarting the app via npm run dev I see that Next.js loads the file (without having to install any other packages - thanks Next.js!): Screen Shot 2020-07-17 at 10.44.16 AM.png

These variables are now available on process.env inside the Next.js application. Only variables that start with NEXT_PUBLIC will be available on the browser client. All variables will be available on server code.

Consuming Public Environment Variables

To illustrate consuming public environment variables, I'm going to introduce some basic styling to the application. In the next part of the series, I'll leverage these variables for Material-UI themes.

Start by creating a file in the /pages directory called _document.tsx with the following content:

// /pages/_document.tsx

import React from 'react';
import Document, {
  Head, Main, NextScript,
} from 'next/document';

class MyDocument extends Document {
  render(): JSX.Element {
    return (
      <html lang="en" dir="ltr">
        <Head>
          <meta charSet="utf-8" />
          <meta name="viewport" content="minimum-scale=1, initial-scale=1, width=device-width, shrink-to-fit=no" />
          <style>
            {`
            body {
              background-color: ${process.env.NEXT_PUBLIC_THEME_BACKGROUND};
              color: ${process.env.NEXT_PUBLIC_THEME_FONT_COLOR};
            }
            `}
          </style>
        </Head>
        <body>
          <Main />
          <NextScript />
        </body>
      </html>
    );
  }
}
export default MyDocument;

Next.js allows you to override the base html template using the _document.tsx file (learn more). For the purpose of this tutorial, I'm just going to add a style tag to leverage the environment variables. Restart the application with npm run dev I see the following (note the background and font color change):

Screen Shot 2020-07-17 at 11.39.26 AM.png Now, technically, the _document.tsx is only rendered on the server, so I'll update src/screens/IndexContent.tsx to control the emoji to ensure the variable is read on the client as well.

import React from 'react';

interface IndexProps { greeting: string }

const IndexContent: React.FC<IndexProps> = (props) => {
  const { greeting } = props;

  return (
    <div>
      <h1>
        {greeting}
        {process.env.NEXT_PUBLIC_THEME_GREETING_EMOJI}
        !
      </h1>
    </div>
  );
};

export default IndexContent;

Reload the page, I see the following (note the ๐Ÿ”ฅ emoji vs the ๐Ÿ‘‹):

Screen Shot 2020-07-17 at 11.42.33 AM.png

I have now illustrated that environment variables starting with NEXT_PUBLIC are available on the server and the client.

Using .env.local for Default Environment Variables

One feature of .env files I liked about Create React App is that you could actually have a hierarchy of .env files. The actual .env file could provide an exhaustive set of defaulted environment variables. Then a .env.local file could be used to provide values specific to the environment. This is a great way to support 12 Factor applications. In production environments, I can simply include an environment specific .env.local that could, for example, contain the production database credentials, etc. Next.js supports this as well!

To illustrate this hierarchy, I will create a .env.local file in the project root with the following content.

# Local Environment Variables
NEXT_PUBLIC_THEME_GREETING_EMOJI="๐ŸŠ"
MY_SECRET="Extra Super Secret"

Now, restarting the server via npm run dev I see: Screen Shot 2020-07-17 at 11.57.38 AM.png Note: The process.env.NEXT_PUBLIC_THEME_GREETING_EMOJI variable took on the overridden value from within .env.local and that the other public THEME variables retained their value from the .env defaults. Neat!

Note: This hierarchy optionally goes further with .env.development and .env.production (corresponding to if you start the app with next dev or next start respectively - thanks Chris Weekly for the correction). I do not utilize this much so I won't include it in my Github Repo Template, but you can read more about it here.

Note: You should never commit passwords, API keys, and other secrets into Github. If you adopt this default approach, ensure that your .env.local file is added to your .gitignore file. If you use the .env file for defaults and do not include any secrets, you can check that in to source code. Just be sure to remove it from .gitignore if it is present.

Server Only Environment Variables

Next, I want to confirm that environment variables that do not start with NEXT_PUBLIC are only available on the server.

To illustrate this, I'll add some logging and leverage Next.js's getServerSideProps in /pages/index.tsx. Note: We cannot use getServerSideProps AND getStaticProps in the same page component, so I have removed the later.

import React from 'react';
import {
  NextPage,
  GetServerSidePropsContext, GetServerSidePropsResult,
} from 'next';
import IndexContent from '~/screens/IndexContent';

interface IndexProps { greeting: string }

const IndexPage:NextPage<IndexProps> = (props: IndexProps) => {
  const { greeting } = props;

  console.log(`Inside IndexPage Render component. Browser: ${!!process.browser}`);
  console.log(process.env.MY_SECRET);
  console.log(process.env.NEXT_PUBLIC_THEME_GREETING_EMOJI);

  return (
    <IndexContent greeting={greeting} />
  );
};

export async function getServerSideProps(context: GetServerSidePropsContext):
Promise<GetServerSidePropsResult<IndexProps>> {
  console.log(`Inside getServerSideProps. Browser: ${!!process.browser}`);
  console.log(process.env.MY_SECRET);
  console.log(process.env.NEXT_PUBLIC_THEME_GREETING_EMOJI);

  return {
    props: { greeting: 'Hello From The Server' }, // will be passed to the page component as props
  };
}

export default IndexPage;

The above will log out MY_SECRET and NEXT_PUBLIC_THEME_GREETING_EMOJI as well as tell me if we're running in the browser or server.

Reloading this page, the server outputs: Screen Shot 2020-07-17 at 12.33.56 PM.png

Whereas the client outputs: Screen Shot 2020-07-17 at 12.34.36 PM.png As we can see, MY_SECRET is available to the IndexPage component when the page is rendered on the server, but not when rendered on the client. Also, note that MY_SECRET is available inside of getServerSideProps which is never run on the client.

Avoid Potential Leaking of Secrets โš ๏ธโš ๏ธ

As we saw in the previous example, MY_SECRET is technically available inside of the IndexPage component when rendered on the server. However, you should not attempt to directly use it in your component for rendering purposes. Since MY_SECRET will not be available on the client when it renders, React will trigger a Content did not match error because the client and the server will render different output. Also, through this error, you may leak your secrets meant only for the server.

To illustrate this error case, I can pass process.env.MY_SECRET as the value of the greeting prop of IndexContent.

// /pages/index.tsx
... 
  return (
    <IndexContent greeting={process.env.MY_SECRET || ''} />
  );
...

In the browser, this will produce: Screen Shot 2020-07-17 at 12.49.34 PM.png

Additionally, I get the content mismatch error between the client and the server. Screen Shot 2020-07-17 at 12.49.04 PM.png Note: That the value of MY_SECRET is exposed because it is part of the server output. Thus anyone viewing the page can look at the server rendered response and see our secret. Do not do this. If an environment variable does not start with NEXT_PUBLIC, only use it in server specific contexts like getServerProps, apiRoutes, etc.

Runtime vs. Build Time Evaluation

For extra confidence, if I do a production build of the application, I want to check the bundles for the values of the environment variables.

rm -rf .next && npm run build

Notice the console message that .env and .env.local are being read at build time. This is potentially worrisome.

Now I'll search the production build bundles for the values of private and public environment variables:

Searching for the value of MY_SECRET:

grep -rn "Extra Super Secret" .next/

This yields nothing, whereas searching for the value of NEXT_PUBLIC_THEME_GREETING_EMOJI

grep -rn "๐ŸŠ" .next/

.next//server/static/1gu-CLKzeq3pp_TQegMjc/pages/index.js:126:  return __jsx("div", null, __jsx("h1", null, greeting, "๐ŸŠ", "!"));
.next//server/static/1gu-CLKzeq3pp_TQegMjc/pages/index.js:141:  console.log("๐ŸŠ");
.next//server/static/1gu-CLKzeq3pp_TQegMjc/pages/index.js:150:  console.log("๐ŸŠ");

As we can see, our secret environment variable is not exposed in the build bundle, whereas our public one is completely replaced, even on the server. This means, public environment variables are interpolated at buildtime and private environment variables are read at runtime. Private variables never get bundled. However, this also means that public environment variables cannot be changed without running a whole new build. This may not be ideal in certain CI/CD cases when we want to create a single build and deploy to different environments (QA, Staging, etc) with different environment variables. Let's see if we can achieve runtime interpolation of public variables similar to that of private environment variables.

Introducing Runtime Configuration

We will leverage Next.js Runtime configuration to achieve this. Note: As stated before, in earlier versions of Next.js, the only way to work with Environment Variables was to leverage Runtime Configuration. This required, a separate dotenv loader dependency to work with the .env files. It worked, but was cumbersome. Now we can use it for what it was designed to do.

First, I'll create a next.config.js file in the project root with the following contents:

// /next.config.js

module.exports = {
  serverRuntimeConfig: {},
  publicRuntimeConfig: {
    // Will be available on both server and client
    greeting_emoji: process.env.NEXT_PUBLIC_THEME_GREETING_EMOJI,
  },
};

Next I'll update the IndexContent component to utilize the publicRuntimeConfig rather than consume the environment variable directly.

import React from 'react';
import getConfig from 'next/config';

const { publicRuntimeConfig } = getConfig();

interface IndexProps { greeting: string }

const IndexContent: React.FC<IndexProps> = (props) => {
  const { greeting } = props;

  console.log(publicRuntimeConfig); // Note: This console log

  return (
    <div>
      <h1>
        {greeting}
        {publicRuntimeConfig.greeting_emoji}
        !
      </h1>
    </div>
  );
};
export default IndexContent;

As a reminder, the contents of .env.local are:

# Local Environment Variables
NEXT_PUBLIC_THEME_GREETING_EMOJI="๐ŸŠ"
MY_SECRET="Extra Super Secret"

Next I'll do a production build via npm run build and search again for the values of the Environment Vars:

grep -rn "Extra Super Secret" .next/

Still nothing. Perfect.

grep -rn "๐ŸŠ" .next/

We have a few comment results that still directly use the Env Var but we also have:

... "runtimeConfig":{"greeting_emoji":"๐ŸŠ"}, ...

This is the runtimeConfig. As per before, the Environment Variable is interpolated at build. However, the ๐ŸŠ value here is simply the initial state.

If we run the production build, we will see: Screen Shot 2020-07-17 at 2.46.06 PM.png

Finally, I'll update the .env.local file with some new content to show that build time values are overwritten at runtime:

# Local Environment Variables
NEXT_PUBLIC_THEME_GREETING_EMOJI="๐Ÿ"
MY_SECRET="DEPLOY TIME SECRET"

Finally, without creating a new build, I simply start the production build again via npm start. Reloading the browser, I see:

Screen Shot 2020-07-17 at 2.48.56 PM.png

On the backend, I see: Screen Shot 2020-07-17 at 2.49.55 PM.png Note: The ๐ŸŠ is from console logging process.env.NEXT_PUBLIC_THEME_GREETING_EMOJI which was interpolated at build time. The ๐Ÿ, however is from console logging the publicRuntimeConfig which is evaluated at runtime. Also, note MY_SECRET has the value of "DEPLOY TIME SECRET". Try changing the values for both of these variables in .env.local and restart to see them change without building. It is slick.

So, did we learn?

  • Private environment variables are not available to the browser, are not included in the bundle and are evaluated at runtime.
  • Public environment variables are available to the browser and the server, are evaluated at buildtime, and cannot be changed at runtime.
  • Leveraging Next.js runtime configuration allows public environment variables to be sent to the browser and change at run time.

Closing Thoughts

Now that we have figured out Next.js's new approach to environment variables, we'll include this in our Github Repository Template project. View the full changeset covered in this post or check out the 0.0.5 release of the repository.

In the next part of this series, I will introduce Material-UI.

Discussion Point

What approach do you take to securely store your .env (or .env.local) files for QA and Production environments?

Image Credit: Photo by Polina Tankilevitch from Pexels