Skip to content

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.

You can configure JWT behavior via the jwt object in createAuth.

auth.ts
export const auth = createAuth({
// ...
jwt: {
secret: process.env.AUTH_SECRET,
algorithm: 'ES256',
ttl: 3600, // 1 hour
},
})

See the jwt option in the configuration guide.

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.


  1. 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 standard jti (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
    }
  2. 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 tables
    export const UsedInviteTokens = sqliteTable('used_invite_tokens', {
    jti: text('jti').primaryKey(),
    usedAt: integer('used_at', { mode: 'timestamp' }).$defaultFn(() => new Date()),
    });
  3. 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 calls verifyInviteToken.

  4. 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=...).

This system works because of gau’s autoLink feature.

  1. The user’s account is created in the database first when they redeem their invite.
  2. When the user signs in using an OAuth provider, they go through the normal gau OAuth flow.
  3. On the callback, gau receives the user’s profile from the provider, including their verified email.
  4. Instead of creating a new user, gau sees that a user with that email already exists (from step 1).
  5. 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.