auth.do.dev Implementation Guide

Prerequisites

Convex Auth Deployment

The dedicated auth Convex deployment has been created and configured:

# Auth Convex Deployment (Unified Authentication Service)
CONVEX_DEPLOYMENT_AUTH="dev:dependable-pika-747"
NEXT_PUBLIC_CONVEX_URL_AUTH="https://dependable-pika-747.convex.cloud"

These environment variables are already configured in the root .env.local file.

Required Environment Variables

Ensure the following are set in your root .env.local:

# Convex Auth Deployment
CONVEX_DEPLOYMENT_AUTH="dev:dependable-pika-747"
NEXT_PUBLIC_CONVEX_URL_AUTH="https://dependable-pika-747.convex.cloud"

# Email Service (already configured)
<<<<<<< HEAD
RESEND_API_KEY="re_VDEseoNb_Cf2QYfWZqTNwS23B5G61njqh"
=======
RESEND_API_KEY="re_xxxxxxxxxxxx"  # Replace with your actual Resend API key
>>>>>>> fcd62685a9d930d1de5a3007c43332340dff891c
AUTH_RESEND_FROM="hello@do.dev"

# OAuth (to be configured)
GOOGLE_CLIENT_ID="your-google-client-id"
GOOGLE_CLIENT_SECRET="your-google-client-secret"
GITHUB_CLIENT_ID="your-github-client-id"
GITHUB_CLIENT_SECRET="your-github-client-secret"

Project Setup

1. Create the Auth Service App

# In apps/
pnpm create next-app auth --typescript --tailwind --app
cd auth

# Add dependencies
pnpm add convex @convex-dev/auth lucide-react
pnpm add -D @types/node

2. Convex Schema Design

// apps/auth/convex/schema.ts
import { defineSchema, defineTable } from "convex/server"
import { v } from "convex/values"

export default defineSchema({
  // Core user table
  users: defineTable({
    // Identity
    email: v.string(),
    emailVerified: v.boolean(),
    name: v.optional(v.string()),
    image: v.optional(v.string()),
    
    // Auth providers
    providers: v.array(v.object({
      provider: v.string(), // "email", "google", "github"
      providerId: v.string(),
      connectedAt: v.number(),
    })),
    
    // Multi-app access
    apps: v.array(v.object({
      appId: v.string(), // "dodev", "promptnow", etc.
      roles: v.array(v.string()), // ["user", "admin"]
      firstAccess: v.number(),
      lastAccess: v.number(),
    })),
    
    // Metadata
    createdAt: v.number(),
    updatedAt: v.number(),
  }).index("by_email", ["email"]),
  
  // Session management
  sessions: defineTable({
    userId: v.id("users"),
    token: v.string(),
    refreshToken: v.string(),
    
    // App context
    appId: v.string(),
    returnUrl: v.optional(v.string()),
    
    // Expiration
    expiresAt: v.number(),
    refreshExpiresAt: v.number(),
    
    // Device info
    userAgent: v.optional(v.string()),
    ipAddress: v.optional(v.string()),
  }).index("by_token", ["token"])
    .index("by_refresh", ["refreshToken"])
    .index("by_user_app", ["userId", "appId"]),
  
  // OAuth state management
  oauthStates: defineTable({
    state: v.string(),
    provider: v.string(),
    appId: v.string(),
    returnUrl: v.string(),
    createdAt: v.number(),
    expiresAt: v.number(),
  }).index("by_state", ["state"]),
  
  // App registry
  apps: defineTable({
    appId: v.string(),
    name: v.string(),
    domain: v.string(),
    allowedRedirects: v.array(v.string()),
    
    // App-specific settings
    settings: v.object({
      allowEmailAuth: v.boolean(),
      allowGoogleAuth: v.boolean(),
      allowGithubAuth: v.boolean(),
      requireEmailVerification: v.boolean(),
      sessionDuration: v.number(), // in seconds
    }),
    
    // Branding
    branding: v.object({
      primaryColor: v.string(),
      logo: v.optional(v.string()),
    }),
    
    active: v.boolean(),
  }).index("by_app_id", ["appId"]),
})

