All integrations

Next.js Integration

API Routes, Route Handlers, and Server Actions

Next.js is the primary reference implementation for Turalogin. The integration works naturally with API routes, route handlers, or server actions. Your backend calls Turalogin to start auth, the user clicks the magic link in their email, and your callback page verifies the token and sets its own session cookie. The frontend never sees a token.

  • Works with App Router and Pages Router
  • Magic link flow with customizable callback URL
  • Edge Runtime compatible
  • TypeScript-first with full type safety
  • The Turalogin site itself uses this integration

Implementation Examples

1. Environment Configuration

Set up your environment variables for local development and production.

.env.local
1# Your Turalogin API key from the dashboard
2TURALOGIN_API_KEY=tl_live_xxxxxxxxxxxxx
3
4# The URL where magic links will redirect to
5# Development:
6APP_LOGIN_VALIDATION_URL=http://localhost:3000/auth/callback
7
8# Production (uncomment and update):
9# APP_LOGIN_VALIDATION_URL=https://myapp.com/auth/callback

2. Start Authentication

Create an API route to initiate authentication. This sends a magic link to the user's email that redirects to your callback URL.

app/api/auth/start/route.ts
1import { NextResponse } from 'next/server';
2
3const TURALOGIN_API_KEY = process.env.TURALOGIN_API_KEY!;
4const TURALOGIN_API_URL = 'https://api.turalogin.com/api/v1';
5const VALIDATION_URL = process.env.APP_LOGIN_VALIDATION_URL!;
6
7export async function POST(request: Request) {
8 try {
9 const { email } = await request.json();
10
11 if (!email) {
12 return NextResponse.json(
13 { error: 'Email is required' },
14 { status: 400 }
15 );
16 }
17
18 const response = await fetch(`${TURALOGIN_API_URL}/auth/start`, {
19 method: 'POST',
20 headers: {
21 'Authorization': `Bearer ${TURALOGIN_API_KEY}`,
22 'Content-Type': 'application/json',
23 },
24 body: JSON.stringify({
25 email,
26 validationUrl: VALIDATION_URL // Where the magic link redirects to
27 }),
28 });
29
30 if (!response.ok) {
31 const error = await response.json();
32 return NextResponse.json(error, { status: response.status });
33 }
34
35 const data = await response.json();
36 return NextResponse.json({
37 success: true,
38 message: 'Check your email for the login link'
39 });
40
41 } catch (error) {
42 console.error('Auth start error:', error);
43 return NextResponse.json(
44 { error: 'Internal server error' },
45 { status: 500 }
46 );
47 }
48}

3. Callback Page - Handle Magic Link

Create a callback page that receives the token from the magic link and verifies it with Turalogin.

app/auth/callback/page.tsx
1'use client';
2
3import { useEffect, useState } from 'react';
4import { useRouter, useSearchParams } from 'next/navigation';
5
6export default function AuthCallbackPage() {
7 const router = useRouter();
8 const searchParams = useSearchParams();
9 const [error, setError] = useState('');
10 const [verifying, setVerifying] = useState(true);
11
12 useEffect(() => {
13 const token = searchParams.get('token');
14
15 if (!token) {
16 setError('Invalid login link');
17 setVerifying(false);
18 return;
19 }
20
21 // Verify the token with our API
22 fetch('/api/auth/verify', {
23 method: 'POST',
24 headers: { 'Content-Type': 'application/json' },
25 body: JSON.stringify({ sessionId: token }),
26 })
27 .then(async (res) => {
28 const data = await res.json();
29 if (!res.ok) throw new Error(data.error);
30
31 // Success! Redirect to dashboard
32 router.push('/dashboard');
33 })
34 .catch((err) => {
35 setError(err.message || 'Verification failed');
36 setVerifying(false);
37 });
38 }, [searchParams, router]);
39
40 if (verifying) {
41 return (
42 <div className="min-h-screen flex items-center justify-center">
43 <div className="text-center">
44 <div className="animate-spin w-8 h-8 border-4 border-blue-500 border-t-transparent rounded-full mx-auto mb-4" />
45 <p>Verifying your login...</p>
46 </div>
47 </div>
48 );
49 }
50
51 return (
52 <div className="min-h-screen flex items-center justify-center">
53 <div className="text-center">
54 <p className="text-red-500 mb-4">{error}</p>
55 <a href="/login" className="text-blue-500 hover:underline">
56 Try again
57 </a>
58 </div>
59 </div>
60 );
61}

