# User Management — Implementation Plan

**Status:** Approved approach (Jul 2026)  
**Companion:** [ARCHITECTURE.md](ARCHITECTURE.md), [architecture/DIAGRAMS-AND-ACCESS-AOLL.md](../architecture/DIAGRAMS-AND-ACCESS-AOLL.md)

This document is the authoritative plan for identity, approval, roles, seats, and team
management. It supersedes earlier notes that assigned **owner at first signup**.

---

## 1. Product decisions (locked)

| Decision | Choice |
|----------|--------|
| Signup → full admin automatically | **No** — self-signup gets **limited preview** until approved |
| Who approves new accounts | **Platform superadmin** (AOLL internal, not customer-facing) |
| First approved user on solo tiers | **Workspace admin** (1 seat) — not a separate “owner” SKU |
| Starter / Professional / Intelligence seats | **1 seat each** — solo approved admin only |
| Team Management (Users UI, invites, roles) | **Enterprise only** — multi-seat + `/dashboard/users` |
| Billing before approval | **No** — checkout after superadmin approves (or at approval + plan pick) |
| Microsoft SSO | Out of scope v1 |

---

## 2. Two layers of “user management”

```mermaid
flowchart TB
    subgraph platform["Platform layer (internal)"]
        SA["platform_superadmin"]
        Queue["Approval queue"]
        SA --> Queue
    end

    subgraph lifecycle["Account lifecycle — all tiers"]
        Signup["Self-signup"]
        Limited["limited preview"]
        Approve["Superadmin approve"]
        Active["active + workspace admin"]
        Signup --> Limited --> Approve --> Active
    end

    subgraph enterprise["Team management — Enterprise only"]
        UsersUI["/dashboard/users"]
        Invite["Invite / remove / roles"]
        Active --> UsersUI --> Invite
    end

    Queue --> Approve
```

| Layer | What | Gated by plan? |
|-------|------|----------------|
| **A. Account lifecycle** | Signup, limited login, superadmin approve/reject, assign solo admin | No — applies to all signups |
| **B. Team management** | Add/remove users, Manager/Member/Viewer roles, extra seats | **Yes — Enterprise only** |

---

## 3. Account statuses

| Status | Can log in? | Access level |
|--------|-------------|--------------|
| `limited` | Yes | Preview only — see §5 |
| `active` | Yes | Full access for **assigned role + plan tier** |
| `pending_invite` | No (until accept) | Enterprise invite not yet accepted |
| `inactive` | No | Removed by admin or rejected by superadmin |
| `locked` | No | Payment failed (future Stripe) |

**Note:** Replace old `pending` (invite-only) with `pending_invite` for Enterprise invites;
use `limited` for post-signup pre-approval.

---

## 4. Roles

### 4.1 Platform (internal)

| Role | Code | Who |
|------|------|-----|
| Platform superadmin | `platform_superadmin` | AOLL staff — approval queue, create org, assign first admin, optional auto-rules |

Not shown in customer UI. Enforced via separate admin routes + env allowlist of user IDs or Shield group.

### 4.2 Workspace (customer)

| Role | Code | Solo tiers (S/P/I) | Enterprise |
|------|------|--------------------|------------|
| Workspace admin | `admin` | ✅ Only seat (after approval) | ✅ Can invite/manage team |
| Manager | `manager` | — | ✅ Invited only |
| Member | `member` | — | ✅ Invited only |
| Viewer | `viewer` | — | ✅ Invited only |

**Solo tiers:** only `admin` exists (one user). No Manager/Member/Viewer until Enterprise.

**Enterprise:** admin uses Users page; maps to design UI (Admin toggle + role dropdown).

---

## 5. Feature access by state and tier

### 5.1 Pre-approval (`limited`) — all tiers, identical caps

| Feature | Allowed |
|---------|---------|
| Log in, dashboard shell, settings (own profile) | ✅ |
| Banner: “Account pending verification” | ✅ |
| Real search / contact reveal / export | ❌ (or cap: 3 preview searches — pick one at build) |
| Billing / upgrade | ❌ |
| Users page / invites | ❌ |
| CRM / AI / workflows / intent | ❌ |

### 5.2 After approval — by plan tier (solo admin, 1 seat)

| Feature | Starter | Professional | Intelligence | Enterprise |
|---------|---------|--------------|--------------|------------|
| Seats | 1 | 1 | 1 | Custom (multi) |
| Full search / export (per plan limits) | ✅ | ✅ | ✅ | ✅ |
| Intent signals | ❌ | ✅ | ✅ | ✅ |
| CRM / workflows / AI | ❌ | ❌ | ✅ | ✅ |
| **Users page / team invites** | ❌ | ❌ | ❌ | ✅ |
| **Role assignment (mgr/member/viewer)** | ❌ | ❌ | ❌ | ✅ |
| API access | ❌ | ❌ | ❌ | ✅ |

### 5.3 Authorization stack (unchanged)

```
Authenticated → account_status OK → Role allowed → Plan tier → Usage limit → Allow
```

Add **`AccountStatusFilter`** before role check: `limited` users blocked from non-preview routes.

---

