hazo_auth
v10.4.1
Published
Zero-config authentication UI components for Next.js with RBAC, OAuth, scope-based multi-tenancy, and invitations
Downloads
3,359
Maintainers
Readme
hazo_auth - Authentication UI Component Package
A reusable authentication UI component package powered by Next.js, TailwindCSS, and shadcn. It integrates hazo_config for configuration management and hazo_connect for data access, enabling future components to stay aligned with platform conventions.
What's New in v10.2.0 🧪
Test-friendly hazo_connect injection + autotest/middleware fixes.
set_hazo_connect_instance(adapter)/reset_hazo_connect_instance()fromhazo_auth/server-lib— inject a pre-built adapter into both the hazo_auth singleton cache and the underlying hazo_connect singleton (companion to hazo_connect 3.6.0 FR-001). Callset_*inbeforeAllandreset_*inafterAllto swap in a test SQLite adapter without touching the production config:import { set_hazo_connect_instance, reset_hazo_connect_instance } from "hazo_auth/server-lib"; beforeAll(() => set_hazo_connect_instance(testAdapter)); afterAll(() => reset_hazo_connect_instance());Middleware no longer redirects
/api/requests to the login page — protected API routes return JSON401/403and the login redirect is now built from the request's own origin (fixesFailed to fetchon any non-default dev/prod port)./api/hazo_auth/menow exposeslegal_acceptanceat the top level;/api/hazo_auth/healthnow returns a top-levelokboolean.Browser autotest scenarios corrected for browser
fetchsemantics (opaque redirects,*_default.jpgimages, admin-gated401/403, currentrelationshipsbody shape).
Requires hazo_connect ^3.6.0.
What's New in v10.1.0 🚀
Incremental Google OAuth Scopes & Server-Side Token Access — Grant additional Google API scopes (Analytics, Sheets, Drive, etc.) without creating a separate OAuth app. Store encrypted tokens server-side and access them programmatically.
New Features:
hazo_google_oauth_tokenstable (migration 021) — Encrypted AES-256-GCM storage for Google OAuth tokens with scope tracking- Token Service Exports from
hazo_auth/server-lib:store_google_oauth_token(user_id, tokens, scopes)— Save OAuth tokens after user grants new scopesgetGoogleToken(user_id, opts?)— Get a fresh access token (refreshes if expired)revoke_google_oauth_token(user_id)— Permanently revoke stored token and forget scopesget_google_token_status(user_id)— Check connection status, scopes, and expiry
- Client Helper —
requestGoogleScopes(scopes, opts?)fromhazo_auth/clienttriggers Google consent prompt for incremental scopes - HTTP Routes:
GET /api/hazo_auth/google/token— Returns{ connected, scopes, expires_at }DELETE /api/hazo_auth/google/token— Revoke stored token without signing user out
- NextAuth Config Updates — Captures
refresh_tokenand addsinclude_granted_scopes: trueparameter for incremental scope flow - Route Handler Exports from
hazo_auth/server/routes:googleTokenGET— implements GET status endpointgoogleTokenDELETE— implements DELETE revoke endpoint
Setup:
# 1. Run the migration
npm run migrate -- migrations/021_hazo_google_oauth_tokens.sql
# 2. Set encryption environment variables
HAZO_AUTH_OAUTH_KEY_CURRENT=v1
HAZO_AUTH_OAUTH_KEY_V1=$(openssl rand -base64 32)
# 3. Install optional peer (for token encryption)
npm install hazo_secure// 4. Add hazo_secure to serverExternalPackages in next.config.mjs / next.config.js
// so the bundler leaves the literal import("hazo_secure/crypto") as a native
// external import. Without this, Google sign-in fails with
// GoogleTokenStorageUnconfigured even when hazo_secure is installed.
const nextConfig = {
serverExternalPackages: ["hazo_secure", /* ...existing entries */],
};Client Usage:
import { requestGoogleScopes } from "hazo_auth/client";
<button onClick={() => requestGoogleScopes([
"https://www.googleapis.com/auth/analytics.readonly"
])}>
Connect Google Analytics
</button>Server Usage:
import { getGoogleToken } from "hazo_auth/server-lib";
const result = await getGoogleToken(userId, {
scopes: ["https://www.googleapis.com/auth/analytics.readonly"]
});
if (result.ok) {
// use result.access_token to call Google APIs
}See Google API Access (Incremental Scopes) below for full details.
What's New in v9.1.1 🔧
Dev-server noise fixes for Next.js 16 + Turbopack
next.config.mjs:hazo_coreandhazo_configadded toserverExternalPackages. Turbopack was bundling hazo_config and breakingini.parse()CJS interop, causing every config-section read to throw and producingconfig_loader_read_section_failed×9 +me_endpoint_errorper request.config/hazo_auth_config.ini: renamed[log.overrides]→[log_overrides]. Theiniv4 library parses dots in section names as nesting separators, creating null-prototype objects that hazo_config cannot stringify. Dotted section names must be avoided.src/lib/config/config_loader.server.ts:HazoConfiginstances are now memoized per resolved file path — one INI parse per server process instead of one per getter call.- Companion fixes in [email protected] (registerSingleton moved to module level, hazo_debug probe → lazy import) and [email protected] (hazo_core/errors probe → lazy import, graceful skip of null-prototype nested objects in refresh()).
Action required if you use
[log.overrides]in your app's config: rename it to[log_overrides](or any non-dotted name). This applies to any hazo_config-backed INI file, not just hazo_auth.
What's New in v8.0.1 🔧
Auto-test and middleware bug fixes
- Fixed 307 redirects blocking OTP, strings, consent, and legal_docs API routes when hit without auth cookies — all four are now in
middleware.tspublic_routes. - Auto-test runner register calls now include
legal_acceptedhashes when legal docs are configured, fixing 24 test failures that cascaded from the initial registration step.
What's New in v8.0.0 ⚠️ BREAKING CHANGE
Legal Document Acceptance — opt-in, INI-configured. Add [hazo_auth__legal_docs] to hazo_auth_config.ini to require users to accept terms before registering. Each doc is a markdown file; hazo_auth hashes it, tracks acceptance history, and blocks register until all current hashes are accepted.
Breaking: Deprecated page-component wrappers removed (LoginPage, RegisterPage, ForgotPasswordPage, ResetPasswordPage, VerifyEmailPage from hazo_auth/page_components/*). Use *Layout components directly in a server component instead.
Run these migrations (in order) to upgrade an existing database:
017_legal_acceptance_column.sql018_hazo_legal_acceptances.sql019_hazo_legal_doc_versions.sql
New exports — LegalAcceptanceGate, LegalDocDrawer, LegalDocCheckboxList, LegalDocCombinedView from hazo_auth/client; legalDocsGET, legalDocsAcceptPOST, legalDocsPublishPOST from hazo_auth/server/routes; LegalDoc, LegalAcceptanceRecord, LegalAcceptanceMap types from hazo_auth.
New dependency — react-markdown (markdown rendering for legal doc drawers).
# config/hazo_auth_config.ini
[hazo_auth__legal_docs]
display_mode = separate # separate | combined
doc_1_key = tos
doc_1_title = Terms of Service
doc_1_path = legal/tos.md
doc_2_key = privacy
doc_2_title = Privacy Policy
doc_2_path = legal/privacy.mdWhat's New in v5.3.1 🔧
get_client_ip(request) exported from hazo_auth/server-lib — extracts the client IP from x-forwarded-for (first element), falling back to x-real-ip, then "unknown". Previously private to hazo_get_auth.server.ts. Useful for consumers that need consistent IP extraction across handlers (e.g., hazo_feedback audit logging).
import { get_client_ip } from "hazo_auth/server-lib";
export async function POST(request: NextRequest) {
const ip = get_client_ip(request);
// ...
}What's New in v5.1.39 🔧
Postgres-Compatibility Schema Alignment — fixes two long-standing drifts between the canonical SQLite schema and what the runtime actually writes. SQLite consumers see no behaviour change; Postgres + PostgREST consumers can now sign-up users without bespoke schema patches.
hazo_refresh_tokens.token_hash—token_service.tswrites the argon2-hashed token, but the canonical schema only declaredtoken. Addedtoken_hash TEXTcolumn and an index on it; relaxed the legacy plaintexttokencolumn to nullable. Runtime no longer writes plaintext, so this is purely additive for new installs.- Boolean-typed columns —
hazo_users.email_verifiedand the four flags onhazo_user_relationships(can_view_progress,can_edit_profile,can_delete,is_self) are now declaredBOOLEANinstead ofINTEGER. SQLite toleratesBOOLEANas a NUMERIC affinity (existing 0/1 data evaluates correctly). PostgreSQL via PostgREST was previously rejecting the runtime'semail_verified: falsepayload with400 — invalid input syntax for type integer: "false"; now it accepts the boolean cleanly. - Migration
014_align_schema_with_runtime.sql— applies the two changes above to existing deployments. SQLite version is active (additiveADD COLUMN); PostgreSQL ALTERs are commented blocks for consumers to opt in (withNOTIFY pgrst, 'reload schema';reminders).
Reported by Kinstripe (Postgres + PostgREST consumer) during sign-up smoke tests on 2026-04-28.
What's New in v5.1.28
Schema Validation, Permission Constants & DX Improvements
- Schema validation -
npx hazo_auth validatenow checks SQLite schema: required tables, TEXT ID types,hazo_user_scopescolumns, admin permissions, and warns about v4 remnant tables - Permission constants - New
HAZO_AUTH_PERMISSIONSandALL_ADMIN_PERMISSIONSexports from bothhazo_authandhazo_auth/clientfor programmatic permission checks - CLI fix - CLI wrapper now sets
--conditions react-serverin NODE_OPTIONS, fixing "server-only" import errors when runningnpx hazo_auth validate,init-permissions, etc. - Silent permission fix -
hazo_get_authnow appliesString()normalization to role_id/permission_id comparisons, fixing empty permissions when SQLite returns INTEGER IDs - DB-generated IDs -
init-permissionsno longer generates UUIDs client-side; lets the database generate IDs (supports both TEXT UUID and INTEGER PK schemas) - Dev debug info -
withAuth403 responses andUserManagementLayout"Access Denied" view now include permission debug details in development mode - Import cleanup - Removed
.jsextensions from internal imports for better TypeScript/bundler compatibility
What's New in v5.1.27
Mandatory Cookie Prefix - cookie_prefix is now required for all consuming apps.
- Breaking:
get_cookies_config()throws ifcookie_prefixis not set in[hazo_auth__cookies]config section - Breaking:
get_cookie_prefix_edge()throws ifHAZO_AUTH_COOKIE_PREFIXenv var is not set - Validation:
npx hazo_auth validatenow checks for cookie_prefix configuration - Init:
.env.local.exampletemplate includesHAZO_AUTH_COOKIE_PREFIXas required - Docs: shadcn/ui components are bundled — consumers do NOT need to install them separately
What's New in v5.1.26
Consumer Setup Improvements - Five fixes that improve the out-of-box experience for new consumers:
- Multi-tenancy bypass -
enable_multi_tenancy = false(the default) now correctly skips scope/invitation checks in OAuth and post-verification flows. No more redirect loops to/hazo_auth/create_firmfor simple apps. - Clear SQLite errors - Missing
sqlite_pathin config now throws a clear error instead of silently falling back to a test fixture database. Unrecognized config keys produce warnings with typo suggestions. - Auto-schema creation -
npx hazo_auth initnow creates the SQLite database with all required tables automatically. Also available standalone vianpx hazo_auth init-db. Usenpx hazo_auth schemato print the canonical SQL. - Auth page images -
npx hazo_auth initnow copies default login/register/forgot-password images topublic/hazo_auth/images/. - Graceful image fallback - Visual panels on auth pages fall back to a colored background instead of crashing when images are missing.
What's New in v5.2.0 ⚠️ BREAKING CHANGE
Server/Client Module Separation - Complete fix for "Module not found: Can't resolve 'fs'" errors.
Breaking Change - New Import Path:
// BEFORE (v5.1.x) - Server imports from main entry
import { hazo_get_auth, get_login_config } from "hazo_auth";
// AFTER (v5.2.0) - Server imports from dedicated entry point
import { hazo_get_auth, get_login_config } from "hazo_auth/server-lib";
// Client imports unchanged
import { ProfilePicMenu, use_auth_status } from "hazo_auth/client";
import { cn } from "hazo_auth"; // Still worksKey Changes:
- ✅ New
hazo_auth/server-libentry point - All server-only exports (auth functions, services, config loaders) now here - ✅ Clean main entry -
hazo_authis now client-safe (components, types, utilities only) - ✅ Peer dependencies -
hazo_configandhazo_connectare now peer dependencies (install in your app) - ✅ Fixed import path - Uses
hazo_config/server(not deprecatedhazo_config/dist/lib)
Required Migration:
# 1. Install peer dependencies
npm install hazo_config hazo_connect hazo_logs
# 2. Update imports in your server-side code
# Change: import { hazo_get_auth } from "hazo_auth"
# To: import { hazo_get_auth } from "hazo_auth/server-lib"What's New in v5.1.23 🔧
FIX: Server/Client Bundling Issue - Added import "server-only" guards to prevent accidental client bundling.
Key Changes:
- ✅ Server-Only Guards - Added
import "server-only"to all server files preventing accidental client bundling - ✅ hazo_logs v1.0.10 - Upgraded with conditional exports for browser/node environments
- ✅ Client Logging Support - Logs API route now exports POST for client-side log ingestion
What's New in v5.1.5 🐛
CRITICAL BUGFIX: Fixed incomplete migration from v4.x to v5.x - several files were still referencing the deprecated hazo_user_roles table instead of hazo_user_scopes. This release completes the scope-based role assignment architecture introduced in v5.0.
Key Fixes:
- ✅ hazo_get_auth - Now correctly fetches roles from
hazo_user_scopes - ✅ Role IDs - Changed from
number[]tostring[](UUIDs) throughout codebase - ✅ User Management - Updated for scope-based role assignments
- ✅ Cache System - Fixed type inconsistencies with UUID role IDs
If you're on v5.x and experiencing permission/role issues, upgrade to v5.1.5 immediately.
What's New in v5.0 🚀
BREAKING CHANGE: Scope-Based Multi-Tenancy - Complete architectural redesign for simpler, more flexible multi-tenancy!
- ✅ Unified Scope System - Single
hazo_scopestable replaces 8 separate tables (1 org + 7 scope levels) - ✅ Membership-Based - Users assigned to scopes via
hazo_user_scopes(not org_id on user record) - ✅ Invitation System - Built-in invitation flow for onboarding new users to existing scopes
- ✅ Create Firm Flow - New users create their own firm (scope) after email verification
- ✅ Post-Verification Routing - Smart routing after email verification: invitations → create firm → default redirect
- ✅ Unlimited Hierarchy - Flexible parent-child relationships, no fixed depth limit
- ✅ Simpler Architecture - Fewer tables, fewer joins, easier to understand
- ✅ New CLI Command -
npx hazo_auth init-permissionsfor flexible permission setup
Migrating from v4.x? This is a breaking change. Run the migration:
# 1. Backup your database first!
# 2. Run the scope consolidation migration
npm run migrate migrations/009_scope_consolidation.sql
# 3. Update configuration (remove org settings, add invitation/create firm settings)
# 4. Update code (remove org-related API calls and components)
# 5. Test thoroughlySee CHANGE_LOG.md for detailed migration guide, rationale, and breaking changes.
What's New in v2.0
Zero-Config Server Components - Authentication pages now work out-of-the-box with ZERO configuration required!
- ✅ True "Drop In and Use" - Pages initialize everything server-side, no loading state
- ✅ Better Performance - Smaller JS bundles, faster page loads, immediate rendering
- ✅ Flexible API Paths - Customize endpoints globally via
HazoAuthProvidercontext - ✅ Embeddable Components - MySettings and UserManagement adapt to any layout
- ✅ Sensible Defaults - INI files are now optional, defaults built-in
Also Includes (v1.6.6+)
- JWT Session Tokens for Edge-Compatible Authentication: Secure Edge Runtime authentication in Next.js proxy/middleware files. See Proxy/Middleware Authentication for details.
Table of Contents
- Installation
- Quick Start
- Configuration Setup
- Database Setup
- Google OAuth Setup
- Using Components
- Authentication Service
- Proxy/Middleware Authentication
- Profile Picture Menu Widget
- User Types (Optional Feature)
- User Profile Service
- Local Development
Installation
# Install hazo_auth and required peer dependencies
npm install hazo_auth hazo_core hazo_config hazo_connect hazo_logs next react react-dom next-auth
# UI peer dependencies (required if using hazo_auth UI components)
npm install hazo_ui lucide-react sonner next-themes @radix-ui/react-accordion @radix-ui/react-alert-dialog @radix-ui/react-avatar @radix-ui/react-checkbox @radix-ui/react-dialog @radix-ui/react-dropdown-menu @radix-ui/react-hover-card @radix-ui/react-label @radix-ui/react-select @radix-ui/react-separator @radix-ui/react-slot @radix-ui/react-switch @radix-ui/react-tabs @radix-ui/react-tooltipNote: next, react, react-dom, next-auth, and UI packages are peer dependencies. Your consuming project controls the versions, preventing duplication and type conflicts.
Next.js config: If using password features, add argon2 to serverExternalPackages in your next.config.mjs:
const nextConfig = {
serverExternalPackages: ['argon2'],
};Quick Start
The fastest way to get started is using the CLI commands:
# 1. Install the package and peer dependencies
npm install hazo_auth hazo_core hazo_config hazo_connect hazo_logs next react react-dom next-auth
# 2. Initialize your project (directories, config, database, images)
npx hazo_auth init
# 3. Configure cookie prefix (REQUIRED)
# Edit config/hazo_auth_config.ini and set a unique prefix:
# [hazo_auth__cookies]
# cookie_prefix = myapp_
# 4. Set up environment variables
cp .env.local.example .env.local
# Edit .env.local and set:
# HAZO_AUTH_COOKIE_PREFIX=myapp_ (MUST match cookie_prefix above)
# JWT_SECRET=<your-secret>
# ZEPTOMAIL_API_KEY=<your-key>
# 5. Initialize default permissions and roles
npx hazo_auth init-users
# 6. Generate API routes and pages
npx hazo_auth generate-routes --pages
# 7. Start your dev server
npm run devThat's it! Visit http://localhost:3000/hazo_auth/login to see the login page.
Tailwind v4 Setup (Required for Tailwind v4 Users)
If you're using Tailwind v4, add this to your globals.css AFTER the tailwindcss import:
@import "tailwindcss";
/* Required: Enable Tailwind to scan hazo_auth package classes */
@source "../node_modules/hazo_auth/dist";Important: Without this directive, Tailwind classes in hazo_auth components (hover states, colors, spacing) will not be compiled, resulting in broken styling.
CLI Commands
npx hazo_auth init # Initialize project (creates dirs, copies config)
npx hazo_auth generate-routes # Generate API routes only
npx hazo_auth generate-routes --pages # Generate API routes + pages
npx hazo_auth validate # Check your setup and configuration
npx hazo_auth --help # Show all commandsDev-only demo account seeder
Seed a login-ready test user (email pre-verified, assigned to the default
system scope with a role, no verification email sent) for local development.
This is physically incapable of running in production: it refuses unless the
resolved env (HAZO_ENV ?? NODE_ENV) is non-production and
HAZO_AUTH_ALLOW_DEMO_SEED=true is set.
# Create (member by default; --admin grants the global-admin role)
HAZO_AUTH_ALLOW_DEMO_SEED=true npx hazo_auth demo-create --admin [email protected]
# or via npm script
HAZO_AUTH_ALLOW_DEMO_SEED=true npm run demo:create -- --admin
# Delete (by email, prefix, or all __demo_* users)
HAZO_AUTH_ALLOW_DEMO_SEED=true npx hazo_auth demo-delete [email protected]
HAZO_AUTH_ALLOW_DEMO_SEED=true npm run demo:delete -- --allProgrammatic equivalents are exported from hazo_auth/server-lib:
create_demo_user, delete_demo_users, assert_demo_seed_allowed,
DemoSeedNotAllowed.
Using Zero-Config Page Components (v2.0+)
NEW in v2.0: All pages are now React Server Components that initialize everything on the server. No configuration, no loading state, no hassle!
// app/login/page.tsx - That's literally it!
import { LoginPage } from "hazo_auth/pages/login";
export default function Page() {
return <LoginPage />;
}What happens behind the scenes:
- ✅ Database connection initialized server-side via hazo_connect singleton
- ✅ Configuration loaded from hazo_auth_config.ini (or uses sensible defaults)
- ✅ All props automatically configured
- ✅ Navbar automatically rendered based on config (no manual wrapping needed)
- ✅ Page renders immediately - NO loading state!
Available zero-config pages:
| Page | Import | Description |
|------|--------|-------------|
| LoginPage | hazo_auth/pages/login | Login form with forgot password link |
| RegisterPage | hazo_auth/pages/register | Registration with password validation and OAuth support |
| ForgotPasswordPage | hazo_auth/pages/forgot_password | Request password reset email |
| ResetPasswordPage | hazo_auth/pages/reset_password | Set new password with token |
| VerifyEmailPage | hazo_auth/pages/verify_email | Email verification handler |
| MySettingsPage | hazo_auth/pages/my_settings | User profile and password change |
Example - Complete Auth Flow:
// app/login/page.tsx
import { LoginPage } from "hazo_auth/pages/login";
export default function Page() {
return <LoginPage />;
}
// app/register/page.tsx
import { RegisterPage } from "hazo_auth/pages/register";
export default function Page() {
return <RegisterPage />;
}
// app/settings/page.tsx
import { MySettingsPage } from "hazo_auth/pages/my_settings";
export default function Page() {
return <MySettingsPage />;
}Customizing Visual Appearance (Optional):
// All pages accept optional visual props
import { LoginPage } from "hazo_auth/pages/login";
export default function Page() {
return (
<LoginPage
image_src="/custom-login-image.jpg"
image_alt="My company logo"
image_background_color="#f0f0f0"
/>
);
}Embedding MySettings in Your Dashboard:
// MySettings is now container-agnostic!
import { MySettingsPage } from "hazo_auth/pages/my_settings";
export default function DashboardPage() {
return (
<DashboardLayout>
<Sidebar />
<main className="p-6">
<MySettingsPage className="max-w-4xl mx-auto" />
</main>
</DashboardLayout>
);
}Custom API Paths:
If you use custom API endpoints (not /api/hazo_auth/), wrap your app with HazoAuthProvider:
// app/layout.tsx
import { HazoAuthProvider } from "hazo_auth";
export default function RootLayout({ children }) {
return (
<html>
<body>
<HazoAuthProvider apiBasePath="/api/v1/auth">
{children}
</HazoAuthProvider>
</body>
</html>
);
}Manual Setup (Advanced)
If you prefer manual control, you can use the layout components directly:
// Import layout components
import { LoginLayout } from "hazo_auth/components/layouts/login";
import { RegisterLayout } from "hazo_auth/components/layouts/register";
import { ForgotPasswordLayout } from "hazo_auth/components/layouts/forgot_password";
import { ResetPasswordLayout } from "hazo_auth/components/layouts/reset_password";
import { EmailVerificationLayout } from "hazo_auth/components/layouts/email_verification";
import { MySettingsLayout } from "hazo_auth/components/layouts/my_settings";
import { UserManagementLayout } from "hazo_auth/components/layouts/user_management";
// Import shared components and hooks from barrel export
import {
ProfilePicMenu,
ProfilePicMenuWrapper,
ProfileStamp,
use_hazo_auth,
use_auth_status
} from "hazo_auth/components/layouts/shared";
// Import server-side utilities
import { hazo_get_auth } from "hazo_auth/lib/auth/hazo_get_auth.server";Required Dependencies
Peer Dependencies (Required):
npm install hazo_core hazo_config hazo_connect hazo_logsUI Components: All shadcn/ui components are bundled with hazo_auth. You do NOT need to install them separately.
Toast Notifications: Add the Toaster component to your app layout:
// app/layout.tsx
import { Toaster } from "sonner";
export default function RootLayout({ children }) {
return (
<html>
<body>
{children}
<Toaster />
</body>
</html>
);
}Client vs Server Imports
hazo_auth provides separate entry points for client and server code to avoid bundling Node.js modules in the browser:
Client Components
For client components (browser-safe, no Node.js dependencies):
// Use hazo_auth/client for client components
import {
ProfilePicMenu,
ProfileStamp,
use_auth_status,
use_hazo_auth,
cn
} from "hazo_auth/client";Server Components / API Routes
For server-side code (API routes, Server Components):
// Use hazo_auth/server-lib for server-side code
import { hazo_get_auth, get_config_value } from "hazo_auth/server-lib";
import { hazo_get_user_profiles } from "hazo_auth/server-lib";Why This Matters
Server-only code (Node.js APIs like fs, path, database access) must be kept separate from client bundles. The hazo_auth/server-lib entry point:
- Contains all server-only exports (auth functions, services, config loaders)
- Includes
import "server-only"guard that throws build errors if imported in client code - Uses peer dependencies (
hazo_config,hazo_connect,hazo_logs) from your app
If you accidentally import from hazo_auth/server-lib in a client component, you'll get a helpful build error instead of a cryptic "Can't resolve 'fs'" message.
Dark Mode / Theming
hazo_auth supports dark mode via CSS custom properties. To enable dark mode:
1. Import the theme CSS
Copy the variables file to your project:
cp node_modules/hazo_auth/src/styles/hazo-auth-variables.css ./app/hazo-auth-theme.cssImport in your globals.css:
@import "./hazo-auth-theme.css";2. CSS Variables Reference
You can customize the theme by overriding these variables:
:root {
/* Backgrounds */
--hazo-bg-subtle: #f8fafc; /* Light background */
--hazo-bg-muted: #f1f5f9; /* Slightly darker background */
/* Text */
--hazo-text-primary: #0f172a; /* Primary text */
--hazo-text-secondary: #334155; /* Secondary text */
--hazo-text-muted: #64748b; /* Muted/subtle text */
/* Borders */
--hazo-border: #e2e8f0; /* Standard border */
}
.dark {
/* Dark mode overrides */
--hazo-bg-subtle: #18181b;
--hazo-bg-muted: #27272a;
--hazo-text-primary: #fafafa;
--hazo-text-secondary: #d4d4d8;
--hazo-text-muted: #a1a1aa;
--hazo-border: #3f3f46;
}The dark class is typically added by next-themes or similar theme providers.
Configuration Setup
After installing the package, you need to set up configuration files in your project root:
1. Copy the example config files to your project root:
cp node_modules/hazo_auth/hazo_auth_config.example.ini ./hazo_auth_config.ini
cp node_modules/hazo_auth/hazo_notify_config.example.ini ./hazo_notify_config.ini2. Customize the configuration files:
- Edit
hazo_auth_config.inito configure authentication settings, database connection, UI labels, and more - Edit
hazo_notify_config.inito configure email service settings (Zeptomail, SMTP, etc.)
3. Set up environment variables (recommended for sensitive data):
- Create a
.env.localfile in your project root - Add
ZEPTOMAIL_API_KEY=your_api_key_here(if using Zeptomail) - Add
JWT_SECRET=your_secure_random_string_at_least_32_characters(required for JWT session tokens) - Add other sensitive configuration values as needed
Note: JWT_SECRET is required for JWT session token functionality (used for Edge-compatible proxy/middleware authentication). Generate a secure random string at least 32 characters long.
For Google OAuth (optional):
# NextAuth.js configuration (required for OAuth)
NEXTAUTH_SECRET=your_secure_random_string_at_least_32_characters
NEXTAUTH_URL=http://localhost:3000 # Change to production URL in production
# Google OAuth credentials (from Google Cloud Console)
HAZO_AUTH_GOOGLE_CLIENT_ID=your_google_client_id
HAZO_AUTH_GOOGLE_CLIENT_SECRET=your_google_client_secretSee Google OAuth Setup for detailed instructions.
For Cookie Customization (optional):
# Cookie prefix (prevents conflicts when running multiple apps on localhost)
HAZO_AUTH_COOKIE_PREFIX=myapp_
# Cookie domain (optional, for cross-subdomain sharing)
HAZO_AUTH_COOKIE_DOMAIN=.example.comThese environment variables are required for Edge Runtime (middleware) when using cookie customization. Also set in hazo_auth_config.ini:
[hazo_auth__cookies]
cookie_prefix = myapp_
cookie_domain = .example.comImportant: The configuration files must be located in your project root directory (where process.cwd() points to), not inside node_modules. The package reads configuration from process.cwd() at runtime, so storing them elsewhere (including node_modules/hazo_auth) will break runtime access.
Database Setup
Before using hazo_auth, you need to create the required database tables. The package supports both PostgreSQL (for production) and SQLite (for local development/testing).
The v5.x schema consists of 9 tables:
| Table | Purpose |
|-------|---------|
| hazo_users | User accounts and profile data |
| hazo_refresh_tokens | Refresh, password-reset, and email-verification tokens |
| hazo_roles | Role definitions (e.g. super_user, firm_admin) |
| hazo_permissions | Permission definitions |
| hazo_role_permissions | Role → permission assignments (composite PK) |
| hazo_scopes | Unified hierarchical multi-tenancy with firm branding |
| hazo_user_scopes | User → scope membership with scope-specific role (replaces hazo_user_roles) |
| hazo_invitations | Invitations to onboard new users into existing scopes |
| hazo_user_relationships | Managed sub-profile parent/child links (shared-device support) |
Removed in v5.0: the legacy
hazo_organdhazo_scopes_l1..l7tables are gone. The unifiedhazo_scopestable replaces them with an arbitrary-depthparent_idhierarchy. Thehazo_user_rolestable is also gone — roles are now assigned per-scope onhazo_user_scopes.role_id.
Quickest path: use the CLI
For SQLite development databases, the canonical schema (in src/lib/schema/sqlite_schema.ts) ships with the package and can be applied via the CLI:
npx hazo_auth init-db # Create/recreate the SQLite database with full schema
npx hazo_auth schema # Print the canonical schema SQL (does not modify DB)For PostgreSQL or PostgREST deployments, run the script below.
PostgreSQL Setup
-- ============================================================
-- hazo_auth canonical PostgreSQL schema (v5.x)
-- ============================================================
SET search_path TO public;
-- 1. Enum types
CREATE TYPE hazo_enum_profile_source_enum AS ENUM ('gravatar', 'custom', 'predefined');
CREATE TYPE hazo_enum_user_status AS ENUM ('PENDING', 'ACTIVE', 'BLOCKED');
CREATE TYPE hazo_enum_user_scope_status_type AS ENUM ('INVITED', 'ACTIVE', 'SUSPENDED', 'DEPARTED');
CREATE TYPE hazo_enum_invitation_status AS ENUM ('PENDING', 'ACCEPTED', 'EXPIRED', 'REVOKED');
-- 2. Users
CREATE TABLE hazo_users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email_address TEXT NOT NULL UNIQUE,
password_hash TEXT, -- NULL for OAuth-only users
name TEXT,
email_verified BOOLEAN NOT NULL DEFAULT FALSE,
login_attempts INTEGER NOT NULL DEFAULT 0,
last_logon TIMESTAMP WITH TIME ZONE,
profile_picture_url TEXT,
profile_source hazo_enum_profile_source_enum,
mfa_secret TEXT,
url_on_logon TEXT, -- per-user post-login redirect
google_id TEXT UNIQUE, -- Google OAuth ID
auth_providers TEXT DEFAULT 'email', -- 'email', 'google', or 'email,google'
user_type TEXT, -- optional categorisation
app_user_data JSONB, -- consumer-app JSON blob
status hazo_enum_user_status NOT NULL DEFAULT 'ACTIVE',
managed_by_user_id UUID REFERENCES hazo_users(id) ON DELETE SET NULL,
pin_hash TEXT, -- simple PIN auth on shared devices
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
changed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_hazo_users_email ON hazo_users(email_address);
CREATE INDEX idx_hazo_users_status ON hazo_users(status);
CREATE INDEX idx_hazo_users_user_type ON hazo_users(user_type);
CREATE UNIQUE INDEX idx_hazo_users_google_id ON hazo_users(google_id);
CREATE INDEX idx_hazo_users_managed_by ON hazo_users(managed_by_user_id);
-- 3. Refresh tokens (also used for password reset / email verification)
CREATE TABLE hazo_refresh_tokens (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES hazo_users(id) ON DELETE CASCADE,
token_hash TEXT NOT NULL,
token_type TEXT NOT NULL DEFAULT 'refresh',
expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_hazo_refresh_tokens_user_id ON hazo_refresh_tokens(user_id);
CREATE INDEX idx_hazo_refresh_tokens_token_type ON hazo_refresh_tokens(token_type);
-- 4. Roles
CREATE TABLE hazo_roles (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
role_name TEXT NOT NULL UNIQUE,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
changed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
);
-- 5. Permissions
CREATE TABLE hazo_permissions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
permission_name TEXT NOT NULL UNIQUE,
description TEXT,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
changed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
);
-- 6. Role-permission assignments (composite PK, no id column)
CREATE TABLE hazo_role_permissions (
role_id UUID NOT NULL REFERENCES hazo_roles(id) ON DELETE CASCADE,
permission_id UUID NOT NULL REFERENCES hazo_permissions(id) ON DELETE CASCADE,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
PRIMARY KEY (role_id, permission_id)
);
-- 7. Unified scope hierarchy (firms, divisions, departments, ... with branding)
CREATE TABLE hazo_scopes (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
parent_id UUID REFERENCES hazo_scopes(id) ON DELETE CASCADE,
name TEXT NOT NULL,
level TEXT NOT NULL, -- descriptive label e.g. 'HQ', 'Division'
logo_url TEXT,
primary_color TEXT,
secondary_color TEXT,
tagline TEXT,
slug TEXT, -- URL-friendly identifier
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
changed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_hazo_scopes_parent ON hazo_scopes(parent_id);
CREATE INDEX idx_hazo_scopes_level ON hazo_scopes(level);
CREATE INDEX idx_hazo_scopes_slug ON hazo_scopes(slug);
-- 7a. Reserved system scopes
-- Note: Super Admin scope retired in v10; use hazo_org_global_admin permission instead (see migration 020)
INSERT INTO hazo_scopes (id, parent_id, name, level)
VALUES ('00000000-0000-0000-0000-000000000001', NULL, 'System', 'default')
ON CONFLICT (id) DO NOTHING;
-- 8. User-scope membership (replaces v4.x hazo_user_roles)
CREATE TABLE hazo_user_scopes (
user_id UUID NOT NULL REFERENCES hazo_users(id) ON DELETE CASCADE,
scope_id UUID NOT NULL REFERENCES hazo_scopes(id) ON DELETE CASCADE,
root_scope_id UUID NOT NULL REFERENCES hazo_scopes(id) ON DELETE CASCADE,
role_id UUID NOT NULL REFERENCES hazo_roles(id) ON DELETE CASCADE,
status hazo_enum_user_scope_status_type NOT NULL DEFAULT 'ACTIVE',
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
changed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
PRIMARY KEY (user_id, scope_id)
);
CREATE INDEX idx_hazo_user_scopes_scope ON hazo_user_scopes(scope_id);
CREATE INDEX idx_hazo_user_scopes_root ON hazo_user_scopes(root_scope_id);
CREATE INDEX idx_hazo_user_scopes_role ON hazo_user_scopes(role_id);
-- 9. Invitations
CREATE TABLE hazo_invitations (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email_address TEXT NOT NULL,
token TEXT NOT NULL UNIQUE,
scope_id UUID NOT NULL REFERENCES hazo_scopes(id) ON DELETE CASCADE,
root_scope_id UUID NOT NULL REFERENCES hazo_scopes(id) ON DELETE CASCADE,
role_id UUID NOT NULL REFERENCES hazo_roles(id) ON DELETE CASCADE,
invited_by UUID REFERENCES hazo_users(id) ON DELETE SET NULL,
status hazo_enum_invitation_status NOT NULL DEFAULT 'PENDING',
expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
accepted_at TIMESTAMP WITH TIME ZONE,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
changed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_hazo_invitations_email ON hazo_invitations(email_address);
CREATE INDEX idx_hazo_invitations_token ON hazo_invitations(token);
CREATE INDEX idx_hazo_invitations_scope ON hazo_invitations(scope_id);
CREATE INDEX idx_hazo_invitations_status ON hazo_invitations(status);
CREATE INDEX idx_hazo_invitations_expires ON hazo_invitations(expires_at);
-- 10. Managed sub-profile relationships (parent/child accounts on shared devices)
CREATE TABLE hazo_user_relationships (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
parent_user_id UUID NOT NULL REFERENCES hazo_users(id) ON DELETE CASCADE,
child_user_id UUID NOT NULL REFERENCES hazo_users(id) ON DELETE CASCADE,
relationship_type TEXT NOT NULL DEFAULT 'parent',
can_view_progress BOOLEAN NOT NULL DEFAULT TRUE,
can_edit_profile BOOLEAN NOT NULL DEFAULT TRUE,
can_delete BOOLEAN NOT NULL DEFAULT FALSE,
is_self BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
UNIQUE (parent_user_id, child_user_id)
);
CREATE INDEX idx_hazo_user_relationships_parent ON hazo_user_relationships(parent_user_id);
CREATE INDEX idx_hazo_user_relationships_child ON hazo_user_relationships(child_user_id);
-- 11. Built-in firm_admin role (used when a user creates their first firm)
INSERT INTO hazo_roles (id, role_name)
VALUES (gen_random_uuid(), 'firm_admin')
ON CONFLICT (role_name) DO NOTHING;SQLite Setup (for local development)
The SQLite version of the schema. Equivalent to running npx hazo_auth init-db and identical to src/lib/schema/sqlite_schema.ts.
-- ============================================================
-- hazo_auth canonical SQLite schema (v5.x)
-- ============================================================
-- Users
CREATE TABLE IF NOT EXISTS hazo_users (
id TEXT PRIMARY KEY,
email_address TEXT NOT NULL UNIQUE,
password_hash TEXT,
name TEXT,
email_verified INTEGER DEFAULT 0,
login_attempts INTEGER DEFAULT 0,
last_logon TEXT,
profile_picture_url TEXT,
profile_source TEXT CHECK(profile_source IN ('gravatar', 'custom', 'predefined')),
mfa_secret TEXT,
url_on_logon TEXT,
google_id TEXT UNIQUE,
auth_providers TEXT DEFAULT 'email',
user_type TEXT,
app_user_data TEXT,
status TEXT DEFAULT 'ACTIVE' CHECK(status IN ('PENDING', 'ACTIVE', 'BLOCKED')),
managed_by_user_id TEXT REFERENCES hazo_users(id) ON DELETE SET NULL,
pin_hash TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
changed_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX IF NOT EXISTS idx_hazo_users_email ON hazo_users(email_address);
CREATE INDEX IF NOT EXISTS idx_hazo_users_google_id ON hazo_users(google_id);
CREATE INDEX IF NOT EXISTS idx_hazo_users_status ON hazo_users(status);
CREATE INDEX IF NOT EXISTS idx_hazo_users_managed_by ON hazo_users(managed_by_user_id);
-- Refresh tokens
CREATE TABLE IF NOT EXISTS hazo_refresh_tokens (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL REFERENCES hazo_users(id) ON DELETE CASCADE,
token TEXT NOT NULL UNIQUE,
token_type TEXT DEFAULT 'refresh',
expires_at TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX IF NOT EXISTS idx_hazo_refresh_tokens_user ON hazo_refresh_tokens(user_id);
CREATE INDEX IF NOT EXISTS idx_hazo_refresh_tokens_token ON hazo_refresh_tokens(token);
-- Roles
CREATE TABLE IF NOT EXISTS hazo_roles (
id TEXT PRIMARY KEY,
role_name TEXT NOT NULL UNIQUE,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
changed_at TEXT NOT NULL DEFAULT (datetime('now'))
);
-- Permissions
CREATE TABLE IF NOT EXISTS hazo_permissions (
id TEXT PRIMARY KEY,
permission_name TEXT NOT NULL UNIQUE,
description TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
changed_at TEXT NOT NULL DEFAULT (datetime('now'))
);
-- Role-permission assignments (composite PK, no id column)
CREATE TABLE IF NOT EXISTS hazo_role_permissions (
role_id TEXT NOT NULL REFERENCES hazo_roles(id) ON DELETE CASCADE,
permission_id TEXT NOT NULL REFERENCES hazo_permissions(id) ON DELETE CASCADE,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
PRIMARY KEY (role_id, permission_id)
);
-- Unified scope hierarchy (firms, divisions, departments, ... with branding)
CREATE TABLE IF NOT EXISTS hazo_scopes (
id TEXT PRIMARY KEY,
parent_id TEXT REFERENCES hazo_scopes(id) ON DELETE CASCADE,
name TEXT NOT NULL,
level TEXT NOT NULL,
logo_url TEXT,
primary_color TEXT,
secondary_color TEXT,
tagline TEXT,
slug TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
changed_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX IF NOT EXISTS idx_hazo_scopes_parent ON hazo_scopes(parent_id);
CREATE INDEX IF NOT EXISTS idx_hazo_scopes_level ON hazo_scopes(level);
CREATE INDEX IF NOT EXISTS idx_hazo_scopes_slug ON hazo_scopes(slug);
-- Reserved system scopes
-- Note: Super Admin scope retired in v10; use hazo_org_global_admin permission instead (see migration 020)
INSERT OR IGNORE INTO hazo_scopes (id, parent_id, name, level, created_at, changed_at)
VALUES ('00000000-0000-0000-0000-000000000001', NULL, 'System', 'default', datetime('now'), datetime('now'));
-- User-scope membership (replaces v4.x hazo_user_roles)
CREATE TABLE IF NOT EXISTS hazo_user_scopes (
user_id TEXT NOT NULL REFERENCES hazo_users(id) ON DELETE CASCADE,
scope_id TEXT NOT NULL REFERENCES hazo_scopes(id) ON DELETE CASCADE,
root_scope_id TEXT NOT NULL REFERENCES hazo_scopes(id) ON DELETE CASCADE,
role_id TEXT NOT NULL REFERENCES hazo_roles(id) ON DELETE CASCADE,
status TEXT DEFAULT 'ACTIVE' CHECK (status IN ('INVITED', 'ACTIVE', 'SUSPENDED', 'DEPARTED')),
created_at TEXT NOT NULL DEFAULT (datetime('now')),
changed_at TEXT NOT NULL DEFAULT (datetime('now')),
PRIMARY KEY (user_id, scope_id)
);
CREATE INDEX IF NOT EXISTS idx_hazo_user_scopes_scope ON hazo_user_scopes(scope_id);
CREATE INDEX IF NOT EXISTS idx_hazo_user_scopes_root ON hazo_user_scopes(root_scope_id);
CREATE INDEX IF NOT EXISTS idx_hazo_user_scopes_role ON hazo_user_scopes(role_id);
-- Invitations
CREATE TABLE IF NOT EXISTS hazo_invitations (
id TEXT PRIMARY KEY,
email_address TEXT NOT NULL,
token TEXT NOT NULL UNIQUE,
scope_id TEXT NOT NULL REFERENCES hazo_scopes(id) ON DELETE CASCADE,
root_scope_id TEXT NOT NULL REFERENCES hazo_scopes(id) ON DELETE CASCADE,
role_id TEXT NOT NULL REFERENCES hazo_roles(id) ON DELETE CASCADE,
invited_by TEXT REFERENCES hazo_users(id) ON DELETE SET NULL,
status TEXT NOT NULL DEFAULT 'PENDING' CHECK(status IN ('PENDING', 'ACCEPTED', 'EXPIRED', 'REVOKED')),
expires_at TEXT NOT NULL,
accepted_at TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
changed_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX IF NOT EXISTS idx_hazo_invitations_email ON hazo_invitations(email_address);
CREATE INDEX IF NOT EXISTS idx_hazo_invitations_token ON hazo_invitations(token);
CREATE INDEX IF NOT EXISTS idx_hazo_invitations_scope ON hazo_invitations(scope_id);
CREATE INDEX IF NOT EXISTS idx_hazo_invitations_status ON hazo_invitations(status);
CREATE INDEX IF NOT EXISTS idx_hazo_invitations_expires ON hazo_invitations(expires_at);
-- Managed sub-profile relationships
CREATE TABLE IF NOT EXISTS hazo_user_relationships (
id TEXT PRIMARY KEY,
parent_user_id TEXT NOT NULL REFERENCES hazo_users(id) ON DELETE CASCADE,
child_user_id TEXT NOT NULL REFERENCES hazo_users(id) ON DELETE CASCADE,
relationship_type TEXT NOT NULL DEFAULT 'parent',
can_view_progress INTEGER DEFAULT 1,
can_edit_profile INTEGER DEFAULT 1,
can_delete INTEGER DEFAULT 0,
is_self INTEGER DEFAULT 0,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
UNIQUE(parent_user_id, child_user_id)
);
CREATE INDEX IF NOT EXISTS idx_hazo_user_relationships_parent ON hazo_user_relationships(parent_user_id);
CREATE INDEX IF NOT EXISTS idx_hazo_user_relationships_child ON hazo_user_relationships(child_user_id);
-- Built-in firm_admin role (used when a user creates their first firm)
INSERT OR IGNORE INTO hazo_roles (id, role_name, created_at, changed_at)
VALUES (
lower(hex(randomblob(4)) || '-' || hex(randomblob(2)) || '-4' || substr(hex(randomblob(2)),2) || '-' || substr('89ab',abs(random()) % 4 + 1, 1) || substr(hex(randomblob(2)),2) || '-' || hex(randomblob(6))),
'firm_admin',
datetime('now'),
datetime('now')
);Initialize Default Permissions and Super User
After creating the tables, you can use the init-users script to set up default permissions and a super user:
npm run init-usersThis script reads from hazo_auth_config.ini and:
- Creates default permissions from
application_permission_list_defaults - Creates a
default_super_user_rolerole with all permissions - Assigns the role to the user specified in
default_super_user_email
Apply Migrations
To apply database migrations (e.g., adding new fields):
# Apply a specific migration
npx tsx scripts/apply_migration.ts migrations/003_add_url_on_logon_to_hazo_users.sql
# Or apply all pending migrations
npx tsx scripts/apply_migration.tsGoogle OAuth Setup
hazo_auth supports Google Sign-In via NextAuth.js v4, allowing users to authenticate with their Google accounts.
Features
- Dual authentication: Users can have BOTH Google OAuth and email/password login
- Auto-linking: Automatically links Google login to existing unverified email/password accounts
- Graceful degradation: Login and register pages adapt based on enabled authentication methods
- Set password feature: Google-only users can add a password later via My Settings
- Profile data: Full name and profile picture automatically populated from Google
Step 1: Get Google OAuth Credentials
- Go to Google Cloud Console
- Create a project or select an existing project
- Enable Google+ API (or Google Identity Services)
- Navigate to Credentials → Create Credentials → OAuth 2.0 Client ID
- Configure OAuth consent screen if prompted
- Set Application type to "Web application"
- Add Authorized JavaScript origins:
- Development:
http://localhost:3000 - Production:
https://yourdomain.com
- Development:
- Add Authorized redirect URIs:
- Development:
http://localhost:3000/api/auth/callback/google - Production:
https://yourdomain.com/api/auth/callback/google
- Development:
- Copy the Client ID and Client Secret
Step 2: Add Environment Variables
Add to your .env.local:
# NextAuth.js configuration (REQUIRED for OAuth)
NEXTAUTH_SECRET=your_secure_random_string_at_least_32_characters
NEXTAUTH_URL=http://localhost:3000 # Change to production URL in production
# Google OAuth credentials (from Google Cloud Console)
HAZO_AUTH_GOOGLE_CLIENT_ID=your_google_client_id_from_step_1
HAZO_AUTH_GOOGLE_CLIENT_SECRET=your_google_client_secret_from_step_1Generate NEXTAUTH_SECRET:
openssl rand -base64 32Step 3: Run Database Migration
Add OAuth fields to the hazo_users table:
npm run migrate migrations/005_add_oauth_fields_to_hazo_users.sqlThis migration adds:
google_id- Google's unique user ID (TEXT, UNIQUE)auth_providers- Tracks authentication methods: 'email', 'google', or 'email,google'- Index on
google_idfor fast OAuth lookups
Manual migration (if needed):
PostgreSQL:
ALTER TABLE hazo_users
ADD COLUMN google_id TEXT UNIQUE;
ALTER TABLE hazo_users
ADD COLUMN auth_providers TEXT DEFAULT 'email';
CREATE INDEX IF NOT EXISTS idx_hazo_users_google_id ON hazo_users(google_id);SQLite:
ALTER TABLE hazo_users
ADD COLUMN google_id TEXT;
ALTER TABLE hazo_users
ADD COLUMN auth_providers TEXT DEFAULT 'email';
CREATE UNIQUE INDEX IF NOT EXISTS idx_hazo_users_google_id_unique ON hazo_users(google_id);
CREATE INDEX IF NOT EXISTS idx_hazo_users_google_id ON hazo_users(google_id);Step 4: Configure OAuth in hazo_auth_config.ini
[hazo_auth__oauth]
# Enable Google OAuth login (default: true)
enable_google = true
# Enable traditional email/password login (default: true)
enable_email_password = true
# Auto-link Google login to existing unverified email/password accounts (default: true)
auto_link_unverified_accounts = true
# Customize button text (optional)
google_button_text = Continue with Google
google_button_text_register = Sign up with Google
oauth_divider_text = or
# Post-Login Redirect Configuration (v5.1.16+)
# URL for users who need to create a firm (default: /hazo_auth/create_firm)
# create_firm_url = /hazo_auth/create_firm
# Default redirect after OAuth login for users with scopes (default: /)
# default_redirect = /
# Skip invitation table check (set true if not using invitations)
# skip_invitation_check = false
# Redirect when skip_invitation_check=true and user has no scope (default: /)
# no_scope_redirect = /Step 5: Create NextAuth API Routes
Create app/api/auth/[...nextauth]/route.ts:
export { GET, POST } from "hazo_auth/server/routes/nextauth";Create app/api/hazo_auth/oauth/google/callback/route.ts:
export { GET } from "hazo_auth/server/routes/oauth_google_callback";Create app/api/hazo_auth/set_password/route.ts:
export { POST } from "hazo_auth/server/routes/set_password";Or use the CLI generator:
npx hazo_auth generate-routes --oauthStep 6: Test Google OAuth
- Start your dev server:
npm run dev - Visit
http://localhost:3000/hazo_auth/login - You should see the "Sign in with Google" button
- Click it and authenticate with your Google account
- You'll be redirected back and logged in
User Flows
New User - Google Sign-In:
- User clicks "Sign in with Google"
- Authenticates with Google
- Account created with Google profile data (email, name, profile picture)
- Email is automatically verified
- User can log in with Google anytime
Existing Unverified User - Google Sign-In:
- User has email/password account but hasn't verified email
- Clicks "Sign in with Google" with same email
- System auto-links Google account (if
auto_link_unverified_accounts = true) - Email becomes verified
- User can now log in with EITHER Google OR email/password
Google-Only User Adds Password:
- Google-only user visits My Settings
- "Set Password" section appears
- User sets a password
- User can now log in with EITHER method
Google-Only User Tries Forgot Password:
- User registered with Google tries "Forgot Password"
- System shows: "You registered with Google. Please sign in with Google instead."
Configuration Options
Disable email/password login (Google-only):
[hazo_auth__oauth]
enable_google = true
enable_email_password = falseHide "Create account" link (e.g., OAuth-only apps with no email registration):
[hazo_auth__login_layout]
show_create_account_link = falseHide links by setting path/label to empty (v5.1.30+):
[hazo_auth__login_layout]
; Hide "Forgot password?" link
forgot_password_path =
forgot_password_label =
; Hide "Create account" link (alternative to show_create_account_link = false)
create_account_path =
create_account_label =Disable Google OAuth (email/password only):
[hazo_auth__oauth]
enable_google = false
enable_email_password = trueAPI Response Changes
The /api/hazo_auth/me endpoint now includes OAuth status:
{
authenticated: true,
// ... existing fields
auth_providers: "email,google", // NEW: Tracks authentication methods
has_password: true, // NEW: Whether user has password set
google_connected: true, // NEW: Whether Google account is linked
}Dependencies
Google OAuth adds one new dependency:
next-auth@^4.24.11- NextAuth.js for OAuth handling (automatically installed with hazo_auth)
Troubleshooting
"Sign in with Google" button not showing:
- Verify
enable_google = truein[hazo_auth__oauth]section - Check
HAZO_AUTH_GOOGLE_CLIENT_IDandHAZO_AUTH_GOOGLE_CLIENT_SECRETare set - Check
NEXTAUTH_URLmatches your current URL
OAuth callback error:
- OAuth errors (e.g.,
AccessDenied,OAuthSignin) are automatically displayed as a banner on the login page via?error=query param - Verify redirect URI in Google Cloud Console matches exactly:
http://localhost:3000/api/auth/callback/google - Check
NEXTAUTH_SECRETis set and at least 32 characters - Verify API routes are created:
/api/auth/[...nextauth]/route.tsand/api/hazo_auth/oauth/google/callback/route.ts
User created but not logged in:
- Check browser console for errors
- Verify
/api/hazo_auth/oauth/google/callbackroute exists - Check server logs for errors during session creation
OAuth redirect goes to localhost behind a reverse proxy (Cloudflare Tunnel, nginx):
- Set
NEXTAUTH_URLto your public domain (e.g.,https://gotimer.org) - hazo_auth automatically rewrites the request URL so NextAuth constructs the correct
redirect_uri - Verify
AUTH_TRUST_HOST=trueis set in your environment - No
next.configchanges needed — the fix is built intohazo_auth/server/routes
404 after Google OAuth login (v5.1.16+ fix):
- If users get 404 after Google OAuth, the
hazo_invitationstable may be missing - Option 1: Run migration
009_scope_consolidation.sqlto create the table - Option 2: Set
skip_invitation_check = truein[hazo_auth__oauth]if not using invitations - Check logs for
invitation_table_missingwarnings - If using custom paths, set
create_firm_urlto your app's create firm page URL
Google API Access (Incremental Scopes)
hazo_auth v10.1+ supports granting additional Google API scopes (Analytics, Sheets, etc.) without a separate OAuth app.
Setup:
- Run the migration:
npm run migrate -- migrations/021_hazo_google_oauth_tokens.sql - Set encryption env vars:
HAZO_AUTH_OAUTH_KEY_CURRENT=v1 HAZO_AUTH_OAUTH_KEY_V1=$(openssl rand -base64 32) - Install the optional peer:
npm install hazo_secure - Add
hazo_securetoserverExternalPackagesinnext.config.mjs/next.config.js. The service loads encryption via a literalimport("hazo_secure/crypto"); if the bundler inlineshazo_secureinstead of treating it as an external import, Google sign-in fails withGoogleTokenStorageUnconfiguredeven when the peer is installed and keys are set.
Usage:
// Client component — triggers Google consent for extra scopes
import { requestGoogleScopes } from "hazo_auth/client";
<button onClick={() => requestGoogleScopes([
"https://www.googleapis.com/auth/analytics.readonly"
])}>
Connect Analytics
</button>// Server action / API route — get a fresh access token
import { getGoogleToken } from "hazo_auth/server-lib";
const result = await getGoogleToken(userId, {
scopes: ["https://www.googleapis.com/auth/analytics.readonly"]
});
if (result.ok) {
// use result.access_token to call Google APIs
}The incremental scope token is stored and revoked independently of the user's sign-in session. DELETE /api/hazo_auth/google/token removes the stored token without signing the user out.
Status endpoint: GET /api/hazo_auth/google/token returns { connected, scopes, expires_at }.
Using Components
Package Exports
The package exports components through these paths:
// Main entry point - exports all public APIs
import { ... } from "hazo_auth";
// Zero-config page components (recommended for quick setup)
import { LoginPage } from "hazo_auth/pages/login";
import { RegisterPage } from "hazo_auth/pages/register";
import { ForgotPasswordPage } from "hazo_auth/pages/forgot_password";
import { ResetPasswordPage } from "hazo_auth/pages/reset_password";
import { VerifyEmailPage } from "hazo_auth/pages/verify_email";
import { MySettingsPage } from "hazo_auth/pages/my_settings";
// Or import all pages at once
import {
LoginPage,
RegisterPage,
ForgotPasswordPage,
ResetPasswordPage,
VerifyEmailPage,
MySettingsPage
} from "hazo_auth/pages";
// Layout components - for custom implementations
import { LoginLayout } from "hazo_auth/components/layouts/login";
import { RegisterLayout } from "hazo_auth/components/layouts/register";
import { ForgotPasswordLayout } from "hazo_auth/components/layouts/forgot_password";
import { ResetPasswordLayout } from "hazo_auth/components/layouts/reset_password";
import { EmailVerificationLayout } from "hazo_auth/components/layouts/email_verification";
import { MySettingsLayout } from "hazo_auth/components/layouts/my_settings";
import { UserManagementLayout } from "hazo_auth/components/layouts/user_management";
import { RbacTestLayout } from "hazo_auth/components/layouts/rbac_test";
// Shared layout components and hooks (barrel import - recommended)
import {
ProfilePicMenu,
ProfilePicMenuWrapper,
FormActionButtons,
use_hazo_auth,
use_auth_status
} from "hazo_auth/components/layouts/shared";
// Server-side authentication utility
import { hazo_get_auth } from "hazo_auth/lib/auth/hazo_get_auth.server";
// Server utilities
import { ... } from "hazo_auth/server";
// Edge-compatible proxy/middleware authentication (v1.6.6+)
import { validate_session_cookie } from "hazo_auth/server/middleware";Note: The package uses relative imports internally. Consumers should only import from the exposed entry points listed above. Do not import from internal paths like hazo_auth/components/ui/* - these are internal modules.
Using Layout Components
Prefer to drop the forms into your own routes without using the pre-built pages? Import the layouts directly and feed them a data_client plus any label/button overrides:
// app/(auth)/login/page.tsx in your project
import { LoginLayout, createLayoutDataClient } from "hazo_auth";
import { create_postgrest_hazo_connect } from "hazo_auth/lib/hazo_connect_setup";
export default async function LoginPage() {
const hazoConnect = create_postgrest_hazo_connect();
const dataClient = createLayoutDataClient(hazoConnect);
return (
<div className="my-app-shell">
<LoginLayout
image_src="/marketing/login-hero.svg"
image_alt="Login hero image"
data_client={dataClient}
redirectRoute="/dashboard"
/>
</div>
);
}Available Layout Components:
LoginLayout- Login form with email/passwordRegisterLayout- Registration form with password requirements and OAuth (Google Sign-In)ForgotPasswordLayout- Request password resetResetPasswordLayout- Set new password with tokenEmailVerificationLayout- Verify email addressMySettingsLayout- User profile and settingsUserManagementLayout- Admin user/role management (requires user_management API routes)RbacTestLayout- RBAC/HRBAC permission and scope testing tool (requires admin_test_access permission)
User Management Component
The UserManagementLayout component provides a comprehensive admin interface for managing users, roles, and permissions. It requires the user_management API routes to be set up in your project.
Required Permissions:
admin_user_management- Access to Users tabadmin_role_management- Access to Roles tabadmin_permission_management- Access to Permissions tabadmin_scope_hierarchy_management- Access to Scope Hierarchy tab (HRBAC)admin_system- Access to Scope Labels tab (HRBAC)admin_user_scope_assignment- Access to User Scopes tab (HRBAC)
Required API Routes:
The UserManagementLayout component requires the following API routes to be created in your project:
// app/api/hazo_auth/user_management/users/route.ts
export { GET, PATCH, POST } from "hazo_auth/server/routes";
// app/api/hazo_auth/user_management/permissions/route.ts
export { GET, POST, PUT, DELETE } from "hazo_auth/server/routes";
// app/api/hazo_auth/user_management/roles/route.ts
export { GET, POST, PUT } from "hazo_auth/server/routes";
// app/api/hazo_auth/user_management/users/roles/route.ts
export { GET, POST, PUT } from "hazo_auth/server/routes";Note: These routes are automatically created when you run npx hazo_auth generate-routes. The routes handle:
- Users: List users, deactivate users, send password reset emails
- Permissions: List permissions (from DB and config), migrate config permissions to DB, create/update/delete permissions
- Roles: List roles with permissions, create roles, update role-permission assignments
- UI Enhancement: The Roles tab uses a tag-based UI for better readability. Each role displays permissions as inline tags/chips (showing up to 4, with "+N more" to expand). Edit permissions via an interactive dialog with Select All/Unselect All buttons.
- User Roles: Get user roles, assign roles to users, bulk update user role assignments
Organization Management Component
The OrgManagementLayout component provides an admin interface for managing the organization hierarchy when multi-tenancy is enabled. It requires the org_management API routes to be set up in your project.
Required Permissions:
hazo_perm_org_management- CRUD operations on organizationshazo_org_global_admin- View/manage all organizations across the system (optional, for global admins)
Required API Routes:
The OrgManagementLayout component requires the following API route to be created in your project:
// app/api/hazo_auth/org_management/orgs/route.ts
export {
orgManagementOrgsGET as GET,
orgManagementOrgsPOST as POST,
orgManagementOrgsPATCH as PATCH,
orgManagementOrgsDELETE as DELETE
} from "hazo_auth/server/routes";Note: This route is automatically created when you run npx hazo_auth generate-routes. The route handles:
- GET: List organizations (with
action=treequery parameter for hierarchical tree structure) - POST: Create new organization
- PATCH: Update existing organization (name, user_limit, active status)
- DELETE: Soft delete organization (sets active=false, does not remove from database)
Example Usage:
// app/hazo_auth/user_management/page.tsx
import { UserManagementLayout } from "hazo_auth/components/layouts/user_management";
export default function UserManagem