# AOLL — Architecture Plan

> B2B sales-intelligence platform (ZoomInfo / Winmo class). Backend and front-end are
> **decoupled**: CodeIgniter 4 is a JSON REST API under `/api`; the front-end is a
> separate layer that fetches data from the API and renders it.

Status: **draft for review**. Grounded in the design prototype at
`public/aoll-design-assets/` (marketing site, auth, 13 dashboard screens, 5 email
templates) and the product spec. Placeholder data in the prototype is treated as the
data-model source of truth.

---

## 0. Current scope — FRONT-END FIRST

**We are building the front-end now. The backend/API and all third-party integrations
(Stripe, Claude, Google OAuth, CRM) are deferred and decided later.** Sections 6–9 and
§12 describe the *target* architecture so today's work is built toward it — but nothing
in this phase requires a database or an external account.

**How we keep it front-end-only without painting ourselves into a corner** (the senior-dev
move): the pages are *dynamic*, not dead static HTML. CI4 controllers render the ported
views and pull data from a **`MockDataProvider` per domain** that returns typed Entities
matching the future `/api` response contracts. Views and controllers code against a
**repository interface**; today it's backed by fixtures, later the same interface is
backed by an HTTP client to `/api/v1`. When the backend lands, we swap the provider
binding in `Config\Services` — **zero view or controller changes.**

```
View  ←  Controller  ←  CompanyRepositoryInterface
                              ├─ (now)   MockCompanyProvider   → PHP fixtures
                              └─ (later) ApiCompanyProvider     → GET /api/v1/companies
```

Decisions already made: front-end = **server-rendered CI4 pages** (§10); data =
**placeholder fixtures**; tenancy = **shared across org/team**; all integrations =
**deferred**.

---

## 1. Guiding principles

1. **API-first.** All data and business logic live behind `/api/v1/*`, returning JSON.
   The API is the single source of truth and is reusable by the web front-end, the
   Enterprise "API Access" feature, workflows, and future clients (mobile, partners).
2. **Thin controllers, fat services.** Controllers validate + shape HTTP; a Services
   layer holds business logic; Models do data access; Entities are typed domain objects.
3. **Multi-tenant from day one.** Every app record is scoped to an **organization**
   (the "workspace"). Users belong to one org; Admin/Non-Admin roles gate team actions.
4. **Entitlements, not hardcoded gates.** Feature access (intent, AI, CRM, workflows,
   API) is derived from the org's plan tier through one `EntitlementService`, so the
   free-vs-paid tiering table is data, not scattered `if`s.
5. **Reuse the prototype's look 1:1.** Bootstrap 5.3 + the existing `--ea-*` (site) and
   `--db-*` (dashboard) design tokens are kept as-is. We port markup, not redesign.

---

## 2. High-level topology

```
                         ┌─────────────────────────────┐
   Browser  ──────────▶  │  Front-end (aoll.com)        │
   (Bootstrap + JS)      │  server-rendered views       │
                         │  → calls /api via fetch/ajax │
                         └───────────────┬─────────────┘
                                         │ Bearer token (JSON)
                                         ▼
                         ┌─────────────────────────────┐
   /api/v1/*    ──────▶  │  CodeIgniter 4 REST API      │
                         │  Controllers → Services →    │
                         │  Models → Entities           │
                         └───────┬───────────┬─────────┘
                                 │           │
                        ┌────────▼───┐  ┌────▼──────────┐
                        │  MySQL     │  │ External svc  │
                        │  (domain + │  │ Stripe / SMTP │
                        │  tenant)   │  │ Google OAuth  │
                        └────────────┘  │ Claude (AI)   │
                                        │ CRM connectors│
                                        └───────────────┘
```

**Route separation** (the model you described):

| Prefix | Serves | Auth |
|---|---|---|
| `/api/v1/*` | JSON backend — data + logic | Bearer access token (Shield) |
| everything else (`/`, `/login`, `/app/*`, `/pricing`, …) | Front-end HTML pages | Session cookie (thin web layer that holds the token) |

