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_token202 — 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
}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:
// 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);
}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);Settings → Email to let users sign in immediately without confirming their address. Session listing/revocation is available too via ichi.auth.listSessions.