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:
committed by
GitHub
parent
4912e402a3
commit
7ebff31475
@@ -1,58 +1,31 @@
|
||||
# Next.js Utilities
|
||||
# @kit/next — Next.js Utilities
|
||||
|
||||
## Quick Reference
|
||||
## Non-Negotiables
|
||||
|
||||
| Function | Import | Purpose |
|
||||
|----------|--------|---------|
|
||||
| `enhanceAction` | `@kit/next/actions` | Server actions with auth + validation |
|
||||
| `enhanceRouteHandler` | `@kit/next/routes` | API routes with auth + validation |
|
||||
|
||||
## Guidelines
|
||||
|
||||
- Server Actions for mutations only, not data-fetching
|
||||
- Keep actions light - move business logic to services
|
||||
- Authorization via RLS, not application code
|
||||
- Use `'use server'` at top of file
|
||||
- Always validate with Zod schema
|
||||
1. ALWAYS validate input with Zod schema via `.inputSchema()`
|
||||
2. ALWAYS use `authActionClient` for authenticated actions, `publicActionClient` for public
|
||||
3. ALWAYS use `useAction` hook from `next-safe-action/hooks` on the client side
|
||||
4. ALWAYS use `revalidatePath` after mutations
|
||||
5. NEVER use server actions for data fetching — mutations only
|
||||
6. NEVER put business logic in actions — extract to service files
|
||||
7. NEVER use `router.refresh()` or `router.push()` after server actions
|
||||
8. NEVER use `adminActionClient` outside admin features — use `authActionClient`
|
||||
|
||||
## Skills
|
||||
|
||||
For detailed implementation patterns:
|
||||
- `/server-action-builder` - Complete server action workflow
|
||||
- `/server-action-builder` — Complete server action workflow
|
||||
|
||||
## Server Action Pattern
|
||||
## Quick Reference
|
||||
|
||||
```typescript
|
||||
'use server';
|
||||
| Function | Import | Purpose |
|
||||
| --------------------- | ----------------------- | ---------------------------------- |
|
||||
| `authActionClient` | `@kit/next/safe-action` | Authenticated server actions |
|
||||
| `publicActionClient` | `@kit/next/safe-action` | Public server actions (no auth) |
|
||||
| `captchaActionClient` | `@kit/next/safe-action` | Server actions with CAPTCHA + auth |
|
||||
| `enhanceRouteHandler` | `@kit/next/routes` | API routes with auth + validation |
|
||||
|
||||
import { enhanceAction } from '@kit/next/actions';
|
||||
## Exemplars
|
||||
|
||||
export const myAction = enhanceAction(
|
||||
async function (data, user) {
|
||||
// data: validated, user: authenticated
|
||||
return { success: true };
|
||||
},
|
||||
{
|
||||
auth: true,
|
||||
schema: MySchema,
|
||||
},
|
||||
);
|
||||
```
|
||||
|
||||
## Route Handler Pattern
|
||||
|
||||
```typescript
|
||||
import { enhanceRouteHandler } from '@kit/next/routes';
|
||||
|
||||
export const POST = enhanceRouteHandler(
|
||||
async function ({ body, user, request }) {
|
||||
return NextResponse.json({ success: true });
|
||||
},
|
||||
{ auth: true, schema: MySchema },
|
||||
);
|
||||
```
|
||||
|
||||
## Revalidation
|
||||
|
||||
- Use `revalidatePath` after mutations
|
||||
- Never use `router.refresh()` or `router.push()` after Server Actions
|
||||
- Server action: `packages/features/accounts/src/server/personal-accounts-server-actions.ts`
|
||||
- Route handler: `apps/web/app/[locale]/home/[account]/members/policies/route.ts`
|
||||
- Client usage: `apps/web/app/[locale]/(marketing)/contact/_components/contact-form.tsx`
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
import eslintConfigBase from '@kit/eslint-config/base.js';
|
||||
|
||||
export default eslintConfigBase;
|
||||
@@ -1,35 +1,33 @@
|
||||
{
|
||||
"name": "@kit/next",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"scripts": {
|
||||
"clean": "git clean -xdf .turbo node_modules",
|
||||
"format": "prettier --check \"**/*.{ts,tsx}\"",
|
||||
"lint": "eslint .",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"prettier": "@kit/prettier-config",
|
||||
"exports": {
|
||||
"./actions": "./src/actions/index.ts",
|
||||
"./routes": "./src/routes/index.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@kit/auth": "workspace:*",
|
||||
"@kit/eslint-config": "workspace:*",
|
||||
"@kit/monitoring": "workspace:*",
|
||||
"@kit/prettier-config": "workspace:*",
|
||||
"@kit/supabase": "workspace:*",
|
||||
"@kit/tsconfig": "workspace:*",
|
||||
"@supabase/supabase-js": "catalog:",
|
||||
"@types/node": "catalog:",
|
||||
"next": "catalog:",
|
||||
"zod": "catalog:"
|
||||
},
|
||||
"private": true,
|
||||
"typesVersions": {
|
||||
"*": {
|
||||
"*": [
|
||||
"src/*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"exports": {
|
||||
"./actions": "./src/actions/index.ts",
|
||||
"./safe-action": "./src/actions/safe-action-client.ts",
|
||||
"./routes": "./src/routes/index.ts"
|
||||
},
|
||||
"scripts": {
|
||||
"clean": "git clean -xdf .turbo node_modules",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"next-safe-action": "catalog:"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@kit/auth": "workspace:*",
|
||||
"@kit/monitoring": "workspace:*",
|
||||
"@kit/supabase": "workspace:*",
|
||||
"@kit/tsconfig": "workspace:*",
|
||||
"@supabase/supabase-js": "catalog:",
|
||||
"next": "catalog:",
|
||||
"zod": "catalog:"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import 'server-only';
|
||||
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
import { ZodType, z } from 'zod';
|
||||
@@ -12,6 +11,7 @@ import { JWTUserData } from '@kit/supabase/types';
|
||||
/**
|
||||
* @name enhanceAction
|
||||
* @description Enhance an action with captcha, schema and auth checks
|
||||
* @deprecated Use [authActionClient](safe-action-client.ts) instead
|
||||
*/
|
||||
export function enhanceAction<
|
||||
Args,
|
||||
@@ -20,19 +20,22 @@ export function enhanceAction<
|
||||
auth?: boolean;
|
||||
captcha?: boolean;
|
||||
schema?: z.ZodType<
|
||||
Config['captcha'] extends true ? Args & { captchaToken: string } : Args,
|
||||
z.ZodTypeDef
|
||||
Config['captcha'] extends true ? Args & { captchaToken: string } : Args
|
||||
>;
|
||||
},
|
||||
>(
|
||||
fn: (
|
||||
params: Config['schema'] extends ZodType ? z.infer<Config['schema']> : Args,
|
||||
params: Config['schema'] extends ZodType
|
||||
? z.output<Config['schema']>
|
||||
: Args,
|
||||
user: Config['auth'] extends false ? undefined : JWTUserData,
|
||||
) => Response | Promise<Response>,
|
||||
config: Config,
|
||||
) {
|
||||
return async (
|
||||
params: Config['schema'] extends ZodType ? z.infer<Config['schema']> : Args,
|
||||
params: Config['schema'] extends ZodType
|
||||
? z.output<Config['schema']>
|
||||
: Args,
|
||||
) => {
|
||||
type UserParam = Config['auth'] extends false ? undefined : JWTUserData;
|
||||
|
||||
@@ -80,6 +83,11 @@ export function enhanceAction<
|
||||
user = auth.data as UserParam;
|
||||
}
|
||||
|
||||
return fn(data, user);
|
||||
return fn(
|
||||
data as Config['schema'] extends ZodType
|
||||
? z.output<Config['schema']>
|
||||
: Args,
|
||||
user,
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
55
packages/next/src/actions/safe-action-client.ts
Normal file
55
packages/next/src/actions/safe-action-client.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import 'server-only';
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
import { createSafeActionClient } from 'next-safe-action';
|
||||
|
||||
import { verifyCaptchaToken } from '@kit/auth/captcha/server';
|
||||
import { requireUser } from '@kit/supabase/require-user';
|
||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||
|
||||
const baseClient = createSafeActionClient({
|
||||
handleServerError: (error) => error.message,
|
||||
});
|
||||
|
||||
/**
|
||||
* @name publicActionClient
|
||||
* @description Safe action client for public actions that don't require authentication.
|
||||
*/
|
||||
export const publicActionClient = baseClient;
|
||||
|
||||
/**
|
||||
* @name authActionClient
|
||||
* @description Safe action client for authenticated actions. Adds user context.
|
||||
*/
|
||||
export const authActionClient = baseClient.use(async ({ next }) => {
|
||||
const auth = await requireUser(getSupabaseServerClient());
|
||||
|
||||
if (!auth.data) {
|
||||
redirect(auth.redirectTo);
|
||||
}
|
||||
|
||||
return next({ ctx: { user: auth.data } });
|
||||
});
|
||||
|
||||
/**
|
||||
* @name captchaActionClient
|
||||
* @description Safe action client for actions that require CAPTCHA and authentication.
|
||||
*/
|
||||
export const captchaActionClient = baseClient.use(
|
||||
async ({ next, clientInput }) => {
|
||||
const input = clientInput as Record<string, unknown>;
|
||||
|
||||
const token =
|
||||
typeof input?.captchaToken === 'string' ? input.captchaToken : '';
|
||||
|
||||
await verifyCaptchaToken(token);
|
||||
|
||||
const auth = await requireUser(getSupabaseServerClient());
|
||||
|
||||
if (!auth.data) {
|
||||
redirect(auth.redirectTo);
|
||||
}
|
||||
|
||||
return next({ ctx: { user: auth.data } });
|
||||
},
|
||||
);
|
||||
@@ -1,9 +1,8 @@
|
||||
import 'server-only';
|
||||
|
||||
import { redirect } from 'next/navigation';
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
import { z } from 'zod';
|
||||
import * as z from 'zod';
|
||||
|
||||
import { verifyCaptchaToken } from '@kit/auth/captcha/server';
|
||||
import { requireUser } from '@kit/supabase/require-user';
|
||||
@@ -22,7 +21,7 @@ interface HandlerParams<
|
||||
> {
|
||||
request: NextRequest;
|
||||
user: RequireAuth extends false ? undefined : JWTUserData;
|
||||
body: Schema extends z.ZodType ? z.infer<Schema> : undefined;
|
||||
body: Schema extends z.ZodType ? z.output<Schema> : undefined;
|
||||
params: Record<string, string>;
|
||||
}
|
||||
|
||||
@@ -48,7 +47,7 @@ interface HandlerParams<
|
||||
*/
|
||||
export const enhanceRouteHandler = <
|
||||
Body,
|
||||
Params extends Config<z.ZodType<Body, z.ZodTypeDef>>,
|
||||
Params extends Config<z.ZodType<Body>>,
|
||||
>(
|
||||
// Route handler function
|
||||
handler:
|
||||
|
||||
Reference in New Issue
Block a user