Authentication

Supabase Google OAuth + email one-time-code sign-in, with an AuthGate backed by the LoginPage block.

Welcome back

Enter your email and we'll send you a one-time code.

*
or

Don't have an account? Sign up

Installation
Add the auth blocks via the CLI (pulls in the LoginPage block and the Supabase lib), then install the Supabase peer dependency and set your env vars.
$CLI
npx shadcn@latest add https://sdk-components.thesqd.com/r/auth-gate.json
$Peer dependency
npm install @supabase/supabase-js
$.env.local
# .env.local
NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key

# Optional employee gating (rippling). Either set these or pass props.
# Require an ACTIVE rippling.workers row:
NEXT_PUBLIC_AUTH_VERIFY_EMPLOYEE=true
# Restrict to ACTIVE employees in these departments (comma-separated names):
NEXT_PUBLIC_AUTH_ALLOWED_DEPARTMENTS=Design Squad,Video Squad
Setup
Wrap your app in AuthProvider once in the root layout. Pass allowedDomain to restrict sign-in to your org.
TSproviders.tsx
// app/providers.tsx
"use client";

import { AuthProvider } from "@/components/blocks/auth-provider";

export function Providers({ children }: { children: React.ReactNode }) {
  return (
    <AuthProvider
      allowedDomain="churchmediasquad.com"
      // Require an ACTIVE rippling.workers row (allow-all if omitted):
      verifyEmployee
      // Optionally restrict to certain departments (implies verifyEmployee).
      // Falls back to NEXT_PUBLIC_AUTH_ALLOWED_DEPARTMENTS if omitted:
      allowedDepartments={["Design Squad", "Video Squad"]}
    >
      {children}
    </AuthProvider>
  );
}
TSlayout.tsx
// app/layout.tsx
import { Providers } from "./providers";

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body className="squad-ui">
        <Providers>{children}</Providers>
      </body>
    </html>
  );
}
Protect a page
Wrap any content in AuthGate — it shows a spinner while the session resolves, the LoginPage when signed out, and your content once authenticated.
TSAuthGate
// app/page.tsx
"use client";

import { AuthGate } from "@/components/blocks/auth-gate";

export default function Home() {
  return (
    <AuthGate>
      <Dashboard />
    </AuthGate>
  );
}
TSuseAuth
"use client";

import { useAuth } from "@/components/blocks/auth-provider";

export function UserBadge() {
  const { user, signOut } = useAuth();
  if (!user) return null;
  return (
    <button onClick={signOut}>
      {user.displayName ?? user.email} — Sign out
    </button>
  );
}
Employee verification
Gate sign-in on the rippling schema: allow all, active employees only, or active employees in specific departments. Enforced server-side via a SECURITY DEFINER RPC, configurable by prop or env var.
TSModes
// Three gating modes — all enforced server-side after sign-in.

// 1) Allow all (default) — any successful Google/OTP sign-in is accepted.
<AuthProvider>{children}</AuthProvider>

// 2) Active employees only — must have an ACTIVE row in rippling.workers.
<AuthProvider verifyEmployee>{children}</AuthProvider>

// 3) Active employees in specific departments (by rippling.departments.name).
<AuthProvider allowedDepartments={["Design Squad", "C-Suite"]}>
  {children}
</AuthProvider>

// Or drive 2 & 3 from the consumer app's env (no prop needed):
//   NEXT_PUBLIC_AUTH_VERIFY_EMPLOYEE=true
//   NEXT_PUBLIC_AUTH_ALLOWED_DEPARTMENTS=Design Squad,Video Squad
//
// Under the hood the authenticated client calls the SECURITY DEFINER RPC
// public.verify_current_employee(p_departments text[]), which keys off
// auth.email() — a user can only ever verify themselves, no PII enumeration.
// Verified users get user.employee = { workEmail, displayName, title,
// department, status }.
//
// When employee gating is on, AuthProvider also runs a pre-flight check on
// signInWithEmail() via public.is_email_allowed(p_email, p_departments) —
// boolean-only RPC granted to anon — so unauthorized users get an inline
// error on the email step instead of being walked through the OTP round-trip.
// AuthGate forwards the auth error to <LoginPage error={…} />.
The login screen
AuthGate renders the LoginPage block (one-time-code + Google) wired to the auth context. Pass loginComponent to replace it. This is the same flow shown in the preview above.
TSLoginScreen
// The default login screen rendered by <AuthGate> when signed out.
import { LoginPage } from "@/components/blocks/login-page";
import { useAuth } from "@/components/blocks/auth-provider";