4. Verify Token and Create Session

Verify the token from the magic link and exchange it for a JWT, then create your session.

app/api/auth/verify/route.ts
1import { NextResponse } from 'next/server';
2import { cookies } from 'next/headers';
3
4const TURALOGIN_API_KEY = process.env.TURALOGIN_API_KEY!;
5const TURALOGIN_API_URL = 'https://api.turalogin.com/api/v1';
6
7export async function POST(request: Request) {
8 try {
9 const { sessionId } = await request.json();
10
11 if (!sessionId) {
12 return NextResponse.json(
13 { error: 'Token is required' },
14 { status: 400 }
15 );
16 }
17
18 // Verify the token with Turalogin
19 const response = await fetch(`${TURALOGIN_API_URL}/auth/verify`, {
20 method: 'POST',
21 headers: {
22 'Authorization': `Bearer ${TURALOGIN_API_KEY}`,
23 'Content-Type': 'application/json',
24 },
25 body: JSON.stringify({ sessionId }),
26 });
27
28 if (!response.ok) {
29 const error = await response.json();
30 return NextResponse.json(error, { status: response.status });
31 }
32
33 const { token, user } = await response.json();
34
35 // Create your own session
36 const session = await createSession(user, token);
37
38 // Set session cookie
39 const cookieStore = await cookies();
40 cookieStore.set('session', session.id, {
41 httpOnly: true,
42 secure: process.env.NODE_ENV === 'production',
43 sameSite: 'lax',
44 maxAge: 60 * 60 * 24 * 7, // 7 days
45 });
46
47 return NextResponse.json({ success: true, user });
48
49 } catch (error) {
50 console.error('Auth verify error:', error);
51 return NextResponse.json(
52 { error: 'Internal server error' },
53 { status: 500 }
54 );
55 }
56}
57
58async function createSession(user: any, token: string) {
59 // Implement your session storage (database, Redis, etc.)
60 // This is where you control your own session logic
61 return { id: crypto.randomUUID(), userId: user.id, token };
62}

5. Server Action Alternative

If you prefer Server Actions, you can use them instead of API routes for a more integrated experience.

app/actions/auth.ts
1'use server';
2
3import { cookies } from 'next/headers';
4import { redirect } from 'next/navigation';
5
6const TURALOGIN_API_KEY = process.env.TURALOGIN_API_KEY!;
7const TURALOGIN_API_URL = 'https://api.turalogin.com/api/v1';
8const VALIDATION_URL = process.env.APP_LOGIN_VALIDATION_URL!;
9
10export async function startAuth(formData: FormData) {
11 const email = formData.get('email') as string;
12
13 const response = await fetch(`${TURALOGIN_API_URL}/auth/start`, {
14 method: 'POST',
15 headers: {
16 'Authorization': `Bearer ${TURALOGIN_API_KEY}`,
17 'Content-Type': 'application/json',
18 },
19 body: JSON.stringify({
20 email,
21 validationUrl: VALIDATION_URL
22 }),
23 });
24
25 const data = await response.json();
26
27 if (!response.ok) {
28 return { error: data.error || 'Failed to start authentication' };
29 }
30
31 return { success: true, message: 'Check your email for the login link' };
32}
33
34export async function verifyAuth(token: string) {
35 const response = await fetch(`${TURALOGIN_API_URL}/auth/verify`, {
36 method: 'POST',
37 headers: {
38 'Authorization': `Bearer ${TURALOGIN_API_KEY}`,
39 'Content-Type': 'application/json',
40 },
41 body: JSON.stringify({ sessionId: token }),
42 });
43
44 const data = await response.json();
45
46 if (!response.ok) {
47 return { error: data.error || 'Invalid or expired link' };
48 }
49
50 // Create session and set cookie
51 const cookieStore = await cookies();
52 cookieStore.set('session', data.token, {
53 httpOnly: true,
54 secure: true,
55 sameSite: 'lax',
56 maxAge: 60 * 60 * 24 * 7,
57 });
58
59 redirect('/dashboard');
60}

6. Middleware Protection

Protect routes using Next.js middleware to check for valid sessions.

