hazo_auth 5.1.37 → 5.1.38

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +261 -258
  2. package/SETUP_CHECKLIST.md +314 -148
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -547,347 +547,355 @@ cookie_domain = .example.com
547
547
 
548
548
  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).
549
549
 
550
- ### PostgreSQL Setup
550
+ The v5.x schema consists of **9 tables**:
551
551
 
552
- Run the following SQL scripts in your PostgreSQL database:
552
+ | Table | Purpose |
553
+ |-------|---------|
554
+ | `hazo_users` | User accounts and profile data |
555
+ | `hazo_refresh_tokens` | Refresh, password-reset, and email-verification tokens |
556
+ | `hazo_roles` | Role definitions (e.g. `super_user`, `firm_admin`) |
557
+ | `hazo_permissions` | Permission definitions |
558
+ | `hazo_role_permissions` | Role → permission assignments (composite PK) |
559
+ | `hazo_scopes` | Unified hierarchical multi-tenancy with firm branding |
560
+ | `hazo_user_scopes` | User → scope membership with scope-specific role (replaces `hazo_user_roles`) |
561
+ | `hazo_invitations` | Invitations to onboard new users into existing scopes |
562
+ | `hazo_user_relationships` | Managed sub-profile parent/child links (shared-device support) |
553
563
 
554
- #### 1. Create the Profile Source Enum Type
564
+ > **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`.
555
565
 
556
- ```sql
557
- -- Enum type for profile picture source
558
- CREATE TYPE hazo_enum_profile_source_enum AS ENUM ('gravatar', 'custom', 'predefined');
566
+ ### Quickest path: use the CLI
559
567
 
560
- -- Note: hazo_enum_scope_types was removed in v5.0
561
- -- The unified hazo_scopes table uses a TEXT "level" column instead
568
+ 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:
569
+
570
+ ```bash
571
+ npx hazo_auth init-db # Create/recreate the SQLite database with full schema
572
+ npx hazo_auth schema # Print the canonical schema SQL (does not modify DB)
562
573
  ```
563
574
 
564
- #### 2. Create the Organization Table (Multi-Tenancy)
575
+ For PostgreSQL or PostgREST deployments, run the script below.
576
+
577
+ ### PostgreSQL Setup
565
578
 
566
579
  ```sql
567
- -- Organization table for multi-tenancy (create before hazo_users)
568
- CREATE TABLE hazo_org (
569
- id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
570
- name TEXT NOT NULL,
571
- parent_org_id UUID REFERENCES hazo_org(id) ON DELETE SET NULL,
572
- root_org_id UUID REFERENCES hazo_org(id) ON DELETE SET NULL,
573
- user_limit INTEGER NOT NULL DEFAULT 0,
574
- active BOOLEAN NOT NULL DEFAULT TRUE,
575
- created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
576
- created_by UUID, -- Will reference hazo_users after it's created
577
- changed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
578
- changed_by UUID
579
- );
580
+ -- ============================================================
581
+ -- hazo_auth canonical PostgreSQL schema (v5.x)
582
+ -- ============================================================
580
583
 
581
- CREATE INDEX idx_hazo_org_parent_org_id ON hazo_org(parent_org_id);
582
- CREATE INDEX idx_hazo_org_root_org_id ON hazo_org(root_org_id);
583
- CREATE INDEX idx_hazo_org_active ON hazo_org(active);
584
- ```
584
+ SET search_path TO public;
585
585
 
586
- #### 3. Create the Users Table
586
+ -- 1. Enum types
587
+ CREATE TYPE hazo_enum_profile_source_enum AS ENUM ('gravatar', 'custom', 'predefined');
588
+ CREATE TYPE hazo_enum_user_status AS ENUM ('PENDING', 'ACTIVE', 'BLOCKED');
589
+ CREATE TYPE hazo_enum_user_scope_status_type AS ENUM ('INVITED', 'ACTIVE', 'SUSPENDED', 'DEPARTED');
590
+ CREATE TYPE hazo_enum_invitation_status AS ENUM ('PENDING', 'ACCEPTED', 'EXPIRED', 'REVOKED');
587
591
 
588
- ```sql
589
- -- Main users table
592
+ -- 2. Users
590
593
  CREATE TABLE hazo_users (
591
594
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
592
595
  email_address TEXT NOT NULL UNIQUE,
593
- password_hash TEXT, -- NULL for OAuth-only users
596
+ password_hash TEXT, -- NULL for OAuth-only users
594
597
  name TEXT,
595
598
  email_verified BOOLEAN NOT NULL DEFAULT FALSE,
596
- is_active BOOLEAN NOT NULL DEFAULT TRUE,
597
599
  login_attempts INTEGER NOT NULL DEFAULT 0,
598
600
  last_logon TIMESTAMP WITH TIME ZONE,
599
601
  profile_picture_url TEXT,
600
602
  profile_source hazo_enum_profile_source_enum,
601
603
  mfa_secret TEXT,
602
- url_on_logon TEXT,
603
- user_type TEXT, -- Optional user categorization
604
- google_id TEXT UNIQUE, -- Google OAuth ID
605
- auth_providers TEXT DEFAULT 'email', -- 'email', 'google', or 'email,google'
606
- org_id UUID REFERENCES hazo_org(id) ON DELETE SET NULL,
607
- root_org_id UUID REFERENCES hazo_org(id) ON DELETE SET NULL,
604
+ url_on_logon TEXT, -- per-user post-login redirect
605
+ google_id TEXT UNIQUE, -- Google OAuth ID
606
+ auth_providers TEXT DEFAULT 'email', -- 'email', 'google', or 'email,google'
607
+ user_type TEXT, -- optional categorisation
608
+ app_user_data JSONB, -- consumer-app JSON blob
609
+ status hazo_enum_user_status NOT NULL DEFAULT 'ACTIVE',
610
+ managed_by_user_id UUID REFERENCES hazo_users(id) ON DELETE SET NULL,
611
+ pin_hash TEXT, -- simple PIN auth on shared devices
608
612
  created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
609
613
  changed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
610
614
  );
611
-
612
- -- Indexes
613
615
  CREATE INDEX idx_hazo_users_email ON hazo_users(email_address);
616
+ CREATE INDEX idx_hazo_users_status ON hazo_users(status);
614
617
  CREATE INDEX idx_hazo_users_user_type ON hazo_users(user_type);
615
618
  CREATE UNIQUE INDEX idx_hazo_users_google_id ON hazo_users(google_id);
616
- CREATE INDEX idx_hazo_users_org_id ON hazo_users(org_id);
617
- CREATE INDEX idx_hazo_users_root_org_id ON hazo_users(root_org_id);
618
-
619
- -- Add FK constraints to hazo_org after hazo_users exists
620
- ALTER TABLE hazo_org ADD CONSTRAINT fk_hazo_org_created_by
621
- FOREIGN KEY (created_by) REFERENCES hazo_users(id) ON DELETE SET NULL;
622
- ALTER TABLE hazo_org ADD CONSTRAINT fk_hazo_org_changed_by
623
- FOREIGN KEY (changed_by) REFERENCES hazo_users(id) ON DELETE SET NULL;
624
- ```
625
-
626
- **Note:** The `url_on_logon` field is used to store a custom redirect URL for users after successful login. This allows per-user customization of post-login navigation.
627
-
628
- #### 4. Create the Refresh Tokens Table
619
+ CREATE INDEX idx_hazo_users_managed_by ON hazo_users(managed_by_user_id);
629
620
 
630
- ```sql
631
- -- Refresh tokens table (used for password reset, email verification, etc.)
621
+ -- 3. Refresh tokens (also used for password reset / email verification)
632
622
  CREATE TABLE hazo_refresh_tokens (
633
623
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
634
624
  user_id UUID NOT NULL REFERENCES hazo_users(id) ON DELETE CASCADE,
635
625
  token_hash TEXT NOT NULL,
636
- token_type TEXT NOT NULL,
626
+ token_type TEXT NOT NULL DEFAULT 'refresh',
637
627
  expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
638
628
  created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
639
629
  );
640
-
641
- -- Index for token lookups
642
630
  CREATE INDEX idx_hazo_refresh_tokens_user_id ON hazo_refresh_tokens(user_id);
643
631
  CREATE INDEX idx_hazo_refresh_tokens_token_type ON hazo_refresh_tokens(token_type);
644
- ```
645
-
646
- #### 5. Create the Permissions Table
647
632
 
648
- ```sql
649
- -- Permissions table for RBAC
650
- CREATE TABLE hazo_permissions (
633
+ -- 4. Roles
634
+ CREATE TABLE hazo_roles (
651
635
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
652
- permission_name TEXT NOT NULL UNIQUE,
653
- description TEXT,
636
+ role_name TEXT NOT NULL UNIQUE,
654
637
  created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
655
638
  changed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
656
639
  );
657
- ```
658
-
659
- #### 6. Create the Roles Table
660
640
 
661
- ```sql
662
- -- Roles table for RBAC
663
- CREATE TABLE hazo_roles (
641
+ -- 5. Permissions
642
+ CREATE TABLE hazo_permissions (
664
643
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
665
- role_name TEXT NOT NULL UNIQUE,
644
+ permission_name TEXT NOT NULL UNIQUE,
645
+ description TEXT,
666
646
  created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
667
647
  changed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
668
648
  );
669
- ```
670
649
 
