Skip to main content
Public invite codes are shareable invitation links that don’t require an email address. They’re perfect for referral systems, community invites, or time-limited access.

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

FeaturePrivate InvitesPublic Invites
Email RequiredYesNo
Email SentYes (via sendUserInvitation)No
Max UsesDefault: 1Default: Unlimited
Response TypeSuccess messageToken or URL
ValidationEmail must matchAnyone can use

Basic Implementation

1

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
    }),
  ],
});
2

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()],
});
3

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

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>
  );
}

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 docs developers (and LLMs) love