One CI4 codebase, two route groups. This keeps deploy simple now while leaving the API
independently consumable. (If you later want a standalone SPA or a separate front-end
repo, nothing about the API changes — see §12 decision D1.)

---

## 3. Tech stack

| Concern | Choice | Why |
|---|---|---|
| Framework | CodeIgniter 4.7.4 (PHP 8.4) | Already scaffolded; fast, batteries-included |
| DB | MySQL 8 (MySQLi driver) | Default driver; relational fits the domain |
| Auth | **CodeIgniter Shield** | Official CI4 auth: users, sessions, **access tokens** (powers API + Enterprise API Access), groups (Admin/Non-Admin), password reset, remember-me |
| Google sign-in | `league/oauth2-google` bridged into Shield social identity | Matches "Continue with Google" on login/signup |
| Front-end | Bootstrap 5.3.3 + jQuery 3.6 (from prototype) | Reuse prototype as-is; pages fetch `/api` via AJAX |
| Billing | **Stripe** (Customer, Subscription, Invoice, PaymentMethod, webhooks) | Self-serve tiers, monthly/annual, dunning → maps to plan/billing screens |
| AI features | **Anthropic Claude API** behind `AiInsightService` | "AI Opportunity Insights" + "AI Outreach Generation" (Intelligence tier) |
| Email | CI4 Email + the 5 existing HTML templates | Welcome, payment ok/failed, reactivated, signed-up-not-subscribed |
| Scheduling | CI4 Tasks (`spark tasks:run` via cron) | Runs due workflows + digest/alert emails |
| Queue (phase 2+) | DB-backed jobs, optional Redis | Async workflow runs, AI calls, exports |

---

## 4. Directory / module layout

Keep the standard CI4 tree; organize by **controller namespace + a Services/Entities
layer**. (Full HMVC modules are an option if the team grows — noted in D6.)

```
app/
  Controllers/
    MarketingController, AuthPageController, DashboardController   ← front-end (server-rendered web pages)
    Api/               ← backend REST JSON API, mounted at /api/v1 (App\Controllers\Api)
      Contracts/RestResourceInterface   ← index/show/create/update/delete contract
      BaseApiController                 ← ResponseTrait + JSON envelope; 501 default verbs
      CompanyController, BrandController, AgencyController, SignalController,
      TopicController, FeedController, SavedSearchController, AlertController,
      WorkflowController, ConnectorController, PlanController, UserController,
      FallbackController (JSON 404)
      — built (read endpoints, off Mock providers); write verbs → 501 until Part B
      — planned (Part B): Auth, Contact, Search, Subscription, Invoice,
        Setting, Ai, Webhook (stripe/crm)
  Services/            ← business logic (framework-agnostic where possible)
    SearchService, SignalService, IntentService, FeedService,
    SavedSearchService, AlertService, WorkflowEngine, ConnectorService,
    BillingService, EntitlementService, UsageService, AiInsightService,
    NotificationService, EmailService
  Models/              ← CI4 Models (one per table/aggregate)
  Entities/            ← typed domain objects (Company, Contact, Signal, Plan, …)
  Filters/             ← ApiAuthFilter, TenantFilter, EntitlementFilter, RoleFilter,
                          RateLimitFilter, CorsFilter
  Database/
    Migrations/        ← schema (§6)
    Seeds/             ← reference data (plans, integrations, topics) + demo data
  Config/
    Routes.php         ← two groups: api/v1 + web (§5)
    Cors.php, Filters.php (register filters), Services.php (DI)
  Views/
    layouts/           site.php, dashboard.php, auth.php
    partials/          dashboard/sidebar.php, dashboard/header.php, site/nav.php, footer.php
    marketing/         home, pricing, features/*, about, contact, demo, blog, legal
    auth/              login, signup, forgot
    app/               home(feed), search, company-details, signals, saved, alerts,
                        workflows, connectors, plan, manage-subscription, users, settings
    emails/            welcome, payment-successful, payment-unsuccessful,
                        subscription-reactivated, signed-up-not-subscribed
public/
  assets/              ← ported css/js/images (site --ea-*, dashboard --db-*), paths via base_url()
```

