How to set up Supabase Auth in a Webflow Cloud app

Supabase Auth gives your Webflow Cloud app server-validated sessions and owned auth tables. Learn to wire up email and password sign-in step by step.

How to set up Supabase Auth in a Webflow Cloud app

Ismail Ajagbe
Technical Author
View author profile
Ismail Ajagbe
Technical Author
View author profile
Table of contents

You can run a full email and password auth system on a Webflow Cloud app, validated on the server on every request, without ever shipping a session token to the browser.

Supabase Auth comes up often in Webflow developer threads, usually once a project needs server-validated sessions and custom JWT claims, or wants auth tables the team controls directly.

With Webflow Cloud running your Next.js app on Cloudflare Workers, Supabase gives you a PostgreSQL-backed auth system that your server code can validate on every request, without shipping session logic to the browser.

In this guide, we explore wiring up email/password auth in a Webflow Cloud app: installing @supabase/ssr, creating the browser and server clients, adding the session middleware, building the Route Handlers that power sign-up and sign-in, and connecting Webflow forms on the front end.

What do you need to set up Supabase Auth in a Webflow Cloud App?

You need a Webflow Cloud project, a Supabase project, and Node.js 22.13.0 or higher, which is what Webflow CLI 2.0 requires. Supabase Auth runs on the free tier, so no paid plan is required to follow this guide.

Here’s the full list:

  • A Webflow site with Webflow Cloud enabled and a Next.js app deployed or running locally
  • A Supabase project (create one at supabase.com if you haven't already)
  • Node.js 22 or higher, required by the Webflow Cloud runtime
  • @supabase/supabase-js and @supabase/ssr, the two packages that handle Supabase operations and cookie-based session management

@supabase/supabase-js is isomorphic: it uses fetch internally. It doesn't depend on Node.js-specific APIs, so it runs cleanly on Cloudflare Workers. The @supabase/ssr package adds the cookie layer needed to manage sessions across server and client without leaking state.

9 steps to add Supabase Auth to your Webflow Cloud app

@supabase/supabase-js handles Supabase operations, and @supabase/ssr handles cookie-based session management across server and browser. The session middleware (middleware.ts) is what keeps the server and browser in sync. It validates and refreshes the auth token on every incoming request before any Route Handler or Server Component runs.

Follow these steps to hook things up.

1. Create your Supabase project and copy the credentials

Head to the Supabase dashboard and create a new project. Give it a name and set a database password. Choose a region close to your users.

Once the project finishes provisioning, open Settings > API Keys in the left sidebar. Supabase now uses a new publishable key format, though the legacy anon and service_role keys still work during the transition. Under Publishable key, copy the value starting with sb_publishable_....

You'll also need the Project URL displayed at the top of the same page.

2. Install the packages and configure environment variables

In your Webflow Cloud Next.js project, install both packages:

npm install @supabase/supabase-js @supabase/ssr

Then create a .env.local file at the project root with your Supabase credentials:

# .env.local
NEXT_PUBLIC_SUPABASE_URL=https://your-project-ref.supabase.co
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=sb_publishable_your_key_here
NEXT_PUBLIC_SITE_URL=https://yoursite.webflow.io/app

NEXT_PUBLIC_SITE_URL is the base URL for your Webflow Cloud mount path and is used in the email confirmation redirect. Add all three to your Webflow Cloud environment as well: open your Cloud project in the Webflow dashboard, navigate to Environments, and add them under Environment Variables so they're available in production deployments.

3. Create the browser and server clients

Supabase needs two client types:

  • One for Client Components (browser)
  • One for Route Handlers and Server Components (server)

Create a lib/supabase/ folder at the project root with two files.

The browser client uses createBrowserClient, which internally follows a singleton pattern.

You can call it as many times as needed without creating duplicate instances:

// lib/supabase/client.ts
import { createBrowserClient } from '@supabase/ssr'

export function createClient() {
  return createBrowserClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!
  )
}

