A Next.js + Convex starter pre-wired to authenticate with VibeAuth. Clone it, set two env vars, have working auth in 5 minutes.
Live demo: vibe.ruixen.app · auth via accounts.ruixen.app
A running VibeAuth instance — deploy one to Vercel in one click before continuing.
User → VibeAuth (sign in) → server-side relay fetches JWT
→ redirect to vibe_client?_vibe_token=<jwt>
→ JWT stored in localStorage → passed to Convex
→ Convex verifies JWT via JWKS → identity confirmed
- User clicks "Sign in" — redirected to your VibeAuth instance
- After sign-in, VibeAuth's server-side relay fetches a signed JWT and appends it to the redirect URL
- vibe_client reads
_vibe_tokenfrom the URL on landing, stores it in localStorage ConvexProviderWithAuthpasses the JWT to Convex on every request- Convex verifies the JWT against VibeAuth's JWKS endpoint
ctx.auth.getUserIdentity()returns the user's identity in any Convex function
The JWT flow works with any backend that can verify a JWT against a JWKS URL — Convex is what this template uses, but Express, FastAPI, or anything else works the same way.
git clone https://github.com/gitcoder89431/vibe_client
cd vibe_client
pnpm installpnpm convex:devThis creates your Convex project and generates convex/_generated/. Copy the NEXT_PUBLIC_CONVEX_URL it outputs.
cp .env.local.example .env.localFill in .env.local:
NEXT_PUBLIC_CONVEX_URL=https://your-project.convex.cloud
NEXT_PUBLIC_VIBE_AUTH_URL=https://accounts.yourdomain.comIn convex.dev → your project → Settings → Environment Variables:
VIBE_AUTH_URL = https://accounts.yourdomain.com
This must match BETTER_AUTH_URL on your VibeAuth instance exactly.
In your VibeAuth admin → Settings → Trusted Origins, add your app's URL (e.g. http://localhost:3001).
pnpm devOpen http://localhost:3000.
| Variable | Where | Description |
|---|---|---|
NEXT_PUBLIC_CONVEX_URL |
.env.local |
Your Convex deployment URL |
NEXT_PUBLIC_VIBE_AUTH_URL |
.env.local |
Your VibeAuth instance URL (e.g. https://accounts.yourdomain.com) |
VIBE_AUTH_URL |
Convex Dashboard | Same as above — used by Convex to verify JWTs |
src/
├── app/
│ ├── page.tsx # Public home — sign in / dashboard link
│ ├── dashboard/
│ │ └── page.tsx # Protected — redirects if not signed in
│ ├── providers.tsx # ConvexProviderWithAuth wired to VibeAuth
│ └── layout.tsx
├── lib/
│ └── auth.ts # JWT utilities — token storage, session parsing, sign out
convex/
├── auth.config.ts # Points Convex to VibeAuth JWKS
├── schema.ts # Users table
└── users.ts # me query + getOrCreate mutation
Clone this repo as your starting point. For each new project, only three things change:
| Thing | Action |
|---|---|
| Convex project | Run pnpm convex:dev — new project, new NEXT_PUBLIC_CONVEX_URL |
NEXT_PUBLIC_VIBE_AUTH_URL |
Point to your VibeAuth instance (e.g. https://accounts.yourdomain.com) |
VIBE_AUTH_URL in Convex dashboard |
Same as above |
These files copy as-is to every project — never touch them:
convex/auth.config.ts— points Convex to VibeAuth JWKS, identical everywheresrc/lib/auth.ts— JWT utilities, identical everywheresrc/app/providers.tsx— ConvexProviderWithAuth setup, identical everywhere
Schema pattern — anchor everything to vibeAuthId:
The sub claim in the JWT is the VibeAuth user ID and is consistent across all your apps — the same user gets the same ID everywhere.
// convex/schema.ts
defineTable({
vibeAuthId: v.string(), // = ctx.auth.getUserIdentity().subject
email: v.string(),
// ...your app-specific fields
}).index("by_vibeAuthId", ["vibeAuthId"])// any Convex query/mutation — look up the current user
const identity = await ctx.auth.getUserIdentity()
const user = await ctx.db.query("users")
.withIndex("by_vibeAuthId", q => q.eq("vibeAuthId", identity.subject))
.first()On first sign-in, call users.getOrCreate to create a Convex record linked to the user's VibeAuth ID:
const getOrCreate = useMutation(api.users.getOrCreate)
useEffect(() => {
if (session) {
getOrCreate({
email: session.user.email,
name: session.user.name ?? undefined,
})
}
}, [session])After that, ctx.auth.getUserIdentity().subject in any Convex function is the user's VibeAuth user ID — use it to scope all data.
- Next.js 15 — App Router, server components
- Convex — real-time backend + database
- VibeAuth — self-hosted auth hub (JWT issuer)