3. Core Auth Functions

// apps/auth/convex/auth.ts
import { mutation, query } from "./_generated/server"
import { v } from "convex/values"
import { nanoid } from "nanoid"

// Generate secure tokens
const generateToken = () => nanoid(32)
const generateRefreshToken = () => nanoid(64)

export const createSession = mutation({
  args: {
    userId: v.id("users"),
    appId: v.string(),
    returnUrl: v.optional(v.string()),
  },
  handler: async (ctx, args) => {
    const token = generateToken()
    const refreshToken = generateRefreshToken()
    
    // 15 minute access token, 30 day refresh
    const expiresAt = Date.now() + (15 * 60 * 1000)
    const refreshExpiresAt = Date.now() + (30 * 24 * 60 * 60 * 1000)
    
    await ctx.db.insert("sessions", {
      userId: args.userId,
      token,
      refreshToken,
      appId: args.appId,
      returnUrl: args.returnUrl,
      expiresAt,
      refreshExpiresAt,
    })
    
    return { token, refreshToken, expiresAt }
  },
})

export const validateSession = query({
  args: { token: v.string() },
  handler: async (ctx, args) => {
    const session = await ctx.db
      .query("sessions")
      .withIndex("by_token", q => q.eq("token", args.token))
      .first()
    
    if (!session || session.expiresAt < Date.now()) {
      return null
    }
    
    const user = await ctx.db.get(session.userId)
    return { session, user }
  },
})

export const initiateOAuth = mutation({
  args: {
    provider: v.string(),
    appId: v.string(),
    returnUrl: v.string(),
  },
  handler: async (ctx, args) => {
    const state = generateToken()
    
    await ctx.db.insert("oauthStates", {
      state,
      provider: args.provider,
      appId: args.appId,
      returnUrl: args.returnUrl,
      createdAt: Date.now(),
      expiresAt: Date.now() + (10 * 60 * 1000), // 10 minutes
    })
    
    return { state }
  },
})

4. Auth UI Components

// apps/auth/components/auth-form.tsx
"use client"

import { useState } from "react"
import { useSearchParams } from "next/navigation"

export function UnifiedAuthForm() {
  const searchParams = useSearchParams()
  const appId = searchParams.get("app") || "unknown"
  const returnUrl = searchParams.get("return") || "/"
  
  const [email, setEmail] = useState("")
  const [flow, setFlow] = useState<"signin" | "verify">("signin")
  
  return (
    <div className="max-w-md mx-auto p-8">
      <h1 className="text-2xl font-bold mb-8">
        Sign in to {getAppName(appId)}
      </h1>
      
      {flow === "signin" ? (
        <EmailSignIn 
          onSubmit={async (email) => {
            // Send OTP
            setFlow("verify")
          }}
        />
      ) : (
        <VerifyCode 
          email={email}
          onVerify={async (code) => {
            // Verify and redirect
            window.location.href = returnUrl
          }}
        />
      )}
      
      <div className="mt-6">
        <OAuthButtons appId={appId} returnUrl={returnUrl} />
      </div>
    </div>
  )
}

function OAuthButtons({ appId, returnUrl }: { appId: string; returnUrl: string }) {
  const handleOAuth = (provider: string) => {
    window.location.href = `/api/auth/oauth/${provider}?app=${appId}&return=${encodeURIComponent(returnUrl)}`
  }
  
  return (
    <div className="space-y-3">
      <button
        onClick={() => handleOAuth("google")}
        className="w-full flex items-center justify-center gap-3 px-4 py-2 border rounded-lg hover:bg-gray-50"
      >
        <GoogleIcon />
        Continue with Google
      </button>
      
      <button
        onClick={() => handleOAuth("github")}
        className="w-full flex items-center justify-center gap-3 px-4 py-2 border rounded-lg hover:bg-gray-50"
      >
        <GithubIcon />
        Continue with GitHub
      </button>
    </div>
  )
}

