Create new

Email & Password

Sign up, sign in, and password reset — pre-configured and ready to use.

Email and password authentication is enabled by default in launch.now. The forms, validation, and API routes are all wired up — you open the app and it works.


How it works

The auth config lives in lib/auth.ts. Better Auth handles password hashing (Bcrypt), session creation, and token rotation automatically — you never touch a hash directly.

import { betterAuth } from "better-auth";
import { prismaAdapter } from "better-auth/adapters/prisma";
import { prisma } from "@/lib/prisma";

export const auth = betterAuth({
  appName: "launch.now",
  baseURL: process.env.BETTER_AUTH_BASE_URL,
  secret: process.env.BETTER_AUTH_SECRET,

  database: prismaAdapter(prisma, {
    provider: "postgresql",
  }),

  emailAndPassword: {
    enabled: true,
    requireEmailVerification: false,
    sendResetPassword: async ({ user, url }) => {
      await sendEmail({
        to: user.email,
        subject: "Reset your password",
        body: `Reset link: ${url}`,
      });
    },
  },

  session: {
    expiresIn: 60 * 60 * 24 * 2, // 2 days
    updateAge: 60 * 60 * 24, // refresh expiry daily
    cookieCache: {
      enabled: true,
      maxAge: 5 * 60, // 5 minutes
      strategy: "compact",
    },
  },
});

File structure

lib/
  auth.ts                           ← server config
  auth-client.ts                    ← client helpers
app/
  api/auth/[...all]/route.ts        ← Better Auth catch-all handler
  (auth)/
    sign-in/page.tsx
    sign-up/page.tsx
    forgot-password/page.tsx
    reset-password/page.tsx
src/
  components/features/auth/
    sign-in-form.tsx
    sign-up-form.tsx
    forgot-password-form.tsx
    reset-password-form.tsx

Sign up

The sign up form collects name, email, and password. Validation runs client-side with Zod before the request hits the server.

const { data, error } = await authClient.signUp.email({
  name,
  email,
  password,
  callbackURL: "/dashboard",
});

If you set requireEmailVerification: true in auth.ts, the user lands on a "check your inbox" page instead of being redirected to the dashboard immediately. See the Emails section to configure Resend.


Sign in

const { data, error } = await authClient.signIn.email({
  email,
  password,
  rememberMe, // extends session lifetime when true
  callbackURL: "/dashboard",
});

The rememberMe flag controls whether the session cookie persists across browser restarts. When false, the session expires when the tab closes.


Password reset

Password reset is a two-step flow. The user first requests a link, then submits a new password using the signed token from that link.

Request a reset link

The forgot password form calls forgetPassword with the user's email. Better Auth generates a signed token and calls your sendResetPassword callback.

await authClient.forgetPassword({
  email,
  redirectTo: "/reset-password",
});

Send the email

Wire sendResetPassword in auth.ts to your email provider. The url param is the full reset link with the signed token already appended. See the Emails section for the Resend setup.

sendResetPassword: async ({ user, url }) => {
  await sendEmail({
    to: user.email,
    subject: "Reset your password",
    react: <ResetPasswordEmail url={url} name={user.name} />,
  })
},

Submit a new password

The reset page reads ?token= from the URL and calls resetPassword.

const token = searchParams.get("token") ?? "";

await authClient.resetPassword({
  newPassword: password,
  token,
});

Reset tokens are single-use and expire after 1 hour. If a user requests multiple resets, only the latest token is valid — previous ones are invalidated immediately.


Magic links in launch.now are used exclusively for accepting organization invitations. When you invite a team member, they receive a signed link that authenticates them (creating an account if needed) and accepts the invitation in one click.

This is handled by the Organizations plugin automatically. See Organizations → Invitations for the full setup.


Reading the session

import { auth } from "@/lib/auth";
import { headers } from "next/headers";
import { redirect } from "next/navigation";

const session = await auth.api.getSession({
  headers: await headers(),
});

if (!session) redirect("/sign-in");

const { user } = session;

Prefer reading the session on the server when you can — it avoids a client-side waterfall and lets you redirect before the page renders. Use useSession only when you need reactivity, like updating the UI after a sign out.


Environment variables

BETTER_AUTH_SECRET=        # openssl rand -base64 32
BETTER_AUTH_BASE_URL=      # http://localhost:3000 in dev
NEXT_PUBLIC_APP_URL=       # same value, exposed to the client
DATABASE_URL=              # postgres connection string