The server client reads and writes cookies through Next.js's cookies() API. The try/catch on setAll is intentional: Server Components can't write cookies directly, so errors there are expected and safe to ignore:

// lib/supabase/server.ts
import { createServerClient } from '@supabase/ssr'
import { cookies } from 'next/headers'

export async function createClient() {
  const cookieStore = await cookies()

  return createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,
    {
      cookies: {
        getAll() {
          return cookieStore.getAll()
        },
        setAll(cookiesToSet) {
          try {
            cookiesToSet.forEach(({ name, value, options }) =>
              cookieStore.set(name, value, options)
            )
          } catch {
            // Called from a Server Component.
            // The session middleware handles cookie writes on every request.
          }
        },
      },
    }
  )
}

With both clients in place, your browser code and server code can each reach Supabase with the cookie handling that fits their context. The piece that keeps the two in sync is the session middleware, which runs before any of this code and refreshes the auth token on every request.

4. Add the session middleware

Use middleware.ts here, and do not rename it. Next.js 16 deprecates the middleware.ts convention in favor of proxy.ts, but the two run in different places: middleware defaults to the Edge runtime, while proxy runs on the Node.js runtime. Webflow Cloud supports Edge runtime middleware only, so renaming this file to proxy.ts on Webflow Cloud gives you a file that may never execute, and a session refresh that silently stops happening. Keep middleware.ts and accept the deprecation warning. The file validates the JWT signature and refreshes the auth token on every request.

You should create it at the project root:

// middleware.ts
import { createServerClient } from '@supabase/ssr'
import { NextResponse, type NextRequest } from 'next/server'

export async function middleware(request: NextRequest) {
  let supabaseResponse = NextResponse.next({ request })

  const supabase = createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,
    {
      cookies: {
        getAll() {
          return request.cookies.getAll()
        },
          setAll(cookiesToSet) {
          cookiesToSet.forEach(({ name, value }) =>
            request.cookies.set(name, value)
          )
          supabaseResponse = NextResponse.next({ request })
          cookiesToSet.forEach(({ name, value, options }) =>
            supabaseResponse.cookies.set(name, value, options)
          )
        },
      },
    }
  )

  // getClaims() validates the JWT signature against Supabase's published
  // public keys on every request. Do not substitute getSession() here.
  // It trusts the cookie without revalidating the JWT.
  await supabase.auth.getClaims()

  return supabaseResponse
}

export const config = {
  matcher: [
    '/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
  ],
}

The comment in the code is worth taking seriously. getClaims()validates the JWT signature against Supabase's published public keys every time. getSession() does not. It trusts the session data in the cookie as-is, which makes it unsuitable for server-side auth checks.

5. Build the auth Route Handlers

Route Handlers in Webflow Cloud run on the Node.js runtime provided by the OpenNext adapter, so you do not need to add an edge runtime export. In Route Handlers, pass cookies directly through the request and response objects, not through next/headers.

Each auth action gets its own Route Handler. Start with sign-up, sign-in, and sign-out:

Sign-up (app/api/auth/signup/route.ts):

import { createServerClient } from '@supabase/ssr'
import { NextResponse, type NextRequest } from 'next/server'

export async function POST(request: NextRequest) {
  const { email, password } = await request.json()
  const response = NextResponse.json({
    message: 'Check your email to confirm your account.'
  })

  const supabase = createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,
    {
      cookies: {
        getAll() { return request.cookies.getAll() },
        setAll(cookiesToSet) {
          cookiesToSet.forEach(({ name, value, options }) =>
            response.cookies.set(name, value, options)
          )
        },
      },
    }
  )

  const { error } = await supabase.auth.signUp({
    email,
    password,
    options: {
      emailRedirectTo: `${process.env.NEXT_PUBLIC_SITE_URL}/auth/confirm`,
    },
  })

  if (error) return NextResponse.json({ error: error.message }, { status: 400 })

  return response
}

