ichibaseichibase

Authentication

Email/password sign-up and login for your end users. After login the client uses the user's access token automatically, so your row-level security and realtime rules run as that user.

Sign up & log in

await ichi.auth.signup({ email, password });
const { data, error } = await ichi.auth.login({ email, password });
// data.user, data.access_token, data.refresh_token

const user = await ichi.auth.getUser();
await ichi.auth.logout();

Passwordless sign-in (OTP & magic link)

Let users sign in with a one-time code, a magic link, or both in the same email — no password required. This is additive: email + password keeps working alongside it. Enable it in your project under Settings → Passwordless login; it requires a custom SMTPserver, since the shared sender can't carry sign-in codes. Customize the email under Settings → Email templates → Passwordless sign-in.

Call signInWithOtp to send the email, then finish with verifyOtp (the code the user types) or verifyMagicLink (the token from the tapped link). Both return a full session, exactly like login.

// 1. Send the sign-in email (always succeeds, even for unknown emails)
await ichi.auth.signInWithOtp({ email });

// 2a. User typed the 6-digit code:
const { data, error } = await ichi.auth.verifyOtp({ email, code });

// 2b. …or your magic-link landing page exchanges the token from the URL:
//     https://yourapp.com/auth?token=<t>
const token = new URL(location.href).searchParams.get('token');
const { data } = await ichi.auth.verifyMagicLink(token);
// data.user, data.access_token, data.refresh_token
The request endpoint always returns 202 — it never reveals whether an email is registered (a new email creates the account on first verify). Codes are single-use and expire; treat repeated failures as a wrong code, not a system error.

2-step verification (login)

Optionally require a second factor after a correct password. Enable it under Settings → 2-step verification(custom SMTP required) and pick the method — a 6-digit code, a magic link, or both. When it's on, login does not return a session: it returns { twofa_required: true, methods } and emails the factor. Finish with verifyTwoFactor (the code) or verifyTwoFactorMagic (the token from the tapped link) to get the session. Customize the email under Settings → Email templates → 2-step verification.

const { data, error } = await ichi.auth.login({ email, password });
if (data?.twofa_required) {
  // a code / link was emailed — prompt the user, then:
  await ichi.auth.verifyTwoFactor({ email, code });
  // …or exchange a magic-link token from your landing page:
  // await ichi.auth.verifyTwoFactorMagic(token);
} else {
  // no 2FA — data is the session
}
2-step verification is email-based (a code or link proves inbox control) — not an authenticator/TOTP app. Sign-up email verification has the same three choices under Settings → Email: magic link, 6-digit code, or both in one email.

Keeping users logged in

The session lives in memory by default. Pass a storage adapter to persist it across reloads — window.localStorage on the web, or a secure-store adapter on mobile.

// Web
const ichi = createClient(url, anonKey, { storage: window.localStorage });

// React Native (async store) — hydrate once at startup
const ichi = createClient(url, anonKey, { storage: SecureStoreAdapter });
await ichi.auth.loadSession();

// React to changes
ichi.onAuthStateChange((event, session) => {
  // 'SIGNED_IN' | 'SIGNED_OUT' | 'TOKEN_REFRESHED'
});

Client-side vs server-side (SSR)

The setup above keeps the session in memory or localStorage— perfect for a single-page app, but a server can't read localStorage, so a Next.js Server Component never knows who is signed in. For server-side rendering, import from @ichibase/client/ssr: it stores the session in a cookieshared between the browser and your server and gives you two factories (modeled on Supabase's @supabase/ssr):

createBrowserClient — for Client Components ("use client"). A singleton that reads/writes the session cookie via document.cookie and refreshes an expired token itself. createServerClient— for Server Components, Server Actions, Route Handlers, and middleware. Created per request; you hand it your framework's cookie store.

They read the same cookie (ichibase.session), so a Server Component and a Client Component on the same page see the same session at once — you pick a client per context, not per app. Wire each once:

SSR is a TypeScript feature (Next.js App Router shown). Native/mobile apps use the storage adapter from "Keeping users logged in" instead.
// lib/ichibase/client.ts — use in Client Components
import { createBrowserClient } from '@ichibase/client/ssr';

export const createClient = () =>
  createBrowserClient(
    process.env.NEXT_PUBLIC_ICHIBASE_URL!,
    process.env.NEXT_PUBLIC_ICHIBASE_ANON_KEY!,
  );

// lib/ichibase/server.ts — use in Server Components / Actions / Route Handlers
import { cookies } from 'next/headers';
import { createServerClient } from '@ichibase/client/ssr';

