Supabase Google OAuth + email one-time-code sign-in, with an AuthGate backed by the LoginPage block.
npx shadcn@latest add https://sdk-components.thesqd.com/r/auth-gate.jsonnpm install @supabase/supabase-js# .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// 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>
);
}// 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>
);
}// app/page.tsx
"use client";
import { AuthGate } from "@/components/blocks/auth-gate";
export default function Home() {
return (
<AuthGate>
<Dashboard />
</AuthGate>
);
}"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>
);
}// 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 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 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 />;
}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 sessionAuthProvider
| Prop | Type | Default | Description |
|---|---|---|---|
children | React.ReactNode | — | App content. |
supabaseClient | SupabaseClient | — | Pre-initialized client. Defaults to a shared browser client built from your NEXT_PUBLIC_SUPABASE_* env vars. |
allowedDomain | string | — | Restrict sign-in to one email domain — others are signed out with an error. |
verifyEmployee | boolean | false | Require an ACTIVE rippling.workers row (verified server-side via the verify_current_employee RPC). Also enabled by NEXT_PUBLIC_AUTH_VERIFY_EMPLOYEE=true. |
allowedDepartments | string[] | — | 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). |
preflight | boolean | — | Pre-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. |
redirectUrl | string | window.location.origin | OAuth redirect URL. |
useAuth()
| Prop | Type | Default | Description |
|---|---|---|---|
user | SquadUser | null | — | Current user — `{ id, email, displayName, avatarUrl, employee }`. `employee` is the matched rippling record (workEmail, displayName, title, department, status) when verification is on, else null. |
loading | boolean | — | True while the session is resolving. |
error | string | null | — | Last 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. |
supabase | SupabaseClient | — | The underlying Supabase client. |
AuthGate
| Prop | Type | Default | Description |
|---|---|---|---|
children | React.ReactNode | — | Shown once authenticated. |
title | React.ReactNode | "Welcome back" | Login card heading. |
description | React.ReactNode | "Enter your email…" | Login card description. |
logo | React.ReactNode | — | Brand mark above the heading. |
loadingComponent | React.ReactNode | — | Replace the default centered spinner. |
loginComponent | React.ReactNode | — | Replace the default LoginPage-backed screen entirely. |
className | string | — | Classes on the full-screen wrapper. |
App shell flag
| Prop | Type | Default | Description |
|---|---|---|---|
requireAuth | boolean | false | Wrap the shell in <AuthGate>. Requires an <AuthProvider> ancestor. Available on Sidebar2Demo and Sidebar1PrfDemo. |