Authentication and Permissions

Guest Auth

Instant supports guest authentication. This allows your users to try your app before signing up and ensures they can keep all their data when they decide to create a full account with their email.

Signing in as a Guest

Use db.auth.signInAsGuest() to create a new guest user. This will create a new guest user with an id, but no email address.

'use client';

import React, { useState } from 'react';
import { init, User } from '@instantdb/react';

// Visit https://instantdb.com/dash to get your APP_ID :)
const APP_ID = '__APP_ID__';
const db = init({ appId: APP_ID });

function App() {
  return (
    <>
      <db.SignedIn>
        <Main />
      </db.SignedIn>
      <db.SignedOut>
        <Login />
      </db.SignedOut>
    </>
  );
}

function Main() {
  const user = db.useUser();
  return (
    <div className="space-y-4 p-4">
      <h1 className="text-2xl font-bold">
        Hello {user.isGuest ? 'Guest' : user.email}!
      </h1>
      <button
        onClick={() => db.auth.signOut()}
        className="bg-blue-600 px-3 py-1 font-bold text-white hover:bg-blue-700"
      >
        Sign out
      </button>
    </div>
  );
}

function Login() {
  return (
    <div className="flex min-h-screen items-center justify-center">
      <div className="max-w-sm">
        <button
          onClick={() => db.auth.signInAsGuest()}
          className="w-full bg-blue-600 px-3 py-1 font-bold text-white hover:bg-blue-700"
        >
          Sign in as Guest
        </button>
      </div>
    </div>
  );
}

export default App;

Upgrading to a full user

When a guest user is ready to create a permanent account, you can use any of Instant's sign-in methods. The guest user will be automatically upgraded to a full user.

Here is a full example using magic code auth:

'use client';

import React, { useState } from 'react';
import { init, User } from '@instantdb/react';

// Visit https://instantdb.com/dash to get your APP_ID :)
const APP_ID = '__APP_ID__';
const db = init({ appId: APP_ID });

function App() {
  return (
    <>
      <db.SignedIn>
        <Main />
      </db.SignedIn>
      <db.SignedOut>
        <Login />
      </db.SignedOut>
    </>
  );
}

function Main() {
  const user: User = db.useUser();
  return (
    <div className="space-y-4 p-4">
      <h1 className="text-2xl font-bold">
        Hello {user.isGuest ? 'Guest' : user.email}!
      </h1>
      <button
        onClick={() => db.auth.signOut()}
        className="bg-blue-600 px-3 py-1 font-bold text-white hover:bg-blue-700"
      >
        Sign out
      </button>
      {user.isGuest && <Upgrade />}
    </div>
  );
}

function Upgrade() {
  const [sentEmail, setSentEmail] = useState('');

  return (
    <div className="flex">
      <div className="max-w-sm">
        {!sentEmail ? (
          <EmailStep onSendEmail={setSentEmail} />
        ) : (
          <CodeStep sentEmail={sentEmail} />
        )}
      </div>
    </div>
  );
}

function Login() {
  const [sentEmail, setSentEmail] = useState('');

  return (
    <div className="flex min-h-screen items-center justify-center">
      <div className="max-w-sm">
        {!sentEmail ? (
          <EmailStep onSendEmail={setSentEmail} />
        ) : (
          <CodeStep sentEmail={sentEmail} />
        )}
        <button
          onClick={() => {
            db.auth
              .signInAsGuest()
              .catch((err) => alert('Uh oh: ' + err.body?.message));
          }}
          className="mt-4 w-full bg-gray-600 px-3 py-1 font-bold text-white hover:bg-gray-700"
        >
          Try before signing up
        </button>
      </div>
    </div>
  );
}

