InstantDB magic code login in React

Instant's magic code auth behind an animated code card: the user types an email, Instant sends a 6-digit code, and the card checks it with signInWithMagicCode and opens the app.

Step-Up OTP Gate, 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 { useState } from "react";
import { useRouter } from "next/navigation";
import { StepUpGate } from "@/components/StepUpGate";
import { db } from "@/lib/db";

type InstantError = { body?: { message?: string } };

export default function LoginPage() {
  const router = useRouter();
  const [email, setEmail] = useState("");
  const [codeSent, setCodeSent] = useState(false);
  const [error, setError] = useState("");

  async function sendCode() {
    setError("");
    try {
      await db.auth.sendMagicCode({ email });
      setCodeSent(true);
    } catch (err) {
      setError((err as InstantError).body?.message ?? "Couldn't send the code. Try again.");
    }
  }

  if (codeSent) {
    return (
      <StepUpGate
        title="Check your email"
        description={`Enter the 6-digit code we sent to ${email}.`}
        successTitle="You're in"
        successDescription="Opening your app."
        onVerify={async (code) => {
          try {
            await db.auth.signInWithMagicCode({ email, code });
            return true;
          } catch {
            return false; // wrong or expired code: the cells shake and the user types again
          }
        }}
        onResend={() => void db.auth.sendMagicCode({ email })}
        onCancel={() => setCodeSent(false)}
        onVerified={() => router.push("/app")}
      >
        {/* whatever should sit blurred behind the code card */}
        <div className="grid min-h-dvh place-items-center bg-[#0b0c10] text-white/40">Your app</div>
      </StepUpGate>
    );
  }

  return (
    <main className="grid min-h-dvh place-items-center bg-[#0b0c10] text-white">
      <form
        onSubmit={(e) => {
          e.preventDefault();
          void sendCode();
        }}
        className="grid w-80 gap-3"
      >
        <label htmlFor="email" className="text-sm text-white/70">Email</label>
        <input id="email" type="email" autoComplete="email" required value={email}
          onChange={(e) => setEmail(e.target.value)}
          className="rounded-lg bg-white/10 px-3 py-2 outline-none focus:ring-2 focus:ring-[#C6F432]" />
        <button className="rounded-lg bg-[#C6F432] px-3 py-2 font-medium text-black">Email me a code</button>
        {error ? <p role="alert" className="text-sm text-red-400">{error}</p> : null}
      </form>
    </main>
  );
}

lib/db.ts

import { init } from "@instantdb/react";

export const db = init({ appId: process.env.NEXT_PUBLIC_INSTANT_APP_ID! });

Good to know

  • Type-checked against @instantdb/react 1.0.67 and next 15.5 with TypeScript 5.9. We haven't run it against a live Instant app.
  • The code is 6 digits. Instant's server makes it with rand-num-str 6 (server/src/instant/model/app_user_magic_code.clj in the instantdb/instant repo), so the card's 6 cells match without changing length.
  • A wrong or expired code makes signInWithMagicCode reject. onVerify catches that and returns false, and the cells shake. Instant keeps the message on err.body.message if you want to show it.
  • Instant creates the user on the first successful code, so this one screen covers sign-up too. Its docs say the same in their example: "We'll create an account for you too if you don't already have one."
  • The page doesn't switch on db.SignedIn. If it did, the code card would vanish the moment Instant signs the user in and the unlock animation would never play. It moves on in onVerified instead, after the animation.
  • If you only need a working login and don't care how it looks, the full example on Instant's Magic Code Auth page is free and complete. Paste it and you're done. This guide is for when the code step should feel like part of your app.
  • For testing, Instant's dashboard can give a test email a static code that never expires, so you don't wait for emails while you build.
  • Copy StepUpGate.tsx, step-up-gate.css and timeline.ts from react/src/components. The gate frosts whatever you pass as children.

Install

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

Props

onVerify
(code) → true when Instant signs the user in
onResend
send a fresh code
onCancel
back to the email field
onVerified
runs after the unlock animation
title / description
card text
length
number of cells (6 for Instant)

How qodesign compares

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

Questions

Does it work with Instant's guest auth?

Yes. Instant can upgrade a guest to a real user when they sign in with a code. This page only changes the screen, so Instant's guest docs apply as written.

Can I set fields on the new user?

Yes. signInWithMagicCode takes extraFields, and Instant writes them only on the first sign-up. Pass them in the onVerify call.

What about React Native?

The card is a web component. Instant's React Native docs have their own magic code example for Expo.

Does the code screen work in Next.js?

Yes, as a client component. Keep "use client" at the top of the file that renders it, as in the snippet above. App Router and Pages Router both work.

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.