Sign-in (app/api/auth/login/route.ts):

import { createServerClient } from '@supabase/ssr'
import { NextResponse, type NextRequest } from 'next/server'

export async function POST(request: NextRequest) {
  const { email, password } = await request.json()
  const response = NextResponse.json({ success: true })

  const supabase = createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,
    {
      cookies: {
        getAll() { return request.cookies.getAll() },
        setAll(cookiesToSet) {
          cookiesToSet.forEach(({ name, value, options }) =>
            response.cookies.set(name, value, options)
          )
        },
      },
    }
  )

  const { error } = await supabase.auth.signInWithPassword({ email, password })

  if (error) return NextResponse.json({ error: error.message }, { status: 401 })

  return response
}

Sign-out (app/api/auth/logout/route.ts):

import { createServerClient } from '@supabase/ssr'
import { NextResponse, type NextRequest } from 'next/server'

export async function POST(request: NextRequest) {
  const response = NextResponse.json({ success: true })

  const supabase = createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,
    {
      cookies: {
        getAll() { return request.cookies.getAll() },
        setAll(cookiesToSet) {
          cookiesToSet.forEach(({ name, value, options }) =>
            response.cookies.set(name, value, options)
          )
        },
      },
    }
  )

  await supabase.auth.signOut()
  return response
}

These three handlers cover the core session lifecycle: creating an account, then signing in and out. Sign-up returns a message telling the user to confirm their email, which is the route you'll add next.

6. Add the email confirmation route

Supabase sends a confirmation link to new users after sign-up. That link carries token_hash and type parameters and needs a GET Route Handler to verify them and activate the session. Without this route, email confirmation silently fails.

Create app/auth/confirm/route.ts:

// app/auth/confirm/route.ts
import { createServerClient } from '@supabase/ssr'
import { type EmailOtpType } from '@supabase/supabase-js'
import { NextResponse, type NextRequest } from 'next/server'

export async function GET(request: NextRequest) {
  const { searchParams } = new URL(request.url)
  const token_hash = searchParams.get('token_hash')
  const type = searchParams.get('type') as EmailOtpType | null
  const next = searchParams.get('next') ?? '/'

  if (!token_hash || !type) {
    return NextResponse.redirect(new URL('/auth/auth-code-error', request.url))
  }

  const response = NextResponse.redirect(new URL(next, request.url))

  const supabase = createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,
    {
      cookies: {
        getAll() { return request.cookies.getAll() },
        setAll(cookiesToSet) {
          cookiesToSet.forEach(({ name, value, options }) =>
            response.cookies.set(name, value, options)
          )
        },
      },
    }
  )

  const { error } = await supabase.auth.verifyOtp({ token_hash, type })

  if (error) {
    return NextResponse.redirect(new URL('/auth/auth-code-error', request.url))
  }

  return response
}

After verifyOtp succeeds, Supabase writes a session cookie, and the redirect sends the user to the next path (defaulting to /). Make sure NEXT_PUBLIC_SITE_URL has to match the mount path where this route lives, and that same URL has to be on Supabase's Redirect URLs allowlist under Authentication and URL Configuration. Supabase ignores an emailRedirectTo value that is not allowlisted and falls back to your Site URL, which is a common reason confirmation links land somewhere unexpected.

7. Protect your member routes with getClaims()

Any Route Handler that serves member-only content should verify the session before returning data. Call getClaims() at the top of the handler. If it returns null or an error, the user isn't authenticated.

Here’s the snippet:

// app/api/member-content/route.ts
import { createServerClient } from '@supabase/ssr'
import { NextResponse, type NextRequest } from 'next/server'

