The invite plugin supports custom token generation, allowing you to implement your own token format and generation logic.
Token Types
The plugin provides three token types:
Token 24-character secure ID
Code 6-character alphanumeric code
Custom Your own implementation
Built-in Token Generators
The plugin uses these default generators from src/utils.ts:68-83:
Token Type
Generates a 24-character ID using Better Auth’s generateId function.
invite ({
defaultTokenType: 'token'
})
Example output: a1b2c3d4e5f6g7h8i9j0k1l2
Code Type
Generates a 6-character alphanumeric code using uppercase letters and numbers.
invite ({
defaultTokenType: 'code'
})
Example output: A1B2C3
Implementation:
generateRandomString ( 6 , "0-9" , "A-Z" )
Custom Token Implementation
To implement custom token generation, set defaultTokenType to 'custom' and provide a generateToken function.
Basic Example
import { invite } from 'better-auth-invite-plugin' ;
export const auth = betterAuth ({
plugins: [
invite ({
defaultTokenType: 'custom' ,
generateToken : () => {
return crypto . randomUUID ();
}
})
]
});
UUID Tokens
Generate RFC 4122 compliant UUIDs:
invite ({
defaultTokenType: 'custom' ,
generateToken : () => {
return crypto . randomUUID ();
}
})
Example output: 550e8400-e29b-41d4-a716-446655440000
Nanoid Tokens
Use nanoid for short, URL-safe tokens:
import { nanoid } from 'nanoid' ;
invite ({
defaultTokenType: 'custom' ,
generateToken : () => {
return nanoid ( 16 );
}
})
Example output: V1StGXR8_Z5jdHi6
Prefixed Tokens
Add prefixes to identify token types:
import { generateId } from 'better-auth' ;
invite ({
defaultTokenType: 'custom' ,
generateToken : () => {
return `inv_ ${ generateId ( 20 ) } ` ;
}
})
Example output: inv_a1b2c3d4e5f6g7h8i9j0
Timestamp-based Tokens
Include timestamps in your tokens:
import { generateId } from 'better-auth' ;
invite ({
defaultTokenType: 'custom' ,
generateToken : () => {
const timestamp = Date . now (). toString ( 36 );
const random = generateId ( 12 );
return ` ${ timestamp } _ ${ random } ` ;
}
})
Example output: lm3x9g_a1b2c3d4e5f6
Token Resolution
The resolveTokenGenerator function from src/utils.ts:68-83 determines which generator to use:
export const resolveTokenGenerator = (
tokenType : TokensType ,
options : NewInviteOptions ,
): (() => string ) => {
if ( tokenType === 'custom' && options . generateToken ) {
return options . generateToken ;
}
const tokenGenerators : Record < TokensType , () => string > = {
code : () => generateRandomString ( 6 , "0-9" , "A-Z" ),
token : () => generateId ( 24 ),
custom : () => generateId ( 24 ), // secure fallback
};
return tokenGenerators [ tokenType ];
};
If you set defaultTokenType: 'custom' without providing generateToken, the system falls back to the default token generator for security.
Per-Invite Token Types
You can override the default token type when creating individual invites:
const { data } = await authClient . invite . create ({
email: '[email protected] ' ,
role: 'member' ,
tokenType: 'code' // Override default
});
This allows mixing token types in the same application.
Security Considerations
Token Uniqueness
Ensure your custom generator produces unique tokens. The database schema enforces uniqueness at src/schema.ts:6:
token : { type : "string" , unique : true }
Token Length
Generate tokens with sufficient entropy:
Tokens shorter than 16 characters may be vulnerable to brute force attacks.
Recommended minimum:
Random tokens: 20-24 characters
UUIDs: Use standard format (36 characters with hyphens)
Codes: 6-8 characters (uppercase alphanumeric)
Cryptographic Security
Use cryptographically secure random number generators:
import { generateId } from 'better-auth' ;
import { generateRandomString } from 'better-auth/crypto' ;
import { nanoid } from 'nanoid' ;
// All of these are cryptographically secure
generateId ( 24 );
generateRandomString ( 6 , "0-9" , "A-Z" );
nanoid ( 16 );
crypto . randomUUID ();
Token Expiration
Custom tokens follow the same expiration rules defined in your configuration:
invite ({
defaultTokenType: 'custom' ,
generateToken : () => crypto . randomUUID (),
invitationTokenExpiresIn: 60 * 60 // 1 hour
})
See Security for more details on token expiration and security features.
Advanced Patterns
Context-Aware Tokens
While the generateToken function doesn’t receive parameters, you can use closures to access context:
let currentRole : string | null = null ;
invite ({
defaultTokenType: 'custom' ,
generateToken : () => {
const prefix = currentRole ? currentRole . substring ( 0 , 3 ) : 'inv' ;
return ` ${ prefix } _ ${ generateId ( 20 ) } ` ;
},
inviteHooks: {
beforeCreateInvite : async ({ ctx }) => {
// This is a workaround - the API doesn't pass role to generateToken
// currentRole = inviteData.role;
}
}
})
This pattern has limitations since generateToken doesn’t receive invite data. Use with caution.
Database-Backed Tokens
Generate tokens that check for uniqueness:
import { nanoid } from 'nanoid' ;
invite ({
defaultTokenType: 'custom' ,
generateToken : () => {
// Generate token
// Note: The plugin will handle database uniqueness constraint
return nanoid ( 16 );
}
})
The database unique constraint at src/schema.ts:6 ensures no duplicate tokens exist.
Testing Custom Tokens
Test your custom token generator:
import { describe , it , expect } from 'vitest' ;
const generateToken = () => {
return crypto . randomUUID ();
};
describe ( 'Custom token generator' , () => {
it ( 'generates unique tokens' , () => {
const token1 = generateToken ();
const token2 = generateToken ();
expect ( token1 ). not . toBe ( token2 );
});
it ( 'generates tokens with correct format' , () => {
const token = generateToken ();
// UUID format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
expect ( token ). toMatch ( / ^ [ 0-9a-f ] {8} - [ 0-9a-f ] {4} - [ 0-9a-f ] {4} - [ 0-9a-f ] {4} - [ 0-9a-f ] {12} $ / );
});
it ( 'generates tokens with sufficient length' , () => {
const token = generateToken ();
expect ( token . length ). toBeGreaterThanOrEqual ( 16 );
});
});