Next.js Security Best Practices
Learn best practices for Next.js in the Next.js Supabase Turbo kit.
This section contains a list of best practices for Next.js in general, applicable to both the Next.js Supabase Turbo kit or any other Next.js application.
Best practices for Next.js in the Next.js Supabase Turbo kit
Learn best practices for Next.js in the Next.js Supabase Turbo kit.
What goes in client components will be passed to the client
The first and most important rule you must remember is that what goes in client components will be passed to the client.
From this rule, you can derive the following best practices:
- Do not pass sensitive data to client components. If you're using an API Key server-side, do not pass it to the client.
- Avoid exporting server and client code from the same file. Instead, define separate entry points for server and client code.
- Use the
import 'server-only'
package to raise errors when server code unexpectedly ends up in the client bundle.
Do not pass sensitive data to client components
One common mistake made by Next.js developers is passing sensitive data to client components. This is easier than it sounds, and it can happen without you even noticing it.
Let's look at the following example: we are using an API call in a server component, and we end up passing the API Key to the client.
async function ServerComponent() { const config = { apiKey: process.env.API_KEY, storeId: process.env.STORE_ID, }; const data = await fetchData(config); return <ClientComponent data={data} config={config} />;}
In this example, the config
object contains sensitive data, such as the API Key and the Store ID (which is a public identifier for the store, so safe to pass to the client). This data will be passed to the client, and can be accessed by the client.
'use client';function ClientComponent({ data, config }: { data: any, config: any }) { // ...}
This is a problem, because the config
object contains sensitive data, such as the API Key and the Store ID. The fact we pass it down to a 'use client'
component means that the data will be passed to the client and this means leaking sensitive data to the client, which is a security risk.
This can happen in many other ways, and it's a good idea to be aware of this.
A better approach is to define a service using import 'server-only'
and use it in the server component.
import 'server-only';const config = { apiKey: process.env.API_KEY, storeId: process.env.STORE_ID,};export async function fetchData() { const data = await fecthDataFromExternalApi(config); const storeId = config.storeId; return { data, storeId };}
Now, we can use this service in the server component.
import { fetchData } from './fetch-data';async function ServerComponent() { const data = await fetchData(); return <ClientComponent data={data} />;}
While this doesn't fully solve the problem (you can still pass the config object to the client, but it's a lot harder), it's a good start and will help you separate concerns.
The Taint API will eventually help us solve this issue even better, but it's still experimental.
Do not mix up client and server imports
Sometimes, you have a package that exports both client and server code. This is a problem, because it will end up in the client bundle.
For example, we assume we have a barrel file index.ts
from which we export a fetchData
function and a client component ClientComponent
.
export * from './fetch-data';export * from './client-component';
This is a problem, because it's possible that some server-side code ends up in the client bundle.
Adding import 'server-only'
to the barrel file will solve this issue and raise an error, however, as a best practice, you should avoid this in the first place and use different barrel files for server and client code; then use the exports
field in package.json
to re-export the server and client code from the barrel file.
{ "exports": { "./server": "./server.ts", "./client": "./client.tsx" }}
This way, you can import the server and client code separately and you won't end up with a mix of server and client code in the client bundle.
Environment variables
Environment variables are essential for configuring your application across different environments. However, they require careful management to prevent security vulnerabilities.
Use NEXT_PUBLIC prefix for environment variables that are available on the client
Next.js handles environment variables uniquely, distinguishing between server-only and client-available variables:
- Variables without the
NEXT_PUBLIC_
prefix are only available on the server - Variables with the
NEXT_PUBLIC_
prefix are available on both server and client
Client components can only access environment variables prefixed with NEXT_PUBLIC_
:
// This is available in client componentsconsole.log(process.env.NEXT_PUBLIC_API_URL)// This is undefined in client componentsconsole.log(process.env.SECRET_API_KEY)
Never use NEXT_PUBLIC_ variables for sensitive data
Since NEXT_PUBLIC_
variables are embedded in the JavaScript bundle sent to browsers, they should never contain sensitive information:
# .env# UNSAFE: This will be exposed to the clientNEXT_PUBLIC_API_KEY=sk_live_1234567890 # NEVER DO THIS!
SAFE: This is only available server-side
API_KEY=sk_live_1234567890 # This is correct
Remember:
- API keys, secrets, tokens, and passwords should never use the
NEXT_PUBLIC_
prefix - Use
NEXT_PUBLIC_
only for genuinely public information like public API URLs, feature flags, or public identifiers
Proper use of .env files
Next.js supports various .env files for different environments:
.env # Loaded in all environments.env.local # Loaded in all environments, ignored by git.env.development # Only loaded in development environment.env.production # Only loaded in production environment.env.production.local # Only loaded in production environment, ignored by git.env.test # Only loaded in test environment
As a general rule, never add sensitive data to the .env
file or any other committed file. Instead, add it to the .env.local
file, which by default is ignored by git (though you must check this if you're not using Makerkit).
Best practices:
- Store sensitive values in
.env.local
which should not be committed to your repository - Use environment-specific files for values that differ between environments
- Use environment variables for sensitive data, not hardcoded values
- Use
NEXT_PUBLIC_
prefix for environment variables that are available on the client - Never use
NEXT_PUBLIC_
variables for sensitive data - Use
import 'server-only'
for server-only code - Separate server and client code in different files and never mix them up in barrel files
- Use the Taint API to prevent sensitive data from being exposed to the client (experimental). Makerkit will at some point adopt this API.