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,43 +1,22 @@
|
||||
# Database & Authentication
|
||||
# @kit/supabase — Database & Authentication
|
||||
|
||||
## Non-Negotiables
|
||||
|
||||
1. 3 clients: `getSupabaseServerClient()` (server, RLS enforced), `useSupabase()` (client hook, RLS enforced), `getSupabaseServerAdminClient()` (bypasses RLS, use rarely and only if needed)
|
||||
2. NEVER use admin client without manually validating authorization first
|
||||
3. NEVER modify `database.types.ts` manually — regenerate with `pnpm supabase:web:typegen`
|
||||
4. NEVER add manual auth checks when using standard client — trust RLS
|
||||
5. ALWAYS add indexes on foreign keys
|
||||
6. ALWAYS include `account_id` in storage paths
|
||||
7. Use `Tables<'table_name'>` from `@kit/supabase/database` for type references, don't create new types
|
||||
|
||||
## Skills
|
||||
|
||||
For database work:
|
||||
- `/postgres-expert` - Schemas, RLS, migrations
|
||||
- `/postgres-expert` — Schemas, RLS, migrations, query optimization
|
||||
|
||||
## Client Usage
|
||||
## SQL Helper Functions
|
||||
|
||||
### Server Components (Preferred)
|
||||
|
||||
```typescript
|
||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||
|
||||
const client = getSupabaseServerClient();
|
||||
const { data } = await client.from('table').select('*');
|
||||
// RLS automatically enforced
|
||||
```
|
||||
|
||||
### Client Components
|
||||
|
||||
```typescript
|
||||
'use client';
|
||||
import { useSupabase } from '@kit/supabase/hooks/use-supabase';
|
||||
|
||||
const supabase = useSupabase();
|
||||
```
|
||||
|
||||
### Admin Client (Use Sparingly)
|
||||
|
||||
```typescript
|
||||
import { getSupabaseServerAdminClient } from '@kit/supabase/server-admin-client';
|
||||
|
||||
// CRITICAL: Bypasses RLS - validate manually!
|
||||
const adminClient = getSupabaseServerAdminClient();
|
||||
```
|
||||
|
||||
## Existing Helper Functions
|
||||
|
||||
```sql
|
||||
public.has_role_on_account(account_id, role?)
|
||||
public.has_permission(user_id, account_id, permission)
|
||||
public.is_account_owner(account_id)
|
||||
@@ -46,29 +25,16 @@ public.is_team_member(account_id, user_id)
|
||||
public.is_super_admin()
|
||||
```
|
||||
|
||||
## Type Generation
|
||||
## Key Imports
|
||||
|
||||
```typescript
|
||||
import { Tables } from '@kit/supabase/database';
|
||||
| Function | Import |
|
||||
| ------------- | -------------------------------------------------------------------------------- |
|
||||
| Server client | `getSupabaseServerClient` from `@kit/supabase/server-client` |
|
||||
| Client hook | `useSupabase` from `@kit/supabase/hooks/use-supabase` |
|
||||
| Admin client | `getSupabaseServerAdminClient` from `@kit/supabase/server-admin-client` |
|
||||
| Require user | `requireUser` from `@kit/supabase/require-user` |
|
||||
| MFA check | `checkRequiresMultiFactorAuthentication` from `@kit/supabase/check-requires-mfa` |
|
||||
|
||||
type Account = Tables<'accounts'>;
|
||||
```
|
||||
## Exemplar
|
||||
|
||||
Never modify `database.types.ts` - regenerate with `pnpm supabase:web:typegen`.
|
||||
|
||||
## Authentication
|
||||
|
||||
```typescript
|
||||
import { requireUser } from '@kit/supabase/require-user';
|
||||
import { checkRequiresMultiFactorAuthentication } from '@kit/supabase/check-requires-mfa';
|
||||
|
||||
const user = await requireUser(client);
|
||||
const requiresMfa = await checkRequiresMultiFactorAuthentication(client);
|
||||
```
|
||||
|
||||
## Security Guidelines
|
||||
|
||||
- Standard client: Trust RLS
|
||||
- Admin client: Validate everything manually
|
||||
- Always add indexes for foreign keys
|
||||
- Storage paths must include account_id
|
||||
- `apps/web/app/[locale]/home/(user)/_lib/server/load-user-workspace.ts` — server client with RLS
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
import eslintConfigBase from '@kit/eslint-config/base.js';
|
||||
|
||||
export default eslintConfigBase;
|
||||
@@ -1,14 +1,14 @@
|
||||
{
|
||||
"name": "@kit/supabase",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"scripts": {
|
||||
"clean": "git clean -xdf .turbo node_modules",
|
||||
"format": "prettier --check \"**/*.{ts,tsx}\"",
|
||||
"lint": "eslint .",
|
||||
"typecheck": "tsc --noEmit"
|
||||
"private": true,
|
||||
"typesVersions": {
|
||||
"*": {
|
||||
"*": [
|
||||
"src/*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"prettier": "@kit/prettier-config",
|
||||
"exports": {
|
||||
"./server-client": "./src/clients/server-client.ts",
|
||||
"./server-admin-client": "./src/clients/server-admin-client.ts",
|
||||
@@ -21,27 +21,21 @@
|
||||
"./auth": "./src/auth.ts",
|
||||
"./types": "./src/types.ts"
|
||||
},
|
||||
"scripts": {
|
||||
"clean": "git clean -xdf .turbo node_modules",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@kit/shared": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@kit/eslint-config": "workspace:*",
|
||||
"@kit/prettier-config": "workspace:*",
|
||||
"@kit/tsconfig": "workspace:*",
|
||||
"@supabase/ssr": "^0.8.0",
|
||||
"@supabase/ssr": "catalog:",
|
||||
"@supabase/supabase-js": "catalog:",
|
||||
"@tanstack/react-query": "catalog:",
|
||||
"@types/node": "catalog:",
|
||||
"@types/react": "catalog:",
|
||||
"next": "catalog:",
|
||||
"react": "catalog:",
|
||||
"zod": "catalog:"
|
||||
},
|
||||
"typesVersions": {
|
||||
"*": {
|
||||
"*": [
|
||||
"src/*"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import 'server-only';
|
||||
|
||||
import {
|
||||
AuthError,
|
||||
type EmailOtpType,
|
||||
@@ -306,16 +305,16 @@ function getAuthErrorMessage(params: { error: string; code?: string }) {
|
||||
// this error arises when the user tries to sign in with an expired email link
|
||||
if (params.code) {
|
||||
if (params.code === 'otp_expired') {
|
||||
return 'auth:errors.otp_expired';
|
||||
return 'auth.errors.otp_expired';
|
||||
}
|
||||
}
|
||||
|
||||
// this error arises when the user is trying to sign in with a different
|
||||
// browser than the one they used to request the sign in link
|
||||
if (isVerifierError(params.error)) {
|
||||
return 'auth:errors.codeVerifierMismatch';
|
||||
return 'auth.errors.codeVerifierMismatch';
|
||||
}
|
||||
|
||||
// fallback to the default error message
|
||||
return `auth:authenticationErrorAlertBody`;
|
||||
return `auth.authenticationErrorAlertBody`;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import 'server-only';
|
||||
|
||||
import { type NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
import { createServerClient } from '@supabase/ssr';
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import 'server-only';
|
||||
|
||||
import { createClient } from '@supabase/supabase-js';
|
||||
|
||||
import { Database } from '../database.types';
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import 'server-only';
|
||||
|
||||
import { cookies } from 'next/headers';
|
||||
|
||||
import { createServerClient } from '@supabase/ssr';
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import 'server-only';
|
||||
|
||||
import { z } from 'zod';
|
||||
import * as z from 'zod';
|
||||
|
||||
const message =
|
||||
'Invalid Supabase Secret Key. Please add the environment variable SUPABASE_SECRET_KEY or SUPABASE_SERVICE_ROLE_KEY.';
|
||||
'Invalid Supabase Secret Key. Please add the environment variable SUPABASE_SECRET_KEY.';
|
||||
|
||||
/**
|
||||
* @name getSupabaseSecretKey
|
||||
@@ -13,14 +12,12 @@ const message =
|
||||
export function getSupabaseSecretKey() {
|
||||
return z
|
||||
.string({
|
||||
required_error: message,
|
||||
error: message,
|
||||
})
|
||||
.min(1, {
|
||||
message: message,
|
||||
})
|
||||
.parse(
|
||||
process.env.SUPABASE_SECRET_KEY || process.env.SUPABASE_SERVICE_ROLE_KEY,
|
||||
);
|
||||
.parse(process.env.SUPABASE_SECRET_KEY);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { z } from 'zod';
|
||||
import * as z from 'zod';
|
||||
|
||||
/**
|
||||
* Returns and validates the Supabase client keys from the environment.
|
||||
@@ -7,18 +7,14 @@ export function getSupabaseClientKeys() {
|
||||
return z
|
||||
.object({
|
||||
url: z.string({
|
||||
description: `This is the URL of your hosted Supabase instance. Please provide the variable NEXT_PUBLIC_SUPABASE_URL.`,
|
||||
required_error: `Please provide the variable NEXT_PUBLIC_SUPABASE_URL`,
|
||||
error: `Please provide the variable NEXT_PUBLIC_SUPABASE_URL`,
|
||||
}),
|
||||
publicKey: z.string({
|
||||
description: `This is the public key provided by Supabase. It is a public key used client-side. Please provide the variable NEXT_PUBLIC_SUPABASE_PUBLIC_KEY.`,
|
||||
required_error: `Please provide the variable NEXT_PUBLIC_SUPABASE_PUBLIC_KEY`,
|
||||
error: `Please provide the variable NEXT_PUBLIC_SUPABASE_PUBLIC_KEY`,
|
||||
}),
|
||||
})
|
||||
.parse({
|
||||
url: process.env.NEXT_PUBLIC_SUPABASE_URL,
|
||||
publicKey:
|
||||
process.env.NEXT_PUBLIC_SUPABASE_PUBLIC_KEY ||
|
||||
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY,
|
||||
publicKey: process.env.NEXT_PUBLIC_SUPABASE_PUBLIC_KEY,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ export function useSignInWithEmailPassword() {
|
||||
const response = await client.auth.signInWithPassword(credentials);
|
||||
|
||||
if (response.error) {
|
||||
throw response.error.message;
|
||||
throw response.error;
|
||||
}
|
||||
|
||||
const user = response.data?.user;
|
||||
|
||||
@@ -12,7 +12,7 @@ export function useSignInWithProvider() {
|
||||
const response = await client.auth.signInWithOAuth(credentials);
|
||||
|
||||
if (response.error) {
|
||||
throw response.error.message;
|
||||
throw response.error;
|
||||
}
|
||||
|
||||
return response.data;
|
||||
|
||||
@@ -49,7 +49,7 @@ export function useSignUpWithEmailAndPassword() {
|
||||
throw new WeakPasswordError(errorObj.reasons ?? []);
|
||||
}
|
||||
|
||||
throw response.error.message;
|
||||
throw response.error;
|
||||
}
|
||||
|
||||
const user = response.data?.user;
|
||||
|
||||
Reference in New Issue
Block a user