export async function GET(request: NextRequest) {
  const response = NextResponse.json({ content: null })

  const supabase = createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,
    {
      cookies: {
        getAll() { return request.cookies.getAll() },
        setAll(cookiesToSet) {
          cookiesToSet.forEach(({ name, value, options }) =>
            response.cookies.set(name, value, options)
          )
        },
      },
    }
  )

  const { data, error } = await supabase.auth.getClaims()
  const claims = data?.claims

  if (error || !claims) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
  }

  // claims.sub is the authenticated user's ID
  return NextResponse.json({
    userId: claims.sub,
    content: 'Member-only content returned here'
  })
}

To protect entire page routes rather than individual API endpoints, add redirect logic inside middleware.ts after the getClaims() call. Check request.nextUrl.pathname and if the path is protected and claims is null, return NextResponse.redirect() to your login page.

8. Wire up the forms on your Webflow pages

From a Webflow page, fetch() calls in a custom code embed or a Client Component trigger the auth endpoints. The session cookie is httpOnly, so the browser stores it automatically and sends it with subsequent requests, but JavaScript can't read it. That's the security advantage over client-side auth patterns.

Add a Code Embed to your Webflow login page with a form that calls the login Route Handler:

<!-- Code Embed on your Webflow login page -->
<form id="login-form">
  <input type="email" name="email" placeholder="Email" required />
  <input type="password" name="password" placeholder="Password" required />
  <button type="submit">Sign in</button>
  <p id="error-msg" style="color:red; display:none;"></p>
</form>

<script>
  document.getElementById('login-form').addEventListener('submit', async (e) => {
    e.preventDefault()
    const errorMsg = document.getElementById('error-msg')
    errorMsg.style.display = 'none'

    const formData = new FormData(e.target)
    const response = await fetch('/app/api/auth/login', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        email: formData.get('email'),
        password: formData.get('password'),
      }),
    })

    if (response.ok) {
      window.location.href = '/members'
    } else {
      const { error } = await response.json()
      errorMsg.textContent = error
      errorMsg.style.display = 'block'
    }
  })
</script>

Replace /app/api/auth/login with your actual mount path if it differs. The sign-up form follows the same pattern. Swap the endpoint to /app/api/auth/signup and adjust the success redirect to prompt the user to check their email rather than navigate immediately.

9. Protect your Supabase tables with Row-Level Security

If you store member data in Supabase tables (subscription status or saved content, for example), enable Row Level Security to prevent one user from accessing another's rows.

In the Supabase dashboard, open Table Editor, select your table, and enable Row Level Security in the table settings.

Then create policies in the SQL editor. For a profiles table where users should only access their own row:

-- Supabase SQL Editor
CREATE POLICY "Users can view their own profile"
ON profiles
FOR SELECT
USING (auth.uid() = user_id);

CREATE POLICY "Users can update their own profile"
ON profiles
FOR UPDATE
USING (auth.uid() = user_id);

auth.uid() returns the authenticated user's ID from the validated JWT. Supabase evaluates it automatically on every query. Without an RLS policy explicitly allowing the operation, queries return no rows. The table is locked down by default once RLS is enabled.

What causes Supabase Auth to fail on your Webflow Cloud app?

The auth usually wires up cleanly, but one of these three issues tends to appear first in testing.

Sessions don't persist after sign-in

This almost always means the setAll handler isn't writing cookies to the response object. Check that your Route Handlers create the response object before the Supabase client does, and pass cookies to that exact object.

If you create a new NextResponse() after the setAll call, the cookies are written to a discarded object and lost. Build the response once, before the client, and write to it throughout.

getClaims() returns null immediately after sign-in

There's a short window on the first request after sign-in before the token propagates through the middleware. In protected Route Handlers, treat null claims as unauthenticated and return 401. Don't fall back to getSession(). On the client side, retry the request or redirect to the login page. Don't suppress the 401 to mask the symptom.

Email confirmation link returns 404

The confirmation link lands at [mount-path]/auth/confirm?token_hash=...&type=.... If it 404s, check two things: that app/auth/confirm/route.ts exists at the right path in your project, and that NEXT_PUBLIC_SITE_URL matches your actual mount path exactly, including the subdirectory if you're mounted at /app rather than the root.

