Jun 25, 2025

Building Passwordless Authentication with Stytch

Add passwordless authentication to your applications with Stytch's magic links and enterprise-grade security

14 Min Read

Building a Stytch Authentication Starter with Encore.ts

In this tutorial, we'll build a simple authentication application that showcases Stytch's powerful passwordless authentication capabilities. Stytch provides a modern, developer-friendly authentication platform that eliminates the complexity of building secure user authentication from scratch.

Stytch's Consumer Authentication product offers passwordless authentication through magic links, making user onboarding seamless while maintaining enterprise-grade security. Instead of managing password hashing, session tokens, and security vulnerabilities, we can leverage Stytch's robust infrastructure to focus on building our core application features.

What makes Stytch special:

  • Passwordless authentication: Users log in with magic links sent to their email
  • Enterprise security: Built-in protection against common attacks and vulnerabilities
  • Developer experience: Simple APIs and comprehensive SDKs for easy integration
  • Scalability: Handles authentication for applications of any size
  • Compliance: SOC 2 Type II certified with built-in security best practices
Want to build this faster? Try using Leap to generate this entire application with a simple prompt like "Build an app that uses Stytch consumer authentication." Leap can scaffold the complete backend and frontend code, letting you focus on customization rather than boilerplate.

This is what we'll build:

A simple authentication application featuring:

  • Stytch magic link authentication: Passwordless login via email
  • Session management: Secure token validation and user session handling
  • Frontend login form: Easy-to-use authentication interface
  • Protected user dashboard: Authenticated user area
  • Production-ready security: Proper authentication middleware and error handling

Getting Started

To get started, you'll need Encore installed on your machine:

# Install Encore CLI curl -L https://encore.dev/install.sh | bash

Once that's ready, we can create our project:

encore app create stytch-auth-starter --example=hello-world

Setting Up Stytch Authentication

1. Creating Your Stytch Account

  1. Go to Stytch and create a free account
  2. Complete the email verification process
  3. Create a new project in the Stytch dashboard
  4. Choose "Consumer Authentication" as your product type

Stytch offers different authentication products for different use cases. Consumer Authentication is perfect for applications where individual users need to authenticate and access their personal data.

2. Configuring Stytch Settings

Magic links are Stytch's flagship authentication method. They provide a passwordless experience where users simply click a link sent to their email to authenticate.

  1. In your Stytch dashboard, go to ConfigurationEmail magic links
  2. Configure your redirect URLs:
    • Login redirect URL: http://localhost:4000 (development)
    • Signup redirect URL: http://localhost:4000 (development)
  3. Set expiration times:
    • Login expiration: 60 minutes (recommended)
    • Signup expiration: 60 minutes (recommended)

These redirect URLs determine where users land after clicking the magic link. Our frontend will handle the authentication token on this page.

Frontend SDKs Configuration (Important!)

  1. Navigate to ConfigurationFrontend SDKs
  2. In the "Authorized applications" section, add your domains:
    • For development: localhost:4000
    • For production: your actual domain (e.g., yourdomain.com)

Critical: This step prevents initialization errors. Stytch validates that requests come from authorized domains for security.