671
- #### 7. Create the Role-Permissions Junction Table
672
-
673
- ```sql
674
- -- Junction table linking roles to permissions
650
+ -- 6. Role-permission assignments (composite PK, no id column)
675
651
  CREATE TABLE hazo_role_permissions (
676
652
  role_id UUID NOT NULL REFERENCES hazo_roles(id) ON DELETE CASCADE,
677
653
  permission_id UUID NOT NULL REFERENCES hazo_permissions(id) ON DELETE CASCADE,
678
654
  created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
679
- changed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
680
655
  PRIMARY KEY (role_id, permission_id)
681
656
  );
682
657
 
683
- -- Indexes for lookups
684
- CREATE INDEX idx_hazo_role_permissions_role_id ON hazo_role_permissions(role_id);
685
- CREATE INDEX idx_hazo_role_permissions_permission_id ON hazo_role_permissions(permission_id);
686
- ```
687
-
688
- #### 8. Create the User-Roles Junction Table
689
-
690
- ```sql
691
- -- Junction table linking users to roles
692
- CREATE TABLE hazo_user_roles (
693
- user_id UUID NOT NULL REFERENCES hazo_users(id) ON DELETE CASCADE,
694
- role_id UUID NOT NULL REFERENCES hazo_roles(id) ON DELETE CASCADE,
695
- created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
696
- changed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
697
- PRIMARY KEY (user_id, role_id)
698
- );
699
-
700
- -- Indexes for lookups
701
- CREATE INDEX idx_hazo_user_roles_user_id ON hazo_user_roles(user_id);
702
- CREATE INDEX idx_hazo_user_roles_role_id ON hazo_user_roles(role_id);
703
- ```
704
-
705
- ### Complete PostgreSQL Setup Script
706
-
707
- For convenience, here's the complete SQL script to create all tables at once:
708
-
709
- ```sql
710
- -- ============================================
711
- -- hazo_auth Database Setup Script (PostgreSQL)
712
- -- ============================================
713
-
714
- -- 1. Create enum types
715
- CREATE TYPE hazo_enum_profile_source_enum AS ENUM ('gravatar', 'custom', 'predefined');
716
- -- Note: hazo_enum_scope_types was removed in v5.0 (uses unified hazo_scopes table)
717
-
718
- -- 2. Create organization table (multi-tenancy)
719
- CREATE TABLE hazo_org (
658
+ -- 7. Unified scope hierarchy (firms, divisions, departments, ... with branding)
659
+ CREATE TABLE hazo_scopes (
720
660
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
661
+ parent_id UUID REFERENCES hazo_scopes(id) ON DELETE CASCADE,
721
662
  name TEXT NOT NULL,
722
- parent_org_id UUID REFERENCES hazo_org(id) ON DELETE SET NULL,
723
- root_org_id UUID REFERENCES hazo_org(id) ON DELETE SET NULL,
724
- user_limit INTEGER NOT NULL DEFAULT 0,
725
- active BOOLEAN NOT NULL DEFAULT TRUE,
726
- created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
727
- created_by UUID,
728
- changed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
729
- changed_by UUID
730
- );
731
- CREATE INDEX idx_hazo_org_parent_org_id ON hazo_org(parent_org_id);
732
- CREATE INDEX idx_hazo_org_root_org_id ON hazo_org(root_org_id);
733
- CREATE INDEX idx_hazo_org_active ON hazo_org(active);
734
-
735
- -- 3. Create users table
736
- CREATE TABLE hazo_users (
737
- id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
738
- email_address TEXT NOT NULL UNIQUE,
739
- password_hash TEXT,
740
- name TEXT,
741
- email_verified BOOLEAN NOT NULL DEFAULT FALSE,
742
- is_active BOOLEAN NOT NULL DEFAULT TRUE,
743
- login_attempts INTEGER NOT NULL DEFAULT 0,
744
- last_logon TIMESTAMP WITH TIME ZONE,
745
- profile_picture_url TEXT,
746
- profile_source hazo_enum_profile_source_enum,
747
- mfa_secret TEXT,
748
- url_on_logon TEXT,
749
- user_type TEXT,
750
- google_id TEXT UNIQUE,
751
- auth_providers TEXT DEFAULT 'email',
752
- org_id UUID REFERENCES hazo_org(id) ON DELETE SET NULL,
753
- root_org_id UUID REFERENCES hazo_org(id) ON DELETE SET NULL,
663
+ level TEXT NOT NULL, -- descriptive label e.g. 'HQ', 'Division'
664
+ logo_url TEXT,
665
+ primary_color TEXT,
666
+ secondary_color TEXT,
667
+ tagline TEXT,
668
+ slug TEXT, -- URL-friendly identifier
754
669
  created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
755
670
  changed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
756
671
  );
757
- CREATE INDEX idx_hazo_users_email ON hazo_users(email_address);
758
- CREATE INDEX idx_hazo_users_user_type ON hazo_users(user_type);
759
- CREATE UNIQUE INDEX idx_hazo_users_google_id ON hazo_users(google_id);
760
- CREATE INDEX idx_hazo_users_org_id ON hazo_users(org_id);
761
- CREATE INDEX idx_hazo_users_root_org_id ON hazo_users(root_org_id);
762
-
763
- -- Add FK constraints to hazo_org after hazo_users exists
764
- ALTER TABLE hazo_org ADD CONSTRAINT fk_hazo_org_created_by
765
- FOREIGN KEY (created_by) REFERENCES hazo_users(id) ON DELETE SET NULL;
766
- ALTER TABLE hazo_org ADD CONSTRAINT fk_hazo_org_changed_by
767
- FOREIGN KEY (changed_by) REFERENCES hazo_users(id) ON DELETE SET NULL;
768
-
769
- -- 4. Create refresh tokens table
770
- CREATE TABLE hazo_refresh_tokens (
771
- id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
672
+ CREATE INDEX idx_hazo_scopes_parent ON hazo_scopes(parent_id);
673
+ CREATE INDEX idx_hazo_scopes_level ON hazo_scopes(level);
674
+ CREATE INDEX idx_hazo_scopes_slug ON hazo_scopes(slug);
675
+
676
+ -- 7a. Reserved system scopes
677
+ INSERT INTO hazo_scopes (id, parent_id, name, level)
678
+ VALUES ('00000000-0000-0000-0000-000000000000', NULL, 'Super Admin', 'system')
679
+ ON CONFLICT (id) DO NOTHING;
680
+ INSERT INTO hazo_scopes (id, parent_id, name, level)
681
+ VALUES ('00000000-0000-0000-0000-000000000001', NULL, 'System', 'default')
682
+ ON CONFLICT (id) DO NOTHING;
683
+
684
+ -- 8. User-scope membership (replaces v4.x hazo_user_roles)
685
+ CREATE TABLE hazo_user_scopes (
772
686
  user_id UUID NOT NULL REFERENCES hazo_users(id) ON DELETE CASCADE,
773
- token_hash TEXT NOT NULL,
774
- token_type TEXT NOT NULL,
775
- expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
776
- created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
777
- );
778
- CREATE INDEX idx_hazo_refresh_tokens_user_id ON hazo_refresh_tokens(user_id);
779
- CREATE INDEX idx_hazo_refresh_tokens_token_type ON hazo_refresh_tokens(token_type);
780
-
781
- -- 4. Create permissions table
782
- CREATE TABLE hazo_permissions (
783
- id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
784
- permission_name TEXT NOT NULL UNIQUE,
785
- description TEXT,
687
+ scope_id UUID NOT NULL REFERENCES hazo_scopes(id) ON DELETE CASCADE,
688
+ root_scope_id UUID NOT NULL REFERENCES hazo_scopes(id) ON DELETE CASCADE,
689
+ role_id UUID NOT NULL REFERENCES hazo_roles(id) ON DELETE CASCADE,
690
+ status hazo_enum_user_scope_status_type NOT NULL DEFAULT 'ACTIVE',
786
691
  created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
787
- changed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
692
+ changed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
693
+ PRIMARY KEY (user_id, scope_id)
788
694
  );
695
+ CREATE INDEX idx_hazo_user_scopes_scope ON hazo_user_scopes(scope_id);
696
+ CREATE INDEX idx_hazo_user_scopes_root ON hazo_user_scopes(root_scope_id);
697
+ CREATE INDEX idx_hazo_user_scopes_role ON hazo_user_scopes(role_id);
789
698
 
790
- -- 5. Create roles table
791
- CREATE TABLE hazo_roles (
699
+ -- 9. Invitations
700
+ CREATE TABLE hazo_invitations (
792
701
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
793
- role_name TEXT NOT NULL UNIQUE,
702
+ email_address TEXT NOT NULL,
703
+ token TEXT NOT NULL UNIQUE,
704
+ scope_id UUID NOT NULL REFERENCES hazo_scopes(id) ON DELETE CASCADE,
705
+ root_scope_id UUID NOT NULL REFERENCES hazo_scopes(id) ON DELETE CASCADE,
706
+ role_id UUID NOT NULL REFERENCES hazo_roles(id) ON DELETE CASCADE,
707
+ invited_by UUID REFERENCES hazo_users(id) ON DELETE SET NULL,
708
+ status hazo_enum_invitation_status NOT NULL DEFAULT 'PENDING',
709
+ expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
710
+ accepted_at TIMESTAMP WITH TIME ZONE,
794
711
  created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
795
712
  changed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
796
713
  );
797
-
798
- -- 6. Create role-permissions junction table
799
- CREATE TABLE hazo_role_permissions (
800
- role_id UUID NOT NULL REFERENCES hazo_roles(id) ON DELETE CASCADE,
801
- permission_id UUID NOT NULL REFERENCES hazo_permissions(id) ON DELETE CASCADE,
714
+ CREATE INDEX idx_hazo_invitations_email ON hazo_invitations(email_address);
715
+ CREATE INDEX idx_hazo_invitations_token ON hazo_invitations(token);
716
+ CREATE INDEX idx_hazo_invitations_scope ON hazo_invitations(scope_id);
717
+ CREATE INDEX idx_hazo_invitations_status ON hazo_invitations(status);
718
+ CREATE INDEX idx_hazo_invitations_expires ON hazo_invitations(expires_at);
719
+
720
+ -- 10. Managed sub-profile relationships (parent/child accounts on shared devices)
721
+ CREATE TABLE hazo_user_relationships (
722
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
723
+ parent_user_id UUID NOT NULL REFERENCES hazo_users(id) ON DELETE CASCADE,
724
+ child_user_id UUID NOT NULL REFERENCES hazo_users(id) ON DELETE CASCADE,
725
+ relationship_type TEXT NOT NULL DEFAULT 'parent',
726
+ can_view_progress BOOLEAN NOT NULL DEFAULT TRUE,
727
+ can_edit_profile BOOLEAN NOT NULL DEFAULT TRUE,
728
+ can_delete BOOLEAN NOT NULL DEFAULT FALSE,
729
+ is_self BOOLEAN NOT NULL DEFAULT FALSE,
802
730
  created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
803
- changed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
804
- PRIMARY KEY (role_id, permission_id)
731
+ UNIQUE (parent_user_id, child_user_id)
805
732
  );
806
- CREATE INDEX idx_hazo_role_permissions_role_id ON hazo_role_permissions(role_id);
807
- CREATE INDEX idx_hazo_role_permissions_permission_id ON hazo_role_permissions(permission_id);
733
+ CREATE INDEX idx_hazo_user_relationships_parent ON hazo_user_relationships(parent_user_id);
734
+ CREATE INDEX idx_hazo_user_relationships_child ON hazo_user_relationships(child_user_id);
808
735
 
809
- -- 7. Create user-roles junction table
810
- CREATE TABLE hazo_user_roles (
811
- user_id UUID NOT NULL REFERENCES hazo_users(id) ON DELETE CASCADE,
812
- role_id UUID NOT NULL REFERENCES hazo_roles(id) ON DELETE CASCADE,
813
- created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
814
- changed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
815
- PRIMARY KEY (user_id, role_id)
816
- );
817
- CREATE INDEX idx_hazo_user_roles_user_id ON hazo_user_roles(user_id);
818
- CREATE INDEX idx_hazo_user_roles_role_id ON hazo_user_roles(role_id);
736
+ -- 11. Built-in firm_admin role (used when a user creates their first firm)
737
+ INSERT INTO hazo_roles (id, role_name)
738
+ VALUES (gen_random_uuid(), 'firm_admin')
739
+ ON CONFLICT (role_name) DO NOTHING;
819
740
  ```
