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

  1. User enters their email address
  2. A workflow starts and creates a webhook with a unique URL
  3. The URL is emailed to the user via Resend
  4. The workflow suspends — zero compute while waiting
  5. User clicks the link in their email
  6. The webhook fires, resuming the workflow, which redirects the user's browser
  7. 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

  1. User clicks "Login with GitHub"
  2. A workflow starts and creates a hook with a deterministic token
  3. The user is redirected to GitHub for authorization
  4. GitHub redirects back to the app's API route with an OAuth code
  5. The API route reconstructs the token and calls resumeHook() to resume the workflow
  6. 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 redirects
  • Promise.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 page

Getting Started

Prerequisites

1. Clone and Install

git clone https://github.com/vercel-labs/workflow-magic-link-example.git
cd workflow-magic-link-example
pnpm install

2. Set Up GitHub OAuth App

  1. Go to GitHub Developer Settings
  2. Create a new OAuth App
  3. Set the Authorization callback URL to http://localhost:3000/api/login/github (update for production)
  4. 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_secret

4. Development

pnpm dev

Open http://localhost:3000 to see the login form.

Deployment

After deploying, update your GitHub OAuth App's callback URL to match your production domain.

Resources