function EmailStep({ onSendEmail }: { onSendEmail: (email: string) => void }) {
  const inputRef = React.useRef<HTMLInputElement>(null);
  const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    const inputEl = inputRef.current!;
    const email = inputEl.value;
    onSendEmail(email);
    db.auth.sendMagicCode({ email }).catch((err) => {
      alert('Uh oh :' + err.body?.message);
      onSendEmail('');
    });
  };
  return (
    <form
      key="email"
      onSubmit={handleSubmit}
      className="flex flex-col space-y-4"
    >
      <h2 className="text-xl font-bold">Let's log you in</h2>
      <p className="text-gray-700">
        Enter your email, and we'll send you a verification code. We'll create
        an account for you too if you don't already have one.
      </p>
      <input
        ref={inputRef}
        type="email"
        className="w-full border border-gray-300 px-3 py-1"
        placeholder="Enter your email"
        required
        autoFocus
      />
      <button
        type="submit"
        className="w-full bg-blue-600 px-3 py-1 font-bold text-white hover:bg-blue-700"
      >
        Send Code
      </button>
    </form>
  );
}

function CodeStep({ sentEmail }: { sentEmail: string }) {
  const inputRef = React.useRef<HTMLInputElement>(null);
  const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    const inputEl = inputRef.current!;
    const code = inputEl.value;
    db.auth.signInWithMagicCode({ email: sentEmail, code }).catch((err) => {
      inputEl.value = '';
      alert('Uh oh :' + err.body?.message);
    });
  };

  return (
    <form
      key="code"
      onSubmit={handleSubmit}
      className="flex flex-col space-y-4"
    >
      <h2 className="text-xl font-bold">Enter your code</h2>
      <p className="text-gray-700">
        We sent an email to <strong>{sentEmail}</strong>. Check your email, and
        paste the code you see.
      </p>
      <input
        ref={inputRef}
        type="text"
        className="w-full border border-gray-300 px-3 py-1"
        placeholder="123456..."
        required
        autoFocus
      />
      <button
        type="submit"
        className="w-full bg-blue-600 px-3 py-1 font-bold text-white hover:bg-blue-700"
      >
        Verify Code
      </button>
    </form>
  );
}

export default App;

Handling conflicting users

If a guest user signs up with an email that is not already associated with an existing account, their user id remains the same, and they retain access to all data they created as a guest.

However, if a user with that email already exists, the guest user's data may need to be merged.

You can fetch the list of guest users for a user with this query:

const query = {
  $users: {
    $: {
      where: {
        linkedPrimaryUser: user.id,
      },
    },
  },
};

The linked guest users are also available on the user itself:

const query = {
  $users: {
    $: {
      where: {
        id: user.id,
      },
      linkedGuestUsers: {},
    },
  },
};

You can then query for the data owned by those guest users and transfer it to the primary account. The specific implementation will depend on your application's data model.

To enable your user to access the data stored by the guest users, you can update your rules to grant access:

{
  "todos": {
   "bind": [
    "isOwner", "data.owner == auth.id",
+   "isGuestOwner", "data.owner in auth.ref('$user.linkedGuestUsers.id')"
   ],
    "allow": {
-     "view": "isOwner",
+     "view": "isOwner || isGuestOwner",
      "create": "isOwner",
-     "update": "isOwner",
+     "update": "isOwner || isGuestOwner",
      "delete": "isOwner"
    }
  }
}

Here's an example of how you might transfer todos from a guest account to the primary user:

function App() {
  const user = db.useUser();

  useEffect(() => {
    if (user.isGuest) return;

    const transferGuestData = async () => {
      // Get the linked guest user
      const {
        data: { $users },
      } = await db.queryOnce({
        $users: {
          $: {
            where: { linkedPrimaryUser: user.id },
            limit: 1,
            order: { serverCreatedAt: desc },
          },
        },
      });
      const guestId = $users[0]?.id;
      if (!guestId) return;

      // Get the data for the guest user
      const {
        data: { todos },
      } = await db.queryOnce({
        todos: {
          $: {
            where: { owner: guestId },
          },
        },
      });
      if (!todos.length) return;

      // Update owner on all of the guest's todo entities
      const txes = todos.map((todo) =>
        db.tx.todos[todo.id].update({ owner: user.id }),
      );
      await db.transact(txes);
    };

    transferGuestData();
  }, [user]);
}