2026-02-16 17:05:30
ByteDance's Seedance 2.0 has been getting a lot of attention lately — 2K resolution, up to 15 seconds, native audio generation, and arguably the best motion quality in the current AI video landscape.
But most of the documentation is in Chinese, and figuring out how to actually use it programmatically can be frustrating. I spent a few weeks building tools around Seedance and want to share what I learned about the API integration.
There are three ways to access Seedance via API:
| Platform | Models Available | Pricing | Auth |
|---|---|---|---|
| fal.ai | 1.0 (all variants), 1.5 Pro | ~$0.01-0.05/sec | API key |
| Replicate | 1.0 Pro, 1.5 Pro | ~$0.01-0.05/sec | API token |
| Volcano Engine | All versions incl. 2.0 | Credits-based | Chinese phone required |
For most developers outside China, fal.ai or Replicate are the practical choices. Let's start with fal.ai.
npm install @fal-ai/client
Set your API key:
export FAL_KEY="your-api-key-here"
import { fal } from "@fal-ai/client";
const result = await fal.subscribe(
"fal-ai/seedance-v1-pro-t2v",
{
input: {
prompt: "A golden retriever running through a sunlit meadow, slow motion, cinematic lighting, shallow depth of field",
negative_prompt: "blurry, distorted, low quality",
num_frames: 120,
guidance_scale: 7.5,
seed: 42,
},
logs: true,
onQueueUpdate: (update) => {
if (update.status === "IN_PROGRESS") {
console.log(update.logs?.map(l => l.message));
}
},
}
);
console.log(result.data.video.url);
This is where Seedance really shines. You provide a reference image and it generates a video that maintains the visual style:
const result = await fal.subscribe(
"fal-ai/seedance-v1-pro-i2v",
{
input: {
prompt: "The woman turns her head slowly and smiles, wind blowing through her hair",
image_url: "https://example.com/portrait.jpg",
num_frames: 120,
guidance_scale: 7.5,
},
}
);
console.log(result.data.video.url);
pip install replicate
export REPLICATE_API_TOKEN="your-token-here"
import replicate
output = replicate.run(
"bytedance/seedance-v1-pro-t2v",
input={
"prompt": "Aerial drone shot of a coastal city at sunset, golden hour lighting, waves crashing against cliffs, 4K cinematic",
"negative_prompt": "blurry, distorted",
"num_frames": 120,
"guidance_scale": 7.5,
"seed": 42,
}
)
print(output)
curl -s -X POST "https://api.replicate.com/v1/predictions" \
-H "Authorization: Bearer $REPLICATE_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"version": "bytedance/seedance-v1-pro-t2v",
"input": {
"prompt": "A cup of coffee with steam rising, macro shot, warm lighting",
"num_frames": 120,
"guidance_scale": 7.5
}
}'
After generating hundreds of videos, here's what actually matters:
1. Be specific about motion
# Bad
"A cat sitting on a table"
# Good
"A tabby cat slowly stretches on a wooden table, then turns its head toward the camera, soft afternoon light from a window"
2. Specify camera movement
Seedance handles camera instructions well:
"Slow dolly-in on a vintage typewriter, shallow depth of field, the keys begin to press themselves"
3. Use the @reference system for image-to-video
When using i2v mode, reference tags give you precise control:
"@Image1 The person in the photo begins to walk forward, camera follows with a steady tracking shot"
4. Negative prompts matter
Always include: "blurry, distorted, low quality, watermark, text overlay"
| Model | Best For | Speed | Quality |
|---|---|---|---|
| 1.0 Lite | Quick prototypes | Fast | Good |
| 1.0 Pro | Production quality | Slow | Excellent |
| 1.5 Pro | Latest features | Medium | Excellent |
| 1.0 Fast | Batch processing | Fastest | Good |
For most use cases, 1.0 Pro gives the best quality-to-cost ratio. Use 1.0 Fast when you're iterating on prompts and need quick feedback.
A rough formula:
cost ≈ duration_seconds × $0.03 (fal.ai average)
So a 5-second video costs roughly $0.15, and a 10-second video about $0.30. Significantly cheaper than Sora 2 (which requires a $200/mo ChatGPT Pro subscription).
NSFW content detected — Seedance has content filters. Rephrase your prompt to avoid triggering them.
Timeout after 300s — Pro models can take 3-5 minutes for longer videos. Increase your timeout or use the async/webhook pattern.
Invalid image format — For i2v, use JPEG or PNG. WebP sometimes causes issues. Image should be at least 512x512.
I built a free resource site that covers all of this in more detail:
The site is open source: github.com/hueshadow/seedance
What's your experience with Seedance? Have you found any interesting use cases? Drop a comment below.
2026-02-16 17:00:28
Open source has always operated on an implicit social contract:
If you care enough to contribute, you probably care enough to contribute well.
But that assumption is breaking down.
With AI tools capable of generating pull requests instantly, maintainers are facing a new problem: high volumes of low-context, low-quality contributions that look legitimate but require real human time to evaluate.
Enter Vouch, a new open-source project by Mitchell Hashimoto (co-founder of HashiCorp and creator of Terraform, Vagrant, and more).
Vouch isn't about better linting, smarter CI, or stricter contribution templates.
It's about trust.
Vouch is a trust management system for open source projects.
Instead of allowing anyone to freely submit issues or pull requests, projects can require contributors to be explicitly vouched for by trusted members.
In other words:
It introduces a human approval layer before contribution - not after.
Historically, open source had natural filters:
AI has flattened those barriers.
Now:
Vouch proposes a simple shift:
Replace effort-based filtering with trust-based filtering.
Instead of evaluating every submission, maintainers evaluate people first.
Vouching
A trusted contributor can vouch for someone else.
Once vouched, that contributor can:
Important: A vouched contributor cannot vouch others. This prevents uncontrolled trust expansion.
Denouncing
Trusted members can also denounce a contributor.
This immediately revokes trust and blocks participation.
The Trust List
The trust system is stored in a simple, version-controlled text file inside the repository.
That means:
Web of Trust
One of Vouch’s most interesting ideas is shared trust.
Projects can reference trust lists from other projects.
So if someone is trusted in:
They may automatically be trusted in:
This creates a decentralized web of trust, similar to PGP-style identity systems - but applied to contributor reputation.
Example Workflow
Here’s what onboarding might look like:
No vouch? The PR may automatically close via CI.
Benefits
It moves from "review every submission" to "review the person once."
Of course, this isn't without trade-offs.
Potential downsides:
Vouch doesn't attempt to solve fairness universally. It simply gives maintainers more control over how open their project should be.
Vouch raises an important question:
Is open source about unlimited access — or sustainable collaboration?
In an era where AI can flood repositories with plausible contributions, unlimited access may no longer scale.
Vouch doesn't close open source.
It makes openness intentional.
Mitchell is already using Vouch in his own project, Ghostty, as a proving ground.
But it's also one of the first serious attempts to rethink open source trust in the AI era.
Vouch is less about tooling and more about sociology.
It acknowledges that:
Whether Vouch becomes widely adopted or remains niche, it forces an important conversation:
How do we protect maintainer time without sacrificing openness?
And that's a question every modern open source project will eventually have to answer.
2026-02-16 16:44:54
Building secure authentication in Next.js applications can be challenging, especially with the framework's unique server-side and client-side rendering capabilities. In this comprehensive guide, I'll show you how to implement JWT (JSON Web Token) authentication in Next.js 14+ with App Router, covering everything from setup to production-ready security practices.
Next.js's hybrid rendering model (SSR, SSG, CSR) requires an authentication solution that works seamlessly across server and client components. JWT authentication provides:
Stateless Authentication: Perfect for serverless deployments and edge functions
API Route Protection: Secure your Next.js API routes effortlessly
SSR Compatibility: Works with server-side rendering out of the box
Scalability: No session storage needed, ideal for distributed systems
Mobile-Friendly: Easy integration with React Native or mobile apps
Edge-Ready: Compatible with Next.js Edge Runtime
By the end of this tutorial, you'll have a fully functional Next.js app with:
Before we start, ensure you have:
npx create-next-app@latest nextjs-jwt-auth
cd nextjs-jwt-auth
When prompted, select:
npm install jsonwebtoken bcryptjs jose
npm install --save-dev @types/jsonwebtoken @types/bcryptjs
Package Breakdown:
jsonwebtoken: JWT creation and verificationbcryptjs: Password hashingjose: JWT handling for Edge Runtime (Next.js compatible)Create the following folder structure:
nextjs-jwt-auth/
├── app/
│ ├── api/
│ │ ├── auth/
│ │ │ ├── login/route.ts
│ │ │ ├── register/route.ts
│ │ │ ├── logout/route.ts
│ │ │ └── refresh/route.ts
│ │ └── protected/route.ts
│ ├── login/page.tsx
│ ├── register/page.tsx
│ ├── dashboard/page.tsx
│ ├── layout.tsx
│ └── page.tsx
├── lib/
│ ├── auth.ts
│ ├── db.ts
│ └── types.ts
├── middleware.ts
└── .env.local
Create a .env.local file in your project root:
JWT_SECRET=your-super-secret-jwt-key-change-this-in-production
JWT_REFRESH_SECRET=your-super-secret-refresh-key-change-this
ACCESS_TOKEN_EXPIRY=15m
REFRESH_TOKEN_EXPIRY=7d
NODE_ENV=development
Important: Generate strong secrets using:
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
Create lib/types.ts:
export interface User {
id: string;
email: string;
name: string;
password?: string;
createdAt: Date;
}
export interface JWTPayload {
userId: string;
email: string;
name: string;
iat?: number;
exp?: number;
}
export interface LoginCredentials {
email: string;
password: string;
}
export interface RegisterData {
name: string;
email: string;
password: string;
}
export interface AuthResponse {
success: boolean;
message?: string;
user?: Omit<User, 'password'>;
accessToken?: string;
}
Create lib/auth.ts:
import { SignJWT, jwtVerify } from 'jose';
import { cookies } from 'next/headers';
import { NextRequest } from 'next/server';
import { JWTPayload } from './types';
const secretKey = process.env.JWT_SECRET!;
const refreshSecretKey = process.env.JWT_REFRESH_SECRET!;
const key = new TextEncoder().encode(secretKey);
const refreshKey = new TextEncoder().encode(refreshSecretKey);
// Generate Access Token
export async function generateAccessToken(payload: JWTPayload): Promise<string> {
return await new SignJWT(payload)
.setProtectedHeader({ alg: 'HS256' })
.setIssuedAt()
.setExpirationTime('15m')
.sign(key);
}
// Generate Refresh Token
export async function generateRefreshToken(payload: JWTPayload): Promise<string> {
return await new SignJWT(payload)
.setProtectedHeader({ alg: 'HS256' })
.setIssuedAt()
.setExpirationTime('7d')
.sign(refreshKey);
}
// Verify Access Token
export async function verifyAccessToken(token: string): Promise<JWTPayload | null> {
try {
const verified = await jwtVerify(token, key);
return verified.payload as JWTPayload;
} catch (error) {
return null;
}
}
// Verify Refresh Token
export async function verifyRefreshToken(token: string): Promise<JWTPayload | null> {
try {
const verified = await jwtVerify(token, refreshKey);
return verified.payload as JWTPayload;
} catch (error) {
return null;
}
}
// Get token from cookies
export async function getTokenFromCookies(): Promise<string | null> {
const cookieStore = await cookies();
const token = cookieStore.get('accessToken');
return token?.value || null;
}
// Get token from request headers
export function getTokenFromHeaders(request: NextRequest): string | null {
const authHeader = request.headers.get('authorization');
if (authHeader && authHeader.startsWith('Bearer ')) {
return authHeader.substring(7);
}
return null;
}
// Get current user from token
export async function getCurrentUser(request: NextRequest): Promise<JWTPayload | null> {
const cookieStore = await cookies();
const token = cookieStore.get('accessToken')?.value || getTokenFromHeaders(request);
if (!token) return null;
return await verifyAccessToken(token);
}
// Set auth cookies
export async function setAuthCookies(accessToken: string, refreshToken: string) {
const cookieStore = await cookies();
cookieStore.set('accessToken', accessToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 60 * 15, // 15 minutes
path: '/',
});
cookieStore.set('refreshToken', refreshToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 60 * 60 * 24 * 7, // 7 days
path: '/',
});
}
// Clear auth cookies
export async function clearAuthCookies() {
const cookieStore = await cookies();
cookieStore.delete('accessToken');
cookieStore.delete('refreshToken');
}
Create lib/db.ts:
import bcrypt from 'bcryptjs';
import { User } from './types';
// In production, use a real database (PostgreSQL, MongoDB, etc.)
const users: Map<string, User> = new Map();
export const db = {
// Find user by email
findUserByEmail: async (email: string): Promise<User | null> => {
for (const user of users.values()) {
if (user.email === email) {
return user;
}
}
return null;
},
// Find user by ID
findUserById: async (id: string): Promise<User | null> => {
return users.get(id) || null;
},
// Create new user
createUser: async (name: string, email: string, password: string): Promise<User> => {
const existingUser = await db.findUserByEmail(email);
if (existingUser) {
throw new Error('User already exists');
}
const hashedPassword = await bcrypt.hash(password, 10);
const userId = Date.now().toString();
const user: User = {
id: userId,
name,
email,
password: hashedPassword,
createdAt: new Date(),
};
users.set(userId, user);
return user;
},
// Verify password
verifyPassword: async (plainPassword: string, hashedPassword: string): Promise<boolean> => {
return await bcrypt.compare(plainPassword, hashedPassword);
},
// Remove password from user object
sanitizeUser: (user: User): Omit<User, 'password'> => {
const { password, ...userWithoutPassword } = user;
return userWithoutPassword;
},
};
Create app/api/auth/register/route.ts:
import { NextRequest, NextResponse } from 'next/server';
import { db } from '@/lib/db';
import { RegisterData } from '@/lib/types';
export async function POST(request: NextRequest) {
try {
const body: RegisterData = await request.json();
const { name, email, password } = body;
// Validation
if (!name || !email || !password) {
return NextResponse.json(
{ success: false, message: 'All fields are required' },
{ status: 400 }
);
}
if (password.length < 6) {
return NextResponse.json(
{ success: false, message: 'Password must be at least 6 characters' },
{ status: 400 }
);
}
// Create user
const user = await db.createUser(name, email, password);
const sanitizedUser = db.sanitizeUser(user);
return NextResponse.json(
{
success: true,
message: 'User registered successfully',
user: sanitizedUser,
},
{ status: 201 }
);
} catch (error: any) {
return NextResponse.json(
{ success: false, message: error.message || 'Registration failed' },
{ status: 400 }
);
}
}
Create app/api/auth/login/route.ts:
import { NextRequest, NextResponse } from 'next/server';
import { db } from '@/lib/db';
import { generateAccessToken, generateRefreshToken, setAuthCookies } from '@/lib/auth';
import { LoginCredentials } from '@/lib/types';
export async function POST(request: NextRequest) {
try {
const body: LoginCredentials = await request.json();
const { email, password } = body;
// Validation
if (!email || !password) {
return NextResponse.json(
{ success: false, message: 'Email and password are required' },
{ status: 400 }
);
}
// Find user
const user = await db.findUserByEmail(email);
if (!user) {
return NextResponse.json(
{ success: false, message: 'Invalid credentials' },
{ status: 401 }
);
}
// Verify password
const isValidPassword = await db.verifyPassword(password, user.password!);
if (!isValidPassword) {
return NextResponse.json(
{ success: false, message: 'Invalid credentials' },
{ status: 401 }
);
}
// Generate tokens
const payload = {
userId: user.id,
email: user.email,
name: user.name,
};
const accessToken = await generateAccessToken(payload);
const refreshToken = await generateRefreshToken(payload);
// Set cookies
await setAuthCookies(accessToken, refreshToken);
const sanitizedUser = db.sanitizeUser(user);
return NextResponse.json(
{
success: true,
message: 'Login successful',
user: sanitizedUser,
accessToken,
},
{ status: 200 }
);
} catch (error: any) {
return NextResponse.json(
{ success: false, message: error.message || 'Login failed' },
{ status: 500 }
);
}
}
Create app/api/auth/refresh/route.ts:
import { NextRequest, NextResponse } from 'next/server';
import { cookies } from 'next/headers';
import { verifyRefreshToken, generateAccessToken, setAuthCookies, generateRefreshToken } from '@/lib/auth';
export async function POST(request: NextRequest) {
try {
const cookieStore = await cookies();
const refreshToken = cookieStore.get('refreshToken')?.value;
if (!refreshToken) {
return NextResponse.json(
{ success: false, message: 'No refresh token provided' },
{ status: 401 }
);
}
const payload = await verifyRefreshToken(refreshToken);
if (!payload) {
return NextResponse.json(
{ success: false, message: 'Invalid refresh token' },
{ status: 401 }
);
}
// Generate new tokens
const newAccessToken = await generateAccessToken({
userId: payload.userId,
email: payload.email,
name: payload.name,
});
const newRefreshToken = await generateRefreshToken({
userId: payload.userId,
email: payload.email,
name: payload.name,
});
// Set new cookies
await setAuthCookies(newAccessToken, newRefreshToken);
return NextResponse.json(
{
success: true,
message: 'Token refreshed successfully',
accessToken: newAccessToken,
},
{ status: 200 }
);
} catch (error: any) {
return NextResponse.json(
{ success: false, message: error.message || 'Token refresh failed' },
{ status: 500 }
);
}
}
Create app/api/auth/logout/route.ts:
import { NextRequest, NextResponse } from 'next/server';
import { clearAuthCookies } from '@/lib/auth';
export async function POST(request: NextRequest) {
try {
await clearAuthCookies();
return NextResponse.json(
{ success: true, message: 'Logged out successfully' },
{ status: 200 }
);
} catch (error: any) {
return NextResponse.json(
{ success: false, message: error.message || 'Logout failed' },
{ status: 500 }
);
}
}
Create app/api/protected/route.ts:
import { NextRequest, NextResponse } from 'next/server';
import { getCurrentUser } from '@/lib/auth';
export async function GET(request: NextRequest) {
try {
const user = await getCurrentUser(request);
if (!user) {
return NextResponse.json(
{ success: false, message: 'Unauthorized' },
{ status: 401 }
);
}
return NextResponse.json(
{
success: true,
message: 'This is protected data',
user,
data: {
secretInfo: 'Only authenticated users can see this',
timestamp: new Date().toISOString(),
},
},
{ status: 200 }
);
} catch (error: any) {
return NextResponse.json(
{ success: false, message: error.message || 'Server error' },
{ status: 500 }
);
}
}
Create middleware.ts in the root directory:
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { verifyAccessToken } from './lib/auth';
export async function middleware(request: NextRequest) {
const token = request.cookies.get('accessToken')?.value;
// Protected routes
const protectedPaths = ['/dashboard', '/profile', '/settings'];
const isProtectedPath = protectedPaths.some(path =>
request.nextUrl.pathname.startsWith(path)
);
// Public routes that should redirect if authenticated
const authPaths = ['/login', '/register'];
const isAuthPath = authPaths.some(path =>
request.nextUrl.pathname.startsWith(path)
);
if (isProtectedPath) {
if (!token) {
return NextResponse.redirect(new URL('/login', request.url));
}
const user = await verifyAccessToken(token);
if (!user) {
const response = NextResponse.redirect(new URL('/login', request.url));
response.cookies.delete('accessToken');
response.cookies.delete('refreshToken');
return response;
}
}
if (isAuthPath && token) {
const user = await verifyAccessToken(token);
if (user) {
return NextResponse.redirect(new URL('/dashboard', request.url));
}
}
return NextResponse.next();
}
export const config = {
matcher: [
'/dashboard/:path*',
'/profile/:path*',
'/settings/:path*',
'/login',
'/register',
],
};
Create app/login/page.tsx:
'use client';
import { useState } from 'react';
import { useRouter } from 'next/navigation';
import Link from 'next/link';
export default function LoginPage() {
const router = useRouter();
const [formData, setFormData] = useState({
email: '',
password: '',
});
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError('');
setLoading(true);
try {
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData),
});
const data = await response.json();
if (data.success) {
router.push('/dashboard');
router.refresh();
} else {
setError(data.message || 'Login failed');
}
} catch (error) {
setError('An error occurred. Please try again.');
} finally {
setLoading(false);
}
};
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
<div className="max-w-md w-full space-y-8">
<div>
<h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">
Sign in to your account
</h2>
</div>
<form className="mt-8 space-y-6" onSubmit={handleSubmit}>
{error && (
<div className="rounded-md bg-red-50 p-4">
<p className="text-sm text-red-800">{error}</p>
</div>
)}
<div className="rounded-md shadow-sm -space-y-px">
<div>
<label htmlFor="email" className="sr-only">Email address</label>
<input
id="email"
name="email"
type="email"
required
className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
placeholder="Email address"
value={formData.email}
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
/>
</div>
<div>
<label htmlFor="password" className="sr-only">Password</label>
<input
id="password"
name="password"
type="password"
required
className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
placeholder="Password"
value={formData.password}
onChange={(e) => setFormData({ ...formData, password: e.target.value })}
/>
</div>
</div>
<div>
<button
type="submit"
disabled={loading}
className="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:opacity-50"
>
{loading ? 'Signing in...' : 'Sign in'}
</button>
</div>
<div className="text-sm text-center">
<Link href="/register" className="font-medium text-indigo-600 hover:text-indigo-500">
Don't have an account? Register
</Link>
</div>
</form>
</div>
</div>
);
}
Create app/register/page.tsx:
'use client';
import { useState } from 'react';
import { useRouter } from 'next/navigation';
import Link from 'next/link';
export default function RegisterPage() {
const router = useRouter();
const [formData, setFormData] = useState({
name: '',
email: '',
password: '',
confirmPassword: '',
});
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError('');
if (formData.password !== formData.confirmPassword) {
setError('Passwords do not match');
return;
}
setLoading(true);
try {
const response = await fetch('/api/auth/register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: formData.name,
email: formData.email,
password: formData.password,
}),
});
const data = await response.json();
if (data.success) {
router.push('/login?registered=true');
} else {
setError(data.message || 'Registration failed');
}
} catch (error) {
setError('An error occurred. Please try again.');
} finally {
setLoading(false);
}
};
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
<div className="max-w-md w-full space-y-8">
<div>
<h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">
Create your account
</h2>
</div>
<form className="mt-8 space-y-6" onSubmit={handleSubmit}>
{error && (
<div className="rounded-md bg-red-50 p-4">
<p className="text-sm text-red-800">{error}</p>
</div>
)}
<div className="rounded-md shadow-sm space-y-4">
<input
type="text"
required
className="appearance-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
placeholder="Full Name"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
/>
<input
type="email"
required
className="appearance-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
placeholder="Email address"
value={formData.email}
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
/>
<input
type="password"
required
className="appearance-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
placeholder="Password"
value={formData.password}
onChange={(e) => setFormData({ ...formData, password: e.target.value })}
/>
<input
type="password"
required
className="appearance-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
placeholder="Confirm Password"
value={formData.confirmPassword}
onChange={(e) => setFormData({ ...formData, confirmPassword: e.target.value })}
/>
</div>
<button
type="submit"
disabled={loading}
className="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:opacity-50"
>
{loading ? 'Creating account...' : 'Register'}
</button>
<div className="text-sm text-center">
<Link href="/login" className="font-medium text-indigo-600 hover:text-indigo-500">
Already have an account? Sign in
</Link>
</div>
</form>
</div>
</div>
);
}
Create app/dashboard/page.tsx:
'use client';
import { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
interface UserData {
userId: string;
email: string;
name: string;
}
export default function DashboardPage() {
const router = useRouter();
const [userData, setUserData] = useState<UserData | null>(null);
const [protectedData, setProtectedData] = useState<any>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchProtectedData();
}, []);
const fetchProtectedData = async () => {
try {
const response = await fetch('/api/protected');
const data = await response.json();
if (data.success) {
setUserData(data.user);
setProtectedData(data.data);
}
} catch (error) {
console.error('Error fetching protected data:', error);
} finally {
setLoading(false);
}
};
const handleLogout = async () => {
try {
await fetch('/api/auth/logout', { method: 'POST' });
router.push('/login');
router.refresh();
} catch (error) {
console.error('Logout error:', error);
}
};
if (loading) {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="text-xl">Loading...</div>
</div>
);
}
return (
<div className="min-h-screen bg-gray-50">
<nav className="bg-white shadow-sm">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between h-16">
<div className="flex items-center">
<h1 className="text-xl font-bold">Dashboard</h1>
</div>
<div className="flex items-center">
<button
onClick={handleLogout}
className="ml-4 px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700"
>
Logout
</button>
</div>
</div>
</div>
</nav>
<main className="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8">
<div className="px-4 py-6 sm:px-0">
<div className="bg-white shadow rounded-lg p-6 mb-6">
<h2 className="text-2xl font-bold mb-4">Welcome, {userData?.name}!</h2>
<div className="space-y-2">
<p className="text-gray-600">
<span className="font-semibold">Email:</span> {userData?.email}
</p>
<p className="text-gray-600">
<span className="font-semibold">User ID:</span> {userData?.userId}
</p>
</div>
</div>
<div className="bg-white shadow rounded-lg p-6">
<h3 className="text-xl font-bold mb-4">Protected Data</h3>
{protectedData && (
<div className="space-y-2">
<p className="text-gray-600">{protectedData.secretInfo}</p>
<p className="text-sm text-gray-500">
Last accessed: {new Date(protectedData.timestamp).toLocaleString()}
</p>
</div>
)}
</div>
</div>
</main>
</div>
);
}
Create lib/useAuth.ts:
'use client';
import { useState, useEffect } from 'react';
import { useRouter } from 'next/navigation';
interface User {
userId: string;
email: string;
name: string;
}
export function useAuth() {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
const router = useRouter();
useEffect(() => {
checkAuth();
}, []);
const checkAuth = async () => {
try {
const response = await fetch('/api/protected');
const data = await response.json();
if (data.success) {
setUser(data.user);
} else {
setUser(null);
}
} catch (error) {
setUser(null);
} finally {
setLoading(false);
}
};
const login = async (email: string, password: string) => {
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
});
const data = await response.json();
if (data.success) {
setUser(data.user);
return { success: true };
}
return { success: false, message: data.message };
};
const logout = async () => {
await fetch('/api/auth/logout', { method: 'POST' });
setUser(null);
router.push('/login');
};
const refreshToken = async () => {
try {
const response = await fetch('/api/auth/refresh', {
method: 'POST',
});
const data = await response.json();
return data.success;
} catch (error) {
return false;
}
};
return { user, loading, login, logout, refreshToken };
}
npm run dev
Register: Navigate to http://localhost:3000/register
Login: Go to http://localhost:3000/login
Dashboard: Access http://localhost:3000/dashboard
Logout: Click logout button
✅ DO:
❌ DON'T:
// Short-lived access tokens
ACCESS_TOKEN_EXPIRY=15m
// Long-lived refresh tokens
REFRESH_TOKEN_EXPIRY=7d
Always use HTTPS in production to prevent token interception:
cookieStore.set('accessToken', accessToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production', // HTTPS only in prod
sameSite: 'lax',
maxAge: 60 * 15,
path: '/',
});
Next.js provides built-in CSRF protection for API routes. Ensure you're using:
export const config = {
api: {
bodyParser: true,
},
};
Install and configure rate limiting:
npm install @upstash/ratelimit @upstash/redis
Never commit .env.local to version control:
# .gitignore
.env.local
.env.*.local
Replace the in-memory storage with a real database. Here's an example with Prisma:
npm install prisma @prisma/client
npx prisma init
Update prisma/schema.prisma:
model User {
id String @id @default(cuid())
email String @unique
name String
password String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
Run migrations:
npx prisma migrate dev --name init
npx prisma generate
Add email verification during registration to ensure valid users.
Implement forgot password functionality with time-limited reset tokens.
Add an extra security layer with 2FA using libraries like speakeasy.
Integrate OAuth providers using NextAuth.js:
npm install next-auth
Extend JWT payload with user roles:
export interface JWTPayload {
userId: string;
email: string;
name: string;
role: 'admin' | 'user' | 'moderator';
}
Solution: Implement automatic token refresh in your API calls.
Solution: Check cookie settings and ensure domain matches.
Solution: Verify middleware.ts is in the root directory and config matcher is correct.
Solution: Configure CORS headers in next.config.js:
module.exports = {
async headers() {
return [
{
source: '/api/:path*',
headers: [
{ key: 'Access-Control-Allow-Credentials', value: 'true' },
{ key: 'Access-Control-Allow-Origin', value: '*' },
{ key: 'Access-Control-Allow-Methods', value: 'GET,POST,PUT,DELETE,OPTIONS' },
{ key: 'Access-Control-Allow-Headers', value: 'Content-Type, Authorization' },
],
},
];
},
};
Cache verified tokens to reduce computation:
const tokenCache = new Map<string, { payload: JWTPayload; exp: number }>();
Deploy middleware to Edge for faster authentication checks:
export const config = {
matcher: ['/dashboard/:path*'],
runtime: 'edge',
};
Lazy load authentication check on client:
const { user } = useAuth();
Implement logging for security events:
// lib/logger.ts
export const logger = {
login: (email: string, success: boolean) => {
console.log(`Login attempt: ${email} - ${success ? 'SUCCESS' : 'FAILED'}`);
},
tokenRefresh: (userId: string) => {
console.log(`Token refreshed for user: ${userId}`);
},
logout: (userId: string) => {
console.log(`User logged out: ${userId}`);
},
};
You've successfully implemented a complete JWT authentication system in Next.js with the App Router. This implementation includes user registration, login, logout, token refresh, protected routes, and middleware protection.
The stateless nature of JWT makes it perfect for Next.js applications, especially when deploying to serverless or edge environments. Remember to follow security best practices, use HTTPS in production, and keep your tokens short-lived.
Have questions about Next.js JWT authentication? Drop them in the comments below!
Found this helpful? Give it a like and follow for more Next.js tutorials!
2026-02-16 16:40:49
Markdown is the backbone of the web, but let's be honest—traditional editors are boring. They are basically text boxes with a side-by-side preview. Boring, static, and often a distraction.
I wanted something more. I wanted a Writing Studio, not just an editor.
That’s why I built Markdown Weaver, and today, I'm thrilled to announce v1.3.0—the most ambitious update yet. We’ve turned "just another editor" into a visual brainstorming and presentation powerhouse.
Hey everyone! 👋
A while back, I teased a project I was building while procrastinating on my developer analytics tool. Well, that "side project" just grew up.
Markdown Weaver v1.3.0 is finally here, and it changes the game for anyone who lives in markdown. This isn't just about bold text and headers; it's about visualizing your thoughts and presenting your ideas instantly.
Here is why you might want to ditch your current markdown workflow.
Most markdown editors let you see your text. Markdown Weaver lets you understand its structure.
With one click, your document transforms into an Interactive Mind Map. Powered by Markmap, it takes your headers and turns them into a node-based visual hierarchy.
Writing slides is a chore. Formatting them is even worse.
In v1.3.0, we’ve introduced Presentation Mode. Just write your content, use --- to separate your ideas, and hit play.
If you love VS Code or Obsidian, you'll feel right at home. Press Ctrl+K (or Cmd+K) to open the Command Palette.
We didn't just slap a chatbot in the sidebar. We integrated AI into the Magic Menu.
It’s like having an editor sitting next to you, helping you polish your work as you type.
The new Project Dashboard and Welcome Experience make managing 50+ documents a breeze.
Under the hood, we’ve moved to TypeScript 5.8 and a more robust Project-based Storage System. This means:
.md file to import it instantly.Markdown Weaver is open-source, free, and built for people who love the simplicity of markdown but crave the power of a modern studio.
Check it out and let's make writing fun again!
🔗 Live Demo: https://markdown-weaver.netlify.app/
📦 Source Code: GitHub Repo
Happy writing! 📝
2026-02-16 16:33:53
In our journey so far, we have explored the high-level intuition of why Transformers exist and mapped out the blueprint and notations in Part 1.
Before we prep the ingredients, let's look at the "Chef."
In the Transformer diagram, the Encoder is the tower on the left.
If you think of a Transformer as a translation system:
While the original paper used both towers for translation, the tech world realized the Encoder is a powerhouse on its own. Below are few samples,
In short: The Encoder's sole job is to turn a human sentence into a "Context-Rich" mathematical map.
Let's learn how to build this map
(Seq, d_model)?
As we discussed in Part 1, the hardware (GPU) is built for speed. It doesn't want to read a sentence word-by-word. It wants a Matrix.
Specifically, the Encoder requires a matrix of shape (Seq, d_model).
Seq (Sequence Length): The number of words (e.g., 5 for "The dog bit the man").d_model (Model Dimension): The "width" of our mathematical understanding (usually 512).The Encoder's job is to understand and refine this matrix.
let's learn how to feed input to encoder in this structure, Meaning (Embedding) and Order (Positional Encoding).
A computer doesn't know what a "dog" is. To a machine, "dog" is just a string of bits. Input Embedding is the process of giving that word a "Semantic Passport."
To understand how we get our (Seq, d_model) matrix, let's follow the word "dog" through the three-step mechanical process we teased in Part 1:
First, the model looks up "dog" in its dictionary (Vocabulary) and finds its unique ID (e.g., ID 432). It creates a One-Hot Vector: a massive list of zeros with a single 1 at position 432.
Recall from Part 1: This is a "Sparse" representation. It's huge, mostly empty, and contains zero meaning.
This is where the magic happens. The model maintains a giant Embedding Matrix of size (Vocab_Size, d_model).
The model uses the ID from Step 1 to select exactly one row from this matrix. This row is our d_model vector. We have now successfully converted a "Sparse" ID into a "Dense" list of numbers.
Imagine every word in our sentence, "The dog bit the man," gets a passport with 512 pages (our d_model). Each page describes a feature that the model has learned:
Unlike a fixed dictionary, these embedding values are learnable weights.
In Part 1, we praised Transformers for their Parallelism. Unlike the "Drunken Narrator" (RNN), the Transformer looks at the entire input matrix at the same time.
The Paradox: If you look at every word simultaneously, you lose the sense of time.
To a Transformer, these two sentences look identical because the words are the same:
We have Meaning from our Embeddings, but we are missing Order.
To solve this, we need to "stamp" a position onto our embeddings. This tells the model where each word is standing in line.
For an index i in our 512-dimensional vector:
2i):
PE(pos, 2i) = sin(pos / 10000^(2i/d_model))
2i+1):
PE(pos, 2i+1) = cos(pos / 10000^(2i/d_model))
The researchers used Sine and Cosine waves because they allow the model to understand relative positions. Because these functions oscillate, the model can easily learn that "word A is specific distance away from word B."
> Using waves to create a unique mathematical "signature" for every seat in the sentence.
Unlike Embeddings, Positional Encodings are fixed.
Meaning + Order)
The researchers chose to ADD the Positional Encoding vector directly to the Input Embedding vector.
> Final Vector = Learnable Meaning + Constant Position
Why add instead of just attaching it to the end (concatenation)?
It keeps the matrix size at (Seq, d_model). We don't make the matrix "fatter," which keeps the hardware processing fast.
We have successfully prepared our ingredients. We started with raw text and ended with a refined (Seq, d_model) matrix where every word knows who it is and where it sits.
This matrix is finally ready to enter the first actual "box" of the Encoder.
Now that the input is prepared, it’s time to feed the Encoder.
In Part 3, we will explore Multi-Head Self-Attention. This is where the words actually start "talking" to each other using the Matrix Multiplication logic we teased in Part 1. We’ll see how the model decides which words are the most important in the sentence.
2026-02-16 16:31:48
Structured data is one of those things every developer knows they should implement, but few actually do. Why? Because writing JSON-LD by hand is error-prone, tedious, and easy to get wrong.
What if you could generate type-safe structured data with autocomplete, validation, and zero runtime overhead?
Here's what typical JSON-LD looks like:
{
"@context": "https://schema.org",
"@type": "Article",
"headline": "My Blog Post",
"author": {
"@type": "Person",
"name": "Jane Doe"
},
"datePublished": "2026-02-10"
}
Looks simple, right? But try maintaining this across dozens of pages:
@context?datePublished in the right format?Article as Aticle?// ❌ Error-prone manual JSON
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify({
"@context": "https://schema.org",
"@type": "Article",
"headline": "My Post",
// Did I get all required fields? Who knows!
})
}}
/>
Use type-safe builders:
import { Schema, Article } from "@schemasentry/next";
// ✅ Type-safe with autocomplete
const article = Article({
headline: "My Post",
authorName: "Jane Doe",
datePublished: "2026-02-10",
url: "https://example.com/blog/post",
});
export default function Page() {
return <Schema data={article} />;
}
Same output, zero guesswork.
Good news: Schema Sentry works identically with both Next.js routing patterns.
// app/blog/[slug]/page.tsx
import { Schema, Article, BreadcrumbList } from "@schemasentry/next";
export default function BlogPost({ params }: { params: { slug: string } }) {
const article = Article({
headline: "Getting Started",
authorName: "Jane Doe",
datePublished: "2026-02-10",
url: `https://example.com/blog/${params.slug}`,
});
const breadcrumbs = BreadcrumbList({
items: [
{ name: "Home", url: "https://example.com" },
{ name: "Blog", url: "https://example.com/blog" },
],
});
return (
<>
<Schema data={[article, breadcrumbs]} />
<article>{/* content */}</article>
</>
);
}
// pages/blog/[slug].tsx
import Head from "next/head";
import { Schema, Article, BreadcrumbList } from "@schemasentry/next";
export default function BlogPost() {
const article = Article({
headline: "Getting Started",
authorName: "Jane Doe",
datePublished: "2026-02-10",
url: "https://example.com/blog/post",
});
const breadcrumbs = BreadcrumbList({
items: [
{ name: "Home", url: "https://example.com" },
{ name: "Blog", url: "https://example.com/blog" },
],
});
return (
<>
<Head>
<title>Getting Started - My Blog</title>
</Head>
<Schema data={[article, breadcrumbs]} />
<article>{/* content */}</article>
</>
);
}
The only difference? App Router doesn't need next/head because it supports native metadata.
Schema Sentry includes builders for the most common types:
// schema-sentry.manifest.json
{
"routes": {
"/": ["Organization", "WebSite"],
"/blog": ["WebSite"],
"/blog/getting-started": ["Article"],
"/products/widget": ["Organization", "Product"],
"/faq": ["FAQPage", "Organization"]
}
}
Add to your GitHub Actions:
# .github/workflows/schema-check.yml
name: Schema Validation
on: [push, pull_request]
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v2
- run: pnpm install
- run: pnpm build
- run: pnpm schemasentry validate
Now your CI will fail if:
# Install packages
npm install @schemasentry/next
npm install -D @schemasentry/cli
# Or with pnpm
pnpm add @schemasentry/next
pnpm add -D @schemasentry/cli
Create your first schema:
import { Schema, Organization } from "@schemasentry/next";
const org = Organization({
name: "My Company",
url: "https://example.com",
logo: "https://example.com/logo.png",
description: "We build amazing things",
});
export default function Home() {
return (
<>
<Schema data={org} />
<h1>Welcome</h1>
</>
);
}
That's it! The <Schema /> component injects the JSON-LD into your page's <head> automatically.
Check out the complete working examples: