Overview
Public invites allow you to:- Generate shareable invitation links or codes
- Control max uses per invitation
- Create invite codes without sending emails
- Build referral and community invite systems
Key Differences
| Feature | Private Invites | Public Invites |
|---|---|---|
| Email Required | Yes | No |
| Email Sent | Yes (via sendUserInvitation) | No |
| Max Uses | Default: 1 | Default: Unlimited |
| Response Type | Success message | Token or URL |
| Validation | Email must match | Anyone can use |
Basic Implementation
Server Configuration
No special configuration needed for public invites:
lib/auth.ts
import { betterAuth } from "better-auth";
import { invite } from "better-auth-invite-plugin";
export const auth = betterAuth({
database: {
// Your database config
},
plugins: [
invite({
// Public invites don't require sendUserInvitation
// But you can still have it for private invites
defaultSenderResponse: "url", // Return full URL by default
defaultMaxUses: 10, // Limit uses for public invites
}),
],
});
Client Setup
Configure the client plugin:
lib/auth-client.ts
import { createAuthClient } from "better-auth/react";
import { inviteClient } from "better-auth-invite-plugin/client";
export const authClient = createAuthClient({
baseURL: process.env.NEXT_PUBLIC_AUTH_URL,
plugins: [inviteClient()],
});
Create Public Invite Component
Build a component to generate public invite codes:
components/public-invite-generator.tsx
"use client";
import { useState } from "react";
import { authClient } from "@/lib/auth-client";
import { Check, Copy } from "lucide-react";
export function PublicInviteGenerator() {
const [role, setRole] = useState("user");
const [maxUses, setMaxUses] = useState(10);
const [senderResponse, setSenderResponse] = useState<"token" | "url">("url");
const [inviteResult, setInviteResult] = useState<string | null>(null);
const [copied, setCopied] = useState(false);
const [loading, setLoading] = useState(false);
const handleGenerate = async () => {
setLoading(true);
setInviteResult(null);
try {
const { data, error } = await authClient.invite.create({
// No email = public invite
role,
maxUses,
senderResponse,
senderResponseRedirect: "signUp",
});
if (error) {
alert(`Error: ${error.message}`);
} else if (data?.message) {
setInviteResult(data.message);
}
} catch (err) {
alert("Failed to generate invite");
} finally {
setLoading(false);
}
};
const handleCopy = async () => {
if (inviteResult) {
await navigator.clipboard.writeText(inviteResult);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
}
};
return (
<div className="max-w-2xl mx-auto space-y-6">
<div className="bg-white rounded-lg shadow p-6">
<h2 className="text-2xl font-bold mb-4">Generate Public Invite Code</h2>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium mb-1">Role</label>
<select
value={role}
onChange={(e) => setRole(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md"
>
<option value="user">User</option>
<option value="moderator">Moderator</option>
<option value="admin">Admin</option>
</select>
</div>
<div>
<label className="block text-sm font-medium mb-1">
Max Uses (0 = unlimited)
</label>
<input
type="number"
value={maxUses}
onChange={(e) => setMaxUses(Number(e.target.value))}
min="0"
className="w-full px-3 py-2 border border-gray-300 rounded-md"
/>
</div>
<div>
<label className="block text-sm font-medium mb-2">Response Type</label>
<div className="flex gap-4">
<label className="flex items-center">
<input
type="radio"
value="url"
checked={senderResponse === "url"}
onChange={(e) => setSenderResponse("url")}
className="mr-2"
/>
Full URL
</label>
<label className="flex items-center">
<input
type="radio"
value="token"
checked={senderResponse === "token"}
onChange={(e) => setSenderResponse("token")}
className="mr-2"
/>
Token Only
</label>
</div>
</div>
<button
onClick={handleGenerate}
disabled={loading}
className="w-full bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 disabled:opacity-50"
>
{loading ? "Generating..." : "Generate Invite"}
</button>
</div>
</div>
{inviteResult && (
<div className="bg-green-50 border border-green-200 rounded-lg p-6">
<h3 className="text-lg font-semibold text-green-900 mb-3">
Invite Generated!
</h3>
<div className="flex items-center gap-2">
<code className="flex-1 bg-white px-4 py-3 rounded border border-green-300 break-all text-sm">
{inviteResult}
</code>
<button
onClick={handleCopy}
className="p-3 bg-green-600 text-white rounded hover:bg-green-700 transition-colors"
title="Copy to clipboard"
>
{copied ? <Check size={20} /> : <Copy size={20} />}
</button>
</div>
<p className="text-sm text-green-700 mt-3">
Share this {senderResponse === "url" ? "link" : "code"} with anyone you want to invite!
</p>
</div>
)}
</div>
);
}
Advanced Use Cases
- Referral System
- Time-Limited Invites
- QR Code Invites
Building a Referral System
Create a system where users can generate their own referral codes:components/referral-system.tsx
"use client";
import { useState, useEffect } from "react";
import { authClient } from "@/lib/auth-client";
export function ReferralSystem() {
const [referralCode, setReferralCode] = useState<string | null>(null);
const [referralCount, setReferralCount] = useState(0);
useEffect(() => {
// Generate user's personal referral code on mount
generateReferralCode();
}, []);
const generateReferralCode = async () => {
const { data } = await authClient.invite.create({
role: "user",
maxUses: 0, // Unlimited uses
senderResponse: "token",
tokenType: "code", // Generate a 6-character code
});
if (data?.message) {
setReferralCode(data.message);
}
};
const referralUrl = referralCode
? `${window.location.origin}/invite/${referralCode}`
: "";
return (
<div className="max-w-md mx-auto bg-white rounded-lg shadow-lg p-6">
<h2 className="text-2xl font-bold mb-4">Your Referral Code</h2>
{referralCode ? (
<>
<div className="bg-gradient-to-r from-blue-500 to-purple-600 text-white p-6 rounded-lg text-center mb-4">
<div className="text-sm uppercase tracking-wide mb-2">Your Code</div>
<div className="text-4xl font-bold tracking-wider">{referralCode}</div>
</div>
<div className="space-y-3">
<button
onClick={() => navigator.clipboard.writeText(referralUrl)}
className="w-full bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700"
>
Copy Referral Link
</button>
<div className="bg-gray-50 p-4 rounded-md">
<div className="text-sm text-gray-600 mb-1">Successful Referrals</div>
<div className="text-3xl font-bold text-gray-900">{referralCount}</div>
</div>
</div>
<p className="text-sm text-gray-600 mt-4">
Share your code with friends to invite them to the platform!
</p>
</>
) : (
<div className="text-center py-8">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto"></div>
<p className="text-gray-600 mt-4">Generating your referral code...</p>
</div>
)}
</div>
);
}
Time-Limited Community Invites
Create invites that expire after a certain time:components/time-limited-invite.tsx
"use client";
import { useState } from "react";
import { authClient } from "@/lib/auth-client";
const EXPIRATION_OPTIONS = [
{ label: "1 hour", seconds: 60 * 60 },
{ label: "24 hours", seconds: 24 * 60 * 60 },
{ label: "7 days", seconds: 7 * 24 * 60 * 60 },
{ label: "30 days", seconds: 30 * 24 * 60 * 60 },
];
export function TimeLimitedInvite() {
const [expiresIn, setExpiresIn] = useState(EXPIRATION_OPTIONS[1].seconds);
const [maxUses, setMaxUses] = useState(5);
const [inviteUrl, setInviteUrl] = useState<string | null>(null);
const handleGenerate = async () => {
const { data } = await authClient.invite.create({
role: "user",
maxUses,
expiresIn,
senderResponse: "url",
});
if (data?.message) {
setInviteUrl(data.message);
}
};
const expirationDate = new Date(Date.now() + expiresIn * 1000);
return (
<div className="max-w-lg mx-auto bg-white rounded-lg shadow p-6">
<h2 className="text-2xl font-bold mb-4">Create Time-Limited Invite</h2>
<div className="space-y-4 mb-6">
<div>
<label className="block text-sm font-medium mb-2">Expires In</label>
<div className="grid grid-cols-2 gap-2">
{EXPIRATION_OPTIONS.map((option) => (
<button
key={option.seconds}
onClick={() => setExpiresIn(option.seconds)}
className={`px-4 py-2 rounded-md border ${
expiresIn === option.seconds
? "bg-blue-600 text-white border-blue-600"
: "bg-white text-gray-700 border-gray-300 hover:border-blue-600"
}`}
>
{option.label}
</button>
))}
</div>
</div>
<div>
<label className="block text-sm font-medium mb-1">
Max Uses: {maxUses}
</label>
<input
type="range"
min="1"
max="100"
value={maxUses}
onChange={(e) => setMaxUses(Number(e.target.value))}
className="w-full"
/>
</div>
<div className="bg-blue-50 border border-blue-200 rounded-md p-3">
<p className="text-sm text-blue-800">
This invite will expire on{" "}
<strong>{expirationDate.toLocaleString()}</strong>
</p>
</div>
</div>
<button
onClick={handleGenerate}
className="w-full bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700"
>
Generate Invite Link
</button>
{inviteUrl && (
<div className="mt-6 p-4 bg-green-50 border border-green-200 rounded-md">
<p className="text-sm font-medium text-green-900 mb-2">Invite Link Generated!</p>
<code className="block bg-white p-3 rounded border border-green-300 text-sm break-all">
{inviteUrl}
</code>
<button
onClick={() => navigator.clipboard.writeText(inviteUrl)}
className="mt-3 w-full bg-green-600 text-white py-2 px-4 rounded-md hover:bg-green-700"
>
Copy Link
</button>
</div>
)}
</div>
);
}
QR Code Invitations
Generate QR codes for easy mobile sharing:components/qr-invite.tsx
"use client";
import { useState } from "react";
import { authClient } from "@/lib/auth-client";
import QRCode from "qrcode.react"; // npm install qrcode.react
export function QRInvite() {
const [inviteUrl, setInviteUrl] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
const handleGenerate = async () => {
setLoading(true);
const { data } = await authClient.invite.create({
role: "user",
maxUses: 1, // One-time use QR code
senderResponse: "url",
});
if (data?.message) {
setInviteUrl(data.message);
}
setLoading(false);
};
return (
<div className="max-w-md mx-auto bg-white rounded-lg shadow-lg p-8 text-center">
<h2 className="text-2xl font-bold mb-6">QR Code Invite</h2>
{inviteUrl ? (
<>
<div className="bg-white p-6 rounded-lg border-2 border-gray-200 inline-block mb-4">
<QRCode value={inviteUrl} size={256} level="H" />
</div>
<p className="text-sm text-gray-600 mb-4">
Scan this QR code to accept the invitation
</p>
<button
onClick={() => setInviteUrl(null)}
className="bg-gray-600 text-white py-2 px-6 rounded-md hover:bg-gray-700"
>
Generate New Code
</button>
</>
) : (
<button
onClick={handleGenerate}
disabled={loading}
className="bg-blue-600 text-white py-3 px-8 rounded-md hover:bg-blue-700 disabled:opacity-50"
>
{loading ? "Generating..." : "Generate QR Code"}
</button>
)}
</div>
);
}
Token Types
You can specify different token types for public invites:const { data } = await authClient.invite.create({
role: "user",
tokenType: "code", // Generates a 6-character code like "ABC123"
senderResponse: "token",
});
// Or use the default 24-character token
const { data } = await authClient.invite.create({
role: "user",
tokenType: "token", // Default
senderResponse: "token",
});
Token Type:
"code" generates a 6-character alphanumeric code (0-9, A-Z), while "token" generates a 24-character random string. Codes are easier to share verbally, while tokens are more secure.Tracking Usage
Monitor how many times a public invite has been used:components/invite-usage.tsx
"use client";
import { useState, useEffect } from "react";
import { authClient } from "@/lib/auth-client";
interface InviteStats {
token: string;
maxUses: number;
currentUses: number;
expiresAt: Date;
}
export function InviteUsage({ token }: { token: string }) {
const [stats, setStats] = useState<InviteStats | null>(null);
useEffect(() => {
fetchStats();
}, [token]);
const fetchStats = async () => {
// You would implement a custom endpoint to get usage stats
// This is a simplified example
const response = await fetch(`/api/invite/stats?token=${token}`);
const data = await response.json();
setStats(data);
};
if (!stats) return <div>Loading...</div>;
const usagePercentage = (stats.currentUses / stats.maxUses) * 100;
return (
<div className="bg-white rounded-lg shadow p-4">
<h3 className="font-semibold mb-3">Invite Usage</h3>
<div className="space-y-2">
<div>
<div className="flex justify-between text-sm mb-1">
<span>Uses</span>
<span>
{stats.currentUses} / {stats.maxUses}
</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-2">
<div
className="bg-blue-600 h-2 rounded-full"
style={{ width: `${usagePercentage}%` }}
/>
</div>
</div>
<div className="text-sm text-gray-600">
Expires: {new Date(stats.expiresAt).toLocaleDateString()}
</div>
</div>
</div>
);
}
Next Steps
- Build an invite dashboard to manage all invites
- Learn about role-based invites
- Implement custom email handlers for private invites