export async function createClient() {
  const store = await cookies();
  return createServerClient(URL, ANON_KEY, {
    cookies: {
      getAll: () => store.getAll(),
      // Server Components can't set cookies — this only runs in Actions /
      // Route Handlers / middleware; wrap in try/catch elsewhere.
      setAll: (list) =>
        list.forEach(({ name, value, options }) => store.set(name, value, options)),
    },
  });
}

Middleware is the key piece.Server Components can't write cookies, so a refreshed token would never reach the browser. Run a middleware on every request: it refreshes the token when it's about to expire, writes the fresh cookie onto the response, and gates protected routes. (Auth calls like getUser() use a raw fetch and do not auto-refresh — only data calls do — so refresh here explicitly.)

// lib/ichibase/middleware.ts
import { NextResponse, type NextRequest } from 'next/server';
import { createServerClient } from '@ichibase/client/ssr';

const PROTECTED = ['/account', '/notes'];

export async function updateSession(request: NextRequest) {
  let response = NextResponse.next({ request });
  const ichi = createServerClient(URL, ANON_KEY, {
    cookies: {
      getAll: () => request.cookies.getAll(),
      setAll: (list) => {
        list.forEach(({ name, value }) => request.cookies.set(name, value));
        response = NextResponse.next({ request });
        list.forEach(({ name, value, options }) => response.cookies.set(name, value, options));
      },
    },
  });

  // Proactively refresh a token that's expired / about to expire.
  const session = ichi.getSession();
  if (session?.refresh_token && session.expires_at * 1000 - Date.now() < 60_000) {
    await ichi.auth.refresh();
  }

  const user = await ichi.auth.getUser();
  const path = request.nextUrl.pathname;
  if (!user && PROTECTED.some((p) => path === p || path.startsWith(p + '/'))) {
    const url = request.nextUrl.clone();
    url.pathname = '/login';
    return NextResponse.redirect(url);
  }
  return response;
}

// middleware.ts (project root)
import { type NextRequest } from 'next/server';
import { updateSession } from '@/lib/ichibase/middleware';

export async function middleware(req: NextRequest) {
  return updateSession(req);
}
export const config = { matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'] };

Then use whichever client fits the context:

// app/account/page.tsx — protected Server Component (renders on the server)
import { createClient } from '@/lib/ichibase/server';

export default async function Account() {
  const ichi = await createClient();
  const user = await ichi.auth.getUser();              // read from the cookie, server-side
  const { data } = await ichi.from('notes').select('*'); // RLS runs AS this user
  return <pre>{JSON.stringify({ user, data }, null, 2)}</pre>;
}

// app/login/actions.ts — Server Action: login writes the session to the cookie
'use server';
import { redirect } from 'next/navigation';
import { createClient } from '@/lib/ichibase/server';

export async function login(formData: FormData) {
  const ichi = await createClient();
  const { error } = await ichi.auth.login({
    email: String(formData.get('email')),
    password: String(formData.get('password')),
  });
  if (!error) redirect('/account');
}

// app/notes/notes.tsx — Client Component: same session, self-refreshing
'use client';
import { createClient } from '@/lib/ichibase/client';

export function Notes() {
  // const { data } = await createClient().from('notes').select('*');
  // const sub = createClient().realtime.subscribe({ kind: 'postgres', table: 'notes' }, fn);
}
The session cookie is not httpOnly — the browser client has to read it to refresh itself, and that keeps one isomorphic model that also works for SPAs. Your access token is short-lived and your RLS / Mongo / realtime rules are the real gate, but if you need an httpOnly hardened setup, keep the session server-only and proxy data calls through Route Handlers. A complete runnable app is in the SDK repo under examples/nextjs. (Needs @ichibase/client 0.5+.)

Password reset

Call requestPasswordReset(email) — always returns 202 (no account enumeration). The email carries a magic link, a 6-digit code, or both, per Settings → Email → Password reset delivery (same three modes as verification). Finish with whichever the user has:

await ichi.auth.requestPasswordReset(email);

// Link mode: your reset page reads ?token= and submits a new password
await ichi.auth.confirmPasswordReset(token, newPassword);

// Code mode ('otp' / 'both'): collect the code + new password in your app
await ichi.auth.confirmPasswordResetOtp(email, code, newPassword);
Reset and signup verification enforce the same password policy (your configured min length + regex). Email verification on signup is on by default but optional — turn it off under Settings → Email to let users sign in immediately without confirming their address. Session listing/revocation is available too via ichi.auth.listSessions.