**Asset porting notes** (from the prototype): rewrite all *relative* `assets/...` hrefs to
`base_url('assets/...')`; the header/footer nav is currently injected by `main.js` into
empty divs — convert to server-rendered `partials/` for SEO and no-JS; consolidate the
duplicated site/landing/dashboard asset copies into one `public/assets` tree.

---

## 5. Routing

Target design below (full API-first surface). The **current build** implements the
read-only subset — `resource()` routes for companies/brands/agencies/signals/topics/
saved-searches/alerts/workflows/connectors/plans/users + `GET feed` under namespace
`App\Controllers\Api` — without the auth/tenant/filter layer yet (Part B).

```php
// API — JSON, versioned, token-auth
$routes->group('api/v1', ['namespace' => 'App\Controllers\Api',
                          'filter' => ['cors','apiauth','tenant']], function($r){
  $r->post('auth/login', 'AuthController::login');          // returns access token
  $r->post('auth/google', 'AuthController::google');
  $r->post('auth/register', 'AuthController::register');
  $r->post('auth/forgot', 'AuthController::forgot');

  $r->get('feed', 'FeedController::index');                 // home intent feed
  $r->resource('companies');  $r->get('companies/(:num)/intent', 'CompanyController::intent/$1');
  $r->resource('brands');     $r->resource('agencies');     $r->resource('contacts');
  $r->post('search', 'SearchController::query');            // audience + filters → results
  $r->resource('saved-searches');
  $r->get('signals', 'SignalController::index');            // signal explorer (filters, date range)
  $r->resource('topics');  $r->get('topics/mine', 'TopicController::mine');
  $r->resource('alerts');  $r->post('alerts/(:num)/read', 'AlertController::markRead/$1');
  $r->resource('workflows'); $r->post('workflows/(:num)/run', 'WorkflowController::run/$1');
  $r->get('connectors', 'ConnectorController::catalog');
  $r->resource('connections');
  $r->get('plans', 'PlanController::index');
  $r->resource('subscription'); $r->resource('invoices'); $r->resource('payment-methods');
  $r->resource('users');                                     // team mgmt (Admin only via RoleFilter)
  $r->get('settings', 'SettingController::show'); $r->put('settings', 'SettingController::update');
  $r->post('ai/insights/(:num)', 'AiController::insights/$1');   // Intelligence tier (EntitlementFilter)
  $r->post('ai/outreach/(:num)', 'AiController::outreach/$1');
});

// Webhooks (no tenant/auth filter; verified by signature)
$routes->post('api/webhooks/stripe', 'Api\WebhookController::stripe');

// Front-end (server-rendered; session-authed)
$routes->group('', ['namespace' => 'App\Controllers'], function($r){
  $r->get('/', 'MarketingController::home');
  $r->get('pricing', 'MarketingController::pricing');       // + features/*, about, contact, demo, blog, legal
  $r->get('login', 'AuthPageController::login');            // + signup, forgot
  $r->group('app', ['filter' => 'websession'], function($r){
    $r->get('/', 'DashboardController::feed');                    // dashboard home
    $r->get('search', 'DashboardController::search');             // ?tab=companies|brands|agencies
    $r->get('company/(:num)', 'DashboardController::company/$1');
    // signals, saved, alerts, workflows, connectors, plan, subscription, users, settings …
  });
});
```

Filters, in order: `cors → apiauth (Bearer) → tenant (scope org_id) → entitlement/role`
per route. `EntitlementFilter` blocks tier-gated endpoints with `402/403 + upgrade hint`.

---

## 6. Data model

Grouped by concern. PKs are `id BIGINT UNSIGNED AUTO_INCREMENT`; all app tables carry
`org_id` (tenant scope) + timestamps unless noted. JSON columns used for flexible
filter/config blobs.