export function LoginScreen() {
  const { signInWithGoogle, signInWithEmail, verifyOtp } = useAuth();
  return (
    <LoginPage
      title="Welcome back"
      description="Enter your email and we'll send you a one-time code."
      otp
      googleEnabled
      footerText={null}
      onGoogle={() => signInWithGoogle()}
      onRequestCode={(email) => signInWithEmail(email)}
      onVerifyCode={({ email, code }) => verifyOtp(email, code)}
    />
  );
}
Gate an app shell
The Sidebar app shells take a requireAuth flag that wraps the whole shell in AuthGate — one prop to put a login wall in front of your app.
TSApp
// Gate an entire app shell behind auth with one flag.
import { Sidebar2Demo } from "@/components/blocks/sidebar-2-block";

export default function App() {
  // Requires an <AuthProvider> ancestor (see app/providers.tsx).
  return <Sidebar2Demo requireAuth />;
}
Composition
How the provider, hook, gate, and LoginPage block fit together.
AuthProvider                     — Supabase session context (wrap once in layout)
└── useAuth()                     — { user, loading, error, signInWithGoogle,
                                       signInWithEmail, verifyOtp, signOut, supabase }

AuthGate                          — guard: spinner → LoginPage → children
└── LoginPage (otp + googleEnabled)
    ├── signInWithGoogle()        — Google OAuth redirect
    ├── signInWithEmail(email)    — sends the one-time code
    └── verifyOtp(email, code)    — verifies the code, sets the session
API Reference
Props and return values for the auth surface.

AuthProvider

PropTypeDefaultDescription
childrenReact.ReactNodeApp content.
supabaseClientSupabaseClientPre-initialized client. Defaults to a shared browser client built from your NEXT_PUBLIC_SUPABASE_* env vars.
allowedDomainstringRestrict sign-in to one email domain — others are signed out with an error.
verifyEmployeebooleanfalseRequire an ACTIVE rippling.workers row (verified server-side via the verify_current_employee RPC). Also enabled by NEXT_PUBLIC_AUTH_VERIFY_EMPLOYEE=true.
allowedDepartmentsstring[]Restrict to ACTIVE employees in these rippling department names. Implies verifyEmployee. Falls back to NEXT_PUBLIC_AUTH_ALLOWED_DEPARTMENTS (comma-separated).
verifyQuery(supabase, departments) => Promise<VerifyResult>Override the verification call entirely (e.g. a custom endpoint).
preflightbooleanPre-flight check on signInWithEmail — calls the is_email_allowed RPC and short-circuits BEFORE sending the OTP if the email isn't allowed. Defaults to true whenever employee gating is on.
redirectUrlstringwindow.location.originOAuth redirect URL.

useAuth()

PropTypeDefaultDescription
userSquadUser | nullCurrent user — `{ id, email, displayName, avatarUrl, employee }`. `employee` is the matched rippling record (workEmail, displayName, title, department, status) when verification is on, else null.
loadingbooleanTrue while the session is resolving.
errorstring | nullLast auth error message.
signInWithGoogle() => Promise<void>Trigger the Google OAuth redirect.
signInWithEmail(email: string) => Promise<{ error?: string }>Send a one-time code to the email.
verifyOtp(email: string, token: string) => Promise<{ error?: string }>Verify the 6-digit code and establish the session.
signOut() => Promise<void>Sign the user out.
supabaseSupabaseClientThe underlying Supabase client.

AuthGate

PropTypeDefaultDescription
childrenReact.ReactNodeShown once authenticated.
titleReact.ReactNode"Welcome back"Login card heading.
descriptionReact.ReactNode"Enter your email…"Login card description.
logoReact.ReactNodeBrand mark above the heading.
loadingComponentReact.ReactNodeReplace the default centered spinner.
loginComponentReact.ReactNodeReplace the default LoginPage-backed screen entirely.
classNamestringClasses on the full-screen wrapper.

App shell flag

PropTypeDefaultDescription
requireAuthbooleanfalseWrap the shell in <AuthGate>. Requires an <AuthProvider> ancestor. Available on Sidebar2Demo and Sidebar1PrfDemo.