@mailflix/identity-ui
v0.3.0
Published
Drop-in auth UI for solana-shuffle identity. One catch-all page + one catch-all API route + one config file = full auth (login / signup / forgot / reset / magic-link / OAuth / wallet) in any Next.js app. Includes a scaffold CLI: npx @mailflix/identity-ui
Downloads
38
Readme
@mailflix/identity-ui
Drop-in auth UI and server-side handlers for solana-shuffle identity. Adding auth to a new Next.js app is one command + one config file:
cd apps/your-new-app
pnpm add @mailflix/identity-ui
npx @mailflix/identity-ui init
# edit lib/auth-config.ts to set brand + methodsThat scaffolds the four files you need (catch-all page, catch-all API route, auth-client adapter, config). Login + signup + forgot-password + reset-password + magic-link + OAuth + wallet sign-in all work out of the box, branded to your app, talking to your identity service.
Why this package exists
10+ Next.js apps across two monorepos were going to rebuild the same auth UI. The package collapses that to one source of truth: bumping the version updates every consumer; new endpoints (TOTP, etc.) land once and propagate.
Quick start (new app)
0. Install
pnpm add @mailflix/identity-ui1. Run the scaffold
npx @mailflix/identity-ui initWrites:
lib/auth-config.ts— your brand + methods + handler config (consumer-specific)lib/auth-client.ts— thin browser client that hits/api/auth/*app/auth/[[...segment]]/page.tsx— UI catch-all (1-line re-export)app/api/auth/[...path]/route.ts— API catch-all (3 lines)
Idempotent — re-running skips files that exist (use --force to overwrite).
2. Mount the provider once
// app/providers.tsx (or app/layout.tsx if you don't have a Providers wrapper)
"use client";
import { IdentityUIProvider } from "@mailflix/identity-ui";
import "@mailflix/identity-ui/styles.css";
import { authConfig } from "../lib/auth-config";
export function Providers({ children }: { children: React.ReactNode }) {
return <IdentityUIProvider config={authConfig}>{children}</IdentityUIProvider>;
}3. Set env vars
IDENTITY_BASE_URL=https://identity.your-host.com
IDENTITY_TENANT_SLUG=mailflix4. Edit lib/auth-config.ts
Set your brand accent + methods + redirect target. The TODOs in the scaffolded file walk you through it.
Done. Visit /auth/login, /auth/signup, /auth/forgot, /auth/reset?token=….
URL surface
| URL | View / Endpoint |
|---|---|
| /auth or /auth/login | LoginForm |
| /auth/signup | SignupForm |
| /auth/forgot | ForgotForm (sends reset email) |
| /auth/reset?token=... | ResetForm (token from email) |
| POST /api/auth/login | identity /auth/email/login + sets session cookies |
| POST /api/auth/signup | identity /auth/email/signup + sets session cookies |
| POST /api/auth/logout | identity /auth/logout + clears session cookies |
| POST /api/auth/refresh | identity /auth/refresh + rotates session cookies |
| GET /api/auth/me | current user (from cookie) |
| POST /api/auth/forgot | identity /auth/email/forgot |
| POST /api/auth/reset | identity /auth/email/reset |
Both /auth/* and /api/auth/* are catch-alls — adding new flows in the package exposes them everywhere on the next pnpm install.
Auth methods
Toggle via the methods array in lib/auth-config.ts:
| Method | Description | Required client method |
|---|---|---|
| password | Email + password (always available) | (built-in) |
| oauth-google | "Continue with Google" button | startOAuth |
| oauth-apple | "Continue with Apple" button | startOAuth |
| magic-link | Email-only one-tap sign-in | signInWithMagicLink |
| wallet | Solana wallet sign-in | signInWithWallet |
password works without writing any extra code — the scaffold's auth-client.ts covers it. The other methods need extra AuthClient methods (uncomment the stubs in auth-client.ts and wire to your existing flows).
Handler config (server-side)
The handlerConfig exported from lib/auth-config.ts configures the API catch-all. Most fields default sensibly from env:
import type { HandlerConfig } from "@mailflix/identity-ui/next/handlers";
export const handlerConfig: HandlerConfig = {
// identityBaseUrl: defaults to process.env.IDENTITY_BASE_URL
// tenantSlug: defaults to process.env.IDENTITY_TENANT_SLUG ?? "mailflix"
// cookieNames: defaults to { access: "mf_id_access", refresh: "mf_id_refresh" }
// cookieDomain: unset = host-only cookie. Set to ".mailflix.com" for cross-subdomain.
// secure: defaults to NODE_ENV === "production"
// sameSite: defaults to "lax"
};Brand config
import type { BrandConfig } from "@mailflix/identity-ui";
export const brand: BrandConfig = {
name: "Mailflix",
accent: "#FF5B1F", // CSS color — applied to primary button + focus ring
logo: <YourBrandMark />, // Optional JSX — falls back to name if omitted
tagline: "Newsletters that ship.",
};Theming
Brand color flows through inline (no CSS-vars-on-root needed). For deeper theming, override these CSS variables in your stylesheet:
:root {
--siu-bg: #...;
--siu-surface: #...;
--siu-surface-2: #...;
--siu-line: ...;
--siu-line-strong: ...;
--siu-ink: #...;
--siu-ink-muted: #...;
--siu-danger: #...;
--siu-good: #...;
--siu-radius: 8px;
}Defaults are dark-mode only today.
Escape hatches
Custom page chrome (e.g. tenant picker before the form)
Skip the /next re-export. Import <AuthFlow> directly and write your own page:
"use client";
import { AuthFlow, useIdentityUI } from "@mailflix/identity-ui";
import { useRouter, useSearchParams } from "next/navigation";
export default function CustomAuthPage({ params }) {
const config = useIdentityUI();
const router = useRouter();
const search = useSearchParams();
return (
<YourCustomShell>
<YourPreFormGate />
<AuthFlow
segment={...}
resetToken={search.get("token") ?? undefined}
redirectAfter={search.get("next")}
methods={config.methods}
client={config.client}
brand={config.brand}
onAuthenticated={(result, to) => {
config.onAuthenticated?.(result);
router.push(to ?? "/");
}}
/>
</YourCustomShell>
);
}Per-endpoint API customisation (e.g. extra rate limit on /forgot)
Skip the catch-all. Use the per-endpoint factories:
import {
createLoginHandler,
createSignupHandler,
createForgotHandler,
resolveConfig,
} from "@mailflix/identity-ui/next/handlers";
const cfg = resolveConfig(handlerConfig);
export const POST = withRateLimit(createForgotHandler(cfg), { rps: 1 });Individual form components
LoginForm, SignupForm, ForgotForm, ResetForm, MagicLinkForm, OauthRow, WalletRow — all exported. Compose any layout you want.
Versioning
Semver. v0.x means breaking changes can land on minor bumps — pin to an exact version until 1.0.
| Version | Highlights |
|---|---|
| 0.1.0 | Initial: components only; consumer wrote a 30-line catch-all page. |
| 0.2.0 | Breaking. Added IdentityUIProvider + /next page subpath. Consumer becomes 1-line page + 1 provider wrap. |
| 0.3.0 | Breaking. Added /next/handlers sub-export + scaffold CLI. Consumer is now 1 init command + 1 config file. |
What's NOT in here yet
- TOTP / 2FA challenge UI (planned v0.4)
- Email verification confirm page (
/auth/verify?token=...) - OAuth callback handler (lives per-app since the redirect target differs)
Development
cd packages/identity-ui
pnpm install
pnpm typecheck
pnpm buildCross-repo consumption
Published publicly under the @mailflix scope on npmjs.org. Both solana-shuffle/* and mailflix/* consume via standard pnpm add. Bumps follow semver.
The package source lives in solana-shuffle/packages/identity-ui/ (next to the identity service whose contract it implements). The npm scope is @mailflix because the @solana-shuffle org doesn't exist on npm; nothing else hinges on the scope choice.