### Identity & tenancy
- **organizations** — id, name, slug, created_at *(the workspace)*
- **users** *(Shield-backed)* — id, org_id, first_name, last_name, email, password_hash,
  role `admin|non_admin`, status `active|invited|disabled`, avatar_color, created_at
- **auth_identities / auth_access_tokens / auth_remember_tokens** — Shield tables
  (email+password, Google `oauth_google`, and the API Bearer tokens)
- **notification_preferences** — user_id, signal_alerts, weekly_digest, product_updates,
  billing_account *(bool each)*

### Data domain (the intelligence database)
- **companies** — name, website, hq_city, hq_state, country, primary_industry,
  employee_range, employee_count, revenue, revenue_range, yoy_growth,
  revenue_per_employee, founded_year, funding_stage, ticker, phone, description,
  avatar_color *(NOT org-scoped — shared reference data)*
- **brands** — name, parent_company_id→companies, category, markets, media_spend, website
- **agencies** — name, hq_city, hq_state, type `holding|network|full_service`,
  employee_count, billings, website
- **contacts** — company_id, first_name, last_name, title, seniority
  `c_level|vp|director|manager|non_manager`, email, phone, linkedin_url,
  has_mobile, has_direct_dial, has_business_email, avatar_color
- **technologies** — name, category · **company_technologies** — (company_id, technology_id)
- **intent_topics** — name, category, weekly_company_count *(38-topic library; reference)*
- **company_intent** — company_id, topic_id, last_signal_date, digital_signal_score(0–100),
  audience_strength(1–5), audience_surges, spikes_in_org, accounts_count, period
- **business_events** *(Scoops)* — company_id, event_type
  `funding|leadership|product|hiring|expansion|ma|other`, title, description, event_date
- **event_ai_insights** — business_event_id, opportunity_type, services_needed(json),
  budget_estimate *(AI layer; generated)*
- **outreach_recommendations** — company_id, business_event_id, recipient_role,
  match_score, subject, body, tags(json) *(AI layer)*

### User-generated app data (org-scoped)
- **saved_searches** — org_id, user_id, audience `companies|brands|agencies`, name,
  description, filters(json), is_favorite, timestamps
- **saved_search_alerts** — saved_search_id, enabled, triggers(json:
  contacts_added/companies_added/scoops), frequency `daily|weekly`, day_of_week,
  email_enabled *(this drives the "Email Alerts" tab)*
- **saved_feed_items** — user_id, subject_type, subject_id, saved_at *(bookmarked signals)*
- **feed_preferences** — user_id, industries(json), locations(json), company_size_min/max,
  media_spend_min/max, topics(json), technologies(json) *(the "Edit Feed" modal)*
- **user_topics** — user_id, topic_id, assigned_at *("My Topics" subscriptions)*
- **alerts** — org_id, company_id, category *(event taxonomy)*, title, description,
  alert_date, is_read *(in-app pipeline alerts)*

### Workflows
- **workflows** — org_id, user_id, name, trigger_type `intent|saved_search|scoops`,
  trigger_config(json), frequency `daily|weekly`, schedule_day, schedule_time,
  action_type `discover_contacts|hubspot|salesforce|webhook`, action_config(json),
  status `active|inactive`, last_run_at
- **workflow_runs** — workflow_id, run_at, status, records_affected, result(json), error

### Integrations
- **integrations** — key, name, category, brand_color, is_popular, description *(catalog seed)*
- **connections** — org_id, integration_id, status `connected|disconnected|error`,
  auth_data(json, encrypted), connected_by→users, created_at

### Billing
- **plans** — key `starter|professional|intelligence|enterprise`, name, description,
  monthly_price, annual_price, is_popular, features(json), limit_searches,
  limit_contact_views, limit_exports, sort_order *(seed from pricing table)*
- **subscriptions** — org_id, plan_id, billing_period `monthly|annual`,
  status `active|past_due|canceled|paused`, current_period_start/end, renews_at,
  stripe_customer_id, stripe_subscription_id