## 6. End-to-end flows

### 6.1 Self-signup (all tiers)

```mermaid
sequenceDiagram
    participant U as User
    participant App as AOLL
    participant DB as MySQL
    participant SA as Platform superadmin
    participant Email as Email

    U->>App: POST /signup
    App->>DB: Create org + user (status=limited, role=admin)
    App->>Email: Welcome + pending verification
    App->>SA: Notify new signup queue
    App-->>U: Login → limited dashboard

    SA->>App: Approve (assign plan tier if not chosen)
    App->>DB: status=active
    App->>Email: Account activated
    App-->>U: Full access for plan tier (still 1 seat)
```

### 6.2 Enterprise — admin invites teammate

```mermaid
sequenceDiagram
    participant A as Workspace admin
    participant App as AOLL
    participant DB as MySQL
    participant N as New user

    A->>App: POST /api/v1/users (Enterprise + seats available)
    App->>DB: user pending_invite, role=member|manager|viewer|admin
    App->>N: Invitation email
    N->>App: Accept invite / set password
    App->>DB: status=active
```

### 6.3 Superadmin creates Enterprise org (sales-led)

1. Superadmin creates organization + first admin email (no self-signup).
2. Admin receives invite → active on Enterprise.
3. Admin adds team via Users page.

### 6.4 Domain / duplicate policy

| Case | Rule |
|------|------|
| First `@company.com` signup | New org; user stays `limited` until approved |
| Second `@company.com` while first org exists | Join existing org as `pending_invite` or hold for superadmin merge — **do not auto-create second org** |
| Free email domain | Block at signup or auto-reject in queue |
| Auto-approve (optional P2) | Verified work domain + allowlist → skip manual queue |

---

## 7. Data model (MySQL)

### 7.1 Core tables

**`organizations`**

| Column | Notes |
|--------|-------|
| id | PK |
| name | From onboarding or superadmin |
| slug | Unique |
| plan_tier | `starter` \| `professional` \| `intelligence` \| `enterprise` |
| seat_limit | 1 for S/P/I; N for Enterprise |
| account_status | Org-level: `active` \| `suspended` (optional) |
| stripe_customer_id | Nullable — set after approval + checkout |
| created_at | |

**`users`** (Shield-backed)

| Column | Notes |
|--------|-------|
| id | PK |
| org_id | FK |
| first_name, last_name, email | |
| role | `admin` \| `manager` \| `member` \| `viewer` |
| account_status | `limited` \| `active` \| `pending_invite` \| `inactive` \| `locked` |
| is_platform_superadmin | Boolean — internal only |
| avatar_color | Optional |
| approved_at, approved_by | Superadmin audit |

**`user_invitations`** (Enterprise invites)

| Column | Notes |
|--------|-------|
| id, org_id, email, role | |
| token_hash, expires_at | |
| invited_by | user id |
| accepted_at | Nullable |

**`audit_logs`**

| Column | Notes |
|--------|-------|
| actor_id, action, target_type, target_id, metadata, created_at | |

Shield tables: `auth_identities`, `auth_access_tokens`, `auth_remember_tokens`.

### 7.2 Session payload (after login)

```json
{
  "user_id": 123,
  "org_id": 456,
  "role": "admin",
  "account_status": "active",
  "plan_tier": "professional",
  "seat_limit": 1,
  "can_manage_team": false
}
```

`can_manage_team = (plan_tier === 'enterprise' && role === 'admin')`

---

## 8. API & routes

### 8.1 Public / auth

| Method | Route | Notes |
|--------|-------|-------|
| POST | `/api/v1/auth/register` | Creates org + user, `limited` |
| POST | `/api/v1/auth/login` | |
| POST | `/api/v1/auth/google` | Same limited flow for new users |
| GET | `/api/v1/auth/me` | Returns status + entitlements |

### 8.2 Platform superadmin (internal)

Prefix: `/admin/platform/` or `/api/v1/platform/` — IP allowlist + superadmin flag.

| Method | Route | Notes |
|--------|-------|-------|
| GET | `.../signups/pending` | Approval queue |
| POST | `.../signups/{id}/approve` | Set active, plan_tier, email user |
| POST | `.../signups/{id}/reject` | Set inactive, email user |
| POST | `.../organizations` | Sales-led org + admin invite |

### 8.3 Team management (Enterprise + admin only)

| Method | Route | Gate |
|--------|-------|------|
| GET | `/api/v1/users` | Enterprise, admin, active |
| POST | `/api/v1/users` | + seat limit check |
| PATCH | `/api/v1/users/{id}` | Role change |
| DELETE | `/api/v1/users/{id}` | Cannot delete last admin |
| POST | `/api/v1/users/invites/{token}/accept` | Public with token |

### 8.4 Web routes

| Route | Gate |
|-------|------|
| `/dashboard/users` | Enterprise + admin; else upgrade modal or 403 |
| `/dashboard/*` | Login; `AccountStatusFilter` for limited |

### 8.5 Filters (register in order)