820
741
 
821
742
  ### SQLite Setup (for local development)
822
743
 
823
- For local development and testing, you can use SQLite. The SQLite schema is slightly different (no UUID type, TEXT used instead):
744
+ The SQLite version of the schema. Equivalent to running `npx hazo_auth init-db` and identical to `src/lib/schema/sqlite_schema.ts`.
824
745
 
825
746
  ```sql
826
- -- ============================================
827
- -- hazo_auth Database Setup Script (SQLite)
828
- -- ============================================
747
+ -- ============================================================
748
+ -- hazo_auth canonical SQLite schema (v5.x)
749
+ -- ============================================================
829
750
 
830
- -- Users table
751
+ -- Users
831
752
  CREATE TABLE IF NOT EXISTS hazo_users (
832
- id TEXT PRIMARY KEY,
833
- email_address TEXT NOT NULL UNIQUE,
834
- password_hash TEXT NOT NULL,
835
- name TEXT,
836
- email_verified INTEGER NOT NULL DEFAULT 0,
837
- is_active INTEGER NOT NULL DEFAULT 1,
838
- login_attempts INTEGER NOT NULL DEFAULT 0,
839
- last_logon TEXT,
840
- profile_picture_url TEXT,
841
- profile_source TEXT,
842
- mfa_secret TEXT,
843
- url_on_logon TEXT,
844
- created_at TEXT NOT NULL DEFAULT (datetime('now')),
845
- changed_at TEXT NOT NULL DEFAULT (datetime('now'))
753
+ id TEXT PRIMARY KEY,
754
+ email_address TEXT NOT NULL UNIQUE,
755
+ password_hash TEXT,
756
+ name TEXT,
757
+ email_verified INTEGER DEFAULT 0,
758
+ login_attempts INTEGER DEFAULT 0,
759
+ last_logon TEXT,
760
+ profile_picture_url TEXT,
761
+ profile_source TEXT CHECK(profile_source IN ('gravatar', 'custom', 'predefined')),
762
+ mfa_secret TEXT,
763
+ url_on_logon TEXT,
764
+ google_id TEXT UNIQUE,
765
+ auth_providers TEXT DEFAULT 'email',
766
+ user_type TEXT,
767
+ app_user_data TEXT,
768
+ status TEXT DEFAULT 'ACTIVE' CHECK(status IN ('PENDING', 'ACTIVE', 'BLOCKED')),
769
+ managed_by_user_id TEXT REFERENCES hazo_users(id) ON DELETE SET NULL,
770
+ pin_hash TEXT,
771
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
772
+ changed_at TEXT NOT NULL DEFAULT (datetime('now'))
846
773
  );
774
+ CREATE INDEX IF NOT EXISTS idx_hazo_users_email ON hazo_users(email_address);
775
+ CREATE INDEX IF NOT EXISTS idx_hazo_users_google_id ON hazo_users(google_id);
776
+ CREATE INDEX IF NOT EXISTS idx_hazo_users_status ON hazo_users(status);
777
+ CREATE INDEX IF NOT EXISTS idx_hazo_users_managed_by ON hazo_users(managed_by_user_id);
847
778
 
848
- -- Refresh tokens table
779
+ -- Refresh tokens
849
780
  CREATE TABLE IF NOT EXISTS hazo_refresh_tokens (
850
- id TEXT PRIMARY KEY,
851
- user_id TEXT NOT NULL REFERENCES hazo_users(id) ON DELETE CASCADE,
852
- token_hash TEXT NOT NULL,
853
- token_type TEXT NOT NULL,
854
- expires_at TEXT NOT NULL,
855
- created_at TEXT NOT NULL DEFAULT (datetime('now'))
781
+ id TEXT PRIMARY KEY,
782
+ user_id TEXT NOT NULL REFERENCES hazo_users(id) ON DELETE CASCADE,
783
+ token TEXT NOT NULL UNIQUE,
784
+ token_type TEXT DEFAULT 'refresh',
785
+ expires_at TEXT NOT NULL,
786
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
856
787
  );
788
+ CREATE INDEX IF NOT EXISTS idx_hazo_refresh_tokens_user ON hazo_refresh_tokens(user_id);
789
+ CREATE INDEX IF NOT EXISTS idx_hazo_refresh_tokens_token ON hazo_refresh_tokens(token);
857
790
 
858
- -- Permissions table
859
- CREATE TABLE IF NOT EXISTS hazo_permissions (
860
- id TEXT PRIMARY KEY,
861
- permission_name TEXT NOT NULL UNIQUE,
862
- description TEXT,
863
- created_at TEXT NOT NULL DEFAULT (datetime('now')),
864
- changed_at TEXT NOT NULL DEFAULT (datetime('now'))
791
+ -- Roles
792
+ CREATE TABLE IF NOT EXISTS hazo_roles (
793
+ id TEXT PRIMARY KEY,
794
+ role_name TEXT NOT NULL UNIQUE,
795
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
796
+ changed_at TEXT NOT NULL DEFAULT (datetime('now'))
865
797
  );
866
798
 
867
- -- Roles table
868
- CREATE TABLE IF NOT EXISTS hazo_roles (
869
- id TEXT PRIMARY KEY,
870
- role_name TEXT NOT NULL UNIQUE,
871
- created_at TEXT NOT NULL DEFAULT (datetime('now')),
872
- changed_at TEXT NOT NULL DEFAULT (datetime('now'))
799
+ -- Permissions
800
+ CREATE TABLE IF NOT EXISTS hazo_permissions (
801
+ id TEXT PRIMARY KEY,
802
+ permission_name TEXT NOT NULL UNIQUE,
803
+ description TEXT,
804
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
805
+ changed_at TEXT NOT NULL DEFAULT (datetime('now'))
873
806
  );
874
807
 
875
- -- Role-permissions junction table
808
+ -- Role-permission assignments (composite PK, no id column)
876
809
  CREATE TABLE IF NOT EXISTS hazo_role_permissions (
877
- role_id TEXT NOT NULL REFERENCES hazo_roles(id) ON DELETE CASCADE,
878
- permission_id TEXT NOT NULL REFERENCES hazo_permissions(id) ON DELETE CASCADE,
879
- created_at TEXT NOT NULL DEFAULT (datetime('now')),
880
- changed_at TEXT NOT NULL DEFAULT (datetime('now')),
881
- PRIMARY KEY (role_id, permission_id)
810
+ role_id TEXT NOT NULL REFERENCES hazo_roles(id) ON DELETE CASCADE,
811
+ permission_id TEXT NOT NULL REFERENCES hazo_permissions(id) ON DELETE CASCADE,
812
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
813
+ PRIMARY KEY (role_id, permission_id)
882
814
  );
883
815
 
884
- -- User-roles junction table
885
- CREATE TABLE IF NOT EXISTS hazo_user_roles (
886
- user_id TEXT NOT NULL REFERENCES hazo_users(id) ON DELETE CASCADE,
887
- role_id TEXT NOT NULL REFERENCES hazo_roles(id) ON DELETE CASCADE,
888
- created_at TEXT NOT NULL DEFAULT (datetime('now')),
889
- changed_at TEXT NOT NULL DEFAULT (datetime('now')),
890
- PRIMARY KEY (user_id, role_id)
816
+ -- Unified scope hierarchy (firms, divisions, departments, ... with branding)
817
+ CREATE TABLE IF NOT EXISTS hazo_scopes (
818
+ id TEXT PRIMARY KEY,
819
+ parent_id TEXT REFERENCES hazo_scopes(id) ON DELETE CASCADE,
820
+ name TEXT NOT NULL,
821
+ level TEXT NOT NULL,
822
+ logo_url TEXT,
823
+ primary_color TEXT,
824
+ secondary_color TEXT,
825
+ tagline TEXT,
826
+ slug TEXT,
827
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
828
+ changed_at TEXT NOT NULL DEFAULT (datetime('now'))
829
+ );
830
+ CREATE INDEX IF NOT EXISTS idx_hazo_scopes_parent ON hazo_scopes(parent_id);
831
+ CREATE INDEX IF NOT EXISTS idx_hazo_scopes_level ON hazo_scopes(level);
832
+ CREATE INDEX IF NOT EXISTS idx_hazo_scopes_slug ON hazo_scopes(slug);
833
+
834
+ -- Reserved system scopes
835
+ INSERT OR IGNORE INTO hazo_scopes (id, parent_id, name, level, created_at, changed_at)
836
+ VALUES ('00000000-0000-0000-0000-000000000000', NULL, 'Super Admin', 'system', datetime('now'), datetime('now'));
837
+ INSERT OR IGNORE INTO hazo_scopes (id, parent_id, name, level, created_at, changed_at)
838
+ VALUES ('00000000-0000-0000-0000-000000000001', NULL, 'System', 'default', datetime('now'), datetime('now'));
839
+
840
+ -- User-scope membership (replaces v4.x hazo_user_roles)
841
+ CREATE TABLE IF NOT EXISTS hazo_user_scopes (
842
+ user_id TEXT NOT NULL REFERENCES hazo_users(id) ON DELETE CASCADE,
843
+ scope_id TEXT NOT NULL REFERENCES hazo_scopes(id) ON DELETE CASCADE,
844
+ root_scope_id TEXT NOT NULL REFERENCES hazo_scopes(id) ON DELETE CASCADE,
845
+ role_id TEXT NOT NULL REFERENCES hazo_roles(id) ON DELETE CASCADE,
846
+ status TEXT DEFAULT 'ACTIVE' CHECK (status IN ('INVITED', 'ACTIVE', 'SUSPENDED', 'DEPARTED')),
847
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
848
+ changed_at TEXT NOT NULL DEFAULT (datetime('now')),
849
+ PRIMARY KEY (user_id, scope_id)
850
+ );
851
+ CREATE INDEX IF NOT EXISTS idx_hazo_user_scopes_scope ON hazo_user_scopes(scope_id);
852
+ CREATE INDEX IF NOT EXISTS idx_hazo_user_scopes_root ON hazo_user_scopes(root_scope_id);
853
+ CREATE INDEX IF NOT EXISTS idx_hazo_user_scopes_role ON hazo_user_scopes(role_id);
854
+
855
+ -- Invitations
856
+ CREATE TABLE IF NOT EXISTS hazo_invitations (
857
+ id TEXT PRIMARY KEY,
858
+ email_address TEXT NOT NULL,
859
+ token TEXT NOT NULL UNIQUE,
860
+ scope_id TEXT NOT NULL REFERENCES hazo_scopes(id) ON DELETE CASCADE,
861
+ root_scope_id TEXT NOT NULL REFERENCES hazo_scopes(id) ON DELETE CASCADE,
862
+ role_id TEXT NOT NULL REFERENCES hazo_roles(id) ON DELETE CASCADE,
863
+ invited_by TEXT REFERENCES hazo_users(id) ON DELETE SET NULL,
864
+ status TEXT NOT NULL DEFAULT 'PENDING' CHECK(status IN ('PENDING', 'ACCEPTED', 'EXPIRED', 'REVOKED')),
865
+ expires_at TEXT NOT NULL,
866
+ accepted_at TEXT,
867
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
868
+ changed_at TEXT NOT NULL DEFAULT (datetime('now'))
869
+ );
870
+ CREATE INDEX IF NOT EXISTS idx_hazo_invitations_email ON hazo_invitations(email_address);
871
+ CREATE INDEX IF NOT EXISTS idx_hazo_invitations_token ON hazo_invitations(token);
872
+ CREATE INDEX IF NOT EXISTS idx_hazo_invitations_scope ON hazo_invitations(scope_id);
873
+ CREATE INDEX IF NOT EXISTS idx_hazo_invitations_status ON hazo_invitations(status);
874
+ CREATE INDEX IF NOT EXISTS idx_hazo_invitations_expires ON hazo_invitations(expires_at);
875
+
876
+ -- Managed sub-profile relationships
877
+ CREATE TABLE IF NOT EXISTS hazo_user_relationships (
878
+ id TEXT PRIMARY KEY,
879
+ parent_user_id TEXT NOT NULL REFERENCES hazo_users(id) ON DELETE CASCADE,
880
+ child_user_id TEXT NOT NULL REFERENCES hazo_users(id) ON DELETE CASCADE,
881
+ relationship_type TEXT NOT NULL DEFAULT 'parent',
882
+ can_view_progress INTEGER DEFAULT 1,
883
+ can_edit_profile INTEGER DEFAULT 1,
884
+ can_delete INTEGER DEFAULT 0,
885
+ is_self INTEGER DEFAULT 0,
886
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
887
+ UNIQUE(parent_user_id, child_user_id)
888
+ );
889
+ CREATE INDEX IF NOT EXISTS idx_hazo_user_relationships_parent ON hazo_user_relationships(parent_user_id);
890
+ CREATE INDEX IF NOT EXISTS idx_hazo_user_relationships_child ON hazo_user_relationships(child_user_id);
891
+
892
+ -- Built-in firm_admin role (used when a user creates their first firm)
893
+ INSERT OR IGNORE INTO hazo_roles (id, role_name, created_at, changed_at)
894
+ VALUES (
895
+ 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))),
896
+ 'firm_admin',
897
+ datetime('now'),
898
+ datetime('now')
891
899
  );
892
900
  ```