- **invoices** — org_id, subscription_id, statement_no, invoice_date, amount, currency,
  status `paid|failed|open`, description, stripe_invoice_id, pdf_url
- **payment_methods** — org_id, brand, last4, exp_month, exp_year, holder_name, zip,
  stripe_pm_id, is_default
- **usage_counters** — org_id, period(YYYY-MM), searches_used, contact_views_used,
  exports_used *(EntitlementService enforcement)*

### Marketing leads
- **contact_requests** — name, email, company, topic, message
- **demo_requests** — first_name, last_name, email, phone, company, job_title, country, team_size
- **newsletter_subscribers** — email
- **blog_posts** *(optional)* — slug, title, excerpt, body, cover_image, author,
  published_at, is_featured

**Shared enums** (constants class): event/alert/scoop categories, workflow trigger &
action types, frequency, plan tier, contact seniority.

---

## 7. Auth & multi-tenancy

- **Shield** issues an **access token** on login (`/api/v1/auth/login`) and on Google
  sign-in. Every API request carries `Authorization: Bearer <token>`; `ApiAuthFilter`
  resolves the user, `TenantFilter` pins `org_id` so services never leak cross-tenant.
- The **web layer** logs in via Shield session, requests a token server-side, and stores
  it in the session; page JS calls `/api` using that token (never exposed in markup as a
  long-lived secret — proxied or short-lived).
- **Roles**: `admin` (team management, billing, connectors, users) vs `non_admin`.
  `RoleFilter` guards admin-only API routes.
- **Enterprise "API Access"** = the same Shield access tokens, surfaced in Settings for
  Enterprise-tier orgs to call `/api/v1` directly.

---

## 8. Entitlements (free-vs-paid tiering)

`EntitlementService::can(org, 'intent_signals')` reads the org's active subscription →
plan → a **feature map** seeded from the tiering table:

| Capability | Min tier |
|---|---|
| Basic search, limited contact views, limited exports | Starter |
| Full company/contact intelligence, intent signals, brand/agency + media spend, saved searches & alerts | Professional |
| AI insights & outreach, custom alerts, CRM integrations, workflows | Intelligence |
| API access, team management, custom feeds, dedicated support | Enterprise |

Volume limits (searches / contact views / exports per month) come from `plans.limit_*`
and are counted in `usage_counters` by `UsageService`; `EntitlementFilter` returns
`402 Payment Required` + an upgrade hint when a cap or tier gate is hit.

---

## 9. Subsystem designs

- **Search** (`SearchService`): one endpoint, `audience ∈ {companies,brands,agencies}` +
  filter set (name, industry, employees, revenue, media spend, location, technology,
  contact, email). Returns three result facets (Contacts / Companies / Scoops) with
  pagination + "search within results". Save → `saved_searches` (+ optional alert).
- **Signals / Intent** (`SignalService`, `IntentService`): the Signal Explorer reads
  `company_intent` joined to topics with date-range presets, signal score, and the 1–5
  audience-strength meter; "My Topics" manages `user_topics` against the topic library.
- **Home feed** (`FeedService`): merges recent `business_events` for the org's ICP
  (via `feed_preferences`) with the intent overview; supports save-a-signal.
- **Alerts** (`AlertService` + `NotificationService`): categorized in-app alerts +
  email; "Email Alerts" tab is the enable/disable view over `saved_search_alerts`.
- **Workflows** (`WorkflowEngine`): trigger (intent/saved-search/scoops) → optional
  discover-contacts → action (Salesforce/HubSpot/webhook/discover). A scheduled
  `spark workflows:run` task finds due workflows, executes via `ConnectorService`,
  logs to `workflow_runs`. Enable/disable = `status`.
- **Connectors** (`ConnectorService`): catalog from `integrations`; OAuth-style connect
  stores encrypted tokens in `connections`; Salesforce/HubSpot/webhook push adapters.
