Workflow SDK Magic Link Example
A passwordless login demo built with Workflow SDK and Next.js. Demonstrates two authentication flows — email magic link and GitHub OAuth — both implemented as durable workflows that suspend while waiting for user interaction.
How It Works
Email Magic Link
- User enters their email address
- A workflow starts and creates a webhook with a unique URL
- The URL is emailed to the user via Resend
- The workflow suspends — zero compute while waiting
- User clicks the link in their email
- The webhook fires, resuming the workflow, which redirects the user's browser
- The workflow returns the user's Gravatar and login info
export async function doEmailLogin(url: string, email: string) {
"use workflow";
const webhook = createWebhook({ respondWith: "manual" });
await sendLoginEmail(email, new URL(webhookUrl.pathname, url));
const req = await Promise.race([webhook, sleep("5m")]);
if (!req) throw new Error("Login email timed out");
await redirectWebhook(req, new URL("/login-success", url));
const avatar_url = await getGravatarUrl(email);
return { type: "email", login: email, avatar_url };
}GitHub OAuth
- User clicks "Login with GitHub"
- A workflow starts and creates a hook with a deterministic token
- The user is redirected to GitHub for authorization
- GitHub redirects back to the app's API route with an OAuth code
- The API route reconstructs the token and calls
resumeHook()to resume the workflow - The workflow exchanges the code for an access token, fetches the user's profile, and returns
export async function doGitHubLogin(url: string, token: string) {
"use workflow";
const hook = createHook<GitHubOAuthCallback>({ token });
const result = await Promise.race([hook, sleep("5m")]);
if (!result) throw new Error("GitHub login timed out");
const accessToken = await getAccessToken(result.code);
const user = await getUser(accessToken);
return { type: "github", login: user.login, avatar_url: user.avatar_url };
}Key Patterns
createWebhook()— generates a unique public URL for the email magic link (human-in-the-loop pattern)createHook()with deterministic token — the API route reconstructs the token from a cookie to resume the correct workflow (no database lookup needed)respondWith: 'manual'— the workflow controls the HTTP response when the webhook is hit, enabling browser redirectsPromise.race([hook, sleep("5m")])— built-in timeout with no infrastructure
Project Structure
workflows/
login-email.ts # Email magic link workflow
login-github.ts # GitHub OAuth workflow
steps/
email.ts # Send login email via Resend
github.ts # GitHub OAuth token exchange and user fetch
gravatar.ts # Gravatar URL lookup
redirect.ts # Redirect the user's browser via webhook response
app/
api/
login/email/route.ts # POST: start email login workflow
login/github/route.ts # POST: start GitHub login, GET: OAuth callback
workflow/status/ # GET: poll workflow run status
page.tsx # Login form UI
login-success/ # Success page (shown in popup after OAuth)
login-error/ # Error pageGetting Started
Prerequisites
- Node.js 18+
- pnpm
- A Resend account (for sending emails)
- A GitHub OAuth App (for GitHub login)
1. Clone and Install
git clone https://github.com/vercel-labs/workflow-magic-link-example.git
cd workflow-magic-link-example
pnpm install2. Set Up GitHub OAuth App
- Go to GitHub Developer Settings
- Create a new OAuth App
- Set the Authorization callback URL to
http://localhost:3000/api/login/github(update for production) - Note the Client ID and generate a Client Secret
3. Environment Variables
Create a .env.local file:
RESEND_API_KEY=re_your_resend_api_key
GITHUB_CLIENT_ID=your_github_client_id
GITHUB_CLIENT_SECRET=your_github_client_secret4. Development
pnpm devOpen http://localhost:3000 to see the login form.
Deployment
After deploying, update your GitHub OAuth App's callback URL to match your production domain.