next-supa-utils
v0.1.8
Published
Eliminate Supabase boilerplate in Next.js App Router — hooks, middleware helpers, and server action wrappers.
Maintainers
Readme
🚀 Why Use This Over Raw @supabase/ssr?
If you use @supabase/ssr directly, you have to write boilerplate for every environment. next-supa-utils eliminates this entirely:
- One-Line Middleware: No more manually copying the 40-line chunking/cookie-setting logic from the Supabase docs. Just pass your routes to
withSupaAuth(). - Type-Safe Server Actions: Stop writing
try/catchandcookies().getAll()in every server action.createAction()handles it automatically and forces you to check for errors. - Instant Client Hooks:
useSupaUser()anduseSupaSession()wrapcreateBrowserClient, fetch the initial state, and subscribe to real-timeonAuthStateChangeevents out of the box. - App Router Ready: Strictly separated entry points (
/clientand/server) guarantee you won't accidentally import server code into client components.
📦 Installation
npm install next-supa-utilsRequires react >=18, next >=14, @supabase/supabase-js ^2, and @supabase/ssr >=0.5.
Environment Variables (Default)
Add these to your .env.local:
NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-keyExplicit Configuration (Optional)
If you are using a self-hosted Supabase instance or need to pass credentials dynamically, you can skip the environment variables and pass them explicitly:
Client Setup:
Wrap your application in <SupaProvider> to inject credentials into all client hooks.
// app/layout.tsx
import { SupaProvider } from "next-supa-utils/client";
export default function RootLayout({ children }) {
return (
<html>
<body>
<SupaProvider supabaseUrl="https://custom..." supabaseAnonKey="ey...">
{children}
</SupaProvider>
</body>
</html>
);
}Server Setup:
All server-side helpers accept supabaseUrl and supabaseAnonKey in their options.
⚡ Quick Start
1. Middleware (Route Protection in 1 Line)
Protect your routes and auto-refresh sessions without copying boilerplate.
// middleware.ts
import { withSupaAuth } from "next-supa-utils/server";
export default withSupaAuth({
protectedRoutes: ["/dashboard", "/admin"],
redirectTo: "/login",
});
export const config = { matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"] };2. Server Actions (No More Try/Catch)
Automatically initializes the server client with cookies and returns a type-safe { data, error } object.
// app/actions.ts
"use server";
import { createAction } from "next-supa-utils/server";
export const getProfile = createAction(async (supabase, userId: string) => {
const { data, error } = await supabase.from("profiles").select().eq("id", userId).single();
if (error) throw error; // Handled automatically!
return data;
});// Usage
const { data, error } = await getProfile("123");
if (error) console.error(error.message);3. Client Components (Real-Time User State)
Get the user and listen to auth changes immediately.
"use client";
import { useSupaUser } from "next-supa-utils/client";
export default function Avatar() {
const { user, loading } = useSupaUser();
if (loading) return <p>Loading...</p>;
if (!user) return <p>Please sign in</p>;
return <p>Hello, {user.email}!</p>;
}4. Route Handlers (API Routes)
Wrap your Next.js API endpoints to automatically handle Supabase errors and enforce authentication.
// app/api/posts/route.ts
import { routeWrapper } from "next-supa-utils/server";
import { NextResponse } from "next/server";
export const POST = routeWrapper(
async (request, { supabase, user }) => {
const body = await request.json();
const { data, error } = await supabase
.from("posts")
.insert({ ...body, user_id: user!.id })
.single();
if (error) throw error; // Auto-caught and returned as 500
return NextResponse.json({ data });
},
{ requireAuth: true } // Auto-returns 401 if not logged in
);API Reference
Server — next-supa-utils/server
withSupaAuth(config)
Creates a Next.js middleware function that handles session refresh and route protection.
Parameters:
| Property | Type | Required | Default | Description |
|---|---|---|---|---|
| protectedRoutes | string[] | ✅ | — | Route prefixes that require authentication |
| redirectTo | string | — | "/login" | Where to redirect unauthenticated users |
| publicRoutes | string[] | — | [] | Routes that are always public, even if matching a protected prefix |
| onAuthSuccess | (user: { id: string; email?: string }) => void \| Promise<void> | — | — | Optional callback after successful auth verification |
| supabaseUrl | string | — | process.env | Explicit Supabase URL (overrides env vars) |
| supabaseAnonKey | string | — | process.env | Explicit Supabase Anon Key (overrides env vars) |
Returns: (request: NextRequest) => Promise<NextResponse>
Behavior:
- Creates a Supabase server client with proper cookie forwarding
- Calls
supabase.auth.getUser()to refresh the session - If the current path matches
publicRoutes, allows access immediately - If the current path matches
protectedRoutesand the user is not authenticated, redirects toredirectTowith a?next=<original_path>query parameter - Calls
onAuthSuccessif the user is authenticated and the callback is provided
createAction(fn)
Wraps an async function into a server action with automatic Supabase client initialization and error handling.
Signature:
function createAction<TArgs extends unknown[], TResult>(
fn: (supabase: SupabaseClient, ...args: TArgs) => Promise<TResult>
): (...args: TArgs) => Promise<ActionResponse<TResult>>Returns: ActionResponse<TResult> — a discriminated union:
// On success:
{ data: TResult; error: null }
// On failure:
{ data: null; error: SupaError }Behavior:
- Creates a Supabase server client using
cookies()fromnext/headers - Passes the client as the first argument to your function
- Wraps execution in try/catch — any thrown error is normalized into a
SupaError
routeWrapper(handler, options?)
Wraps a Next.js App Router Route Handler (e.g., GET, POST) with automatic try-catch, error normalization, and optional authentication gating.
Signature:
function routeWrapper<TContext>(
handler: (request: NextRequest, context: RouteHandlerContext<TContext>) => Promise<NextResponse | Response>,
options?: RouteWrapperOptions
): (request: NextRequest, context: NextRouteContext) => Promise<NextResponse>Options (RouteWrapperOptions):
requireAuth(boolean): Iftrue, returns a401 Unauthorizedresponse if the user has no valid session.supabaseUrl(string): Explicit Supabase URL.supabaseAnonKey(string): Explicit Supabase Anon Key.
Context (RouteHandlerContext):
params: Auto-resolved dynamic route params (e.g.,{ id: "123" }).supabase: An initialized Supabase server client (always available).user: The authenticated user (guaranteed non-null ifrequireAuth: true).
Client — next-supa-utils/client
⚠️ All client exports include the
"use client"directive. They must be used inside Client Components only.
<SupaProvider>
A React Context Provider to explicitly inject your Supabase URL and Anon Key into the React tree. It is optional if you are using the standard NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_ANON_KEY environment variables.
<SupaProvider supabaseUrl="https://..." supabaseAnonKey="ey...">
{children}
</SupaProvider>useSupaUser()
React hook that provides the current authenticated user and subscribes to auth state changes.
Returns: UseSupaUserReturn
| Property | Type | Description |
|---|---|---|
| user | User \| null | The current Supabase user object, or null if not authenticated |
| loading | boolean | true while the initial fetch is in progress |
| error | SupaError \| null | Error details if the fetch failed |
Behavior:
- Creates a browser client via
createBrowserClientfrom@supabase/ssr - Calls
supabase.auth.getUser()on mount - Subscribes to
onAuthStateChangefor real-time updates (sign in, sign out, token refresh) - Cleans up the subscription on unmount
useSupaSession()
React hook that provides the current session (access token, refresh token, expiry) and subscribes to auth state changes.
Returns: UseSupaSessionReturn
| Property | Type | Description |
|---|---|---|
| session | Session \| null | The current Supabase session, or null if not authenticated |
| loading | boolean | true while the initial fetch is in progress |
| error | SupaError \| null | Error details if the fetch failed |
useSupaUpload(bucketName)
React hook that simplifies uploading files to Supabase Storage with real-time progress tracking (using direct XHR to bypass the JS SDK's lack of progress events).
Returns: UseSupaUploadReturn
| Property | Type | Description |
|---|---|---|
| upload | (file: File, options?: UploadOptions) => Promise<void> | Upload function. Options admit path, upsert, cacheControl, contentType. |
| isUploading | boolean | true while the upload is in progress |
| progress | number | Upload progress percentage (0–100) updated in real-time |
| data | { path: string; fullPath: string } \| null | Successful upload result |
| error | SupaError \| null | Error details if upload failed |
| cancel | () => void | Aborts the in-flight upload |
| reset | () => void | Resets the hook state |
useSupaRealtime(table, event, callback, schema?)
React hook that subscribes to Supabase Realtime postgres_changes events. Safely cleans up the subscription on unmount to prevent memory leaks and duplicate listeners.
Parameters:
table(string): The database table to listen to.event("INSERT" \| "UPDATE" \| "DELETE" \| "*"): The event type.callback((payload: RealtimePayload) => void): Function invoked on each event.schema(string, default"public"): The database schema.
Example:
useSupaRealtime("messages", "INSERT", (payload) => {
console.log("New message:", payload.new);
});Shared — next-supa-utils
handleSupaError(error)
Normalizes any thrown value into a consistent SupaError shape. Used internally by createAction and the hooks, but also exported for direct use.
function handleSupaError(error: unknown): SupaErrorHandles:
- Supabase
AuthError/PostgrestError(extractsmessage,code,status) - Standard
Errorinstances - Plain objects with a
messageproperty - Strings
- Unknown values (fallback:
"An unknown error occurred")
Types
interface SupaError {
message: string;
code?: string;
status?: number;
}
type ActionResponse<T> =
| { data: T; error: null }
| { data: null; error: SupaError };
interface SupaAuthConfig {
protectedRoutes: string[];
redirectTo?: string;
publicRoutes?: string[];
onAuthSuccess?: (user: { id: string; email?: string }) => void | Promise<void>;
}Project Structure
src/
├── client/ # "use client" — browser-only code
│ ├── hooks/
│ │ ├── useSupaUser.ts
│ │ └── useSupaSession.ts
│ └── index.ts
├── server/ # Server-only (Node/Edge runtime)
│ ├── middleware/
│ │ └── withSupaAuth.ts
│ ├── actions/
│ │ └── actionWrapper.ts
│ └── index.ts
├── shared/ # Isomorphic utilities
│ ├── utils/
│ │ └── error-handler.ts
│ └── index.ts
└── types/
└── index.tsImport Paths
| Import | Environment | Contains |
|---|---|---|
| next-supa-utils/client | Client Components | useSupaUser, useSupaSession, useSupaUpload, useSupaRealtime |
| next-supa-utils/server | Server Components, Middleware, Server Actions, Route Handlers | withSupaAuth, createAction, routeWrapper |
| next-supa-utils | Anywhere | handleSupaError, all types |
Changelog
v0.1.6
- ⚙️ Explicit Configuration Support — Added support for custom/self-hosted Supabase instances. You are no longer strictly required to use
process.env.- Added
<SupaProvider>for client components. - Added
supabaseUrlandsupabaseAnonKeyoptions towithSupaAuth,routeWrapper, andcreateAction.
- Added
v0.1.5
- 🚀 New Features:
useSupaUpload— React hook for uploading files to Supabase Storage with real-time progress tracking (using direct XHR to REST API, no extra dependencies) and abort capabilities.useSupaRealtime— React hook to subscribe to Supabase Realtimepostgres_changesevents with safe auto-cleanup (prevents memory leaks on unmount).routeWrapper— Higher-order function for Next.js Route Handlers (API routes) with auto try-catch, standardized error responses, and optional auth gating (requireAuth: true).
v0.1.3
- 🔐 RBAC Support —
withSupaAuthnow supports role-based access control.- New
RouteConfigtype: defineallowedRolesper route (e.g.["admin", "editor"]). - New
MiddlewareOptionsreplaces the oldSupaAuthConfig. - New
roleExtractoroption: read roles fromuser_metadata,app_metadata, or a custom function. - Wildcard path matching supported (e.g.
"/admin/:path*"). - Unauthorized users are redirected with
?error=forbidden&next=<path>.
- New
- ⚠️ Breaking:
SupaAuthConfigtype removed. UseMiddlewareOptionswithroutes: RouteConfig[]instead.
v0.1.0
- 🎉 Initial release
withSupaAuth— Middleware helper for route protection with session refresh.createAction— Type-safe server action wrapper with automatic{ data, error }responses.useSupaUser— React hook for real-time user state.useSupaSession— React hook for real-time session state.handleSupaError— Universal error normalizer.- Multi-entry package:
next-supa-utils/client,next-supa-utils/server. - Dual format output (ESM + CJS) with TypeScript declarations.
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Install dependencies (
npm install) - Make your changes
- Run the type checker (
npm run typecheck) - Build the project (
npm run build) - Commit your changes (
git commit -m 'feat: add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
