# Implementation Plan: Entra Group Directory Cache (Groups v1) **Branch**: `051-entra-group-directory-cache` | **Date**: 2026-01-11 | **Spec**: [specs/051-entra-group-directory-cache/spec.md](spec.md) **Input**: Feature specification from `/specs/051-entra-group-directory-cache/spec.md` **Note**: This template is filled in by the `/speckit.plan` command. See `.specify/scripts/` for helper scripts. ## Summary Provide a tenant-scoped Entra ID Groups metadata cache (no membership/owners) populated by queued sync runs (manual + scheduled), so the admin UI and other modules can resolve group IDs to friendly names without making live directory calls during page render. ## Technical Context **Language/Version**: PHP 8.4 (Laravel 12) **Primary Dependencies**: Filament v4, Livewire v3, Microsoft Graph integration via internal `GraphClientInterface` **Storage**: PostgreSQL **Testing**: Pest v4 **Target Platform**: Web application (Sail-first locally, Dokploy-first deploy) **Project Type**: web **Performance Goals**: background sync handles large tenants via paging; UI list/search remains responsive with DB indexes **Constraints**: no live directory calls during page render; app-only Graph auth; safe error summaries; strict tenant isolation; idempotent runs; retention purge after 90 days last-seen **Scale/Scope**: full-tenant group enumeration; plan for up to ~100k groups/tenant for v1 ## Constitution Check *GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.* - Inventory-first: cache is “last observed” directory state; no snapshot payloads. - Read/write separation: read-only to the directory; only internal DB writes. - Graph contract path: directory group list endpoint/fields/permissions must be added to `config/graph_contracts.php` and accessed via `GraphClientInterface`. - Deterministic capabilities: selection identifier for groups sync is deterministic (stable per tenant + groups-v1). - Tenant isolation: all cached groups + run records tenant-scoped; no cross-tenant access paths. - Automation: manual + scheduled sync runs use locks/idempotency, run records, safe error codes; handle 429/503 with backoff+jitter. - Data minimization: store group metadata only (no membership/owners); logs contain no secrets/tokens. **Gate status (pre-Phase 0)**: PASS (no violations). Re-check after Phase 1 design. **Gate status (post-Phase 1)**: PASS (design artifacts present: research.md, data-model.md, contracts/*, quickstart.md). ## Project Structure ### Documentation (this feature) ```text specs/051-entra-group-directory-cache/ ├── plan.md # This file (/speckit.plan command output) ├── research.md # Phase 0 output (/speckit.plan command) ├── data-model.md # Phase 1 output (/speckit.plan command) ├── quickstart.md # Phase 1 output (/speckit.plan command) ├── contracts/ # Phase 1 output (/speckit.plan command) └── tasks.md # Phase 2 output (/speckit.tasks command - NOT created by /speckit.plan) ``` ### Source Code (repository root) ```text app/ ├── Filament/ │ ├── Pages/ │ └── Resources/ ├── Jobs/ ├── Models/ ├── Services/ │ ├── Graph/ │ └── Directory/ └── Support/ config/ ├── graph.php └── graph_contracts.php database/ └── migrations/ routes/ └── web.php tests/ ├── Feature/ └── Unit/ ``` **Structure Decision**: Laravel web application. Implement directory group sync + cache as tenant-scoped Models, Services, Jobs, Filament pages/resources, migrations, and Pest tests. ## Key Implementation Decisions (Pinned) - **Schedule default**: daily at **02:00 UTC** (environment default) + manual sync always available. - **Auth mode**: app-only (service principal). UI must not require delegated tokens. - **Permission**: `Group.Read.All` (application). - **Graph strategy (v1)**: `GET /groups` with `$select=id,displayName,groupTypes,securityEnabled,mailEnabled` + paging via `@odata.nextLink`. - **Retention**: stale threshold default 30 days; purge after 90 days since `last_seen_at`. - **Render safety**: fail-hard test ensuring no Graph client calls during render. ## Execution Model ### Sync Run Lifecycle - Run statuses: `pending` → `running` → `succeeded` | `failed` (optionally `partial` if we intentionally record partial completion). - Each run records: initiator (nullable for scheduled), timestamps, observed/upserted/error counters, and a safe error summary + category. ### Idempotency & Concurrency - Deterministic selection key for v1: `groups-v1:all`. - Deduplication rule: at most one active run per tenant + selection key. - Scheduled dispatcher must be idempotent per tenant and schedule slot (no duplicate run creation). ### Error Categorization - Supported categories: `permission`, `throttling`, `transient`, `unknown`. - Store only safe-to-display summaries (no secrets/tokens). ## Definition of Done (per phase) ### Phase 2 (Foundational) - Migrations created for EntraGroup and EntraGroupSyncRun. - Models + factories exist and are tenant-scoped. - Graph contract registry has a groups directory read contract. ### Phase 3 (US1) - Manual sync creates a run and dispatches a job. - Job pages through Graph, upserts groups, updates counters, and enforces retention purge. - Filament run list/detail exists; scheduler dispatch is wired. - Pest tests for run creation, upsert behavior, retention purge, and scheduled run dispatch are green. ### Phase 4 (US2) - Directory → Groups list/detail exists with search + stale filter + type filter. - All browsing renders from DB cache only. ### Phase 5 (US3) - Shared label resolver resolves from DB cache and provides consistent fallback formatting. - Key pages render friendly labels without Graph calls. - Fail-hard render guard test is green. ## Complexity Tracking > **Fill ONLY if Constitution Check has violations that must be justified** N/A (no constitution violations anticipated)