# Implementation Plan: Global Policy Search **Feature**: 001-global-policy-search **Branch**: `001-global-policy-search` **Version**: 1.0.0 **Created**: 2025-12-05 --- ## Technical Context | Aspect | Decision | Notes | |--------|----------|-------| | Database ORM | Drizzle ORM | Existing in project | | Search Method | PostgreSQL `ilike` | Case-insensitive substring search | | Auth Provider | Azure AD (NextAuth) | Extract `tenantId` from `tid` claim | | UI Components | Shadcn UI | Input, Table, Card | | Data Fetching | Server Actions | Constitution: Server-First | | API Security | X-API-SECRET header | For n8n ingestion | --- ## Constitution Check | Principle | Compliant | Implementation | |-----------|-----------|----------------| | Server-First | ✅ | Server Actions for search, API route for ingestion | | TypeScript Strict | ✅ | Full types for schema, validators, actions | | Drizzle ORM | ✅ | `policySettings` table with proper indexes | | Shadcn UI | ✅ | Input, Table components for search UI | | Azure AD Multi-Tenancy | ✅ | tenantId from session, all queries filtered | --- ## Gate Evaluation ### Gate 1: Scope Check - [x] Feature spec exists: `specs/001-global-policy-search/spec.md` - [x] User stories defined: 3 stories (Search, Tenant Isolation, Ingestion API) - [x] Requirements numbered: F-001 through F-009 - **Result**: PASS ### Gate 2: Constitution Compliance - [x] No client-side data fetching for primary flows - [x] All types explicitly defined (no `any`) - [x] Database via Drizzle ORM only - [x] UI components from Shadcn library - [x] Tenant isolation enforced - **Result**: PASS ### Gate 3: Security Review - [x] Authentication required for search (getUserAuth) - [x] Tenant isolation in all queries (tenantId filter) - [x] API secret for ingestion endpoint (X-API-SECRET) - [x] Input validation with Zod schemas - **Result**: PASS --- ## Phase 0: Research (Complete) See: `specs/001-global-policy-search/research.md` Resolved: - Drizzle `ilike` for case-insensitive search - Server Actions pattern for search - API_SECRET header authentication - Azure AD tenantId extraction from session - Upsert with `onConflictDoUpdate` --- ## Phase 1: Design (Complete) ### Artifacts Generated | Artifact | Path | Status | |----------|------|--------| | Data Model | `specs/001-global-policy-search/data-model.md` | ✅ | | API Contract | `specs/001-global-policy-search/contracts/policy-settings-api.yaml` | ✅ | | Server Actions Contract | `specs/001-global-policy-search/contracts/server-actions.md` | ✅ | | Quickstart Guide | `specs/001-global-policy-search/quickstart.md` | ✅ | --- ## Phase 2: Implementation Tasks ### Task 1: Database Schema **File**: `lib/db/schema/policySettings.ts` **Priority**: P1 **Estimate**: 30 min - Create `policySettings` pgTable - Add indexes for tenant filtering and search - Export types: `PolicySetting`, `NewPolicySetting` - Run `npm run db:push` ### Task 2: Zod Validators **File**: `lib/validators/policySettings.ts` **Priority**: P1 **Estimate**: 15 min - `policySettingSchema` for single setting - `bulkPolicySettingsSchema` for API input - Export input types ### Task 3: Extend NextAuth Session **File**: `lib/auth/utils.ts` **Priority**: P1 **Estimate**: 20 min - Add `jwt` callback to extract `tid` claim - Add `session` callback to include `tenantId` - Update TypeScript types for extended session ### Task 4: Server Actions **File**: `lib/actions/policySettings.ts` **Priority**: P1 **Estimate**: 45 min - `searchPolicySettings(searchTerm)` with ilike query - `getPolicySettingById(id)` with tenant check - `getRecentPolicySettings(limit)` sorted by lastSyncedAt - Full tenant isolation in all queries ### Task 5: Ingestion API Route **File**: `app/api/policy-settings/route.ts` **Priority**: P2 **Estimate**: 30 min - POST handler with X-API-SECRET validation - Bulk upsert with `onConflictDoUpdate` - DELETE handler for tenant cleanup - Proper error responses (400, 401, 500) ### Task 6: Environment Variable **File**: `.env` (local), `lib/env.mjs` **Priority**: P2 **Estimate**: 10 min - Add `POLICY_API_SECRET` to .env - Add optional validation in env.mjs ### Task 7: Search Page UI **File**: `app/(app)/search/page.tsx` **Priority**: P1 **Estimate**: 45 min - Search input with debounce - Results table with columns - Loading and empty states - Error handling ### Task 8: Search Components **Files**: `components/search/*.tsx` **Priority**: P1 **Estimate**: 30 min - `SearchInput.tsx` - Input with search icon - `ResultsTable.tsx` - Table with policy data - `EmptyState.tsx` - No results message ### Task 9: Navigation Update **File**: `config/nav.ts` **Priority**: P1 **Estimate**: 5 min - Add search link to sidebar - Icon: Search or MagnifyingGlass --- ## Dependencies (Implementation Order) ``` Task 1 (Schema) ↓ Task 2 (Validators) ↓ Task 3 (Auth) ──────┬──→ Task 4 (Server Actions) │ └──→ Task 5 (API Route) ↓ Task 6 (Env Var) Task 4 (Server Actions) ↓ Task 7 (Search Page) ←── Task 8 (Components) ↓ Task 9 (Navigation) ``` --- ## Acceptance Criteria From spec requirements: - [ ] F-001: Full-text search across settings (ilike on name/value) - [ ] F-002: Display policy name, type, setting name/value - [ ] F-003: Debounced instant search (300ms) - [ ] F-004: User only sees own tenant's data - [ ] F-005: No tenant impersonation possible - [ ] F-006: API_SECRET header required for ingestion - [ ] F-007: Bulk upsert endpoint operational - [ ] F-008: Zod validation on all API inputs - [ ] F-009: Upsert logic prevents duplicates --- ## Test Plan ### Manual Testing 1. **Search Flow** - Login with Azure AD - Navigate to /search - Enter search term - Verify results from own tenant only 2. **Ingestion API** - POST without secret → 401 - POST with wrong secret → 401 - POST with correct secret → 200 - Verify data appears in search 3. **Tenant Isolation** - Ingest data for tenant A - Login as tenant B user - Search should return empty --- ## Environment Variables Required | Variable | Purpose | Required In | |----------|---------|-------------| | `POLICY_API_SECRET` | n8n ingestion auth | Production, Development | --- ## Rollback Plan If issues arise: 1. Revert branch to previous commit 2. Run `npm run db:push` to restore schema 3. No data migration needed (new table)