Appwrite login page for Next.js

The pull-cord lamp login on Appwrite's server-side auth for Next.js. A server action creates the email and password session and stores it in an httpOnly cookie, and a wrong password shakes the card.

Pull-Cord Login, preview
One payment$3

By buying you agree to the Terms.

Ready forCursorv0Lovableshadcn CLI

Full product pageReact · Tailwind · TypeScript

Use it in Next.js

app/login/page.tsx

"use client";
import { useRouter } from "next/navigation";
import PullCordLogin from "@/components/PullCordLogin";
import { signInWithEmail } from "./actions";

export default function LoginPage() {
  const router = useRouter();

  return (
    <main className="grid min-h-dvh place-items-center overflow-hidden bg-[#07080c]">
      <PullCordLogin
        defaultOn
        subtitle="Sign in to your account"
        forgotHref="/forgot-password"
        onFooterAction={() => router.push("/signup")}
        onSubmit={async (email, password) => {
          const res = await signInWithEmail(email, password);
          // A rejected promise makes the card shake and resets the button.
          if (!res.ok) throw new Error(res.error);
          setTimeout(() => router.push("/account"), 900);
        }}
      />
    </main>
  );
}

app/login/actions.ts

"use server";
import { cookies } from "next/headers";
import { AppwriteException } from "node-appwrite";
import { createAdminClient } from "@/lib/server/appwrite";

export async function signInWithEmail(email: string, password: string) {
  try {
    const { account } = await createAdminClient();
    const session = await account.createEmailPasswordSession({ email, password });
    (await cookies()).set("my-custom-session", session.secret, {
      path: "/",
      httpOnly: true,
      sameSite: "strict",
      secure: true,
      expires: new Date(session.expire),
    });
    return { ok: true as const };
  } catch (err) {
    if (err instanceof AppwriteException && err.type === "user_invalid_credentials") {
      return { ok: false as const, error: "Wrong email or password." };
    }
    return { ok: false as const, error: "Sign-in failed. Try again." };
  }
}

lib/server/appwrite.ts

import { Account, Client } from "node-appwrite";
import { cookies } from "next/headers";

const base = () =>
  new Client()
    .setEndpoint(process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT!)
    .setProject(process.env.NEXT_PUBLIC_APPWRITE_PROJECT!);

// Signs people in. Needs an API key with the sessions.write scope.
export async function createAdminClient() {
  const client = base().setKey(process.env.NEXT_APPWRITE_KEY!);
  return { account: new Account(client) };
}

// Acts as the signed-in user, from the session cookie.
export async function createSessionClient() {
  const session = (await cookies()).get("my-custom-session");
  if (!session?.value) throw new Error("No session");
  const client = base().setSession(session.value);
  return { account: new Account(client) };
}

export async function getLoggedInUser() {
  try {
    const { account } = await createSessionClient();
    return await account.get();
  } catch {
    return null;
  }
}

Good to know

  • The session secret never reaches the browser. The server action sets it as an httpOnly cookie, so client JavaScript can't read it.
  • createAdminClient needs an Appwrite API key with the sessions.write scope in NEXT_APPWRITE_KEY. Keep that variable without the NEXT_PUBLIC_ prefix.
  • A wrong password comes back from Appwrite with the type user_invalid_credentials. The card shakes but the piece doesn't print text, so if you want the error shown, render res.error under it yourself.
  • The room is drawn at 1000 x 660. On a phone, scale it down with the className or style props.
  • We type-checked the snippets against node-appwrite 31 (the object-argument form of createEmailPasswordSession) and Next.js 15.

Install

  1. Copy from the zipPut react/src/components into src/, then npm i framer-motion node-appwrite.
  2. Or ask your AIPaste PROMPT.md into Cursor, Lovable or v0.

Props

onSubmit
(email, password) → promise; reject to shake
defaultOn
start with the light on
forgotHref
your reset-password page
onFooterAction
the Create one link
accent
bead and button colour

How qodesign compares

All comparisons, with prices read on each library's own site.

Questions

How do I read the signed-in user on the server?

Call getLoggedInUser() from lib/server/appwrite.ts in a server component or layout. It returns null when there is no valid session, so you can redirect to /login.

Why not use the Appwrite web SDK in the browser?

You can, but then the session lives in the browser and server components can't see it. The server action keeps one session that both sides can use.

Does it work with self-hosted Appwrite?

Yes. Point NEXT_PUBLIC_APPWRITE_ENDPOINT at your own server's /v1 URL.

Can Cursor or v0 add it for me?

Yes. Each download has a PROMPT.md. Paste it into Cursor, Lovable, v0 or Claude Code and it fits the piece into your app.

Can I use it in client work?

Yes, in personal and client projects. You can't resell the code itself as a kit.