middleware.ts
1import { NextResponse } from 'next/server';
2import type { NextRequest } from 'next/server';
3
4const protectedRoutes = ['/dashboard', '/settings', '/api/user'];
5const authRoutes = ['/login', '/register'];
6
7export function middleware(request: NextRequest) {
8 const session = request.cookies.get('session');
9 const { pathname } = request.nextUrl;
10
11 // Check if accessing protected route without session
12 if (protectedRoutes.some(route => pathname.startsWith(route))) {
13 if (!session) {
14 const loginUrl = new URL('/login', request.url);
15 loginUrl.searchParams.set('redirect', pathname);
16 return NextResponse.redirect(loginUrl);
17 }
18 }
19
20 // Redirect authenticated users away from auth pages
21 if (authRoutes.some(route => pathname.startsWith(route))) {
22 if (session) {
23 return NextResponse.redirect(new URL('/dashboard', request.url));
24 }
25 }
26
27 return NextResponse.next();
28}
29
30export const config = {
31 matcher: [
32 '/dashboard/:path*',
33 '/settings/:path*',
34 '/api/user/:path*',
35 '/login',
36 '/register',
37 ],
38};

Complete Login Flow

A complete login page component with email input and success message after sending the magic link.

app/login/page.tsx
1'use client';
2
3import { useState } from 'react';
4
5export default function LoginPage() {
6 const [email, setEmail] = useState('');
7 const [error, setError] = useState('');
8 const [success, setSuccess] = useState(false);
9 const [loading, setLoading] = useState(false);
10
11 async function handleSubmit(e: React.FormEvent) {
12 e.preventDefault();
13 setError('');
14 setLoading(true);
15
16 try {
17 const response = await fetch('/api/auth/start', {
18 method: 'POST',
19 headers: { 'Content-Type': 'application/json' },
20 body: JSON.stringify({ email }),
21 });
22
23 const data = await response.json();
24
25 if (!response.ok) {
26 throw new Error(data.error || 'Failed to send login link');
27 }
28
29 setSuccess(true);
30 } catch (err) {
31 setError(err instanceof Error ? err.message : 'Something went wrong');
32 } finally {
33 setLoading(false);
34 }
35 }
36
37 if (success) {
38 return (
39 <div className="min-h-screen flex items-center justify-center p-4">
40 <div className="w-full max-w-md text-center space-y-4">
41 <div className="w-16 h-16 bg-green-100 rounded-full flex items-center justify-center mx-auto">
42 <svg className="w-8 h-8 text-green-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
43 <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
44 </svg>
45 </div>
46 <h1 className="text-2xl font-bold">Check your email</h1>
47 <p className="text-gray-600">
48 We sent a login link to <strong>{email}</strong>
49 </p>
50 <p className="text-sm text-gray-500">
51 Click the link in the email to sign in. The link expires in 15 minutes.
52 </p>
53 <button
54 onClick={() => { setSuccess(false); setEmail(''); }}
55 className="text-blue-600 hover:underline text-sm"
56 >
57 Use a different email
58 </button>
59 </div>
60 </div>
61 );
62 }
63
64 return (
65 <div className="min-h-screen flex items-center justify-center p-4">
66 <div className="w-full max-w-md space-y-8">
67 <div className="text-center">
68 <h1 className="text-2xl font-bold">Sign in</h1>
69 <p className="text-gray-600 mt-2">
70 Enter your email and we'll send you a login link
71 </p>
72 </div>
73
74 {error && (
75 <div className="p-4 bg-red-50 text-red-600 rounded-lg">
76 {error}
77 </div>
78 )}
79
80 <form onSubmit={handleSubmit} className="space-y-4">
81 <input
82 type="email"
83 value={email}
84 onChange={(e) => setEmail(e.target.value)}
85 placeholder="Enter your email"
86 required
87 className="w-full p-3 border rounded-lg"
88 />
89 <button
90 type="submit"
91 disabled={loading}
92 className="w-full p-3 bg-blue-600 text-white rounded-lg disabled:opacity-50"
93 >
94 {loading ? 'Sending...' : 'Send login link'}
95 </button>
96 </form>
97 </div>
98 </div>
99 );
100}

Ready to integrate?

Create your Turalogin account and get your API key in minutes.