Auth

Who is calling: guest sessions, ctx.auth guards, Google sign-in from the client, and what a user ID means in Fold.

The three states

Every caller is one of these:

StateuserIdisGuestisSignedInprovider
Signed outnullfalsefalsenull
Guesta stringtruefalse"guest"
Signed in with Googlea stringfalsetrue"google"

displayName is always a string. Signed-in users may also have email, emailVerified, and picture, if they agreed to share them.

Guests

A new visitor gets a guest session automatically. It is an HTTP-only cookie for that app, so a guest's data is private to that browser without any setup. Guest access ends if the browser's data is cleared or the session expires before the guest signs in.

Guests work the same in local dev and hosted. In local dev only, you can act as a named test identity with ?fold_guest=<name> in the URL or npx @aozumi/fold auth as <name> (reset with npx @aozumi/fold auth reset). Named identities cannot be upgraded to an account. Hosted folds ignore ?fold_guest=.

ctx.auth on the server

query(async (ctx) => {
  const { userId } = ctx.auth.requireIdentity();
  return ctx.db.notes.withIndex("by_owner", (q) => q.eq("ownerId", userId)).collect();
})
MemberUse
ctx.auth.userId`string \null`
ctx.auth.isGuest, ctx.auth.isSignedIn, ctx.auth.isAuthenticatedSeparate flags. Neither isGuest nor isSignedIn is true without a session.
ctx.auth.requireIdentity()Returns the guest or signed-in identity. Throws A guest session or sign-in is required. when signed out. Use it for data that belongs to a person.
ctx.auth.requireSignedIn()Returns the signed-in identity. Throws Sign in is required. for guests. Use it for account-only actions.

Rules:

Require sign-in for the whole app

export default fold({
  auth: { requireSignIn: true },
  // ...
});

With requireSignIn: true, no query, mutation, action, or endpoint runs until the caller signs in with Google. The page and the sign-in button still load. Client-side checks alone do not protect data; this setting does.

Google sign-in on the client

import { canAccessApp, retryAuth, SignInWithGoogle, signOut, useAuth } from "@aozumi/fold/client";

export function AccountBar() {
  const auth = useAuth();
  if (auth.isLoading) {
    return <p>Checking session</p>;
  }
  if (auth.error) {
    return (
      <div>
        <p role="alert">{auth.error}</p>
        <button type="button" onClick={() => void retryAuth()}>Retry</button>
        <SignInWithGoogle />
      </div>
    );
  }
  return auth.isSignedIn ? (
    <button type="button" onClick={() => void signOut()}>Sign out {auth.displayName}</button>
  ) : (
    <SignInWithGoogle className="border px-3 py-1" />
  );
}
ExportWhat it does
useAuth()Current auth state: the fields above plus isLoading, requireSignIn, and error?. Check isLoading before showing signed-out UI.
SignInWithGoogleA button that starts Google sign-in. Accepts className, children, returnTo, disabled, and other button props.
signInWithGoogle(options?)Starts sign-in from your own button. Navigates the page to the sign-in service.
signOut()Ends the session. If sign-in is not required, the browser gets a fresh guest session.
getIdentity(){ userId, token?, expired? }. token is the Google identity token, for calls to your own endpoints.
retryAuth()Retries a session check that failed.
canAccessApp()true when the session is ready, has no error, and meets requireSignIn. Gate data components on it.

Sign-in uses the AOZUMI sign-in service at auth.aozumi.dev, with Google as the identity provider. It works from http://localhost and http://127.0.0.1 in local dev and from https://*.aozumi.site when hosted. The first time a person signs in to an app, they are asked whether to share their name and email with that app. Pass requestPii: false (or requestProfile: false) to signInWithGoogle to ask for no profile data.

Per-app user IDs

A signed-in person gets a different userId in each app. Two folds cannot match up the same person by user ID. Within one app the ID stays the same across sign-ins and redeploys. Email, name, and picture are profile data that can change. Do not use them as keys.

Guest upgrade

When a guest signs in, Fold moves the guest's data to the account in one transaction:

If the account already has data, both sets end up under the account. For app-specific merge rules, add a hook. It runs before the automatic transfer, in the same transaction:

export default fold({
  auth: {
    onGuestUpgrade: async (ctx, { guestUserId, userId }) => {
      ctx.log.info("guest signed in", { guestUserId, userId });
    }
  },
  // ...
});

Calling your own endpoints

From the app's own pages, fetch to a same-origin endpoint sends the guest cookie automatically. For a signed-in user, also send the identity token:

import { getIdentity } from "@aozumi/fold/client";

const token = getIdentity().token;
const response = await fetch("api/notes", {
  method: "POST",
  headers: { "Content-Type": "application/json", ...(token ? { "X-Fold-Token": token } : {}) },
  body: JSON.stringify({ body: "hello" })
});

Fold reads identity from X-Fold-Token, not from Authorization. Authorization is passed to your endpoint untouched, for your own use. Guest cookies are only accepted on same-origin requests.

What works hosted

FeatureLocal devHosted, anonymous
Guest sessionsYesYes
requireIdentity(), requireSignedIn()YesYes
Google sign-inYesYes
requireSignIn: trueYesYes
Guest upgrade and onGuestUpgradeYesYes
?fold_guest= and auth as test identitiesYesNo
Approving or removing users (users command)NoComing soon