JWT
gau
uses JSON Web Tokens (JWTs) for session management. By default, it creates a session JWT when a user signs in and stores it in a cookie or passes it to the client for token-based authentication. JWTs are stateless, so there’s no need to store them in a database.
Config
Section titled “Config”You can configure JWT behavior via the jwt
object in createAuth
.
export const auth = createAuth({ // ... jwt: { secret: process.env.AUTH_SECRET, algorithm: 'ES256', ttl: 3600, // 1 hour },})
See the jwt
option in the configuration guide.
Advanced Usage: Private Beta Invites
Section titled “Advanced Usage: Private Beta Invites”gau
exposes its internal signJWT
and verifyJWT
methods on the auth
object. This allows you to create and validate your own custom JWTs for use cases beyond sessions.
A great example is building a private beta invite system where new users can only sign up if they have a valid, single-use invite token. This works seamlessly thanks to automatic Account Linking.
Here’s and example implementation.
-
Generate Invite Tokens
Section titled “Generate Invite Tokens”This function can be part of an admin panel or a CLI script. You can use
auth.signJWT
to generate an expiring token.We add a custom
purpose
claim to distinguish it from session tokens, and a standardjti
(JWT ID) claim to give it a unique, trackable ID.invites.ts import { auth } from './auth'export async function createInviteToken() {const inviteToken = await auth.signJWT({purpose: 'beta-invite',jti: crypto.randomUUID(),},{ttl: 60 * 60 * 24 * 7, // expires in 7 days},)return inviteToken} -
Store Invite Tokens
Section titled “Store Invite Tokens”We need a way to prevent tokens from being redeemed multiple times. Let’s store them in a database.
db/schema.ts // ... existing Users and Accounts tablesexport const UsedInviteTokens = sqliteTable('used_invite_tokens', {jti: text('jti').primaryKey(),usedAt: integer('used_at', { mode: 'timestamp' }).$defaultFn(() => new Date()),}); -
Verify Invite Tokens
Section titled “Verify Invite Tokens”We can verify tokens using
auth.verifyJWT
.invites.ts // ...export async function verifyInviteToken(token: string, email: string) {const payload = await auth.verifyJWT<{ purpose?: string, jti?: string }>(token)if (payload?.purpose !== 'beta-invite' || !payload.jti)throw new Error('Invalid invite token')const isTokenUsed = await db.query.UsedInviteTokens.findFirst({where: (tokens, { eq }) => eq(tokens.jti, payload.jti),})if (isTokenUsed)throw new Error('Invite has already been used')const existingUser = await auth.getUserByEmail(email)if (existingUser)throw new Error('Email already exists')await db.transaction(async (tx) => {await tx.insert(UsedInviteTokens).values({ jti: payload.jti })await auth.createUser({ email })})}You’ll need an API endpoint (e.g.,
POST /api/redeem-invite
) that accepts the token and the user’s email, and callsverifyInviteToken
. -
Build the Frontend
Section titled “Build the Frontend”Create a page for the user to redeem their invite, verify it, then ask them to create an account using an OAuth provider.
The token can be pre-filled from a URL query parameter (e.g.,
/invite?token=...
).
How It All Connects
Section titled “How It All Connects”This system works because of gau
’s autoLink
feature.
- The user’s account is created in the database first when they redeem their invite.
- When the user signs in using an OAuth provider, they go through the normal
gau
OAuth flow. - On the callback,
gau
receives the user’s profile from the provider, including their verified email. - Instead of creating a new user,
gau
sees that a user with that email already exists (from step 1). - It automatically links their account to the existing user record and logs them in.
This ensures that only users with a valid invite token can create an account in your app.