Next.js Supabase V3 (#463)

Version 3 of the kit:
- Radix UI replaced with Base UI (using the Shadcn UI patterns)
- next-intl replaces react-i18next
- enhanceAction deprecated; usage moved to next-safe-action
- main layout now wrapped with [locale] path segment
- Teams only mode
- Layout updates
- Zod v4
- Next.js 16.2
- Typescript 6
- All other dependencies updated
- Removed deprecated Edge CSRF
- Dynamic Github Action runner
This commit is contained in:
Giancarlo Buomprisco
2026-03-24 13:40:38 +08:00
committed by GitHub
parent 4912e402a3
commit 7ebff31475
840 changed files with 71395 additions and 20095 deletions

83
docs/security/csp.mdoc Normal file
View File

@@ -0,0 +1,83 @@
---
label: "Content Security Policy (CSP)"
title: "Content Security Policy (CSP) in the Next.js Supabase Turbo kit"
description: "Learn how to secure your Next.js application with Content Security Policy (CSP) using the Next.js Supabase Turbo kit."
order: 4
---
The Next.js Supabase Turbo kit provides a secure way to include [CSP headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CSP) in your application. This functionality is available starting from version 2.8.0 and uses [Nosecone](https://docs.arcjet.com/nosecone/reference#headers) to generate the CSP headers.
{% sequence title="Steps to configure CSP" description="Learn how to configure CSP in the Next.js Supabase Turbo kit." %}
[How to enable a strict CSP](#how-to-enable-a-strict-csp)
[Making your application compliant with CSP](#making-your-application-compliant-with-csp)
[Adding a nonce to your application](#adding-a-nonce-to-your-application)
[Modifying the default CSP configuration](#modifying-the-default-csp-configuration)
{% /sequence %}
## How to enable a strict CSP
To enable a strict CSP, you need to set the following environment variable:
```bash
ENABLE_STRICT_CSP=true
```
By default, this setting is disabled due to the overhead required for development. While this is a bit advanced, it is recommended to enable it before going to production - or at some point in your development process.
Once enabled, the CSP headers will be automatically added to the response headers using the Next.js middleware.
## Making your application compliant with CSP
Enabling a strict CSP has a few consequences on your application - you will need to make sure your application is compliant with the CSP rules.
1. You will need to add the `nonce` to any third-party script tags or stylesheets you have in your application.
2. If you make external HTTP requests, you will need to add the domains to the default list of allowed domains (by default, only requests to your Supabase project are allowed).
## Adding a nonce to your application
From a Server Component, you can retrieve a nonce from the `headers()` object.
```tsx
import { headers } from "next/headers";
const headersStore = await headers();
const nonce = headersStore.get("x-nonce");
```
If you want to pass the nonce to a Script tag, you can do so by adding the `nonce` attribute to the script tag.
```tsx
<script nonce={nonce} src="https://example.com/script.js"></script>
```
## Modifying the default CSP configuration
In the application middleware, modify the `apps/web/lib/create-csp-response.ts` file to modify the CSP configuration. You may need to do this to allow safe external requests that your application makes.
Please refer to the [Nosecone documentation](https://docs.arcjet.com/nosecone/reference#headers) for more information on the available options.
```ts {% filename="apps/web/lib/create-csp-response.ts" %}
const config: NoseconeOptions = {
...noseconeConfig,
contentSecurityPolicy: {
directives: {
...noseconeConfig.contentSecurityPolicy.directives,
connectSrc: [
...noseconeConfig.contentSecurityPolicy.directives.connectSrc,
...ALLOWED_ORIGINS,
],
imgSrc: [
...noseconeConfig.contentSecurityPolicy.directives.imgSrc,
...IMG_SRC_ORIGINS,
],
upgradeInsecureRequests: UPGRADE_INSECURE_REQUESTS,
},
},
crossOriginEmbedderPolicy: CROSS_ORIGIN_EMBEDDER_POLICY,
};
```

View File

@@ -0,0 +1,168 @@
---
label: "Data Validation"
title: "Data Validation in the Next.js Supabase Turbo kit"
description: "Learn how to validate data in the Next.js Supabase Turbo kit."
order: 3
---
Data Validation is a crucial aspect of building secure applications. In this section, we will look at how to validate data in the Next.js Supabase Turbo kit.
{% sequence title="Steps to validate data" description="Learn how to validate data in the Next.js Supabase Turbo kit." %}
[What are we validating?](#what-are-we-validating)
[Using Zod to validate data](#using-zod-to-validate-data)
[Validating payload data to Server Side API](#validating-payload-data-to-server-side-api)
[Validating Cookies](#validating-cookies)
[Validating URL parameters](#validating-url-parameters)
{% /sequence %}
## What are we validating?
A general rule, is that all client-side provided data should be validated/sanitized. This includes:
- URL parameters
- Search params
- Form data
- Cookies
- Any data provided by the user
## Using Zod to validate data
The Next.js Supabase Turbo kit uses [Zod](https://zod.dev/) to validate data. You can use the `z` object to validate data in your application.
```ts
import * as z from "zod";
const userSchema = z.object({
id: z.string(),
name: z.string(),
email: z.string().email(),
});
// Validate the data
const validatedData = userSchema.parse(data);
```
## Validating payload data to Server Side API
We generally use two ways for sending data from a client to a server:
1. Server Actions
2. API Route Handlers
Let's look at how we can validate data for both of these cases.
### Server Actions: Using authActionClient
The `authActionClient` creates type-safe server actions with built-in Zod validation and authentication.
```ts
'use server';
import { authActionClient } from "@kit/next/safe-action";
import * as z from "zod";
const UserSchema = z.object({
id: z.string(),
name: z.string(),
email: z.string().email(),
});
export const createUserAction = authActionClient
.inputSchema(UserSchema)
.action(async ({ parsedInput, ctx: { user } }) => {
// parsedInput is now validated against the UserSchema
// do something with the validated data
});
```
When you define an action using `authActionClient`, the `parsedInput` is validated against the `schema` automatically. The `ctx.user` provides the authenticated user.
### API Route Handlers: Using the "enhanceRouteHandler" utility
The `enhanceRouteHandler` hook is a utility hook that enhances Next.js API Route Handlers with Zod validation.
```ts
import { enhanceRouteHandler } from "@kit/next/routes";
import * as z from "zod";
const UserSchema = z.object({
id: z.string(),
name: z.string(),
email: z.string().email(),
});
export const POST = enhanceRouteHandler(async ({ body, user, request }) => {
// body is now validated against the UserSchema
// user is the authenticated user
// request is the incoming request
// do something with the validated data
}, {
schema: UserSchema,
auth: true,
});
```
Very similar to `authActionClient`, the `enhanceRouteHandler` utility enhances the handler function with Zod validation and authentication.
When you define an API route using `enhanceRouteHandler`, the `body` argument is validated against the `schema` option automatically. The `user` argument provides the authenticated user, and `request` is the incoming request object.
## Validating Cookies
Whenever you use a cookie in your application, you should validate the cookie data.
Let's assume you receive a cookie with the name `my-cookie`, and you expect it to be a number. You can validate the cookie data as follows:
```ts
import * as z from "zod";
import { cookies } from "next/headers";
const cookieStore = await cookies();
const cookie = z.coerce.number()
.safeParse(cookieStore.get("my-cookie")?.value);
if (!cookie.success) {
// handle the error or provide a default value
}
```
## Validating URL parameters
Whenever you receive a URL parameter, you should validate the parameter data. Let's assume you receive a URL parameter with the name `my-param`, and you expect it to be a number. You can validate the parameter data as follows:
```ts
interface PageProps {
searchParams: Promise<{ myParam: string }>;
}
async function Page({ searchParams }: PageProps) {
const params = await searchParams;
const param = z.coerce.number()
.safeParse(params.myParam);
if (!param.success) {
// handle the error or provide a default value
}
// render the page with the validated data
}
```
Going forward, remember to validate all data that you receive from the client, and never trust anything the client provides you with. Always have a default value ready to handle invalid data, which can prevent potential security issues or bugs in your application.
## Upgrading from v2
{% callout title="Differences with v2" %}
In v2, server actions used `enhanceAction` from `@kit/next/actions` and Zod was imported as `import { z } from 'zod'`. In v3, server actions use `authActionClient` from `@kit/next/safe-action` and Zod is imported as `import * as z from 'zod'`.
For the full migration guide, see [Upgrading from v2 to v3](/docs/next-supabase-turbo/installation/v3-migration).
{% /callout %}

View File

@@ -0,0 +1,190 @@
---
label: "Next.js Best Practices"
title: "Next.js Security Best Practices"
description: "Learn best practices for Next.js in the Next.js Supabase Turbo kit."
order: 1
---
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.
{% sequence title="Best practices for Next.js in the Next.js Supabase Turbo kit" description="Learn best practices for Next.js in the Next.js Supabase Turbo kit." %}
[Do not pass sensitive data to client components](#do-not-pass-sensitive-data-to-client-components)
[Do not mix up client and server imports](#do-not-mix-up-client-and-server-imports)
[Environment variables](#environment-variables)
[Proper use of .env files](#proper-use-of-.env-files)
{% /sequence %}
## 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.
```tsx
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.
```tsx
'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.
```ts
import 'server-only';
const config = {
apiKey: process.env.API_KEY,
storeId: process.env.STORE_ID,
};
export async function fetchData() {
const data = await fetchDataFromExternalApi(config);
const storeId = config.storeId;
return { data, storeId };
}
```
Now, we can use this service in the server component.
```tsx
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](https://nextjs.org/docs/app/building-your-application/data-fetching/fetching#preventing-sensitive-data-from-being-exposed-to-the-client) 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`.
```ts
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.
```json
{
"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_`:
```tsx
// This is available in client components
console.log(process.env.NEXT_PUBLIC_API_URL)
// This is undefined in client components
console.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 client
NEXT_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:
1. API keys, secrets, tokens, and passwords should never use the `NEXT_PUBLIC_` prefix
2. 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:
1. Store sensitive values in `.env.local` which should not be committed to your repository
2. Use environment-specific files for values that differ between environments
3. Use environment variables for sensitive data, not hardcoded values
4. Use `NEXT_PUBLIC_` prefix for environment variables that are available on the client
5. Never use `NEXT_PUBLIC_` variables for sensitive data
6. Use `import 'server-only'` for server-only code
7. Separate server and client code in different files and never mix them up in barrel files
8. Use the Taint API to prevent sensitive data from being exposed to the client (experimental). Makerkit will at some point adopt this API.

View File

@@ -0,0 +1,194 @@
---
label: "Row Level Security"
title: "Row Level Security in the Next.js Supabase Turbo kit"
description: "Learn how to secure your Next.js application with Row Level Security (RLS) using the Next.js Supabase Turbo kit."
order: 2
---
If you've opted to using the Data API in your application, you can use Row Level Security (RLS) to secure your data (which Makerkit does by default).
{% sequence title="Steps to secure your data with RLS" description="Learn how to secure your data with RLS using the Next.js Supabase Turbo kit." %}
[Enable RLS for your tables](#enable-rls-for-your-tables)
[Add the RLS policy](#add-the-rls-policy)
[Using Makerkit's Functions to write RLS policies](#using-makerkits-functions-to-write-rls-policies)
[Testing RLS policies](#testing-rls-policies)
{% /sequence %}
The general rule of thumb is that you must always ensure RLS is enabled for your tables. **Failure to do so will result in a security vulnerability** because the table will be exposed to the public - and everyone will be able to read and write to it. You don't want that.
Supabase has a great [guide on how to use RLS](https://supabase.com/docs/guides/database/postgres/row-level-security) - don't skip it!
## Enable RLS for your tables
When you write your table migrations, you must ensure that RLS is enabled for the table.
First, create a table with the following command:
```sql
create table if not exists public.documents (
id uuid primary key default uuid_generate_v4(),
user_id uuid not null references auth.users(id),
title text not null,
content text not null,
created_at timestamp with time zone default now(),
updated_at timestamp with time zone default now()
);
```
### Enable RLS for the table
Now, you need to enable RLS for the table.
```sql
alter table public.documents enable row level security;
```
PS: you can also enable RLS for the table using the Supabase Studio - however I am not sure I'd recommend this approach.
## Add the RLS policy for restricted access
Now that we have a table with RLS enabled, **and no policies added**, you will notice that you won't be able to read or write to the table. This is good, because it means that the table is secure. However, we want to be able to read and write to the table **for the users that are authorized to do so**.
To do this, we need to add a policy to the table.
```sql
create policy "Users can view their own documents"
on public.documents
to authenticated
for select
using ((select auth.uid()) = user_id);
```
This policy will allow any **authenticated** user to read **their own documents** - e.g. the documents whose `user_id` matches the authenticated user's ID.
If you wanted to allow all actions at once, you could use the following policy:
```sql
create policy "Users can view their own documents"
on public.documents
to authenticated
for all
using ((select auth.uid()) = user_id);
```
## Using Makerkit's Functions to write RLS policies
Makerkit comes with a set of functions that make it easier to write RLS policies. Below are some of the most common use cases:
### Verifying a user is part of a Team Account
If you want to verify that a user is part of a Team Account, you can use the `public.has_role_on_account` function.
```sql
create policy "Users can view their account's documents"
on public.documents
to authenticated
for select
using (public.has_role_on_account(account_id));
```
If you want to check for a specific role, you can do so by passing the role name to the function.
```sql
create policy "Users can view their account's documents"
on public.documents
to authenticated
for select
using (public.has_role_on_account(account_id, 'admin'));
```
**Note:** We're expecting the `public.documents` table to have an `account_id` column that references the `public.accounts` table.
### Verifying a user has the required permissions
If you want to verify that a user has the required permissions, you can use the `public.has_permission` function.
```sql
create policy "Users can view their account's documents"
on public.documents
to authenticated
for select
using (public.has_permission(auth.uid(), account_id, 'documents.read'));
```
### Verifying a user is the owner of a Team Account
If you want to verify that a user is the owner of a Team Account, you can use the `public.is_owner` function.
```sql
create policy "Users can view their account's documents"
on public.documents
to authenticated
for select
using (public.is_account_owner(account_id));
```
These are just some of the most common use cases and will likely cover the vast majority of your RLS policies.
## Testing RLS policies
We have two different ways to test RLS policies:
1. **Manually**: Using the Supabase Studio impersonation feature
2. **Automatically**: Using [pgTap](https://supabase.com/docs/reference/cli/supabase-test-db) tests
### Manually testing RLS policies
To test RLS policies manually, you can use the Supabase Studio impersonation feature.
1. Go to the Supabase Studio
2. Navigate to the Table you want to test
3. Impersonate a user
4. Try to read, write or delete data using the UI or the SQL editor
5. Verify that the data is restricted based on the RLS policy
### Automatically testing RLS policies
To test RLS policies automatically, you can use the `supabase test db` command. This command uses pgTap to test the RLS policies - which is an invaluable tool for testing RLS policies in an automated and repeatable way.
We have a set of tests in the `supabase/tests/database` folder that are designed to test the RLS policies. You can copy the same structure and add your own tests.
Here's an example of a test:
```sql
-- authenticate with a user
select tests.create_supabase_user('testuser', 'testuser@test.com');
select tests.create_supabase_user('testuser2', 'testuser2@test.com');
-- authenticate as the first user
select tests.authenticate_as('testuser');
-- create a document
insert into public.documents (user_id, title, content)
values (tests.get_supabase_uid('testuser'), 'Test Document', 'This is a test document');
-- test the user can read their own document
select row_eq(
$$ SELECT * from public.documents $$,
row(tests.get_supabase_uid('testuser'), 'Test Document', 'This is a test document'),
'User should be able to read their own document'
);
-- alternatively, check the list is not empty
select not_empty(
$$ SELECT * from public.documents $$,
'User should be able to read their own document'
);
-- authenticate with another user
select tests.authenticate_as('testuser2');
-- test that the document is not visible to the other user
select is_empty(
$$ SELECT * from public.documents $$,
'No documents should be visible to unauthenticated users'
);
```
This is a simple example, but you can see how you can test the RLS policies for different scenarios.