1. `CorsFilter` (API)
2. `ApiAuthFilter` / session auth (web)
3. `AccountStatusFilter` — block non-preview actions when `limited`
4. `TenantFilter` — scope `org_id`
5. `RoleFilter` — admin-only routes
6. `EntitlementFilter` — plan tier + `can_manage_team`
7. `SeatLimitFilter` — on user create (Enterprise)
8. `RateLimitFilter` (P2)

---

## 9. UI / UX

| Surface | Behavior |
|---------|----------|
| Signup success | “Account created — limited preview until verified (usually 24h)” |
| Dashboard (limited) | Persistent banner + disabled/greyed actions with tooltip |
| Dashboard (active solo) | No Users in sidebar **or** Users item shows Enterprise upgrade |
| `/dashboard/users` | Wire to API; hide nav item unless Enterprise |
| Add User drawer | Only on Enterprise; role dropdown: Admin, Manager, Member, Viewer |
| Plan & Billing | Locked until `active`; upgrade to Enterprise unlocks team |
| Superadmin console | Simple table: name, email, company, domain, date, Approve/Reject |

---

## 10. Emails

| Template | Trigger |
|----------|---------|
| Welcome (limited) | Signup — explain preview + pending verification |
| Account activated | Superadmin approve |
| Account rejected | Superadmin reject |
| Team invitation | Enterprise admin adds user |
| Payment failed → locked | Stripe (Phase 3) |

---

## 11. Implementation sprints

### Sprint UM-1 — Schema + Shield + signup (≈1 week)

- [ ] Install Shield; migrations: `organizations`, users extensions, audit_logs
- [ ] Signup creates org + user (`limited`, `role=admin`, `seat_limit=1`)
- [ ] Migrate existing `aoll_users` → Shield (default `active` for legacy or `limited` for review)
- [ ] Session enriched: org_id, role, account_status, plan_tier, can_manage_team
- [ ] `auth_helper.php`: `auth_account_status()`, `auth_can_manage_team()`

### Sprint UM-2 — Limited access + approval (≈1 week)

- [ ] `AccountStatusFilter` + preview capability map
- [ ] Limited dashboard banner + gate search/export/billing APIs
- [ ] Platform superadmin queue (minimal internal UI or CLI `spark user:approve`)
- [ ] Approve/reject endpoints + audit log + emails
- [ ] Notify superadmin on new signup

### Sprint UM-3 — Auth hardening (≈1 week)

- [ ] Work-email blocklist; password bcrypt; reset token hardening
- [ ] Google OAuth → Shield; new Google users → `limited`
- [ ] Filters: ApiAuthFilter, TenantFilter, RoleFilter, CorsFilter
- [ ] API auth endpoints + web session bridge
- [ ] Email verification (optional P1)

### Sprint UM-4 — Entitlements + seats (≈1 week)

- [ ] `EntitlementService` — plan → feature map (include `team_management`, `api_access`)
- [ ] `EntitlementFilter` — 402 + upgrade hint
- [ ] Enforce `seat_limit`: 1 for S/P/I; block POST `/users` unless Enterprise
- [ ] Hide `/dashboard/users` nav unless Enterprise
- [ ] Domain dedup rules on signup

### Sprint UM-5 — Enterprise team management (≈1.5 weeks)

- [ ] `user_invitations` table + invite accept flow
- [ ] Full `/api/v1/users` CRUD
- [ ] Wire `/dashboard/users` to real data
- [ ] Role badges: Admin, Manager, Member, Viewer
- [ ] Seat limit check on invite; cannot remove last admin
- [ ] PHPUnit: approval flow, seat gate, Enterprise-only users API

### Sprint UM-6 — Billing tie-in (when Stripe lands)

- [ ] Checkout only when `account_status=active`
- [ ] Enterprise upgrade → increase `seat_limit`, enable Users
- [ ] `locked` status on payment failure

---

## 12. Testing checklist

- [ ] Signup → `limited` → cannot export/search (or preview cap only)
- [ ] Approve → `active` → Starter features unlocked, still 1 seat
- [ ] Professional admin cannot POST `/users` (403/402)
- [ ] Enterprise admin can invite; seat limit enforced
- [ ] Second user same domain does not create orphan org (or queues for superadmin)
- [ ] Non-admin roles cannot access billing or Users page
- [ ] Superadmin actions written to audit_logs

---

## 13. Doc updates still needed

- [ ] `architecture/DIAGRAMS-AND-ACCESS-AOLL.md` — remove owner-at-signup; add limited + platform superadmin
- [ ] `architecture/PRD-AOLL.md` — AUTH/signup assumptions, seat counts in §9.1
- [ ] `architecture/TECHNICAL_SPECIFICATION-AOLL.md` — §11.1 seats: S/P/I = 1
- [ ] `docs/ARCHITECTURE.md` — §7 auth + §8 entitlements

---

## 14. Resolved open questions

| # | Question | Decision |
|---|----------|----------|
| Q1 | First signup = owner? | **No** — limited → superadmin approve → solo admin |
| Q2 | Team management tier? | **Enterprise only** |
| Q3 | Seats on S/P/I? | **1 each** |
| Q4 | Who creates first admin for sales-led? | **Platform superadmin** |
| Q5 | Pay before approval? | **No** |
