ContentsBetter Auth

Your login

Better Auth

Where verifyInvite, redeemInvite and isInvited go when your login runs on Better Auth.

NearlyIn does not replace your login; it only decides who may sign up. The file is checked against the library's real types; it has not been run against a real account with the provider.

The code

better-auth.ts

// NearlyIn with Better Auth: a database hook is the gate, the invitation code is the fast lane.
import { betterAuth } from "better-auth";
import { APIError } from "better-auth/api";
import { magicLink } from "better-auth/plugins";
import { isInvited, redeemInvite, verifyInvite } from "nearlyin/server";

export const auth = betterAuth({
  // database: …your adapter…
  plugins: [
    magicLink({
      sendMagicLink: async ({ email, url }) => {
        console.log(`Send ${url} to ${email} with your mailer.`);
      },
    }),
  ],
  databaseHooks: {
    user: {
      create: {
        // 1. isInvited: the gate. No user row is ever written for an address that is not invited.
        before: async (user) => {
          if (!(await isInvited(user.email))) {
            throw new APIError("FORBIDDEN", { message: "You are not invited yet." });
          }
          return { data: user };
        },
      },
    },
  },
});

/** GET /join?invite=<code>: the page the invitation mail links to. */
export async function showJoinPage(request: Request): Promise<Response> {
  const code = new URL(request.url).searchParams.get("invite");
  // 2. verifyInvite: who is this? Does not use the code up, so opening the link twice is fine.
  const email = await verifyInvite(code);
  if (!email) return new Response("This invitation is no longer valid.", { status: 410 });
  return Response.json({ email, code }); // render "Welcome, <email>" with a button that posts the code
}

/** POST /join: the person pressed the button. */
export async function acceptInvitation(request: Request): Promise<Response> {
  const { code } = (await request.json()) as { code?: string };
  // 3. redeemInvite: works exactly once. Only go on if it returns the address.
  const email = await redeemInvite(code);
  if (!email) return new Response("This invitation is no longer valid.", { status: 410 });

  // Mails the sign-in link. The hook above lets the new user through: they are invited.
  await auth.api.signInMagicLink({ body: { email, callbackURL: "/" }, headers: request.headers });
  return new Response("Check your inbox for the sign-in link.");
}

More on this

What the three calls do is explained under Invitations and codes.