Build more with Supabase on Webflow Cloud

The auth layer in this guide is the foundation for any member-gated feature. Subscription checks, saved preferences, Realtime database subscriptions, and user-generated content all build on the same session cookie pattern and the same Route Handler structure.

Explore Webflow + Supabase for the full picture of what you can connect to a Webflow site. For deeper customization of your Webflow Cloud app at the edge, Webflow's developer documentation covers environment configuration and deployment options, plus storage.

Frequently asked questions

Can I use social login providers like Google or GitHub?

Yes. Supabase supports Google, GitHub, Twitter, and dozens of other OAuth providers. Configure your chosen provider in the Supabase dashboard under Authentication > Providers, then call supabase.auth.signInWithOAuth() from a Client Component. The same session middleware and getClaims() pattern handles token refresh regardless of which provider you use.

Does Webflow Cloud require a paid plan?

Webflow Cloud is available across Webflow plans, including the free Starter plan, with higher limits on paid tiers for projects and storage, as well as requests. Supabase Auth is free up to 50,000 monthly active users.

How do I add user profile data, such as display names and avatars?

Create a profiles table in Supabase with a user_id column that references auth.users(id). Add a database trigger in the SQL editor to automatically insert a profile row when a new user confirms their email. Enable RLS on the profiles table with SELECT and UPDATE policies scoped to auth.uid() = user_id. Supabase's Next.js user management tutorial covers the full schema and trigger setup.

Can I gate specific Webflow CMS content based on auth status?

Yes, and two patterns work depending on what you're gating. For gating entire CMS pages, deliver the content via a protected Route Handler and render it in your Cloud app rather than on a static Webflow page. For gating elements within a Webflow page, call your protected endpoint with fetch() in a Code Embed after the user authenticates and inject the response into the DOM. The first approach is cleaner for content that's only useful to members; the second works when most of the page is public, with member-only sections.


Last Updated
July 31, 2026
Category

Related articles

How to wire Auth0 to a Webflow Cloud App for server-side session validation
How to wire Auth0 to a Webflow Cloud App for server-side session validation

How to wire Auth0 to a Webflow Cloud App for server-side session validation

How to wire Auth0 to a Webflow Cloud App for server-side session validation

Development
By
Colin Lateano
,
,
Read article
How to use the Webflow custom code API to push scripts to specific pages
How to use the Webflow custom code API to push scripts to specific pages

How to use the Webflow custom code API to push scripts to specific pages

How to use the Webflow custom code API to push scripts to specific pages

Development
By
Colin Lateano
,
,
Read article
How to set up Stripe Checkout in Webflow using payment links or the buy button
How to set up Stripe Checkout in Webflow using payment links or the buy button

How to set up Stripe Checkout in Webflow using payment links or the buy button

How to set up Stripe Checkout in Webflow using payment links or the buy button

Development
By
Colin Lateano
,
,
Read article
How to add Gemini AI to a Webflow site securely using Webflow Cloud
How to add Gemini AI to a Webflow site securely using Webflow Cloud

How to add Gemini AI to a Webflow site securely using Webflow Cloud

How to add Gemini AI to a Webflow site securely using Webflow Cloud

Development
By
Colin Lateano
,
,
Read article

verifone logomonday.com logospotify logoted logogreenhouse logoclear logocheckout.com logosoundcloud logoreddit logothe new york times logoideo logoupwork logodiscord logo
verifone logomonday.com logospotify logoted logogreenhouse logoclear logocheckout.com logosoundcloud logoreddit logothe new york times logoideo logoupwork logodiscord logo

Get started for free

Try Webflow for as long as you like with our free Starter plan. Purchase a paid Site plan to publish, host, and unlock additional features.

Get started — it’s free
Watch demo

Try Webflow for as long as you like with our free Starter plan. Purchase a paid Site plan to publish, host, and unlock additional features.