npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

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

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() from hazo_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). Call set_* in beforeAll and reset_* in afterAll to 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 JSON 401/403 and the login redirect is now built from the request's own origin (fixes Failed to fetch on any non-default dev/prod port).

  • /api/hazo_auth/me now exposes legal_acceptance at the top level; /api/hazo_auth/health now returns a top-level ok boolean.

  • Browser autotest scenarios corrected for browser fetch semantics (opaque redirects, *_default.jpg images, admin-gated 401/403, current relationships body 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_tokens table (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 scopes
    • getGoogleToken(user_id, opts?) — Get a fresh access token (refreshes if expired)
    • revoke_google_oauth_token(user_id) — Permanently revoke stored token and forget scopes
    • get_google_token_status(user_id) — Check connection status, scopes, and expiry
  • Client HelperrequestGoogleScopes(scopes, opts?) from hazo_auth/client triggers 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_token and adds include_granted_scopes: true parameter for incremental scope flow
  • Route Handler Exports from hazo_auth/server/routes:
    • googleTokenGET — implements GET status endpoint
    • googleTokenDELETE — 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_core and hazo_config added to serverExternalPackages. Turbopack was bundling hazo_config and breaking ini.parse() CJS interop, causing every config-section read to throw and producing config_loader_read_section_failed ×9 + me_endpoint_error per request.
  • config/hazo_auth_config.ini: renamed [log.overrides][log_overrides]. The ini v4 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: HazoConfig instances 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.ts public_routes.
  • Auto-test runner register calls now include legal_accepted hashes 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.sql
  • 018_hazo_legal_acceptances.sql
  • 019_hazo_legal_doc_versions.sql

New exportsLegalAcceptanceGate, LegalDocDrawer, LegalDocCheckboxList, LegalDocCombinedView from hazo_auth/client; legalDocsGET, legalDocsAcceptPOST, legalDocsPublishPOST from hazo_auth/server/routes; LegalDoc, LegalAcceptanceRecord, LegalAcceptanceMap types from hazo_auth.

New dependencyreact-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.md

What'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_hashtoken_service.ts writes the argon2-hashed token, but the canonical schema only declared token. Added token_hash TEXT column and an index on it; relaxed the legacy plaintext token column to nullable. Runtime no longer writes plaintext, so this is purely additive for new installs.
  • Boolean-typed columnshazo_users.email_verified and the four flags on hazo_user_relationships (can_view_progress, can_edit_profile, can_delete, is_self) are now declared BOOLEAN instead of INTEGER. SQLite tolerates BOOLEAN as a NUMERIC affinity (existing 0/1 data evaluates correctly). PostgreSQL via PostgREST was previously rejecting the runtime's email_verified: false payload with 400 — 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 (additive ADD COLUMN); PostgreSQL ALTERs are commented blocks for consumers to opt in (with NOTIFY 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 validate now checks SQLite schema: required tables, TEXT ID types, hazo_user_scopes columns, admin permissions, and warns about v4 remnant tables
  • Permission constants - New HAZO_AUTH_PERMISSIONS and ALL_ADMIN_PERMISSIONS exports from both hazo_auth and hazo_auth/client for programmatic permission checks
  • CLI fix - CLI wrapper now sets --conditions react-server in NODE_OPTIONS, fixing "server-only" import errors when running npx hazo_auth validate, init-permissions, etc.
  • Silent permission fix - hazo_get_auth now applies String() normalization to role_id/permission_id comparisons, fixing empty permissions when SQLite returns INTEGER IDs
  • DB-generated IDs - init-permissions no longer generates UUIDs client-side; lets the database generate IDs (supports both TEXT UUID and INTEGER PK schemas)
  • Dev debug info - withAuth 403 responses and UserManagementLayout "Access Denied" view now include permission debug details in development mode
  • Import cleanup - Removed .js extensions 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 if cookie_prefix is not set in [hazo_auth__cookies] config section
  • Breaking: get_cookie_prefix_edge() throws if HAZO_AUTH_COOKIE_PREFIX env var is not set
  • Validation: npx hazo_auth validate now checks for cookie_prefix configuration
  • Init: .env.local.example template includes HAZO_AUTH_COOKIE_PREFIX as 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_firm for simple apps.
  • Clear SQLite errors - Missing sqlite_path in 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 init now creates the SQLite database with all required tables automatically. Also available standalone via npx hazo_auth init-db. Use npx hazo_auth schema to print the canonical SQL.
  • Auth page images - npx hazo_auth init now copies default login/register/forgot-password images to public/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 works

Key Changes:

  • New hazo_auth/server-lib entry point - All server-only exports (auth functions, services, config loaders) now here
  • Clean main entry - hazo_auth is now client-safe (components, types, utilities only)
  • Peer dependencies - hazo_config and hazo_connect are now peer dependencies (install in your app)
  • Fixed import path - Uses hazo_config/server (not deprecated hazo_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[] to string[] (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_scopes table 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-permissions for 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 thoroughly

See 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 HazoAuthProvider context
  • 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

# 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-tooltip

Note: 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 dev

That'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 commands

Dev-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 -- --all

Programmatic 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_logs

UI 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.css

Import 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.ini

2. Customize the configuration files:

  • Edit hazo_auth_config.ini to configure authentication settings, database connection, UI labels, and more
  • Edit hazo_notify_config.ini to configure email service settings (Zeptomail, SMTP, etc.)

3. Set up environment variables (recommended for sensitive data):

  • Create a .env.local file 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_secret

See 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.com

These 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.com

Important: 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_org and hazo_scopes_l1..l7 tables are gone. The unified hazo_scopes table replaces them with an arbitrary-depth parent_id hierarchy. The hazo_user_roles table is also gone — roles are now assigned per-scope on hazo_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-users

This script reads from hazo_auth_config.ini and:

  1. Creates default permissions from application_permission_list_defaults
  2. Creates a default_super_user_role role with all permissions
  3. 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.ts

Google 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

  1. Go to Google Cloud Console
  2. Create a project or select an existing project
  3. Enable Google+ API (or Google Identity Services)
  4. Navigate to CredentialsCreate CredentialsOAuth 2.0 Client ID
  5. Configure OAuth consent screen if prompted
  6. Set Application type to "Web application"
  7. Add Authorized JavaScript origins:
    • Development: http://localhost:3000
    • Production: https://yourdomain.com
  8. Add Authorized redirect URIs:
    • Development: http://localhost:3000/api/auth/callback/google
    • Production: https://yourdomain.com/api/auth/callback/google
  9. 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_1

Generate NEXTAUTH_SECRET:

openssl rand -base64 32

Step 3: Run Database Migration

Add OAuth fields to the hazo_users table:

npm run migrate migrations/005_add_oauth_fields_to_hazo_users.sql

This migration adds:

  • google_id - Google's unique user ID (TEXT, UNIQUE)
  • auth_providers - Tracks authentication methods: 'email', 'google', or 'email,google'
  • Index on google_id for 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 --oauth

Step 6: Test Google OAuth

  1. Start your dev server: npm run dev
  2. Visit http://localhost:3000/hazo_auth/login
  3. You should see the "Sign in with Google" button
  4. Click it and authenticate with your Google account
  5. 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 = false

Hide "Create account" link (e.g., OAuth-only apps with no email registration):

[hazo_auth__login_layout]
show_create_account_link = false

Hide 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 = true

API 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 = true in [hazo_auth__oauth] section
  • Check HAZO_AUTH_GOOGLE_CLIENT_ID and HAZO_AUTH_GOOGLE_CLIENT_SECRET are set
  • Check NEXTAUTH_URL matches 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_SECRET is set and at least 32 characters
  • Verify API routes are created: /api/auth/[...nextauth]/route.ts and /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/callback route exists
  • Check server logs for errors during session creation

OAuth redirect goes to localhost behind a reverse proxy (Cloudflare Tunnel, nginx):

  • Set NEXTAUTH_URL to 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=true is set in your environment
  • No next.config changes needed — the fix is built into hazo_auth/server/routes

404 after Google OAuth login (v5.1.16+ fix):

  • If users get 404 after Google OAuth, the hazo_invitations table may be missing
  • Option 1: Run migration 009_scope_consolidation.sql to create the table
  • Option 2: Set skip_invitation_check = true in [hazo_auth__oauth] if not using invitations
  • Check logs for invitation_table_missing warnings
  • If using custom paths, set create_firm_url to 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:

  1. Run the migration: npm run migrate -- migrations/021_hazo_google_oauth_tokens.sql
  2. Set encryption env vars:
    HAZO_AUTH_OAUTH_KEY_CURRENT=v1
    HAZO_AUTH_OAUTH_KEY_V1=$(openssl rand -base64 32)
  3. Install the optional peer: npm install hazo_secure
  4. Add hazo_secure to serverExternalPackages in next.config.mjs / next.config.js. The service loads encryption via a literal import("hazo_secure/crypto"); if the bundler inlines hazo_secure instead of treating it as an external import, Google sign-in fails with GoogleTokenStorageUnconfigured even 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/password
  • RegisterLayout - Registration form with password requirements and OAuth (Google Sign-In)
  • ForgotPasswordLayout - Request password reset
  • ResetPasswordLayout - Set new password with token
  • EmailVerificationLayout - Verify email address
  • MySettingsLayout - User profile and settings
  • UserManagementLayout - 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 tab
  • admin_role_management - Access to Roles tab
  • admin_permission_management - Access to Permissions tab
  • admin_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 organizations
  • hazo_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=tree query 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