ContentsClerk

Your login

Clerk

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

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

clerk.ts

// NearlyIn with Clerk: people from the waitlist get in with one click, everyone else stays out.
// Set Clerk's sign-up mode to "Restricted", so accounts only come to be through this file.
import { createClerkClient } from "@clerk/backend";
import { verifyWebhook } from "@clerk/backend/webhooks";
import { isInvited, redeemInvite, verifyInvite } from "nearlyin/server";

const clerk = createClerkClient({ secretKey: process.env.CLERK_SECRET_KEY });

/** 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");
  // 1. 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 };
  // 2. redeemInvite: works exactly once. Create the account only if it returns the address.
  const email = await redeemInvite(code);
  if (!email) return new Response("This invitation is no longer valid.", { status: 410 });

  // The code proved that the address is theirs, so no second confirmation mail is needed.
  const user = await clerk.users.createUser({ emailAddress: [email], skipPasswordRequirement: true });
  const { url } = await clerk.signInTokens.createSignInToken({ userId: user.id, expiresInSeconds: 300 });
  return Response.redirect(url, 303); // signs them in
}

/** POST /webhooks/clerk (event `user.created`): a second lock, for accounts created any other way. */
export async function onClerkWebhook(request: Request): Promise<Response> {
  const event = await verifyWebhook(request);
  if (event.type === "user.created") {
    const email = event.data.email_addresses[0]?.email_address;
    // 3. isInvited: for when there is no code, only an address.
    if (!email || !(await isInvited(email))) await clerk.users.deleteUser(event.data.id);
  }
  return new Response(null, { status: 204 });
}

More on this

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