Getting Your Stytch Credentials

  1. In your Stytch dashboard, look for your project credentials
  2. Copy your Public token (starts with public-token-)
  3. Copy your Secret key (starts with secret-)
  4. Note your Project ID (you'll need this for backend verification)

The public token is used for client-side operations, while the secret key is used for server-side token verification. Never expose your secret key in client-side code.

Backend Implementation

We'll build our application using Encore's service architecture. Our authentication logic will integrate directly with Stytch's APIs to verify user sessions and extract user information.

1. Setting Up Stytch Authentication Service

The authentication service handles all Stytch integration and provides user session verification for our application.

Create the auth service directory:

mkdir backend/auth

Define the service by creating backend/auth/encore.service.ts:

import { Service } from "encore.dev/service"; export default new Service("auth");

Create backend/auth/auth.ts for Stytch integration:

import { Header, Cookie, APIError, Gateway } from "encore.dev/api"; import { authHandler } from "encore.dev/auth"; import { secret } from "encore.dev/config"; const stytchProjectId = secret("StytchProjectId"); const stytchSecret = secret("StytchSecret"); interface AuthParams { authorization?: Header<"Authorization">; session?: Cookie<"stytch_session">; } export interface AuthData { userID: string; email: string; } const auth = authHandler<AuthParams, AuthData>( async (data) => { const token = data.authorization?.replace("Bearer ", "") ?? data.session?.value; if (!token) { throw APIError.unauthenticated("missing token"); } try { // Verify the Stytch session token using Stytch's session authentication API const response = await fetch("https://api.stytch.com/v1/sessions/authenticate", { method: "POST", headers: { "Content-Type": "application/json", "Authorization": `Basic ${Buffer.from(`${stytchProjectId()}:${stytchSecret()}`).toString('base64')}`, }, body: JSON.stringify({ session_token: token, }), }); if (!response.ok) { throw APIError.unauthenticated("invalid token"); } const result = await response.json(); return { userID: result.user.user_id, email: result.user.emails[0]?.email || "", }; } catch (err) { throw APIError.unauthenticated("invalid token", err); } } ); export const gw = new Gateway({ authHandler: auth });

This authentication handler showcases Stytch's session verification:

  • Token extraction: Accepts tokens from Authorization headers or cookies
  • Stytch API integration: Uses Stytch's /sessions/authenticate endpoint to verify tokens
  • User data extraction: Retrieves user ID and email from Stytch's response
  • Security: Automatically handles token validation and rejects invalid sessions

The beauty of Stytch is that all the complex session management, token validation, and security is handled by their service. We just need to verify the token and extract user information.

2. Creating a Protected User Endpoint

Let's create a simple endpoint that returns user information for authenticated users.

Create backend/auth/user.ts:

import { api } from "encore.dev/api"; import { getAuthData } from "~encore/auth"; export interface UserInfo { userID: string; email: string; } // Returns the current user's information. export const me = api<void, UserInfo>( { auth: true, expose: true, method: "GET", path: "/auth/me" }, async () => { const auth = getAuthData()!; // Get Stytch user data return { userID: auth.userID, email: auth.email, }; } );

3. Configuring Stytch Secrets

Set up your Stytch credentials as Encore secrets:

# Set your Stytch Project ID encore secret set StytchProjectId # Enter your Stytch project ID when prompted # Set your Stytch Secret Key encore secret set StytchSecret # Enter your Stytch secret key when prompted

These secrets are securely stored and automatically injected into your application at runtime. Never commit these values to your code repository.

Frontend Implementation

Now we'll create a simple frontend that demonstrates Stytch's authentication flow with a login form and protected user dashboard.

1. Initialize Frontend Project

First, create the frontend directory and set up a React TypeScript project with Vite:

mkdir frontend cd frontend npm init vite . -- --template react-ts npm install

2. Install Dependencies

Install all the necessary dependencies for our authentication frontend:

# Core dependencies npm install @tanstack/react-query @stytch/react @stytch/vanilla-js # UI and styling npm install lucide-react clsx tailwind-merge class-variance-authority # Radix UI components npm install @radix-ui/react-dialog @radix-ui/react-dropdown-menu @radix-ui/react-slot @radix-ui/react-toast # Date utilities npm install date-fns # Tailwind CSS npm install tailwindcss @tailwindcss/vite

3. Configure Tailwind and Vite

Update vite.config.ts:

import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' import tailwindcss from '@tailwindcss/vite' import path from "path" export default defineConfig({ plugins: [react(), tailwindcss()], resolve: { alias: { "@": path.resolve(__dirname, "./src"), "~backend": path.resolve(__dirname, "../backend"), }, }, })

Add Tailwind to your src/index.css file:

@tailwind base; @tailwind components; @tailwind utilities;

4. Setup shadcn/ui

Initialize shadcn/ui:

npx shadcn@latest init

Choose Default as the style when prompted.

Add the required shadcn/ui components:

npx shadcn@latest add button npx shadcn@latest add input npx shadcn@latest add card npx shadcn@latest add badge npx shadcn@latest add sonner

5. Generate Backend Client

Update package.json to add the client generation script:

{ "scripts": { "dev": "vite", "build": "tsc && vite build", "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", "preview": "vite preview", "generate-client:local": "encore gen client --output=./src/client.ts --env=local" } }

Generate the backend client:

npm run generate-client:local

6. Frontend Configuration

Create frontend/config.ts:

// The Stytch public token for authentication // TODO: Set this to your Stytch public token from the Stytch dashboard export const stytchPublicToken = "public-token-test-your-token-here"; // The Stytch environment (test or live) export const stytchEnvironment = "test" as const;

Important: Replace "public-token-test-your-token-here" with your actual Stytch public token from the dashboard.

7. Main Application Component

Create frontend/App.tsx:

import { StytchProvider } from "@stytch/react"; import { StytchUIClient } from "@stytch/vanilla-js"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { Toaster } from "@/components/ui/toaster"; import { stytchPublicToken } from "./config"; import { AppInner } from "./components/AppInner"; const queryClient = new QueryClient(); const stytch = new StytchUIClient(stytchPublicToken); export default function App() { // Don't render the app if Stytch token is not configured if (!stytchPublicToken || stytchPublicToken === "public-token-test-your-token-here") { return ( <div className="min-h-screen bg-white flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8"> <div className="max-w-md w-full space-y-8"> <div className="text-center"> <h2 className="mt-6 text-3xl font-black text-black"> Configuration Required </h2> <p className="mt-2 text-sm text-gray-600"> Please configure your Stytch public token in the config.ts file to continue. </p> </div> </div> </div> ); } return ( <StytchProvider stytch={stytch}> <QueryClientProvider client={queryClient}> <AppInner /> <Toaster /> </QueryClientProvider> </StytchProvider> ); }

8. Authentication Flow

Create frontend/components/AppInner.tsx:

import { useStytchUser, useStytchSession } from "@stytch/react"; import { AuthScreen } from "./AuthScreen"; import { Dashboard } from "./Dashboard"; export function AppInner() { const { user } = useStytchUser(); const { session } = useStytchSession(); if (!user || !session) { return <AuthScreen />; } return <Dashboard />; }

9. Authentication Screen

Create frontend/components/AuthScreen.tsx:

import { StytchLogin } from "@stytch/react"; import { Products } from "@stytch/vanilla-js"; import { Shield, Mail, Zap } from "lucide-react"; export function AuthScreen() { return ( <div className="min-h-screen bg-white flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8"> <div className="max-w-md w-full space-y-8"> <div className="text-center"> <div className="flex justify-center"> <div className="bg-black p-4 border-4 border-black shadow-[8px_8px_0px_0px_rgba(0,0,0,1)]"> <Shield className="h-12 w-12 text-white" /> </div> </div> <h2 className="mt-8 text-4xl font-black text-black uppercase tracking-tight"> Stytch Auth </h2> <p className="mt-4 text-lg font-bold text-gray-700"> Experience passwordless authentication with magic links </p> </div> <div className="bg-white border-4 border-black shadow-[8px_8px_0px_0px_rgba(0,0,0,1)] p-8"> <div className="flex justify-center"> <div className="w-full max-w-sm"> <StytchLogin config={{ products: [Products.emailMagicLinks], emailMagicLinksOptions: { loginRedirectURL: window.location.origin, signupRedirectURL: window.location.origin, loginExpirationMinutes: 60, signupExpirationMinutes: 60, }, }} styles={{ container: { width: '100%', }, buttons: { primary: { backgroundColor: '#000000', borderColor: '#000000', color: '#ffffff', fontWeight: 'bold', textTransform: 'uppercase', border: '4px solid #000000', boxShadow: '4px 4px 0px 0px rgba(0,0,0,1)', }, }, input: { backgroundColor: '#ffffff', borderColor: '#000000', color: '#000000', fontWeight: 'bold', border: '2px solid #000000', }, }} /> </div> </div> </div> <div className="mt-8 space-y-6"> <h3 className="text-2xl font-black text-black text-center uppercase"> Why Stytch? </h3> <div className="grid grid-cols-1 gap-4"> <div className="bg-yellow-300 border-4 border-black shadow-[4px_4px_0px_0px_rgba(0,0,0,1)] p-4"> <div className="flex items-center space-x-3"> <Mail className="h-6 w-6 text-black" /> <span className="font-bold text-black">Passwordless authentication via magic links</span> </div> </div> <div className="bg-green-300 border-4 border-black shadow-[4px_4px_0px_0px_rgba(0,0,0,1)] p-4"> <div className="flex items-center space-x-3"> <Shield className="h-6 w-6 text-black" /> <span className="font-bold text-black">Enterprise-grade security and compliance</span> </div> </div> <div className="bg-blue-300 border-4 border-black shadow-[4px_4px_0px_0px_rgba(0,0,0,1)] p-4"> <div className="flex items-center space-x-3"> <Zap className="h-6 w-6 text-black" /> <span className="font-bold text-black">Fast integration with comprehensive SDKs</span> </div> </div> </div> </div> </div> </div> ); }

10. User Dashboard

Create frontend/components/Dashboard.tsx:

import { useStytchUser, useStytchSession } from "@stytch/react"; import { useQuery } from "@tanstack/react-query"; import { LogOut, User, Mail, Calendar, Shield } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { useBackend } from "../hooks/useBackend"; import { formatDistanceToNow } from "date-fns"; export function Dashboard() { const { user } = useStytchUser(); const { session } = useStytchSession(); const backend = useBackend(); const { data: userInfo, isLoading } = useQuery({ queryKey: ["user-info"], queryFn: () => backend.auth.me(), }); const handleLogout = () => { window.location.reload(); }; if (isLoading) { return ( <div className="min-h-screen bg-white flex items-center justify-center"> <div className="text-2xl font-black text-black">Loading...</div> </div> ); } return ( <div className="min-h-screen bg-white"> <div className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 py-8"> <div className="flex items-center justify-between mb-8"> <div> <h1 className="text-5xl font-black text-black uppercase tracking-tight">Dashboard</h1> <p className="text-xl font-bold text-gray-700 mt-2">Welcome back! You're successfully authenticated with Stytch.</p> </div> <Button onClick={handleLogout} className="bg-red-500 hover:bg-red-600 text-white font-black border-4 border-black shadow-[4px_4px_0px_0px_rgba(0,0,0,1)] hover:shadow-[2px_2px_0px_0px_rgba(0,0,0,1)] transition-all uppercase" > <LogOut className="w-4 h-4 mr-2" /> Logout </Button> </div> <div className="grid grid-cols-1 md:grid-cols-2 gap-8"> <Card className="bg-white border-4 border-black shadow-[8px_8px_0px_0px_rgba(0,0,0,1)]"> <CardHeader className="bg-yellow-300 border-b-4 border-black"> <CardTitle className="flex items-center gap-2 text-2xl font-black text-black uppercase"> <User className="w-6 h-6" /> User Information </CardTitle> </CardHeader> <CardContent className="p-6 space-y-6"> <div className="flex items-center justify-between"> <span className="text-lg font-black text-black uppercase">User ID</span> <Badge className="bg-black text-white font-mono text-xs border-2 border-black"> {userInfo?.userID || user?.user_id} </Badge> </div> <div className="flex items-center justify-between"> <span className="text-lg font-black text-black uppercase">Email</span> <div className="flex items-center gap-2"> <Mail className="w-5 h-5 text-black" /> <span className="text-lg font-bold">{userInfo?.email || user?.emails?.[0]?.email}</span> </div> </div> <div className="flex items-center justify-between"> <span className="text-lg font-black text-black uppercase">Created</span> <div className="flex items-center gap-2"> <Calendar className="w-5 h-5 text-black" /> <span className="text-lg font-bold"> {user?.created_at ? formatDistanceToNow(new Date(user.created_at), { addSuffix: true }) : 'Unknown'} </span> </div> </div> </CardContent> </Card> <Card className="bg-white border-4 border-black shadow-[8px_8px_0px_0px_rgba(0,0,0,1)]"> <CardHeader className="bg-green-300 border-b-4 border-black"> <CardTitle className="flex items-center gap-2 text-2xl font-black text-black uppercase"> <Shield className="w-6 h-6" /> Session Information </CardTitle> </CardHeader> <CardContent className="p-6 space-y-6"> <div className="flex items-center justify-between"> <span className="text-lg font-black text-black uppercase">Session ID</span> <Badge className="bg-black text-white font-mono text-xs border-2 border-black"> {session?.session_id?.slice(0, 12)}... </Badge> </div> <div className="flex items-center justify-between"> <span className="text-lg font-black text-black uppercase">Started</span> <span className="text-lg font-bold"> {session?.started_at ? formatDistanceToNow(new Date(session.started_at), { addSuffix: true }) : 'Unknown'} </span> </div> <div className="flex items-center justify-between"> <span className="text-lg font-black text-black uppercase">Expires</span> <span className="text-lg font-bold"> {session?.expires_at ? formatDistanceToNow(new Date(session.expires_at), { addSuffix: true }) : 'Unknown'} </span> </div> <div className="flex items-center justify-between"> <span className="text-lg font-black text-black uppercase">Method</span> <Badge className="bg-blue-300 text-black font-black border-2 border-black uppercase">Magic Link</Badge> </div> </CardContent> </Card> </div> <Card className="mt-8 bg-white border-4 border-black shadow-[8px_8px_0px_0px_rgba(0,0,0,1)]"> <CardHeader className="bg-pink-300 border-b-4 border-black"> <CardTitle className="text-3xl font-black text-black uppercase">🎉 Authentication Successful!</CardTitle> </CardHeader> <CardContent className="p-6"> <p className="text-xl font-bold text-gray-700 mb-6"> You've successfully authenticated using Stytch's passwordless magic link authentication. This demonstrates how easy it is to integrate secure, modern authentication into your applications. </p> <div className="bg-blue-100 border-4 border-black shadow-[4px_4px_0px_0px_rgba(0,0,0,1)] p-6"> <h4 className="font-black text-black mb-4 text-xl uppercase">What happened behind the scenes:</h4> <ul className="text-lg font-bold text-black space-y-2"> <li className="flex items-start"> <span className="bg-yellow-300 border-2 border-black w-6 h-6 flex items-center justify-center text-xs font-black mr-3 mt-1">1</span> You entered your email address </li> <li className="flex items-start"> <span className="bg-yellow-300 border-2 border-black w-6 h-6 flex items-center justify-center text-xs font-black mr-3 mt-1">2</span> Stytch sent you a secure magic link </li> <li className="flex items-start"> <span className="bg-yellow-300 border-2 border-black w-6 h-6 flex items-center justify-center text-xs font-black mr-3 mt-1">3</span> Clicking the link authenticated you without a password </li> <li className="flex items-start"> <span className="bg-yellow-300 border-2 border-black w-6 h-6 flex items-center justify-center text-xs font-black mr-3 mt-1">4</span> Your session is now secured with Stytch's enterprise-grade infrastructure </li> <li className="flex items-start"> <span className="bg-yellow-300 border-2 border-black w-6 h-6 flex items-center justify-center text-xs font-black mr-3 mt-1">5</span> The backend verified your session token and extracted your user information </li> </ul> </div> </CardContent> </Card> </div> </div> ); }

11. Backend Client Integration

Create frontend/hooks/useBackend.ts:

import { useStytchSession } from "@stytch/react"; import backend from "~backend/client"; export function useBackend() { const { session } = useStytchSession(); if (!session) { return backend; } return backend.with({ auth: async () => ({ authorization: `Bearer ${session.session_token}`, }), }); }

This hook automatically attaches the user's Stytch session token to all backend API calls, ensuring proper authentication.

Running and Testing Your Application

Starting the Development Environment

Launch your Encore application:

encore run

This starts your backend services with:

  • Database creation and migration: PostgreSQL database ready for use
  • Service discovery: All APIs automatically registered and available
  • Hot reloading: Changes automatically restart affected services
  • Request routing: Incoming requests routed to correct endpoints

Your application will be available at http://localhost:4000 and the development dashboard at http://localhost:9400.

Testing the Authentication Flow

  1. Navigate to the application at http://localhost:4000
  2. Enter your email address in the Stytch login form
  3. Check your email for the magic link from Stytch
  4. Click the magic link to authenticate and access the dashboard
  5. Explore the dashboard to see your user information and session details
  6. Test logout by clicking the logout button

The entire authentication flow is handled seamlessly by Stytch, demonstrating the power of passwordless authentication.

Understanding Stytch's Security Benefits

By integrating Stytch, you get enterprise-grade security features automatically:

1. Passwordless Security

  • No password storage or hashing complexity
  • Eliminates password-based attacks (credential stuffing, brute force)
  • Reduces user friction while improving security

2. Session Management

  • Secure token generation and validation
  • Automatic token expiration and refresh
  • Protection against session hijacking

3. Built-in Protections

  • Rate limiting on authentication attempts
  • Email verification and validation
  • Protection against common authentication vulnerabilities

4. Compliance and Auditing

  • SOC 2 Type II certified infrastructure
  • Detailed authentication logs and analytics
  • GDPR and privacy compliance features

Deployment

Deploying to Encore Cloud

Deploy your application:

git add . git commit -m "Stytch authentication starter" git push encore

Managing Production Secrets

Set your Stytch credentials for production:

encore secret set --env=production StytchProjectId encore secret set --env=production StytchSecret

Don't forget to update your Stytch dashboard with your production domain in the authorized applications list.

Next Steps with Stytch

Your authentication starter demonstrates Stytch's core capabilities, but there's much more you can explore:

Advanced Stytch Features

  • Multi-factor authentication: Add SMS or TOTP for enhanced security
  • Social login: Integrate Google, GitHub, or other OAuth providers
  • Organizations: Build B2B features with team management
  • Session management: Implement device management and session controls
  • Webhooks: React to authentication events in real-time

Conclusion

You've successfully built a production-ready authentication system that showcases Stytch's powerful capabilities. By leveraging Stytch's infrastructure, you've eliminated the complexity of building secure authentication while providing users with a modern, passwordless experience.

Stytch handles all the security complexities—token validation, session management, and user verification—allowing you to focus on building great application features. This is the power of modern authentication platforms: robust security without the implementation overhead.

Your application now has enterprise-grade authentication with just a few lines of integration code, demonstrating how Stytch can accelerate development while maintaining the highest security standards.

Encore

This blog is presented by Encore, the backend framework for building robust type-safe distributed systems with declarative infrastructure.

Like this article?
Get future ones straight to your mailbox.

You can unsubscribe at any time.