893
901
 
@@ -2147,19 +2155,14 @@ scope_cache_max_entries = 5000
2147
2155
 
2148
2156
  ### Database Setup
2149
2157
 
2150
- HRBAC requires additional database tables. Run the scope consolidation migration:
2158
+ As of v5.0, the HRBAC tables `hazo_scopes`, `hazo_user_scopes`, and `hazo_invitations` — are part of the **core schema** in [Database Setup](#database-setup), so if you ran the canonical PostgreSQL/SQLite script (or `npx hazo_auth init-db`) they already exist. No additional setup is required to enable HRBAC at the database level.
2159
+
2160
+ If you're upgrading from v4.x, run the consolidation migration to drop the legacy `hazo_org`, `hazo_scopes_l1..l7`, and `hazo_user_roles` tables and create the new schema in place:
2151
2161
 
2152
2162
  ```bash
2153
2163
  npm run migrate migrations/009_scope_consolidation.sql
2154
2164
  ```
2155
2165
 
2156
- This creates:
2157
- - `hazo_scopes` - Unified scope hierarchy with branding support
2158
- - `hazo_user_scopes` - User-scope-role assignments
2159
- - `hazo_invitations` - User invitation flow
2160
-
2161
- See `SETUP_CHECKLIST.md` for full PostgreSQL and SQLite scripts.
2162
-
2163
2166
  ### Using hazo_get_auth with Scope Options
2164
2167
 
2165
2168
  When HRBAC is enabled, you can check scope access alongside permissions:
@@ -334,69 +334,179 @@ mkdir -p data
334
334
 
335
335
  The SQLite database will be created automatically on first use if using hazo_connect's SQLite adapter.
336
336
 
337
+ **Recommended:** Run the canonical SQLite schema via the CLI. This is the single source of truth and creates ALL required tables (mirrors `src/lib/schema/sqlite_schema.ts`):
338
+
339
+ ```bash
340
+ npx hazo_auth init-db # Create/recreate the SQLite database with full schema
341
+ npx hazo_auth schema # Print the canonical schema SQL (does not modify DB)
342
+ ```
343
+
337
344
  **Manual creation (if needed):**
338
345
  ```bash
339
- # Create database with initial schema
346
+ # Create database with the full v5.x schema (all tables, including scopes + relationships)
340
347
  cat << 'EOF' | sqlite3 data/hazo_auth.sqlite
348
+ -- ============================================================
349
+ -- hazo_auth canonical SQLite schema (v5.x)
350
+ -- Mirrors src/lib/schema/sqlite_schema.ts
351
+ -- ============================================================
352
+
353
+ -- Users table (status enum + managed sub-profile support)
341
354
  CREATE TABLE IF NOT EXISTS hazo_users (
342
- id TEXT PRIMARY KEY,
343
- email_address TEXT NOT NULL UNIQUE,
344
- password_hash TEXT,
345
- name TEXT,
346
- email_verified INTEGER NOT NULL DEFAULT 0,
347
- status TEXT DEFAULT 'ACTIVE' CHECK(status IN ('PENDING', 'ACTIVE', 'BLOCKED')),
348
- login_attempts INTEGER NOT NULL DEFAULT 0,
349
- last_logon TEXT,
350
- profile_picture_url TEXT,
351
- profile_source TEXT CHECK(profile_source IN ('gravatar', 'custom', 'predefined')),
352
- mfa_secret TEXT,
353
- url_on_logon TEXT,
354
- google_id TEXT UNIQUE,
355
- auth_providers TEXT DEFAULT 'email',
356
- user_type TEXT,
357
- app_user_data TEXT,
358
- created_at TEXT NOT NULL DEFAULT (datetime('now')),
359
- changed_at TEXT NOT NULL DEFAULT (datetime('now'))
355
+ id TEXT PRIMARY KEY,
356
+ email_address TEXT NOT NULL UNIQUE,
357
+ password_hash TEXT,
358
+ name TEXT,
359
+ email_verified INTEGER DEFAULT 0,
360
+ login_attempts INTEGER DEFAULT 0,
361
+ last_logon TEXT,
362
+ profile_picture_url TEXT,
363
+ profile_source TEXT CHECK(profile_source IN ('gravatar', 'custom', 'predefined')),
364
+ mfa_secret TEXT,
365
+ url_on_logon TEXT,
366
+ google_id TEXT UNIQUE,
367
+ auth_providers TEXT DEFAULT 'email',
368
+ user_type TEXT,
369
+ app_user_data TEXT,
370
+ status TEXT DEFAULT 'ACTIVE' CHECK(status IN ('PENDING', 'ACTIVE', 'BLOCKED')),
371
+ managed_by_user_id TEXT REFERENCES hazo_users(id) ON DELETE SET NULL,
372
+ pin_hash TEXT,
373
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
374
+ changed_at TEXT NOT NULL DEFAULT (datetime('now'))
360
375
  );
361
376
 
377
+ CREATE INDEX IF NOT EXISTS idx_hazo_users_email ON hazo_users(email_address);
378
+ CREATE INDEX IF NOT EXISTS idx_hazo_users_google_id ON hazo_users(google_id);
379
+ CREATE INDEX IF NOT EXISTS idx_hazo_users_status ON hazo_users(status);
380
+ CREATE INDEX IF NOT EXISTS idx_hazo_users_managed_by ON hazo_users(managed_by_user_id);
381
+
382
+ -- Refresh tokens (also used for password reset / email verification tokens)
362
383
  CREATE TABLE IF NOT EXISTS hazo_refresh_tokens (
363
- id TEXT PRIMARY KEY,
364
- user_id TEXT NOT NULL REFERENCES hazo_users(id) ON DELETE CASCADE,
365
- token_hash TEXT NOT NULL,
366
- token_type TEXT NOT NULL,
367
- expires_at TEXT NOT NULL,
368
- created_at TEXT NOT NULL DEFAULT (datetime('now'))
384
+ id TEXT PRIMARY KEY,
385
+ user_id TEXT NOT NULL REFERENCES hazo_users(id) ON DELETE CASCADE,
386
+ token TEXT NOT NULL UNIQUE,
387
+ token_type TEXT DEFAULT 'refresh',
388
+ expires_at TEXT NOT NULL,
389
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
369
390
  );
370
391
 
371
- CREATE TABLE IF NOT EXISTS hazo_permissions (
372
- id TEXT PRIMARY KEY,
373
- permission_name TEXT NOT NULL UNIQUE,
374
- description TEXT,
375
- created_at TEXT NOT NULL DEFAULT (datetime('now')),
376
- changed_at TEXT NOT NULL DEFAULT (datetime('now'))
377
- );
392
+ CREATE INDEX IF NOT EXISTS idx_hazo_refresh_tokens_user ON hazo_refresh_tokens(user_id);
393
+ CREATE INDEX IF NOT EXISTS idx_hazo_refresh_tokens_token ON hazo_refresh_tokens(token);
378
394
 
395
+ -- Roles
379
396
  CREATE TABLE IF NOT EXISTS hazo_roles (
380
- id TEXT PRIMARY KEY,
381
- role_name TEXT NOT NULL UNIQUE,
382
- created_at TEXT NOT NULL DEFAULT (datetime('now')),
383
- changed_at TEXT NOT NULL DEFAULT (datetime('now'))
397
+ id TEXT PRIMARY KEY,
398
+ role_name TEXT NOT NULL UNIQUE,
399
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
400
+ changed_at TEXT NOT NULL DEFAULT (datetime('now'))
384
401
  );
385
402
 
403
+ -- Permissions
404
+ CREATE TABLE IF NOT EXISTS hazo_permissions (
405
+ id TEXT PRIMARY KEY,
406
+ permission_name TEXT NOT NULL UNIQUE,
407
+ description TEXT,
408
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
409
+ changed_at TEXT NOT NULL DEFAULT (datetime('now'))
410
+ );
411
+
412
+ -- Role-permission assignments (composite PK, no id column)
386
413
  CREATE TABLE IF NOT EXISTS hazo_role_permissions (
387
- role_id TEXT NOT NULL REFERENCES hazo_roles(id) ON DELETE CASCADE,
388
- permission_id TEXT NOT NULL REFERENCES hazo_permissions(id) ON DELETE CASCADE,
389
- created_at TEXT NOT NULL DEFAULT (datetime('now')),
390
- changed_at TEXT NOT NULL DEFAULT (datetime('now')),
391
- PRIMARY KEY (role_id, permission_id)
414
+ role_id TEXT NOT NULL REFERENCES hazo_roles(id) ON DELETE CASCADE,
415
+ permission_id TEXT NOT NULL REFERENCES hazo_permissions(id) ON DELETE CASCADE,
416
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
417
+ PRIMARY KEY (role_id, permission_id)
392
418
  );
393
419
 
394
- CREATE TABLE IF NOT EXISTS hazo_user_roles (
395
- user_id TEXT NOT NULL REFERENCES hazo_users(id) ON DELETE CASCADE,
396
- role_id TEXT NOT NULL REFERENCES hazo_roles(id) ON DELETE CASCADE,
397
- created_at TEXT NOT NULL DEFAULT (datetime('now')),
398
- changed_at TEXT NOT NULL DEFAULT (datetime('now')),
399
- PRIMARY KEY (user_id, role_id)
420
+ -- Unified scopes table (hierarchical multi-tenancy with firm branding)
421
+ -- v5.0+ replaces hazo_org and hazo_scopes_l1..l7 with a single self-referencing table
422
+ CREATE TABLE IF NOT EXISTS hazo_scopes (
423
+ id TEXT PRIMARY KEY,
424
+ parent_id TEXT REFERENCES hazo_scopes(id) ON DELETE CASCADE,
425
+ name TEXT NOT NULL,
426
+ level TEXT NOT NULL,
427
+ logo_url TEXT,
428
+ primary_color TEXT,
429
+ secondary_color TEXT,
430
+ tagline TEXT,
431
+ slug TEXT,
432
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
433
+ changed_at TEXT NOT NULL DEFAULT (datetime('now'))
434
+ );
435
+
436
+ CREATE INDEX IF NOT EXISTS idx_hazo_scopes_parent ON hazo_scopes(parent_id);
437
+ CREATE INDEX IF NOT EXISTS idx_hazo_scopes_level ON hazo_scopes(level);
438
+ CREATE INDEX IF NOT EXISTS idx_hazo_scopes_slug ON hazo_scopes(slug);
439
+
440
+ -- Reserved system scopes
441
+ INSERT OR IGNORE INTO hazo_scopes (id, parent_id, name, level, created_at, changed_at)
442
+ VALUES ('00000000-0000-0000-0000-000000000000', NULL, 'Super Admin', 'system', datetime('now'), datetime('now'));
443
+
444
+ INSERT OR IGNORE INTO hazo_scopes (id, parent_id, name, level, created_at, changed_at)
445
+ VALUES ('00000000-0000-0000-0000-000000000001', NULL, 'System', 'default', datetime('now'), datetime('now'));
446
+
447
+ -- User-scope assignments (membership model with scope-specific roles)
448
+ -- NOTE: replaces the old hazo_user_roles table from v4.x
449
+ CREATE TABLE IF NOT EXISTS hazo_user_scopes (
450
+ user_id TEXT NOT NULL REFERENCES hazo_users(id) ON DELETE CASCADE,
451
+ scope_id TEXT NOT NULL REFERENCES hazo_scopes(id) ON DELETE CASCADE,
452
+ root_scope_id TEXT NOT NULL REFERENCES hazo_scopes(id) ON DELETE CASCADE,
453
+ role_id TEXT NOT NULL REFERENCES hazo_roles(id) ON DELETE CASCADE,
454
+ status TEXT DEFAULT 'ACTIVE' CHECK (status IN ('INVITED', 'ACTIVE', 'SUSPENDED', 'DEPARTED')),
455
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
456
+ changed_at TEXT NOT NULL DEFAULT (datetime('now')),
457
+ PRIMARY KEY (user_id, scope_id)
458
+ );
459
+
460
+ CREATE INDEX IF NOT EXISTS idx_hazo_user_scopes_scope ON hazo_user_scopes(scope_id);
461
+ CREATE INDEX IF NOT EXISTS idx_hazo_user_scopes_root ON hazo_user_scopes(root_scope_id);
462
+ CREATE INDEX IF NOT EXISTS idx_hazo_user_scopes_role ON hazo_user_scopes(role_id);
463
+
464
+ -- Invitations (onboard new users into existing scopes)
465
+ CREATE TABLE IF NOT EXISTS hazo_invitations (
466
+ id TEXT PRIMARY KEY,
467
+ email_address TEXT NOT NULL,
468
+ token TEXT NOT NULL UNIQUE,
469
+ scope_id TEXT NOT NULL REFERENCES hazo_scopes(id) ON DELETE CASCADE,
470
+ root_scope_id TEXT NOT NULL REFERENCES hazo_scopes(id) ON DELETE CASCADE,
471
+ role_id TEXT NOT NULL REFERENCES hazo_roles(id) ON DELETE CASCADE,
472
+ invited_by TEXT REFERENCES hazo_users(id) ON DELETE SET NULL,
473
+ status TEXT NOT NULL DEFAULT 'PENDING' CHECK(status IN ('PENDING', 'ACCEPTED', 'EXPIRED', 'REVOKED')),
474
+ expires_at TEXT NOT NULL,
475
+ accepted_at TEXT,
476
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
477
+ changed_at TEXT NOT NULL DEFAULT (datetime('now'))
478
+ );
479
+
480
+ CREATE INDEX IF NOT EXISTS idx_hazo_invitations_email ON hazo_invitations(email_address);
481
+ CREATE INDEX IF NOT EXISTS idx_hazo_invitations_token ON hazo_invitations(token);
482
+ CREATE INDEX IF NOT EXISTS idx_hazo_invitations_scope ON hazo_invitations(scope_id);
483
+ CREATE INDEX IF NOT EXISTS idx_hazo_invitations_status ON hazo_invitations(status);
484
+ CREATE INDEX IF NOT EXISTS idx_hazo_invitations_expires ON hazo_invitations(expires_at);
485
+
486
+ -- Managed sub-profile relationships (parent/child accounts on shared devices)
487
+ CREATE TABLE IF NOT EXISTS hazo_user_relationships (
488
+ id TEXT PRIMARY KEY,
489
+ parent_user_id TEXT NOT NULL REFERENCES hazo_users(id) ON DELETE CASCADE,
490
+ child_user_id TEXT NOT NULL REFERENCES hazo_users(id) ON DELETE CASCADE,
491
+ relationship_type TEXT NOT NULL DEFAULT 'parent',
492
+ can_view_progress INTEGER DEFAULT 1,
493
+ can_edit_profile INTEGER DEFAULT 1,
494
+ can_delete INTEGER DEFAULT 0,
495
+ is_self INTEGER DEFAULT 0,
496
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
497
+ UNIQUE(parent_user_id, child_user_id)
498
+ );
499
+
500
+ CREATE INDEX IF NOT EXISTS idx_hazo_user_relationships_parent ON hazo_user_relationships(parent_user_id);
501
+ CREATE INDEX IF NOT EXISTS idx_hazo_user_relationships_child ON hazo_user_relationships(child_user_id);
502
+
503
+ -- Built-in firm_admin role (used when a user creates their first firm)
504
+ INSERT OR IGNORE INTO hazo_roles (id, role_name, created_at, changed_at)
505
+ VALUES (
506
+ 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))),
507
+ 'firm_admin',
508
+ datetime('now'),
509
+ datetime('now')
400
510
  );
401
511
  EOF
402
512
  ```
@@ -404,82 +514,53 @@ EOF
404
514
  **Verify SQLite database:**
405
515
  ```bash
406
516
  sqlite3 data/hazo_auth.sqlite ".tables"
407
- # Expected: hazo_users hazo_refresh_tokens hazo_permissions hazo_roles hazo_role_permissions hazo_user_roles
517
+ # Expected (9 tables):
518
+ # hazo_users hazo_refresh_tokens hazo_roles hazo_permissions hazo_role_permissions
519
+ # hazo_scopes hazo_user_scopes hazo_invitations hazo_user_relationships
408
520
  ```
409
521
 
522
+ > **v4.x → v5.x:** the legacy `hazo_org`, `hazo_scopes_l1..l7`, and `hazo_user_roles` tables have been removed. Roles are now assigned per-scope via `hazo_user_scopes.role_id`. If upgrading, run `migrations/009_scope_consolidation.sql` first.
523
+
410
524
  ### Option B: PostgreSQL (Production)
411
525
 
412
- Run this SQL script in your PostgreSQL database:
526
+ Run this SQL script in your PostgreSQL database. It creates the full v5.x schema, including the unified `hazo_scopes` model, `hazo_user_scopes`, `hazo_invitations`, and `hazo_user_relationships`.
413
527
 
414
- **Important:** Run the entire script in order. The enum type must be created before the table that uses it.
528
+ **Important:** Run the entire script in order enum types and parent tables must be created before tables that depend on them.
415
529
 
416
530
  ```sql
417
- -- Ensure we're in the public schema (or your target schema)
531
+ -- ============================================================
532
+ -- hazo_auth canonical PostgreSQL schema (v5.x)
533
+ -- Single source of truth for production deployments
534
+ -- ============================================================
535
+
418
536
  SET search_path TO public;
419
537
 
420
- -- Create enum types (drop first if they exist to avoid conflicts)
421
- DROP TYPE IF EXISTS hazo_enum_profile_source_enum CASCADE;
538
+ -- 1. Enum types
422
539
  CREATE TYPE hazo_enum_profile_source_enum AS ENUM ('gravatar', 'custom', 'predefined');
540
+ CREATE TYPE hazo_enum_user_status AS ENUM ('PENDING', 'ACTIVE', 'BLOCKED');
541
+ CREATE TYPE hazo_enum_user_scope_status_type AS ENUM ('INVITED', 'ACTIVE', 'SUSPENDED', 'DEPARTED');
542
+ CREATE TYPE hazo_enum_invitation_status AS ENUM ('PENDING', 'ACCEPTED', 'EXPIRED', 'REVOKED');
423
543
 
424
- DROP TYPE IF EXISTS hazo_enum_scope_types CASCADE;
425
- CREATE TYPE hazo_enum_scope_types AS ENUM (
426
- 'hazo_scopes_l1', 'hazo_scopes_l2', 'hazo_scopes_l3',
427
- 'hazo_scopes_l4', 'hazo_scopes_l5', 'hazo_scopes_l6', 'hazo_scopes_l7'
428
- );
429
-
430
- DROP TYPE IF EXISTS hazo_enum_notify_chain_status CASCADE;
431
- CREATE TYPE hazo_enum_notify_chain_status AS ENUM ('draft', 'published', 'inactive');
432
-
433
- DROP TYPE IF EXISTS hazo_enum_notify_email_type CASCADE;
434
- CREATE TYPE hazo_enum_notify_email_type AS ENUM ('system', 'user');
435
-
436
- DROP TYPE IF EXISTS hazo_enum_group_type CASCADE;
437
- CREATE TYPE hazo_enum_group_type AS ENUM ('support', 'peer', 'group');
438
-
439
- DROP TYPE IF EXISTS hazo_enum_group_role CASCADE;
440
- CREATE TYPE hazo_enum_group_role AS ENUM ('client', 'staff', 'owner', 'admin', 'member');
441
-
442
- DROP TYPE IF EXISTS hazo_enum_chat_type CASCADE;
443
- CREATE TYPE hazo_enum_chat_type AS ENUM ('chat', 'field', 'project', 'support', 'general');
444
-
445
- -- Create organization table (multi-tenancy) - MUST be created before hazo_users
446
- CREATE TABLE hazo_org (
447
- id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
448
- name TEXT NOT NULL,
449
- parent_org_id UUID REFERENCES hazo_org(id) ON DELETE SET NULL,
450
- root_org_id UUID REFERENCES hazo_org(id) ON DELETE SET NULL,
451
- user_limit INTEGER NOT NULL DEFAULT 0, -- 0 = unlimited
452
- active BOOLEAN NOT NULL DEFAULT TRUE,
453
- created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
454
- created_by UUID, -- FK added after hazo_users exists
455
- changed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
456
- changed_by UUID -- FK added after hazo_users exists
457
- );
458
- CREATE INDEX idx_hazo_org_parent_org_id ON hazo_org(parent_org_id);
459
- CREATE INDEX idx_hazo_org_root_org_id ON hazo_org(root_org_id);
460
- CREATE INDEX idx_hazo_org_active ON hazo_org(active);
461
- CREATE INDEX idx_hazo_org_name ON hazo_org(name);
462
-
463
- -- Create users table
544
+ -- 2. Users (status enum + managed sub-profile support)
464
545
  CREATE TABLE hazo_users (
465
546
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
466
547
  email_address TEXT NOT NULL UNIQUE,
467
548
  password_hash TEXT, -- NULL for OAuth-only users
468
549
  name TEXT,
469
550
  email_verified BOOLEAN NOT NULL DEFAULT FALSE,
470
- status TEXT NOT NULL DEFAULT 'ACTIVE', -- 'PENDING', 'ACTIVE', or 'BLOCKED'
471
551
  login_attempts INTEGER NOT NULL DEFAULT 0,
472
552
  last_logon TIMESTAMP WITH TIME ZONE,
473
553
  profile_picture_url TEXT,
474
554
  profile_source hazo_enum_profile_source_enum,
475
555
  mfa_secret TEXT,
476
556
  url_on_logon TEXT,
477
- user_type TEXT, -- Optional user categorization
478
- app_user_data TEXT, -- Custom JSON data for consuming apps
479
557
  google_id TEXT UNIQUE, -- Google OAuth ID
480
558
  auth_providers TEXT DEFAULT 'email', -- 'email', 'google', or 'email,google'
481
- org_id UUID REFERENCES hazo_org(id) ON DELETE SET NULL,
482
- root_org_id UUID REFERENCES hazo_org(id) ON DELETE SET NULL,
559
+ user_type TEXT, -- Optional user categorization
560
+ app_user_data JSONB, -- Custom JSON data for consuming apps
561
+ status hazo_enum_user_status NOT NULL DEFAULT 'ACTIVE',
562
+ managed_by_user_id UUID REFERENCES hazo_users(id) ON DELETE SET NULL, -- managed sub-profiles
563
+ pin_hash TEXT, -- simple PIN auth on shared devices
483
564
  created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
484
565
  changed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
485
566
  );
@@ -487,71 +568,142 @@ CREATE INDEX idx_hazo_users_email ON hazo_users(email_address);
487
568
  CREATE INDEX idx_hazo_users_status ON hazo_users(status);
488
569
  CREATE INDEX idx_hazo_users_user_type ON hazo_users(user_type);
489
570
  CREATE UNIQUE INDEX idx_hazo_users_google_id ON hazo_users(google_id);
490
- CREATE INDEX idx_hazo_users_org_id ON hazo_users(org_id);
491
- CREATE INDEX idx_hazo_users_root_org_id ON hazo_users(root_org_id);
571
+ CREATE INDEX idx_hazo_users_managed_by ON hazo_users(managed_by_user_id);
492
572
 
493
- -- Add FK constraints to hazo_org now that hazo_users exists
494
- ALTER TABLE hazo_org ADD CONSTRAINT fk_hazo_org_created_by
495
- FOREIGN KEY (created_by) REFERENCES hazo_users(id) ON DELETE SET NULL;
496
- ALTER TABLE hazo_org ADD CONSTRAINT fk_hazo_org_changed_by
497
- FOREIGN KEY (changed_by) REFERENCES hazo_users(id) ON DELETE SET NULL;
498
-
499
- -- Create refresh tokens table
573
+ -- 3. Refresh tokens (also used for password reset / email verification)
500
574
  CREATE TABLE hazo_refresh_tokens (
501
575
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
502
576
  user_id UUID NOT NULL REFERENCES hazo_users(id) ON DELETE CASCADE,
503
577
  token_hash TEXT NOT NULL,
504
- token_type TEXT NOT NULL,
578
+ token_type TEXT NOT NULL DEFAULT 'refresh', -- 'refresh' | 'password_reset' | 'email_verification'
505
579
  expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
506
580
  created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
507
581
  );
508
582
  CREATE INDEX idx_hazo_refresh_tokens_user_id ON hazo_refresh_tokens(user_id);
509
583
  CREATE INDEX idx_hazo_refresh_tokens_token_type ON hazo_refresh_tokens(token_type);
510
584
 
511
- -- Create permissions table
512
- CREATE TABLE hazo_permissions (
585
+ -- 4. Roles
586
+ CREATE TABLE hazo_roles (
513
587
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
514
- permission_name TEXT NOT NULL UNIQUE,
515
- description TEXT,
588
+ role_name TEXT NOT NULL UNIQUE,
516
589
  created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
517
590
  changed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
518
591
  );
519
592
 
520
- -- Create roles table
521
- CREATE TABLE hazo_roles (
593
+ -- 5. Permissions
594
+ CREATE TABLE hazo_permissions (
522
595
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
523
- role_name TEXT NOT NULL UNIQUE,
596
+ permission_name TEXT NOT NULL UNIQUE,
597
+ description TEXT,
524
598
  created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
525
599
  changed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
526
600
  );
527
601
 
528
- -- Create role-permissions junction table
602
+ -- 6. Role-permission assignments (composite PK, NO id column)
529
603
  CREATE TABLE hazo_role_permissions (
530
604
  role_id UUID NOT NULL REFERENCES hazo_roles(id) ON DELETE CASCADE,
531
605
  permission_id UUID NOT NULL REFERENCES hazo_permissions(id) ON DELETE CASCADE,
532
606
  created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
533
- changed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
534
607
  PRIMARY KEY (role_id, permission_id)
535
608
  );
536
- CREATE INDEX idx_hazo_role_permissions_role_id ON hazo_role_permissions(role_id);
537
- CREATE INDEX idx_hazo_role_permissions_permission_id ON hazo_role_permissions(permission_id);
538
609
 
539
- -- Create user-roles junction table
540
- CREATE TABLE hazo_user_roles (
610
+ -- 7. Unified scopes (hierarchical multi-tenancy with firm branding)
611
+ -- v5.0+ replaces the old hazo_org and hazo_scopes_l1..l7 tables.
612
+ CREATE TABLE hazo_scopes (
613
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
614
+ parent_id UUID REFERENCES hazo_scopes(id) ON DELETE CASCADE,
615
+ name TEXT NOT NULL,
616
+ level TEXT NOT NULL, -- descriptive label e.g. 'HQ', 'Division', 'Team'
617
+ logo_url TEXT,
618
+ primary_color TEXT,
619
+ secondary_color TEXT,
620
+ tagline TEXT,
621
+ slug TEXT,
622
+ created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
623
+ changed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
624
+ );
625
+ CREATE INDEX idx_hazo_scopes_parent ON hazo_scopes(parent_id);
626
+ CREATE INDEX idx_hazo_scopes_level ON hazo_scopes(level);
627
+ CREATE INDEX idx_hazo_scopes_slug ON hazo_scopes(slug);
628
+
629
+ -- 7a. Reserved system scopes
630
+ INSERT INTO hazo_scopes (id, parent_id, name, level, created_at, changed_at)
631
+ VALUES ('00000000-0000-0000-0000-000000000000', NULL, 'Super Admin', 'system', NOW(), NOW())
632
+ ON CONFLICT (id) DO NOTHING;
633
+
634
+ INSERT INTO hazo_scopes (id, parent_id, name, level, created_at, changed_at)
635
+ VALUES ('00000000-0000-0000-0000-000000000001', NULL, 'System', 'default', NOW(), NOW())
636
+ ON CONFLICT (id) DO NOTHING;
637
+
638
+ -- 8. User-scope assignments (membership model with scope-specific roles)
639
+ -- Replaces the v4.x hazo_user_roles table.
640
+ CREATE TABLE hazo_user_scopes (
541
641
  user_id UUID NOT NULL REFERENCES hazo_users(id) ON DELETE CASCADE,
642
+ scope_id UUID NOT NULL REFERENCES hazo_scopes(id) ON DELETE CASCADE,
643
+ root_scope_id UUID NOT NULL REFERENCES hazo_scopes(id) ON DELETE CASCADE,
542
644
  role_id UUID NOT NULL REFERENCES hazo_roles(id) ON DELETE CASCADE,
645
+ status hazo_enum_user_scope_status_type NOT NULL DEFAULT 'ACTIVE',
543
646
  created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
544
647
  changed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
545
- PRIMARY KEY (user_id, role_id)
648
+ PRIMARY KEY (user_id, scope_id)
649
+ );
650
+ CREATE INDEX idx_hazo_user_scopes_scope ON hazo_user_scopes(scope_id);
651
+ CREATE INDEX idx_hazo_user_scopes_root ON hazo_user_scopes(root_scope_id);
652
+ CREATE INDEX idx_hazo_user_scopes_role ON hazo_user_scopes(role_id);
653
+
654
+ -- 9. Invitations (onboard new users into existing scopes)
655
+ CREATE TABLE hazo_invitations (
656
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
657
+ email_address TEXT NOT NULL,
658
+ token TEXT NOT NULL UNIQUE,
659
+ scope_id UUID NOT NULL REFERENCES hazo_scopes(id) ON DELETE CASCADE,
660
+ root_scope_id UUID NOT NULL REFERENCES hazo_scopes(id) ON DELETE CASCADE,
661
+ role_id UUID NOT NULL REFERENCES hazo_roles(id) ON DELETE CASCADE,
662
+ invited_by UUID REFERENCES hazo_users(id) ON DELETE SET NULL,
663
+ status hazo_enum_invitation_status NOT NULL DEFAULT 'PENDING',
664
+ expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
665
+ accepted_at TIMESTAMP WITH TIME ZONE,
666
+ created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
667
+ changed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
668
+ );
669
+ CREATE INDEX idx_hazo_invitations_email ON hazo_invitations(email_address);
670
+ CREATE INDEX idx_hazo_invitations_token ON hazo_invitations(token);
671
+ CREATE INDEX idx_hazo_invitations_scope ON hazo_invitations(scope_id);
672
+ CREATE INDEX idx_hazo_invitations_status ON hazo_invitations(status);
673
+ CREATE INDEX idx_hazo_invitations_expires ON hazo_invitations(expires_at);
674
+
675
+ -- 10. Managed sub-profile relationships (parent/child accounts on shared devices)
676
+ CREATE TABLE hazo_user_relationships (
677
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
678
+ parent_user_id UUID NOT NULL REFERENCES hazo_users(id) ON DELETE CASCADE,
679
+ child_user_id UUID NOT NULL REFERENCES hazo_users(id) ON DELETE CASCADE,
680
+ relationship_type TEXT NOT NULL DEFAULT 'parent',
681
+ can_view_progress BOOLEAN NOT NULL DEFAULT TRUE,
682
+ can_edit_profile BOOLEAN NOT NULL DEFAULT TRUE,
683
+ can_delete BOOLEAN NOT NULL DEFAULT FALSE,
684
+ is_self BOOLEAN NOT NULL DEFAULT FALSE,
685
+ created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
686
+ UNIQUE (parent_user_id, child_user_id)
546
687
  );
547
- CREATE INDEX idx_hazo_user_roles_user_id ON hazo_user_roles(user_id);
548
- CREATE INDEX idx_hazo_user_roles_role_id ON hazo_user_roles(role_id);
688
+ CREATE INDEX idx_hazo_user_relationships_parent ON hazo_user_relationships(parent_user_id);
689
+ CREATE INDEX idx_hazo_user_relationships_child ON hazo_user_relationships(child_user_id);
690
+
691
+ -- 11. Built-in firm_admin role (used when a user creates their first firm)
692
+ INSERT INTO hazo_roles (id, role_name, created_at, changed_at)
693
+ VALUES (gen_random_uuid(), 'firm_admin', NOW(), NOW())
694
+ ON CONFLICT (role_name) DO NOTHING;
549
695
  ```
550
696
 
697
+ > **Migrating from v4.x?** The legacy `hazo_org`, `hazo_scopes_l1..l7`, and `hazo_user_roles` tables have been removed. Run `migrations/009_scope_consolidation.sql` against your existing database — it drops the old tables and creates the new schema in place.
698
+
551
699
  **Verify PostgreSQL tables:**
552
700
  ```sql
553
- SELECT table_name FROM information_schema.tables WHERE table_name LIKE 'hazo_%';
554
- -- Expected: 6 tables listed
701
+ SELECT table_name FROM information_schema.tables
702
+ WHERE table_schema = 'public' AND table_name LIKE 'hazo_%'
703
+ ORDER BY table_name;
704
+ -- Expected (9 tables):
705
+ -- hazo_invitations, hazo_permissions, hazo_refresh_tokens, hazo_role_permissions,
706
+ -- hazo_roles, hazo_scopes, hazo_users, hazo_user_relationships, hazo_user_scopes
555
707
  ```
556
708
 
557
709
  **Grant access to admin user:**
@@ -565,16 +717,19 @@ GRANT USAGE ON SCHEMA public TO your_admin_user;
565
717
  -- Grant all privileges on all hazo_* tables
566
718
  GRANT ALL PRIVILEGES ON TABLE hazo_users TO your_admin_user;
567
719
  GRANT ALL PRIVILEGES ON TABLE hazo_refresh_tokens TO your_admin_user;
568
- GRANT ALL PRIVILEGES ON TABLE hazo_permissions TO your_admin_user;
569
720
  GRANT ALL PRIVILEGES ON TABLE hazo_roles TO your_admin_user;
721
+ GRANT ALL PRIVILEGES ON TABLE hazo_permissions TO your_admin_user;
570
722
  GRANT ALL PRIVILEGES ON TABLE hazo_role_permissions TO your_admin_user;
571
- GRANT ALL PRIVILEGES ON TABLE hazo_user_roles TO your_admin_user;
723
+ GRANT ALL PRIVILEGES ON TABLE hazo_scopes TO your_admin_user;
724
+ GRANT ALL PRIVILEGES ON TABLE hazo_user_scopes TO your_admin_user;
725
+ GRANT ALL PRIVILEGES ON TABLE hazo_invitations TO your_admin_user;
726
+ GRANT ALL PRIVILEGES ON TABLE hazo_user_relationships TO your_admin_user;
572
727
 
573
- -- Grant usage on the enum type
728
+ -- Grant usage on enum types
574
729
  GRANT USAGE ON TYPE hazo_enum_profile_source_enum TO your_admin_user;
575
-
576
- -- Grant privileges on sequences (if using SERIAL instead of UUID, though not needed for UUID)
577
- -- GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO your_admin_user;
730
+ GRANT USAGE ON TYPE hazo_enum_user_status TO your_admin_user;
731
+ GRANT USAGE ON TYPE hazo_enum_user_scope_status_type TO your_admin_user;
732
+ GRANT USAGE ON TYPE hazo_enum_invitation_status TO your_admin_user;
578
733
 
579
734
  -- Optional: Grant privileges on future tables (if you plan to add more hazo_* tables)
580
735
  ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL PRIVILEGES ON TABLES TO your_admin_user;
@@ -598,7 +753,7 @@ GRANT SELECT ON TABLE hazo_users TO anon;
598
753
  GRANT SELECT ON TABLE hazo_permissions TO anon;
599
754
  GRANT SELECT ON TABLE hazo_roles TO anon;
600
755
  GRANT SELECT ON TABLE hazo_role_permissions TO anon;
601
- GRANT SELECT ON TABLE hazo_user_roles TO anon;
756
+ GRANT SELECT ON TABLE hazo_scopes TO anon;
602
757
 
603
758
  -- Grant full access to authenticated users (adjust based on your RLS policies)
604
759
  GRANT ALL PRIVILEGES ON TABLE hazo_users TO authenticated;
@@ -606,30 +761,39 @@ GRANT ALL PRIVILEGES ON TABLE hazo_refresh_tokens TO authenticated;
606
761
  GRANT ALL PRIVILEGES ON TABLE hazo_permissions TO authenticated;
607
762
  GRANT ALL PRIVILEGES ON TABLE hazo_roles TO authenticated;
608
763
  GRANT ALL PRIVILEGES ON TABLE hazo_role_permissions TO authenticated;
609
- GRANT ALL PRIVILEGES ON TABLE hazo_user_roles TO authenticated;
764
+ GRANT ALL PRIVILEGES ON TABLE hazo_scopes TO authenticated;
765
+ GRANT ALL PRIVILEGES ON TABLE hazo_user_scopes TO authenticated;
766
+ GRANT ALL PRIVILEGES ON TABLE hazo_invitations TO authenticated;
767
+ GRANT ALL PRIVILEGES ON TABLE hazo_user_relationships TO authenticated;
610
768
 
611
- -- Grant usage on enum type
769
+ -- Grant usage on enum types
612
770
  GRANT USAGE ON TYPE hazo_enum_profile_source_enum TO anon, authenticated;
771
+ GRANT USAGE ON TYPE hazo_enum_user_status TO anon, authenticated;
772
+ GRANT USAGE ON TYPE hazo_enum_user_scope_status_type TO anon, authenticated;
773
+ GRANT USAGE ON TYPE hazo_enum_invitation_status TO anon, authenticated;
613
774
  ```
614
775
 
615
776
  **Checklist:**
616
777
  - [ ] Database created (SQLite file or PostgreSQL)
617
778
  - [ ] All enum types created (PostgreSQL only):
618
779
  - [ ] `hazo_enum_profile_source_enum`
619
- - [ ] `hazo_enum_scope_types`
620
- - [ ] `hazo_enum_notify_chain_status`
621
- - [ ] `hazo_enum_notify_email_type`
622
- - [ ] `hazo_enum_group_type`
623
- - [ ] `hazo_enum_group_role`
624
- - [ ] `hazo_enum_chat_type`
625
- - [ ] All core tables exist:
626
- - [ ] `hazo_org` (multi-tenancy - must be created before hazo_users)
627
- - [ ] `hazo_users` (with status, google_id, auth_providers, app_user_data, org_id, root_org_id, user_type fields)
780
+ - [ ] `hazo_enum_user_status`
781
+ - [ ] `hazo_enum_user_scope_status_type`
782
+ - [ ] `hazo_enum_invitation_status`
783
+ - [ ] All core tables exist (9 total):
784
+ - [ ] `hazo_users` (with `status`, `google_id`, `auth_providers`, `app_user_data`, `user_type`, `managed_by_user_id`, `pin_hash` fields)
628
785
  - [ ] `hazo_refresh_tokens`
629
- - [ ] `hazo_permissions`
630
786
  - [ ] `hazo_roles`
631
- - [ ] `hazo_role_permissions`
632
- - [ ] `hazo_user_roles`
787
+ - [ ] `hazo_permissions`
788
+ - [ ] `hazo_role_permissions` (composite PK, no `id` column)
789
+ - [ ] `hazo_scopes` (unified hierarchy with branding + `slug`)
790
+ - [ ] `hazo_user_scopes` (composite PK on `user_id`, `scope_id`)
791
+ - [ ] `hazo_invitations`
792
+ - [ ] `hazo_user_relationships` (managed sub-profile parent/child links)
793
+ - [ ] Reserved system scopes inserted:
794
+ - [ ] `00000000-0000-0000-0000-000000000000` (Super Admin)
795
+ - [ ] `00000000-0000-0000-0000-000000000001` (System / non-multi-tenancy default)
796
+ - [ ] `firm_admin` role inserted into `hazo_roles`
633
797
 
634
798
  ---
635
799
 
@@ -1439,7 +1603,9 @@ application_permission_list_defaults = admin_user_management,admin_role_manageme
1439
1603
 
1440
1604
  ### Step 7.3: Create HRBAC Database Tables
1441
1605
 
1442
- **Recommended:** Run the scope consolidation migration which creates all required tables:
1606
+ > **Note (v5.0+):** `hazo_scopes`, `hazo_user_scopes`, and `hazo_invitations` are now part of the **core schema** in Phase 3. If you ran the canonical SQLite or PostgreSQL script there (or `npx hazo_auth init-db`), these tables already exist — you can skip ahead to Step 7.4. The scripts below are kept for upgrades from v4.x or for partial recovery.
1607
+
1608
+ **Upgrading from v4.x?** Run the consolidation migration which drops the legacy tables (`hazo_org`, `hazo_scopes_l1..l7`, `hazo_user_roles`) and creates the new schema:
1443
1609
 
1444
1610
  ```bash
1445
1611
  npm run migrate migrations/009_scope_consolidation.sql
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hazo_auth",
3
- "version": "5.1.37",
3
+ "version": "5.1.38",
4
4
  "description": "Zero-config authentication UI components for Next.js with RBAC, OAuth, scope-based multi-tenancy, and invitations",
5
5
  "keywords": [
6
6
  "authentication",