{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "social-auth-nextjs", "title": "Social Authentication (Next.js)", "description": "Social authentication (OAuth) for Next.js with Convex Auth. Supports GitHub and Google sign-in with full Convex backend.", "dependencies": [ "lucide-react", "@convex-dev/auth@latest", "convex@latest", "@auth/core" ], "registryDependencies": ["button", "card", "alert", "avatar", "skeleton"], "files": [ { "path": "src/registry/convex/blocks/social-auth-nextjs/components/login-form.tsx", "content": "\"use client\";\n\nimport { Alert, AlertDescription } from \"@/components/ui/alert\";\nimport {\n Card,\n CardContent,\n CardDescription,\n CardHeader,\n CardTitle,\n} from \"@/components/ui/card\";\n\nimport { Button } from \"@/components/ui/button\";\nimport { useAuthActions } from \"@convex-dev/auth/react\";\nimport { Loader2 } from \"lucide-react\";\nimport { useState } from \"react\";\n\n// SVG Icons for social providers\nfunction GitHubIcon({ className }: { className?: string }) {\n return (\n \n \n \n );\n}\n\nfunction GoogleIcon({ className }: { className?: string }) {\n return (\n \n \n \n \n \n \n );\n}\n\ntype Provider = \"github\" | \"google\";\n\ninterface SocialLoginFormProps {\n providers?: Provider[];\n}\n\nexport function SocialLoginForm({\n providers = [\"github\", \"google\"],\n}: SocialLoginFormProps) {\n const { signIn } = useAuthActions();\n const [error, setError] = useState(null);\n const [loadingProvider, setLoadingProvider] = useState(null);\n\n const handleSocialLogin = async (provider: Provider) => {\n setError(null);\n setLoadingProvider(provider);\n\n try {\n await signIn(provider);\n } catch (err) {\n setError(\n err instanceof Error\n ? err.message\n : `Failed to sign in with ${provider}`,\n );\n setLoadingProvider(null);\n }\n };\n\n const providerConfig = {\n github: {\n name: \"GitHub\",\n icon: GitHubIcon,\n },\n google: {\n name: \"Google\",\n icon: GoogleIcon,\n },\n };\n\n return (\n \n \n Welcome\n Sign in with your social account\n \n \n {error && (\n \n {error}\n \n )}\n
\n {providers.map((provider) => {\n const config = providerConfig[provider];\n const Icon = config.icon;\n const isLoading = loadingProvider === provider;\n\n return (\n handleSocialLogin(provider)}\n disabled={loadingProvider !== null}\n >\n {isLoading ? (\n \n ) : (\n \n )}\n Continue with {config.name}\n \n );\n })}\n
\n
\n
\n );\n}\n", "type": "registry:component", "target": "src/components/login-form.tsx" }, { "path": "src/registry/convex/blocks/social-auth-nextjs/components/logout-button.tsx", "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { Button } from \"@/components/ui/button\";\nimport { useAuthActions } from \"@convex-dev/auth/react\";\nimport { LogOut } from \"lucide-react\";\n\ninterface LogoutButtonProps extends React.ComponentProps {\n showIcon?: boolean;\n}\n\nexport function LogoutButton({\n showIcon = true,\n children,\n ...props\n}: LogoutButtonProps) {\n const { signOut } = useAuthActions();\n\n return (\n \n );\n}\n", "type": "registry:component", "target": "src/components/logout-button.tsx" }, { "path": "src/registry/convex/blocks/social-auth-nextjs/app/auth/login/page.tsx", "content": "import { SocialLoginForm } from \"@/components/login-form\";\n\nexport default function LoginPage() {\n return (\n
\n \n
\n );\n}\n", "type": "registry:page", "target": "src/app/auth/login/page.tsx" }, { "path": "src/registry/convex/blocks/social-auth-nextjs/app/protected/page.tsx", "content": "\"use client\";\n\nimport { Avatar, AvatarFallback, AvatarImage } from \"@/components/ui/avatar\";\nimport {\n Card,\n CardContent,\n CardDescription,\n CardHeader,\n CardTitle,\n} from \"@/components/ui/card\";\n\nimport { LogoutButton } from \"@/components/logout-button\";\nimport { Skeleton } from \"@/components/ui/skeleton\";\nimport { api } from \"@/convex/_generated/api\";\nimport { useQuery } from \"convex/react\";\n\nexport default function ProtectedPage() {\n const user = useQuery(api.users.current);\n\n if (user === undefined) {\n return (\n
\n \n \n \n \n \n \n \n \n \n
\n );\n }\n\n if (!user) {\n return (\n
\n \n \n Not authenticated\n Please sign in to view this page.\n \n \n
\n );\n }\n\n return (\n
\n \n \n Protected Page\n \n You are signed in and can view this content.\n \n \n \n
\n \n \n \n {(user.name ?? \"U\").slice(0, 2).toUpperCase()}\n \n \n
\n

{user.name ?? \"Anonymous\"}

\n

{user.email}

\n
\n
\n \n
\n
\n
\n );\n}\n", "type": "registry:page", "target": "src/app/protected/page.tsx" }, { "path": "src/registry/convex/blocks/social-auth-nextjs/convex/schema.ts", "content": "import { defineSchema, defineTable } from \"convex/server\";\nimport { authTables } from \"@convex-dev/auth/server\";\nimport { v } from \"convex/values\";\n\n/**\n * Schema for social (OAuth) authentication.\n * Includes the users table and all required auth tables from @convex-dev/auth.\n *\n * Security considerations:\n * - Email is indexed for fast lookups but never exposed in public queries\n * - emailVerificationTime is set when OAuth provider verifies email\n * - Profile data (name, image) is populated from OAuth provider\n */\nexport default defineSchema({\n // Auth tables from @convex-dev/auth (authAccounts, authSessions, etc.)\n ...authTables,\n\n // Users table with profile information from OAuth providers\n users: defineTable({\n // Profile fields (populated from OAuth provider)\n name: v.optional(v.string()),\n email: v.optional(v.string()),\n image: v.optional(v.string()),\n\n // Auth metadata\n emailVerificationTime: v.optional(v.number()),\n isAnonymous: v.optional(v.boolean()),\n })\n .index(\"by_email\", [\"email\"])\n .index(\"by_anonymous\", [\"isAnonymous\"]),\n});\n", "type": "registry:file", "target": "convex/schema.ts" }, { "path": "src/registry/convex/blocks/social-auth-nextjs/convex/auth.ts", "content": "import { convexAuth } from \"@convex-dev/auth/server\";\nimport GitHub from \"@auth/core/providers/github\";\nimport Google from \"@auth/core/providers/google\";\n\n// Build providers list dynamically based on available env vars\nconst providers: Parameters[0][\"providers\"] = [];\n\n// Only add GitHub if credentials are configured\nif (process.env.AUTH_GITHUB_ID && process.env.AUTH_GITHUB_SECRET) {\n providers.push(GitHub);\n}\n\n// Only add Google if credentials are configured\nif (process.env.AUTH_GOOGLE_ID && process.env.AUTH_GOOGLE_SECRET) {\n providers.push(Google);\n}\n\n// Fallback: if no providers configured, add both (they will fail gracefully at runtime)\nif (providers.length === 0) {\n providers.push(GitHub, Google);\n}\n\n/**\n * Convex Auth configuration for social (OAuth) authentication.\n *\n * Features:\n * - GitHub OAuth (if AUTH_GITHUB_ID/SECRET configured)\n * - Google OAuth (if AUTH_GOOGLE_ID/SECRET configured)\n * - OAuth 2.0 with PKCE\n * - Automatic email verification from trusted providers\n * - Secure session management\n *\n * Environment variables:\n * - AUTH_GITHUB_ID, AUTH_GITHUB_SECRET: GitHub OAuth credentials\n * - AUTH_GOOGLE_ID, AUTH_GOOGLE_SECRET: Google OAuth credentials\n *\n * Callback URLs (configure in provider dashboard):\n * - GitHub: https://YOUR_CONVEX_URL/api/auth/callback/github\n * - Google: https://YOUR_CONVEX_URL/api/auth/callback/google\n */\nexport const { auth, signIn, signOut, store, isAuthenticated } = convexAuth({\n providers,\n});\n", "type": "registry:file", "target": "convex/auth.ts" }, { "path": "src/registry/convex/blocks/social-auth-nextjs/convex/auth.config.ts", "content": "import { AuthConfig } from \"convex/server\";\n\n/**\n * Auth configuration for Convex Auth with OAuth providers.\n *\n * The domain should match your Convex deployment URL.\n * This is automatically set via CONVEX_SITE_URL environment variable.\n *\n * Required environment variables:\n * - CONVEX_SITE_URL: Your Convex deployment URL\n * - AUTH_GITHUB_ID, AUTH_GITHUB_SECRET: GitHub OAuth credentials\n * - AUTH_GOOGLE_ID, AUTH_GOOGLE_SECRET: Google OAuth credentials\n *\n * Configure the OAuth callback URLs in your provider's settings:\n * - GitHub: https://YOUR_CONVEX_URL/api/auth/callback/github\n * - Google: https://YOUR_CONVEX_URL/api/auth/callback/google\n */\nexport default {\n providers: [\n {\n domain: process.env.CONVEX_SITE_URL!,\n applicationID: \"convex\",\n },\n ],\n} satisfies AuthConfig;\n", "type": "registry:file", "target": "convex/auth.config.ts" }, { "path": "src/registry/convex/blocks/social-auth-nextjs/convex/users.ts", "content": "import { query, mutation } from \"./_generated/server\";\nimport { v } from \"convex/values\";\nimport { getAuthUserId } from \"@convex-dev/auth/server\";\nimport { ConvexError } from \"convex/values\";\n\n/**\n * User validator for internal use.\n * Includes all fields - use publicProfileValidator for external queries.\n */\nconst userValidator = v.object({\n _id: v.id(\"users\"),\n _creationTime: v.number(),\n name: v.optional(v.string()),\n email: v.optional(v.string()),\n image: v.optional(v.string()),\n emailVerificationTime: v.optional(v.number()),\n isAnonymous: v.optional(v.boolean()),\n});\n\n/**\n * Public profile validator - excludes sensitive fields like email.\n * Use this when returning user data to other users.\n */\nconst publicProfileValidator = v.object({\n _id: v.id(\"users\"),\n name: v.optional(v.string()),\n image: v.optional(v.string()),\n});\n\n/**\n * Get the current authenticated user's full profile.\n * Returns null if not authenticated.\n *\n * Security: Only returns data for the authenticated user themselves.\n */\nexport const current = query({\n args: {},\n returns: v.union(userValidator, v.null()),\n handler: async (ctx: any) => {\n const userId = await getAuthUserId(ctx);\n if (!userId) {\n return null;\n }\n return await ctx.db.get(userId);\n },\n});\n\n/**\n * Get a user's public profile by ID.\n * Only returns safe, public fields (name, image).\n *\n * Security: Never exposes email or other sensitive data.\n */\nexport const get = query({\n args: { userId: v.id(\"users\") },\n returns: v.union(publicProfileValidator, v.null()),\n handler: async (ctx: any, args: any) => {\n const user = await ctx.db.get(args.userId);\n if (!user) {\n return null;\n }\n // Only return public fields\n return {\n _id: user._id,\n name: user.name,\n image: user.image,\n };\n },\n});\n\n/**\n * Update the current user's profile.\n * Only allows updating safe fields (name, image).\n *\n * Security:\n * - Requires authentication\n * - Only allows updating own profile\n * - Validates input types\n */\nexport const updateProfile = mutation({\n args: {\n name: v.optional(v.string()),\n image: v.optional(v.string()),\n },\n returns: v.null(),\n handler: async (ctx: any, args: any) => {\n const userId = await getAuthUserId(ctx);\n if (!userId) {\n throw new ConvexError({\n code: \"UNAUTHORIZED\",\n message: \"You must be logged in to update your profile\",\n });\n }\n\n // Build updates object with only provided fields\n const updates: any = {};\n if (args.name !== undefined) {\n // Sanitize name - trim whitespace, limit length\n const sanitizedName = args.name.trim().slice(0, 100);\n if (sanitizedName) {\n updates.name = sanitizedName;\n }\n }\n if (args.image !== undefined) {\n // Validate image URL format\n if (args.image && !isValidUrl(args.image)) {\n throw new ConvexError({\n code: \"INVALID_INPUT\",\n message: \"Invalid image URL\",\n });\n }\n updates.image = args.image;\n }\n\n if (Object.keys(updates).length > 0) {\n await ctx.db.patch(userId, updates);\n }\n\n return null;\n },\n});\n\n/**\n * Helper to validate URL format.\n */\nfunction isValidUrl(string: string): boolean {\n try {\n const url = new URL(string);\n return url.protocol === \"http:\" || url.protocol === \"https:\";\n } catch {\n return false;\n }\n}\n", "type": "registry:file", "target": "convex/users.ts" }, { "path": "src/registry/convex/blocks/social-auth-nextjs/convex/http.ts", "content": "import { httpRouter } from \"convex/server\";\nimport { auth } from \"./auth\";\n\n/**\n * HTTP router for Convex Auth with OAuth providers.\n *\n * This sets up the required HTTP endpoints for authentication:\n * - GET /api/auth/signin/github - Initiate GitHub OAuth\n * - GET /api/auth/signin/google - Initiate Google OAuth\n * - GET /api/auth/callback/github - GitHub OAuth callback\n * - GET /api/auth/callback/google - Google OAuth callback\n * - POST /api/auth/signout - Sign out\n *\n * These routes are automatically configured by auth.addHttpRoutes().\n */\nconst http = httpRouter();\n\n// Add all auth HTTP routes\nauth.addHttpRoutes(http);\n\nexport default http;\n", "type": "registry:file", "target": "convex/http.ts" }, { "path": "src/registry/convex/clients/nextjs/lib/convex/client.ts", "content": "\"use client\";\n\nimport { ConvexReactClient } from \"convex/react\";\n\nconst convexUrl = process.env.NEXT_PUBLIC_CONVEX_URL!;\n\nexport const convex = new ConvexReactClient(convexUrl);\n", "type": "registry:lib", "target": "src/lib/convex/client.ts" }, { "path": "src/registry/convex/clients/nextjs/lib/convex/provider.tsx", "content": "\"use client\";\n\nimport { ConvexAuthNextjsProvider } from \"@convex-dev/auth/nextjs\";\nimport { ConvexReactClient } from \"convex/react\";\nimport { ReactNode } from \"react\";\n\nconst convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL!);\n\nexport function ConvexClientProvider({ children }: { children: ReactNode }) {\n return (\n \n {children}\n \n );\n}\n", "type": "registry:lib", "target": "src/lib/convex/provider.tsx" }, { "path": "src/registry/convex/clients/nextjs/lib/convex/server.ts", "content": "import {\n fetchAction,\n fetchMutation,\n fetchQuery,\n preloadQuery,\n} from \"convex/nextjs\";\n\nimport { api } from \"@/convex/_generated/api\";\n\nexport { fetchQuery, fetchMutation, fetchAction, preloadQuery, api };\n", "type": "registry:lib", "target": "src/lib/convex/server.ts" }, { "path": "src/registry/convex/clients/nextjs/proxy.ts", "content": "import {\n convexAuthNextjsMiddleware,\n createRouteMatcher,\n} from \"@convex-dev/auth/nextjs/server\";\n\nconst isProtectedRoute = createRouteMatcher([\"/protected(.*)\"]);\n\n// Named export for Next.js 16+ (proxy.ts)\nexport const proxy = convexAuthNextjsMiddleware((request, { convexAuth }) => {\n if (isProtectedRoute(request) && !convexAuth.isAuthenticated()) {\n return Response.redirect(new URL(\"/auth/login\", request.url));\n }\n});\n\nexport const config = {\n matcher: [\"/((?!.*\\\\..*|_next).*)\", \"/\", \"/(api|trpc)(.*)\"],\n};\n", "type": "registry:lib", "target": "src/proxy.ts" } ], "envVars": { "CONVEX_DEPLOYMENT": "", "NEXT_PUBLIC_CONVEX_URL": "", "AUTH_GITHUB_ID": "", "AUTH_GITHUB_SECRET": "", "AUTH_GOOGLE_ID": "", "AUTH_GOOGLE_SECRET": "" }, "docs": "## Post-Install Setup\n\n1. Run `npx convex dev` to start the Convex dev server and get your deployment URL.\n\n2. Set environment variables in the Convex dashboard (Settings > Environment Variables):\n - `AUTH_GITHUB_ID` / `AUTH_GITHUB_SECRET` - GitHub OAuth credentials\n - `AUTH_GOOGLE_ID` / `AUTH_GOOGLE_SECRET` - Google OAuth credentials\n\n3. Configure OAuth callback URLs in each provider's dashboard:\n - GitHub: `https:///api/auth/callback/github`\n - Google: `https:///api/auth/callback/google`\n\nAt least one OAuth provider must be configured for authentication to work.\n\n## Post-Install Setup\n\n1. Run `npx convex dev` to start the Convex dev server and get your deployment URL.\n\n2. Copy the deployment URL to your `.env.local` file as `NEXT_PUBLIC_CONVEX_URL`.\n\n3. Wrap your app with the `ConvexAuthProvider` from `lib/convex/provider.tsx`.", "type": "registry:block" }