5. API Routes

// apps/auth/app/api/auth/validate/route.ts
import { NextRequest, NextResponse } from "next/server"
import { api } from "@/convex/_generated/api"
import { fetchQuery } from "convex/nextjs"

export async function POST(request: NextRequest) {
  const { token } = await request.json()
  
  const result = await fetchQuery(api.auth.validateSession, { token })
  
  if (!result) {
    return NextResponse.json({ valid: false }, { status: 401 })
  }
  
  return NextResponse.json({
    valid: true,
    user: {
      id: result.user._id,
      email: result.user.email,
      name: result.user.name,
    },
    session: {
      appId: result.session.appId,
      expiresAt: result.session.expiresAt,
    },
  })
}

6. Client SDK

// packages/auth-client/src/index.ts
export class AuthClient {
  constructor(private authUrl: string = "https://auth.do.dev") {}
  
  async validateSession(token: string): Promise<SessionInfo | null> {
    const res = await fetch(`${this.authUrl}/api/auth/validate`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ token }),
    })
    
    if (!res.ok) return null
    return res.json()
  }
  
  redirectToLogin(appId: string, returnUrl?: string): void {
    const url = new URL(`${this.authUrl}/login`)
    url.searchParams.set("app", appId)
    if (returnUrl) {
      url.searchParams.set("return", returnUrl)
    }
    window.location.href = url.toString()
  }
  
  async refreshToken(refreshToken: string): Promise<TokenPair | null> {
    const res = await fetch(`${this.authUrl}/api/auth/refresh`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ refreshToken }),
    })
    
    if (!res.ok) return null
    return res.json()
  }
}

7. App Integration

// In any app (dodev, promptnow, etc.)
// hooks/use-unified-auth.ts
import { useEffect, useState } from "react"
import { AuthClient } from "@workspace/auth-client"
import Cookies from "js-cookie"

const authClient = new AuthClient()

export function useUnifiedAuth() {
  const [session, setSession] = useState<SessionInfo | null>(null)
  const [loading, setLoading] = useState(true)
  
  useEffect(() => {
    const token = Cookies.get("auth_token")
    if (!token) {
      setLoading(false)
      return
    }
    
    authClient.validateSession(token)
      .then(setSession)
      .finally(() => setLoading(false))
  }, [])
  
  const signIn = () => {
    authClient.redirectToLogin(
      process.env.NEXT_PUBLIC_APP_ID!,
      window.location.href
    )
  }
  
  const signOut = () => {
    Cookies.remove("auth_token")
    setSession(null)
  }
  
  return { session, loading, signIn, signOut }
}

Security Best Practices

  1. Token Storage

    • Use httpOnly cookies for tokens
    • Implement CSRF protection
    • Short-lived access tokens
  2. OAuth Security

    • Validate state parameter
    • Verify redirect URLs against whitelist
    • Use PKCE for OAuth flows
  3. Rate Limiting

    • Implement per-IP rate limiting
    • Exponential backoff for failed attempts
    • Account lockout after repeated failures
  4. Audit Logging

    • Log all auth events
    • Track IP addresses
    • Monitor for suspicious patterns

Deployment Checklist

Completed

  • Create dedicated auth Convex deployment (dependable-pika-747)
  • Configure environment variables in root .env.local

Pending

  • Create auth.do.dev Next.js app structure
  • Set up auth.do.dev domain and DNS
  • Configure OAuth apps (Google, GitHub) with dynamic redirect URLs
  • Deploy Convex schema and functions to dependable-pika-747
  • Set up monitoring and logging
  • Configure rate limiting
  • Test OAuth with multiple localhost ports
  • Test with production domains
  • Document all API endpoints
  • Create app migration guides

Next Steps

  1. Create the auth app in apps/auth/
  2. Configure it to use the dependable-pika-747 deployment
  3. Implement the schema and auth functions
  4. Set up OAuth with dynamic redirect handling
  5. Create the auth-client SDK package
  6. Test with local-test app first
  7. Migrate other apps one by one

On this page