19 KiB
Adding WorkOS AuthKit to markdown-site (Dashboard Only)
A beginner-friendly, step-by-step guide for adding WorkOS AuthKit authentication to only the /dashboard page of your markdown-site project. The main site remains public—no login required.
Table of Contents
- Overview
- Prerequisites
- Part 1: Create a WorkOS Account
- Part 2: Configure WorkOS Dashboard
- Part 3: Install Dependencies
- Part 4: Add Environment Variables
- Part 5: Configure Convex Auth
- Part 6: Update the Main App Entry Point
- Part 7: Create the Callback Route
- Part 8: Create the Protected Dashboard Page
- Part 9: Add Dashboard Route to Router
- Part 10: Deploy and Test
- Troubleshooting
- Quick Reference
1. Overview
What We're Building
- Public site: All existing pages (Home, Blog posts, About, etc.) remain publicly accessible
- Protected dashboard: Only
/dashboardrequires login via WorkOS AuthKit - Authentication provider: WorkOS AuthKit (supports passwords, social login, SSO, and more)
Architecture
markdown-site/
├── src/
│ ├── main.tsx ← Wrap app with AuthKitProvider
│ ├── App.tsx ← Main router (unchanged for public routes)
│ └── pages/
│ ├── Home.tsx ← Public (no changes)
│ ├── Post.tsx ← Public (no changes)
│ ├── Callback.tsx ← NEW: Handle auth callback
│ └── Dashboard.tsx ← NEW: Protected page
└── convex/
└── auth.config.ts ← NEW: Convex auth configuration
2. Prerequisites
Before starting, make sure you have:
- Node.js 18 or higher installed
- A working markdown-site project
- A Convex account and project already set up
- Your Convex development server running (
npx convex dev)
Don't have markdown-site yet? Clone it first:
git clone https://github.com/waynesutton/markdown-site.git
cd markdown-site
npm install
npx convex dev
3. Part 1: Create a WorkOS Account
Step 1.1: Sign Up for WorkOS
- Go to workos.com/sign-up
- Create a free account with your email
- Verify your email address
Step 1.2: Set Up AuthKit
- Log into the WorkOS Dashboard
- Navigate to Authentication → AuthKit
- Click the Set up AuthKit button
- Select "Use AuthKit's customizable hosted UI"
- Click Begin setup
Step 1.3: Configure Redirect URI
During the AuthKit setup wizard, you'll reach step 4: "Add default redirect endpoint URI"
Enter this for local development:
http://localhost:5173/callback
What is this? After a user logs in, WorkOS redirects them back to this URL with an authorization code. Your app exchanges this code for user information.
Step 1.4: Copy Your Credentials
- Go to dashboard.workos.com/get-started
- Under Quick start, find and copy:
- Client ID (looks like
client_01XXXXXXXXXXXXXXXXX)
- Client ID (looks like
Save this somewhere safe—you'll need it shortly.
4. Part 2: Configure WorkOS Dashboard
Step 2.1: Enable CORS
For the React SDK to work, you need to allow your app's domain:
- Go to Authentication → Sessions in the WorkOS Dashboard
- Find Cross-Origin Resource Sharing (CORS)
- Click Manage
- Add your development URL:
http://localhost:5173 - Click Save
Note
: When you deploy to production (e.g., Netlify), add your production domain here too (e.g.,
https://markdowncms.netlify.app).
Step 2.2: Verify Redirect URI
- Go to Redirects in the WorkOS Dashboard
- Confirm
http://localhost:5173/callbackis listed - If not, add it by clicking Add redirect
5. Part 3: Install Dependencies
Open your terminal in the markdown-site project folder and install the required packages:
npm install @workos-inc/authkit-react @convex-dev/workos
What these packages do:
| Package | Purpose |
|---|---|
@workos-inc/authkit-react |
WorkOS React SDK for handling login/logout |
@convex-dev/workos |
Bridges WorkOS auth with Convex backend |
6. Part 4: Add Environment Variables
Step 4.1: Update .env.local
Open your .env.local file (in the project root) and add these lines:
# Existing Convex URL (should already be here)
VITE_CONVEX_URL=https://your-deployment.convex.cloud
# WorkOS AuthKit Configuration (add these)
VITE_WORKOS_CLIENT_ID=client_01XXXXXXXXXXXXXXXXX
VITE_WORKOS_REDIRECT_URI=http://localhost:5173/callback
Replace client_01XXXXXXXXXXXXXXXXX with your actual Client ID from the WorkOS Dashboard.
Why
VITE_prefix? Vite only exposes environment variables that start withVITE_to the browser.
Step 4.2: Add to .gitignore (if not already)
Make sure .env.local is in your .gitignore to avoid committing secrets:
.env.local
.env.production.local
7. Part 5: Configure Convex Auth
Create a new file to tell Convex how to validate WorkOS tokens.
Step 5.1: Create convex/auth.config.ts
Create a new file at convex/auth.config.ts:
// convex/auth.config.ts
const clientId = process.env.WORKOS_CLIENT_ID;
const authConfig = {
providers: [
{
type: "customJwt",
issuer: "https://api.workos.com/",
algorithm: "RS256",
applicationID: clientId,
jwks: `https://api.workos.com/sso/jwks/${clientId}`,
},
{
type: "customJwt",
issuer: `https://api.workos.com/user_management/${clientId}`,
algorithm: "RS256",
jwks: `https://api.workos.com/sso/jwks/${clientId}`,
},
],
};
export default authConfig;
Step 5.2: Add Environment Variable to Convex
The Convex backend needs the Client ID too:
- Run
npx convex devif not already running - You'll see an error with a link—click it
- It takes you to the Convex Dashboard environment variables page
- Add a new variable:
- Name:
WORKOS_CLIENT_ID - Value: Your WorkOS Client ID (e.g.,
client_01XXXXXXXXXXXXXXXXX)
- Name:
- Save
After saving, npx convex dev should show "Convex functions ready."
8. Part 6: Update the Main App Entry Point
Now we'll wrap the app with authentication providers—but only where needed.
Step 6.1: Modify src/main.tsx
Replace your current src/main.tsx with this:
// src/main.tsx
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { AuthKitProvider, useAuth } from "@workos-inc/authkit-react";
import { ConvexReactClient } from "convex/react";
import { ConvexProviderWithAuthKit } from "@convex-dev/workos";
import "./styles/global.css";
import App from "./App";
// Initialize Convex client
const convex = new ConvexReactClient(import.meta.env.VITE_CONVEX_URL);
createRoot(document.getElementById("root")!).render(
<StrictMode>
<AuthKitProvider
clientId={import.meta.env.VITE_WORKOS_CLIENT_ID}
redirectUri={import.meta.env.VITE_WORKOS_REDIRECT_URI}
>
<ConvexProviderWithAuthKit client={convex} useAuth={useAuth}>
<App />
</ConvexProviderWithAuthKit>
</AuthKitProvider>
</StrictMode>
);
What changed:
- Added
AuthKitProviderwrapper (handles WorkOS authentication state) - Added
ConvexProviderWithAuthKit(connects WorkOS auth to Convex) - Passed the
useAuthhook to bridge the two
9. Part 7: Create the Callback Route
When a user logs in via WorkOS, they're redirected to /callback. This page handles the authentication response.
Step 7.1: Create src/pages/Callback.tsx
Create a new file at src/pages/Callback.tsx:
// src/pages/Callback.tsx
import { useEffect } from "react";
import { useAuth } from "@workos-inc/authkit-react";
import { useNavigate } from "react-router-dom";
export default function Callback() {
const { isLoading, user } = useAuth();
const navigate = useNavigate();
useEffect(() => {
// Once authentication is complete, redirect to dashboard
if (!isLoading && user) {
navigate("/dashboard", { replace: true });
}
}, [isLoading, user, navigate]);
return (
<div
style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
height: "100vh",
fontFamily: "system-ui, sans-serif",
}}
>
<div style={{ textAlign: "center" }}>
<h2>Signing you in...</h2>
<p>Please wait while we complete your authentication.</p>
</div>
</div>
);
}
How it works:
- User completes login on WorkOS hosted page
- WorkOS redirects to
/callback?code=... - The
AuthKitProviderautomatically exchanges the code for a session - Once authenticated, we redirect to
/dashboard
10. Part 8: Create the Protected Dashboard Page
This is where authenticated users will land. We'll protect it so only logged-in users can access it.
Step 8.1: Create src/pages/Dashboard.tsx
Create a new file at src/pages/Dashboard.tsx:
// src/pages/Dashboard.tsx
import { Authenticated, Unauthenticated, AuthLoading } from "convex/react";
import { useAuth } from "@workos-inc/authkit-react";
import { Link } from "react-router-dom";
export default function Dashboard() {
return (
<>
<AuthLoading>
<LoadingState />
</AuthLoading>
<Unauthenticated>
<LoginPrompt />
</Unauthenticated>
<Authenticated>
<DashboardContent />
</Authenticated>
</>
);
}
function LoadingState() {
return (
<div className="dashboard-container">
<p>Loading authentication...</p>
</div>
);
}
function LoginPrompt() {
const { signIn } = useAuth();
return (
<div className="dashboard-container">
<div className="dashboard-card">
<h1>Dashboard</h1>
<p>You need to sign in to access the dashboard.</p>
<button onClick={() => signIn()} className="sign-in-button">
Sign In
</button>
<p style={{ marginTop: "1rem" }}>
<Link to="/">← Back to Home</Link>
</p>
</div>
</div>
);
}
function DashboardContent() {
const { user, signOut } = useAuth();
return (
<div className="dashboard-container">
<div className="dashboard-card">
<h1>Dashboard</h1>
<p>
Welcome, <strong>{user?.firstName || user?.email || "User"}</strong>!
</p>
<div className="user-info">
<h3>Your Account</h3>
<ul>
<li>
<strong>Email:</strong> {user?.email}
</li>
<li>
<strong>Name:</strong> {user?.firstName} {user?.lastName}
</li>
<li>
<strong>User ID:</strong> {user?.id}
</li>
</ul>
</div>
<div className="dashboard-actions">
<button onClick={() => signOut()} className="sign-out-button">
Sign Out
</button>
<Link to="/" className="home-link">
← Back to Home
</Link>
</div>
</div>
</div>
);
}
Step 8.2: Add Dashboard Styles
Add these styles to your src/styles/global.css file:
/* Dashboard Styles */
.dashboard-container {
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
padding: 2rem;
}
.dashboard-card {
background: var(--bg-secondary, #f5f5f5);
border-radius: 12px;
padding: 2rem;
max-width: 500px;
width: 100%;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.dashboard-card h1 {
margin-top: 0;
margin-bottom: 1rem;
}
.user-info {
background: var(--bg-primary, #fff);
border-radius: 8px;
padding: 1rem;
margin: 1.5rem 0;
}
.user-info h3 {
margin-top: 0;
margin-bottom: 0.5rem;
font-size: 1rem;
}
.user-info ul {
list-style: none;
padding: 0;
margin: 0;
}
.user-info li {
padding: 0.25rem 0;
font-size: 0.9rem;
}
.dashboard-actions {
display: flex;
gap: 1rem;
align-items: center;
margin-top: 1.5rem;
}
.sign-in-button,
.sign-out-button {
background: #6366f1;
color: white;
border: none;
padding: 0.75rem 1.5rem;
border-radius: 6px;
font-size: 1rem;
cursor: pointer;
transition: background 0.2s;
}
.sign-in-button:hover,
.sign-out-button:hover {
background: #4f46e5;
}
.home-link {
color: var(--text-secondary, #666);
text-decoration: none;
}
.home-link:hover {
text-decoration: underline;
}
11. Part 9: Add Dashboard Route to Router
Now we need to add routes for /callback and /dashboard.
Step 9.1: Update src/App.tsx
Find your router configuration in src/App.tsx and add the new routes. Your App.tsx likely uses React Router. Add these imports and routes:
// At the top of src/App.tsx, add these imports:
import Callback from "./pages/Callback";
import Dashboard from "./pages/Dashboard";
// In your Routes component, add these routes:
<Route path="/callback" element={<Callback />} />
<Route path="/dashboard" element={<Dashboard />} />
Example of what your full App.tsx might look like:
// src/App.tsx
import { BrowserRouter, Routes, Route } from "react-router-dom";
import Home from "./pages/Home";
import Post from "./pages/Post";
import Callback from "./pages/Callback";
import Dashboard from "./pages/Dashboard";
// ... other imports
function App() {
return (
<BrowserRouter>
<Routes>
{/* Existing public routes */}
<Route path="/" element={<Home />} />
<Route path="/:slug" element={<Post />} />
{/* New auth routes */}
<Route path="/callback" element={<Callback />} />
<Route path="/dashboard" element={<Dashboard />} />
</Routes>
</BrowserRouter>
);
}
export default App;
12. Part 10: Deploy and Test
Step 10.1: Test Locally
- Make sure
npx convex devis running - Start your development server:
npm run dev - Open
http://localhost:5173 - Navigate to
http://localhost:5173/dashboard - Click "Sign In" and complete the WorkOS login flow
- You should be redirected back to the dashboard with your user info displayed
Step 10.2: Deploy to Production
When you're ready to deploy:
-
Add production redirect URI to WorkOS:
- Go to WorkOS Dashboard → Redirects
- Add:
https://your-domain.com/callback
-
Add production CORS origin:
- Go to Authentication → Sessions → CORS
- Add:
https://your-domain.com
-
Update Convex production environment:
- Go to Convex Dashboard → Your Project → Production deployment
- Add
WORKOS_CLIENT_IDenvironment variable
-
Create
.env.production.local:VITE_CONVEX_URL=https://your-production-deployment.convex.cloud VITE_WORKOS_CLIENT_ID=client_01XXXXXXXXXXXXXXXXX VITE_WORKOS_REDIRECT_URI=https://your-domain.com/callback -
Deploy:
npx convex deploy npm run build # Deploy to Netlify or your hosting provider
13. Troubleshooting
"useAuth must be used within AuthKitProvider"
Cause: Component is rendered outside the AuthKitProvider wrapper.
Fix: Make sure AuthKitProvider wraps your entire app in main.tsx.
Login redirects to wrong URL
Cause: Redirect URI mismatch.
Fix:
- Check
VITE_WORKOS_REDIRECT_URImatches exactly what's in WorkOS Dashboard - Include protocol (
http://orhttps://) - Include port number for localhost (
:5173)
"isAuthenticated: false" after login
Cause: Convex auth config doesn't match WorkOS setup.
Fix:
- Verify
WORKOS_CLIENT_IDis set in Convex Dashboard - Re-run
npx convex devto sync configuration - Check browser console for specific errors
CORS errors
Cause: Your domain isn't in WorkOS CORS settings.
Fix:
- Go to WorkOS Dashboard → Authentication → Sessions → CORS
- Add your exact origin (e.g.,
http://localhost:5173) - Don't include trailing slashes
"Failed to fetch" errors
Cause: Network or authentication issues.
Fix:
- Check browser DevTools Network tab for specific errors
- Verify Convex is running (
npx convex dev) - Check that environment variables are loaded (add
console.log(import.meta.env))
14. Quick Reference
File Changes Summary
| File | Action | Purpose |
|---|---|---|
convex/auth.config.ts |
Create | Tell Convex how to validate WorkOS tokens |
src/main.tsx |
Modify | Wrap app with auth providers |
src/pages/Callback.tsx |
Create | Handle auth redirect |
src/pages/Dashboard.tsx |
Create | Protected dashboard page |
src/App.tsx |
Modify | Add routes for callback and dashboard |
src/styles/global.css |
Modify | Add dashboard styles |
.env.local |
Modify | Add WorkOS credentials |
Environment Variables
| Variable | Where Used | Example |
|---|---|---|
VITE_WORKOS_CLIENT_ID |
Browser (Vite) | client_01XXXXXXXXX |
VITE_WORKOS_REDIRECT_URI |
Browser (Vite) | http://localhost:5173/callback |
WORKOS_CLIENT_ID |
Convex Backend | client_01XXXXXXXXX |
Useful Commands
# Start development
npx convex dev # In terminal 1
npm run dev # In terminal 2
# Deploy to production
npx convex deploy # Deploy Convex functions
npm run build # Build frontend
# Sync content to production
npm run sync:prod
WorkOS Dashboard Checklist
- AuthKit enabled and configured
- Redirect URI added (
http://localhost:5173/callback) - CORS origin added (
http://localhost:5173) - Client ID copied to
.env.local - Production URLs added (when deploying)
Next Steps
Once you have the basic setup working, you can:
- Customize the dashboard - Add Convex queries to show user-specific data
- Add more protected routes - Use the same pattern for other admin pages
- Enable social login - Configure Google, GitHub, etc. in WorkOS Dashboard
- Set up organizations - If you need multi-tenant features
- Add role-based access - Use WorkOS roles and permissions
For more information, see:
Last updated: December 2024