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:
This is what we'll build:
A simple authentication application featuring:
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
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.
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.
http://localhost:4000
(development)http://localhost:4000
(development)These redirect URLs determine where users land after clicking the magic link. Our frontend will handle the authentication token on this page.
localhost:4000
yourdomain.com
)Critical: This step prevents initialization errors. Stytch validates that requests come from authorized domains for security.
public-token-
)secret-
)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.
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.
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:
/sessions/authenticate
endpoint to verify tokensThe 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.
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,
};
}
);
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.
Now we'll create a simple frontend that demonstrates Stytch's authentication flow with a login form and protected user dashboard.
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
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
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;
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
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
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.
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>
);
}
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 />;
}
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>
);
}
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>
);
}
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.
Launch your Encore application:
encore run
This starts your backend services with:
Your application will be available at http://localhost:4000 and the development dashboard at http://localhost:9400.
The entire authentication flow is handled seamlessly by Stytch, demonstrating the power of passwordless authentication.
By integrating Stytch, you get enterprise-grade security features automatically:
Deploy your application:
git add .
git commit -m "Stytch authentication starter"
git push encore
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.
Your authentication starter demonstrates Stytch's core capabilities, but there's much more you can explore:
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.