- **Billing** (`BillingService`): Stripe-backed; plan CRUD is read-only from `plans`;
  upgrade/downgrade schedules at next cycle; webhooks reconcile `subscriptions`,
  `invoices`, `payment_methods` and fire the transactional emails.
- **AI** (`AiInsightService`): Claude generates event opportunity analysis
  (opportunity type, services needed, budget estimate, suggested pitch) and outreach
  drafts (subject/body/match score). Cached per event; Intelligence-tier gated. Stubbed
  behind an interface for phase 1 so the UI works before spend is wired.

---

## 10. Front-end integration

Server-rendered pages ported from the prototype render the shell (layout + partials);
page-specific JS (extending the existing `dashboard/assets/js/*`) calls `/api/v1/*` and
hydrates tables/cards. This preserves the exact Bootstrap look while making every screen
data-driven. Two token sets kept separate: site `--ea-*`, dashboard `--db-*`.

Email: the 5 HTML templates become CI4 view templates; `{{placeholders}}` → CI4 view
vars; images hosted absolute under `public/assets/email/`.

---

## 11. Build roadmap

### Part A — Front-end (current scope)

Server-rendered CI4 pages ported from the prototype, dynamic via `MockDataProvider`s.

| Phase | Deliverable |
|---|---|
| **A0 — Foundation** | `.env` from `env`; port assets to `public/assets` + rewrite paths to `base_url()`; build 3 layouts (site / dashboard / auth) + partials (sidebar, header, nav, footer) as server-rendered views; wire base routes. |
| **A1 — Mock data layer** | Repository interfaces per domain + `Mock*Provider` fixtures returning typed Entities that mirror the future `/api` contracts; bind in `Config\Services`. |
| **A2 — Auth & app shell** | Login / signup / forgot pages (form UI only); dashboard shell live (sidebar active-states, header, theme + collapse persistence); Settings screens. |
| **A3 — Search & company detail** | Advanced Search (companies/brands/agencies tabs, filters, results facets, save/export UI); company-details (5 tabs incl. AI Insights & Intent UI). |
| **A4 — Signals & home feed** | Signal Explorer + My Topics; home intent feed, Edit-Feed modal, save-a-signal, intent overview rail. |
| **A5 — Alerts, workflows, saved, connectors** | Alert center + email alerts; workflow list + builder canvas; Saved (searches + feed); Connectors catalog + connect screens. |
| **A6 — Billing & team** | Plan & Billing, Manage Subscription (payment method, invoices, modals); Users & roles; plan cards + comparison. |
| **A7 — Marketing site** | Home, pricing (billing toggle), feature pages, about/contact/demo/blog/legal; lead-capture form UI; port the 5 email templates. |

### Part B — Backend (deferred; for approval later)

API under `/api/v1` + Shield auth/tokens, MySQL schema (§6), then swap each
`Mock*Provider` for an `Api*Provider`; entitlements + usage; then integrations
(Stripe, Claude, Google OAuth, CRM) and the Enterprise public API — each a discrete,
separately-approvable work item.

---

## 12. Decisions

**Resolved:**
- **D1 — Front-end shape:** server-rendered CI4 pages (matches the Bootstrap/jQuery prototype).
- **D2 — Data source:** placeholder fixtures now via `MockDataProvider`, pluggable for a real source later.
- **D5 — Tenancy scope:** saved searches / workflows / alerts are **shared across the org/team**.
- **Scope:** front-end first (Part A); backend + integrations deferred (Part B).

**Deferred to backend phase (Part B) — do not need answers now:**
- Payments provider (Stripe assumed), AI provider (Claude assumed), Google OAuth,
  CRM connectors — all decided when we start Part B.

**Open for this phase:**
- **D6 — Module packaging:** standard `app/` tree with controller namespaces
  (recommended) vs full CI4 HMVC modules per domain.
- **D7 — Dynamic vs static:** dynamic pages backed by `MockDataProvider` (recommended —
  makes the swap-in-backend trivial) vs a plain static HTML port. The roadmap assumes
  dynamic.
