command: sed -n '900,1120p' specs/01-DATABASE-SCHEMA.md

aggregated_output:
| order_line_id | INTEGER | No | - | FK -> order_lines(id) ON DELETE CASCADE | Source order line |
| quantity | INTEGER | No | 1 | - | Units fulfilled from this line |

**Indexes:**

- `idx_fulfillment_lines_fulfillment_id` on (`fulfillment_id`) - FK lookup, listing lines per fulfillment
- `idx_fulfillment_lines_fulfillment_order_line` UNIQUE on (`fulfillment_id`, `order_line_id`) - Prevents duplicate line entries per fulfillment

**Notes:** An order with multiple line items may be partially fulfilled. This table tracks how many units of each line item are included in each shipment.

---

## Epic 7 - Analytics

### analytics_events

Raw event stream for storefront analytics.

| Column | Type | Nullable | Default | Constraints | Notes |
|---|---|---|---|---|---|
| id | INTEGER | No | autoincrement | PRIMARY KEY | |
| store_id | INTEGER | No | - | FK -> stores(id) ON DELETE CASCADE | Owning store |
| type | TEXT | No | - | - | Event type (see notes) |
| session_id | TEXT | Yes | NULL | - | Browser session identifier |
| customer_id | INTEGER | Yes | NULL | FK -> customers(id) ON DELETE SET NULL | Nullable for anonymous visitors |
| properties_json | TEXT | No | '{}' | - | Event-specific data as JSON |
| client_event_id | TEXT | Yes | NULL | UNIQUE per (store_id, client_event_id) | Client-provided idempotency key for deduplication |
| occurred_at | TEXT | Yes | NULL | - | ISO-8601 timestamp of when the event actually occurred on the client |
| created_at | TEXT | Yes | NULL | - | ISO-8601 timestamp |

**Indexes:**

- `idx_analytics_events_store_id` on (`store_id`) - FK lookup
- `idx_analytics_events_store_type` on (`store_id`, `type`) - Filtering events by type per store
- `idx_analytics_events_store_created` on (`store_id`, `created_at`) - Time-range event queries
- `idx_analytics_events_session` on (`session_id`) - Session-based event grouping
- `idx_analytics_events_customer` on (`customer_id`) - Customer activity history
- `idx_analytics_events_client_event` UNIQUE on (`store_id`, `client_event_id`) - Client-side event deduplication

**Notes:** Event types include: `page_view`, `product_view`, `add_to_cart`, `remove_from_cart`, `checkout_started`, `checkout_completed`, `search`. This table will grow fast - consider periodic archiving or partitioning by date for production workloads beyond SQLite.

---

### analytics_daily

Pre-aggregated daily metrics per store. Composite primary key.

| Column | Type | Nullable | Default | Constraints | Notes |
|---|---|---|---|---|---|
| store_id | INTEGER | No | - | PK (composite), FK -> stores(id) ON DELETE CASCADE | |
| date | TEXT | No | - | PK (composite) | ISO-8601 date (YYYY-MM-DD) |
| orders_count | INTEGER | No | 0 | - | Number of orders placed |
| revenue_amount | INTEGER | No | 0 | - | Total revenue in minor units |
| aov_amount | INTEGER | No | 0 | - | Average order value in minor units |
| visits_count | INTEGER | No | 0 | - | Unique sessions |
| add_to_cart_count | INTEGER | No | 0 | - | Add-to-cart events |
| checkout_started_count | INTEGER | No | 0 | - | Checkout-started events |
| checkout_completed_count | INTEGER | No | 0 | - | Checkout-completed events |

**Primary Key:** (`store_id`, `date`)

**Indexes:**

- `idx_analytics_daily_store_date` on (`store_id`, `date`) - Time-range metric queries (covered by PK but explicit for clarity)

**Notes:** `date` is an ISO-8601 date string (YYYY-MM-DD). `revenue_amount` and `aov_amount` (average order value) are in minor units (cents). This table is populated by a scheduled aggregation job.

---

## Epic 8 - Apps and Webhooks

### apps

Third-party or first-party application registry.

| Column | Type | Nullable | Default | Constraints | Notes |
|---|---|---|---|---|---|
| id | INTEGER | No | autoincrement | PRIMARY KEY | |
| name | TEXT | No | - | - | App display name |
| status | TEXT | No | 'active' | CHECK IN ('active', 'disabled') | |
| created_at | TEXT | Yes | NULL | - | ISO-8601 timestamp |

**Indexes:**

- `idx_apps_status` on (`status`) - Filtering apps by status

---

### app_installations

Installation of an app on a specific store.

| Column | Type | Nullable | Default | Constraints | Notes |
|---|---|---|---|---|---|
| id | INTEGER | No | autoincrement | PRIMARY KEY | |
| store_id | INTEGER | No | - | FK -> stores(id) ON DELETE CASCADE | |
| app_id | INTEGER | No | - | FK -> apps(id) ON DELETE CASCADE | |
| scopes_json | TEXT | No | '[]' | - | JSON array of granted permission scopes |
| status | TEXT | No | 'active' | CHECK IN ('active', 'suspended', 'uninstalled') | |
| installed_at | TEXT | Yes | NULL | - | ISO-8601 timestamp |

**Indexes:**

- `idx_app_installations_store_app` UNIQUE on (`store_id`, `app_id`) - One installation per app per store
- `idx_app_installations_store_id` on (`store_id`) - Listing installations per store
- `idx_app_installations_app_id` on (`app_id`) - Finding all stores with an app installed

---

### oauth_clients

OAuth2 client credentials for apps.

| Column | Type | Nullable | Default | Constraints | Notes |
|---|---|---|---|---|---|
| id | INTEGER | No | autoincrement | PRIMARY KEY | |
| app_id | INTEGER | No | - | FK -> apps(id) ON DELETE CASCADE | Parent app |
| client_id | TEXT | No | - | UNIQUE per table | OAuth2 client identifier |
| client_secret_encrypted | TEXT | No | - | - | Encrypted client secret |
| redirect_uris_json | TEXT | No | '[]' | - | JSON array of allowed redirect URIs |

**Indexes:**

- `idx_oauth_clients_client_id` UNIQUE on (`client_id`) - OAuth2 client lookup
- `idx_oauth_clients_app_id` on (`app_id`) - FK lookup, listing clients per app

---

### oauth_tokens

Issued access/refresh tokens for app installations.

| Column | Type | Nullable | Default | Constraints | Notes |
|---|---|---|---|---|---|
| id | INTEGER | No | autoincrement | PRIMARY KEY | |
| installation_id | INTEGER | No | - | FK -> app_installations(id) ON DELETE CASCADE | Parent installation |
| access_token_hash | TEXT | No | - | UNIQUE per table | Hashed access token |
| refresh_token_hash | TEXT | Yes | NULL | - | Hashed refresh token |
| expires_at | TEXT | No | - | - | ISO-8601 expiration timestamp |

**Indexes:**

- `idx_oauth_tokens_installation_id` on (`installation_id`) - FK lookup, listing tokens per installation
- `idx_oauth_tokens_access_hash` UNIQUE on (`access_token_hash`) - Token validation lookup
- `idx_oauth_tokens_expires_at` on (`expires_at`) - Cleanup of expired tokens

---

### webhook_subscriptions

Registered webhook endpoints for event delivery.

| Column | Type | Nullable | Default | Constraints | Notes |
|---|---|---|---|---|---|
| id | INTEGER | No | autoincrement | PRIMARY KEY | |
| store_id | INTEGER | No | - | FK -> stores(id) ON DELETE CASCADE | Owning store |
| app_installation_id | INTEGER | Yes | NULL | FK -> app_installations(id) ON DELETE CASCADE | Nullable for store-level hooks |
| event_type | TEXT | No | - | - | Event to subscribe to |
| target_url | TEXT | No | - | - | Delivery endpoint URL |
| signing_secret_encrypted | TEXT | No | - | - | Encrypted HMAC signing secret |
| status | TEXT | No | 'active' | CHECK IN ('active', 'paused', 'disabled') | |

**Indexes:**

- `idx_webhook_subscriptions_store_id` on (`store_id`) - FK lookup
- `idx_webhook_subscriptions_store_event` on (`store_id`, `event_type`) - Finding subscriptions for a given event
- `idx_webhook_subscriptions_installation` on (`app_installation_id`) - Listing subscriptions per app installation

---

### webhook_deliveries

Delivery log for webhook attempts.

| Column | Type | Nullable | Default | Constraints | Notes |
|---|---|---|---|---|---|
| id | INTEGER | No | autoincrement | PRIMARY KEY | |
| subscription_id | INTEGER | No | - | FK -> webhook_subscriptions(id) ON DELETE CASCADE | Parent subscription |
| event_id | TEXT | No | - | - | Unique event identifier |
| attempt_count | INTEGER | No | 1 | - | Number of delivery attempts |
| status | TEXT | No | 'pending' | CHECK IN ('pending', 'success', 'failed') | |
| last_attempt_at | TEXT | Yes | NULL | - | ISO-8601 timestamp |
| response_code | INTEGER | Yes | NULL | - | HTTP response code |
| response_body_snippet | TEXT | Yes | NULL | - | Truncated response body for debugging |

**Indexes:**

- `idx_webhook_deliveries_subscription_id` on (`subscription_id`) - FK lookup, listing deliveries per subscription
- `idx_webhook_deliveries_event_id` on (`event_id`) - Deduplication and lookup by event
- `idx_webhook_deliveries_status` on (`status`) - Finding pending/failed deliveries for retry
- `idx_webhook_deliveries_last_attempt` on (`last_attempt_at`) - Time-based retry scheduling

---

## Laravel Framework Tables

The following tables are managed by Laravel's built-in migrations and should not be modified.

### sessions

| Column | Type | Nullable | Default | Constraints | Notes |
|---|---|---|---|---|---|
| id | TEXT | No | - | PRIMARY KEY | Session ID |
| user_id | INTEGER | Yes | NULL | - | Associated user |
| ip_address | TEXT | Yes | NULL | - | Client IP |
| user_agent | TEXT | Yes | NULL | - | Client user agent |
| payload | TEXT | No | - | - | Serialized session data |
| last_activity | INTEGER | No | - | - | Unix timestamp |

### cache

| Column | Type | Nullable | Default | Constraints | Notes |
|---|---|---|---|---|---|
| key | TEXT | No | - | PRIMARY KEY | Cache key |
| value | TEXT | No | - | - | Serialized cache value |
| expiration | INTEGER | No | - | - | Unix timestamp |

### cache_locks

| Column | Type | Nullable | Default | Constraints | Notes |
|---|---|---|---|---|---|


command: rg -n "app_installations|oauth_clients|webhook_subscriptions|webhook_deliveries|Apps|Developer|Webhook|webhook|Phase 10|apps" specs

type: search

cmd: rg -n 'app_installations|oauth_clients|webhook_subscriptions|webhook_deliveries|Apps|Developer|Webhook|webhook|Phase 10|apps' specs

query: app_installations|oauth_clients|webhook_subscriptions|webhook_deliveries|Apps|Developer|Webhook|webhook|Phase 10|apps

path: specs

aggregated_output:
specs/01-DATABASE-SCHEMA.md:969:## Epic 8 - Apps and Webhooks
specs/01-DATABASE-SCHEMA.md:971:### apps
specs/01-DATABASE-SCHEMA.md:984:- `idx_apps_status` on (`status`) - Filtering apps by status
specs/01-DATABASE-SCHEMA.md:988:### app_installations
specs/01-DATABASE-SCHEMA.md:996:| app_id | INTEGER | No | - | FK -> apps(id) ON DELETE CASCADE | |
specs/01-DATABASE-SCHEMA.md:1003:- `idx_app_installations_store_app` UNIQUE on (`store_id`, `app_id`) - One installation per app per store
specs/01-DATABASE-SCHEMA.md:1004:- `idx_app_installations_store_id` on (`store_id`) - Listing installations per store
specs/01-DATABASE-SCHEMA.md:1005:- `idx_app_installations_app_id` on (`app_id`) - Finding all stores with an app installed
specs/01-DATABASE-SCHEMA.md:1009:### oauth_clients
specs/01-DATABASE-SCHEMA.md:1011:OAuth2 client credentials for apps.
specs/01-DATABASE-SCHEMA.md:1016:| app_id | INTEGER | No | - | FK -> apps(id) ON DELETE CASCADE | Parent app |
specs/01-DATABASE-SCHEMA.md:1023:- `idx_oauth_clients_client_id` UNIQUE on (`client_id`) - OAuth2 client lookup
specs/01-DATABASE-SCHEMA.md:1024:- `idx_oauth_clients_app_id` on (`app_id`) - FK lookup, listing clients per app
specs/01-DATABASE-SCHEMA.md:1035:| installation_id | INTEGER | No | - | FK -> app_installations(id) ON DELETE CASCADE | Parent installation |
specs/01-DATABASE-SCHEMA.md:1048:### webhook_subscriptions
specs/01-DATABASE-SCHEMA.md:1050:Registered webhook endpoints for event delivery.
specs/01-DATABASE-SCHEMA.md:1056:| app_installation_id | INTEGER | Yes | NULL | FK -> app_installations(id) ON DELETE CASCADE | Nullable for store-level hooks |
specs/01-DATABASE-SCHEMA.md:1064:- `idx_webhook_subscriptions_store_id` on (`store_id`) - FK lookup
specs/01-DATABASE-SCHEMA.md:1065:- `idx_webhook_subscriptions_store_event` on (`store_id`, `event_type`) - Finding subscriptions for a given event
specs/01-DATABASE-SCHEMA.md:1066:- `idx_webhook_subscriptions_installation` on (`app_installation_id`) - Listing subscriptions per app installation
specs/01-DATABASE-SCHEMA.md:1070:### webhook_deliveries
specs/01-DATABASE-SCHEMA.md:1072:Delivery log for webhook attempts.
specs/01-DATABASE-SCHEMA.md:1077:| subscription_id | INTEGER | No | - | FK -> webhook_subscriptions(id) ON DELETE CASCADE | Parent subscription |
specs/01-DATABASE-SCHEMA.md:1087:- `idx_webhook_deliveries_subscription_id` on (`subscription_id`) - FK lookup, listing deliveries per subscription
specs/01-DATABASE-SCHEMA.md:1088:- `idx_webhook_deliveries_event_id` on (`event_id`) - Deduplication and lookup by event
specs/01-DATABASE-SCHEMA.md:1089:- `idx_webhook_deliveries_status` on (`status`) - Finding pending/failed deliveries for retry
specs/01-DATABASE-SCHEMA.md:1090:- `idx_webhook_deliveries_last_attempt` on (`last_attempt_at`) - Time-based retry scheduling
specs/01-DATABASE-SCHEMA.md:1428:### Epics 7-8 - Analytics, Apps, and Webhooks
specs/01-DATABASE-SCHEMA.md:1435:    apps ||--o{ app_installations : "installed via"
specs/01-DATABASE-SCHEMA.md:1436:    stores ||--o{ app_installations : "has"
specs/01-DATABASE-SCHEMA.md:1437:    apps ||--o{ oauth_clients : "authenticates via"
specs/01-DATABASE-SCHEMA.md:1438:    app_installations ||--o{ oauth_tokens : "issues"
specs/01-DATABASE-SCHEMA.md:1439:    stores ||--o{ webhook_subscriptions : "registers"
specs/01-DATABASE-SCHEMA.md:1440:    app_installations ||--o{ webhook_subscriptions : "owns"
specs/01-DATABASE-SCHEMA.md:1441:    webhook_subscriptions ||--o{ webhook_deliveries : "delivers"
specs/01-DATABASE-SCHEMA.md:1455:    apps {
specs/01-DATABASE-SCHEMA.md:1460:    app_installations {
specs/01-DATABASE-SCHEMA.md:1466:    webhook_subscriptions {
specs/01-DATABASE-SCHEMA.md:1472:    webhook_deliveries {
specs/01-DATABASE-SCHEMA.md:1515:        apps
specs/01-DATABASE-SCHEMA.md:1537:        app_installations
specs/01-DATABASE-SCHEMA.md:1538:        oauth_clients
specs/01-DATABASE-SCHEMA.md:1554:        webhook_subscriptions
specs/01-DATABASE-SCHEMA.md:1565:        webhook_deliveries
specs/01-DATABASE-SCHEMA.md:1581:    apps --> app_installations
specs/01-DATABASE-SCHEMA.md:1582:    apps --> oauth_clients
specs/01-DATABASE-SCHEMA.md:1597:    stores --> app_installations
specs/01-DATABASE-SCHEMA.md:1611:    app_installations --> oauth_tokens
specs/01-DATABASE-SCHEMA.md:1612:    stores --> webhook_subscriptions
specs/01-DATABASE-SCHEMA.md:1613:    app_installations --> webhook_subscriptions
specs/01-DATABASE-SCHEMA.md:1627:    webhook_subscriptions --> webhook_deliveries
specs/01-DATABASE-SCHEMA.md:1643:- **Batch 1** (no dependencies): `organizations`, `apps`
specs/01-DATABASE-SCHEMA.md:1645:- **Batch 3** (depends on Batch 2): `store_domains`, `store_users`, `store_settings`, `customers`, `themes`, `pages`, `navigation_menus`, `search_settings`, `shipping_zones`, `tax_settings`, `discounts`, `products`, `collections`, `app_installations`, `oauth_clients`
specs/01-DATABASE-SCHEMA.md:1646:- **Batch 4** (depends on Batch 3): `product_options`, `product_variants…15484 chars truncated…                                      |
specs/03-ADMIN-UI.md:2002:- Webhook subscriptions table: event type, URL, status
specs/03-ADMIN-UI.md:2005:**Empty state:** "No apps installed" with descriptive text.
specs/03-ADMIN-UI.md:2009:## 16. Developers Page
specs/03-ADMIN-UI.md:2013:**Livewire component:** `App\Livewire\Admin\Developers\Index`
specs/03-ADMIN-UI.md:2022:| `webhooks` | `Collection` | Webhook subscriptions |
specs/03-ADMIN-UI.md:2025:| `webhookEventType` | `string` | Event type for webhook form |
specs/03-ADMIN-UI.md:2026:| `webhookUrl` | `string` | URL for webhook form |
specs/03-ADMIN-UI.md:2027:| `editingWebhook` | `?WebhookSubscription` | Webhook being edited |
specs/03-ADMIN-UI.md:2033:| `openWebhookModal(?WebhookSubscription $webhook)` | Opens webhook create/edit modal |
specs/03-ADMIN-UI.md:2034:| `saveWebhook()` | Saves webhook subscription |
specs/03-ADMIN-UI.md:2035:| `deleteWebhook(int $webhookId)` | Deletes a webhook |
specs/03-ADMIN-UI.md:2041:| Developers                                                        |
specs/03-ADMIN-UI.md:2061:| Manage webhook subscriptions for real-time event notifications.  |
specs/03-ADMIN-UI.md:2068:| [+ Add webhook]                                                   |
specs/03-ADMIN-UI.md:2093:**Webhooks section:**
specs/03-ADMIN-UI.md:2094:- Heading "Webhooks" (size lg)
specs/03-ADMIN-UI.md:2096:- Webhook table with columns:
specs/03-ADMIN-UI.md:2105:- "Add webhook" button opens modal
specs/03-ADMIN-UI.md:2107:#### Webhook Modal
specs/03-ADMIN-UI.md:2111:| Modal name | `webhook-form` |
specs/03-ADMIN-UI.md:2113:| Heading | "Edit webhook" or "Add webhook" (dynamic) |
specs/03-ADMIN-UI.md:2117:- Endpoint URL input (type: url, placeholder: "https://example.com/webhooks")
specs/03-ADMIN-UI.md:2425:| `App\Livewire\Admin\Apps\Index` | `livewire.admin.apps.index` | `GET /admin/apps` |
specs/03-ADMIN-UI.md:2426:| `App\Livewire\Admin\Developers\Index` | `livewire.admin.developers.index` | `GET /admin/developers` |
specs/05-BUSINESS-LOGIC.md:80:products, collections, customers, orders, carts, checkouts, discounts, shipping_zones, themes, pages, navigation_menus, analytics_events, analytics_daily, webhook_subscriptions, inventory_items, search_queries.
specs/05-BUSINESS-LOGIC.md:96:| Manage apps | Yes | Yes | No | No |
specs/05-BUSINESS-LOGIC.md:933:The platform uses a **Mock Payment Service Provider (PSP)** instead of real payment gateways. All payment processing happens in-process with no external API calls, no webhooks, and no redirects. This enables a fully functional checkout flow for development, testing, and demonstration purposes.
specs/05-BUSINESS-LOGIC.md:1345:## 13. Webhook Delivery System
specs/05-BUSINESS-LOGIC.md:1349:Stores (or app installations) subscribe to event types via `webhook_subscriptions`.
specs/05-BUSINESS-LOGIC.md:1366:**Listener:** `App\Listeners\DispatchWebhooks` - registered to listen for all domain events that have webhook counterparts.
specs/05-BUSINESS-LOGIC.md:1368:**Job:** `App\Jobs\DeliverWebhook`
specs/05-BUSINESS-LOGIC.md:1379:    Listener->>Listener: Query webhook_subscriptions for store_id + event_type where status = active
specs/05-BUSINESS-LOGIC.md:1381:        Listener->>Queue: Dispatch DeliverWebhook job
specs/05-BUSINESS-LOGIC.md:1388:    Job->>Job: Record result in webhook_deliveries
specs/05-BUSINESS-LOGIC.md:1421:- If a subscription's endpoint fails 5 consecutive times: automatically set `webhook_subscriptions.status = 'paused'`.
specs/05-BUSINESS-LOGIC.md:1426:**Service:** `App\Services\WebhookService`
specs/05-BUSINESS-LOGIC.md:1594:| `OrderCreated` | Send confirmation email, dispatch webhooks (`order.created`), log analytics event |
specs/05-BUSINESS-LOGIC.md:1595:| `OrderRefunded` | Send refund notification email, dispatch webhooks (`order.refunded`), optionally restock |
specs/05-BUSINESS-LOGIC.md:1598:| `CheckoutCompleted` | Dispatch webhooks (`checkout.completed`) |
specs/05-BUSINESS-LOGIC.md:1601:| `ProductCreated` | Sync FTS5 index, dispatch webhooks (`product.created`) |
specs/05-BUSINESS-LOGIC.md:1602:| `ProductUpdated` | Sync FTS5 index, dispatch webhooks (`product.updated`) |
specs/05-BUSINESS-LOGIC.md:1603:| `ProductDeleted` | Remove from FTS5 index, dispatch webhooks (`product.deleted`) |
specs/05-BUSINESS-LOGIC.md:1606:| `FulfillmentShipped` | Send shipping notification email with tracking info, dispatch webhooks (`order.fulfilled` if fully fulfilled) |
specs/05-BUSINESS-LOGIC.md:1607:| `FulfillmentDelivered` | Update fulfillment record, dispatch webhooks |
specs/05-BUSINESS-LOGIC.md:1628:| `App\Jobs\DeliverWebhook` | POST webhook payload to subscriber endpoint |
specs/05-BUSINESS-LOGIC.md:1654:| WebhookService | `App\Services` | Webhook delivery, circuit breaker |
specs/05-BUSINESS-LOGIC.md:1690:| WebhookSubscriptionStatus | Active (`active`), Paused (`paused`), Disabled (`disabled`) |
specs/05-BUSINESS-LOGIC.md:1706:| WebhookResult | success (bool), message (string) | Webhook processing result |

status: completed


command:
/bin/zsh
-lc
sed -n '520,680p' specs/03-ADMIN-UI.md

type: read

cmd: sed -n '520,680p' specs/03-ADMIN-UI.md


command:
/bin/zsh
-lc
git status --short

type: unknown

cmd: git status --short


output:
Chunk ID: a16ead
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 0
Output:


output:
Chunk ID: 9045a7
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 6371
Output:
specs/01-DATABASE-SCHEMA.md:969:## Epic 8 - Apps and Webhooks
specs/01-DATABASE-SCHEMA.md:971:### apps
specs/01-DATABASE-SCHEMA.md:984:- `idx_apps_status` on (`status`) - Filtering apps by status
specs/01-DATABASE-SCHEMA.md:988:### app_installations
specs/01-DATABASE-SCHEMA.md:996:| app_id | INTEGER | No | - | FK -> apps(id) ON DELETE CASCADE | |
specs/01-DATABASE-SCHEMA.md:1003:- `idx_app_installations_store_app` UNIQUE on (`store_id`, `app_id`) - One installation per app per store
specs/01-DATABASE-SCHEMA.md:1004:- `idx_app_installations_store_id` on (`store_id`) - Listing installations per store
specs/01-DATABASE-SCHEMA.md:1005:- `idx_app_installations_app_id` on (`app_id`) - Finding all stores with an app installed
specs/01-DATABASE-SCHEMA.md:1009:### oauth_clients
specs/01-DATABASE-SCHEMA.md:1011:OAuth2 client credentials for apps.
specs/01-DATABASE-SCHEMA.md:1016:| app_id | INTEGER | No | - | FK -> apps(id) ON DELETE CASCADE | Parent app |
specs/01-DATABASE-SCHEMA.md:1023:- `idx_oauth_clients_client_id` UNIQUE on (`client_id`) - OAuth2 client lookup
specs/01-DATABASE-SCHEMA.md:1024:- `idx_oauth_clients_app_id` on (`app_id`) - FK lookup, listing clients per app
specs/01-DATABASE-SCHEMA.md:1035:| installation_id | INTEGER | No | - | FK -> app_installations(id) ON DELETE CASCADE | Parent installation |
specs/01-DATABASE-SCHEMA.md:1048:### webhook_subscriptions
specs/01-DATABASE-SCHEMA.md:1050:Registered webhook endpoints for event delivery.
specs/01-DATABASE-SCHEMA.md:1056:| app_installation_id | INTEGER | Yes | NULL | FK -> app_installations(id) ON DELETE CASCADE | Nullable for store-level hooks |
specs/01-DATABASE-SCHEMA.md:1064:- `idx_webhook_subscriptions_store_id` on (`store_id`) - FK lookup
specs/01-DATABASE-SCHEMA.md:1065:- `idx_webhook_subscriptions_store_event` on (`store_id`, `event_type`) - Finding subscriptions for a given event
specs/01-DATABASE-SCHEMA.md:1066:- `idx_webhook_subscriptions_installation` on (`app_installation_id`) - Listing subscriptions per app installation
specs/01-DATABASE-SCHEMA.md:1070:### webhook_deliveries
specs/01-DATABASE-SCHEMA.md:1072:Delivery log for webhook attempts.
specs/01-DATABASE-SCHEMA.md:1077:| subscription_id | INTEGER | No | - | FK -> webhook_subscriptions(id) ON DELETE CASCADE | Parent subscription |
specs/01-DATABASE-SCHEMA.md:1087:- `idx_webhook_deliveries_subscription_id` on (`subscription_id`) - FK lookup, listing deliveries per subscription
specs/01-DATABASE-SCHEMA.md:1088:- `idx_webhook_deliveries_event_id` on (`event_id`) - Deduplication and lookup by event
specs/01-DATABASE-SCHEMA.md:1089:- `idx_webhook_deliveries_status` on (`status`) - Finding pending/failed deliveries for retry
specs/01-DATABASE-SCHEMA.md:1090:- `idx_webhook_deliveries_last_attempt` on (`last_attempt_at`) - Time-based retry scheduling
specs/01-DATABASE-SCHEMA.md:1428:### Epics 7-8 - Analytics, Apps, and Webhooks
specs/01-DATABASE-SCHEMA.md:1435:    apps ||--o{ app_installations : "installed via"
specs/01-DATABASE-SCHEMA.md:1436:    stores ||--o{ app_installations : "has"
specs/01-DATABASE-SCHEMA.md:1437:    apps ||--o{ oauth_clients : "authenticates via"
specs/01-DATABASE-SCHEMA.md:1438:    app_installations ||--o{ oauth_tokens : "issues"
specs/01-DATABASE-SCHEMA.md:1439:    stores ||--o{ webhook_subscriptions : "registers"
specs/01-DATABASE-SCHEMA.md:1440:    app_installations ||--o{ webhook_subscriptions : "owns"
specs/01-DATABASE-SCHEMA.md:1441:    webhook_subscriptions ||--o{ webhook_deliveries : "delivers"
specs/01-DATABASE-SCHEMA.md:1455:    apps {
specs/01-DATABASE-SCHEMA.md:1460:    app_installations {
specs/01-DATABASE-SCHEMA.md:1466:    webhook_subscriptions {
specs/01-DATABASE-SCHEMA.md:1472:    webhook_deliveries {
specs/01-DATABASE-SCHEMA.md:1515:        apps
specs/01-DATABASE-SCHEMA.md:1537:        app_installations
specs/01-DATABASE-SCHEMA.md:1538:        oauth_clients
specs/01-DATABASE-SCHEMA.md:1554:        webhook_subscriptions
specs/01-DATABASE-SCHEMA.md:1565:        webhook_deliveries
specs/01-DATABASE-SCHEMA.md:1581:    apps --> app_installations
specs/01-DATABASE-SCHEMA.md:1582:    apps --> oauth_clients
specs/01-DATABASE-SCHEMA.md:1597:    stores --> app_installations
specs/01-DATABASE-SCHEMA.md:1611:    app_installations --> oauth_tokens
specs/01-DATABASE-SCHEMA.md:1612:    stores --> webhook_subscriptions
specs/01-DATABASE-SCHEMA.md:1613:    app_installations --> webhook_subscriptions
specs/01-DATABASE-SCHEMA.md:1627:    webhook_subscriptions --> webhook_deliveries
specs/01-DATABASE-SCHEMA.md:1643:- **Batch 1** (no dependencies): `organizations`, `apps`
specs/01-DATABASE-SCHEMA.md:1645:- **Batch 3** (depends on Batch 2): `store_domains`, `store_users`, `store_settings`, `customers`, `themes`, `pages`, `navigation_menus`, `search_settings`, `shipping_zones`, `tax_settings`, `discounts`, `products`, `collections`, `app_installations`, `oauth_clients`
specs/01-DATABASE-SCHEMA.md:1646:- **Batch 4** (depends on Batch 3): `product_options`, `product_variants`, `product_media`, `collection_products`, `customer_addresses`, `carts`, `navigation_items`, `theme_files`, `theme_settings`, `search_queries`, `shipping_rates`, `oauth_tokens`, `webhook_subscriptions`
specs/01-DATABASE-SCHEMA.md:1647:- **Batch 5** (depends on Batch 4): `product_option_values`, `inventory_items`, `cart_lines`, `checkouts`, `orders`, `analytics_events`, `analytics_daily`, `webhook_deliveries`
specs/01-DATABASE-SCHEMA.md:1663:5. **Date-based indexes for time series** - Tables with temporal queries (orders, analytics, search queries, webhook deliveries) include indexes on date/timestamp columns scoped by store for efficient range scans.
specs/01-DATABASE-SCHEMA.md:1680:| 8 - Apps and Webhooks | apps, app_installations, oauth_clients, oauth_tokens, webhook_subscriptions, webhook_deliveries | 6 |
specs/09-IMPLEMENTATION-ROADMAP.md:24:| OAuth (apps) | Passport | Deferred/Stubbed | Simplifies initial build, can add later |
specs/09-IMPLEMENTATION-ROADMAP.md:50:    P10[Phase 10: Apps and Webhooks]
specs/09-IMPLEMENTATION-ROADMAP.md:176:Applied on: products, collections, customers, orders, carts, checkouts, discounts, shipping_zones, themes, pages, navigation_menus, analytics_events, analytics_daily, webhook_subscriptions, inventory_items, search_queries.
specs/09-IMPLEMENTATION-ROADMAP.md:237:| Manage apps | Y | Y | N | N |
specs/09-IMPLEMENTATION-ROADMAP.md:672:- No external API calls, no webhooks, no redirects
specs/09-IMPLEMENTATION-ROADMAP.md:850:| `Admin\Apps\Index` | App directory |
specs/09-IMPLEMENTATION-ROADMAP.md:851:| `Admin\Apps\Show` | Installed app detail |
specs/09-IMPLEMENTATION-ROADMAP.md:852:| `Admin\Developers\Index` | API token management, webhook subscriptions |
specs/09-IMPLEMENTATION-ROADMAP.md:919:### Phase 10: Apps and Webhooks
specs/09-IMPLEMENTATION-ROADMAP.md:925:1. `create_apps_table`
specs/09-IMPLEMENTATION-ROADMAP.md:926:2. `create_app_installations_table` - FK: `store_id`, `app_id`
specs/09-IMPLEMENTATION-ROADMAP.md:927:3. `create_oauth_clients_table` - FK: `app_id`
specs/09-IMPLEMENTATION-ROADMAP.md:929:5. `create_webhook_subscriptions_table` - FK: `store_id`, `app_installation_id`
specs/09-IMPLEMENTATION-ROADMAP.md:930:6. `create_webhook_deliveries_table` - FK: `subscription_id`
specs/09-IMPLEMENTATION-ROADMAP.md:932:#### Step 10.2: Webhook Service
specs/09-IMPLEMENTATION-ROADMAP.md:934:**File to create:** `App\Services\WebhookService`
specs/09-IMPLEMENTATION-ROADMAP.md:940:| `verify(string $payload, string $signature, string $secret): bool` | Verify incoming webhook signatures |
specs/09-IMPLEMENTATION-ROADMAP.md:942:**File to create:** `App\Jobs\DeliverWebhook`
specs/09-IMPLEMENTATION-ROADMAP.md:948:- Records response in `webhook_deliveries`
specs/09-IMPLEMENTATION-ROADMAP.md:1001:| `tests/Feature/Webhooks/` | `WebhookDeliveryTest.php`, `WebhookSignatureTest.php` |
specs/09-IMPLEMENTATION-ROADMAP.md:1610:#### tests/Feature/Webhooks/WebhookDeliveryTest.php
specs/09-IMPLEMENTATION-ROADMAP.md:1614:| delivers a webhook to a subscribed URL | mock HTTP client; subscription for event_type "order.created"; dispatch event | HTTP POST made to target_url | happy path |
specs/09-IMPLEMENTATION-ROADMAP.md:1615:| signs the payload with HMAC | deliver webhook | `X-Platform-Signature` header present with valid signature | happy path |
specs/09-IMPLEMENTATION-ROADMAP.md:1622:#### tests/Feature/Webhooks/WebhookSignatureTest.php
specs/09-IMPLEMENTATION-ROADMAP.md:1734:| `WebhookSubscription` | Webhook subscription per store |
specs/09-IMPLEMENTATION-ROADMAP.md:1735:| `WebhookDelivery` | Webhook delivery log |
specs/09-IMPLEMENTATION-ROADMAP.md:1770:| `Admin/Apps/Index` | App directory |
specs/09-IMPLEMENTATION-ROADMAP.md:1771:| `Admin/Apps/Show` | Installed app detail |
specs/09-IMPLEMENTATION-ROADMAP.md:1772:| `Admin/Developers/Index` | API token and webhook management |
specs/09-IMPLEMENTATION-ROADMAP.md:1847:| `DeliverWebhook` | HTTP POST webhook with retry logic |
specs/09-IMPLEMENTATION-ROADMAP.md:1919:| `Webhooks/WebhookDeliveryTest` | Webhook delivery feature tests |
specs/09-IMPLEMENTATION-ROADMAP.md:1920:| `Webhooks/WebhookSignatureTest` | Webhook signature feature tests |
specs/09-IMPLEMENTATION-ROADMAP.md:1944:- Configure rate limiters: `login` (5/min per IP), `api.admin` (60/min per token), `api.storefront` (120/min per IP), `checkout` (10/min per session), `search` (30/min per IP), `analytics` (60/min per IP), `webhooks` (100/min per IP)
specs/09-IMPLEMENTATION-ROADMAP.md:1952:| `routes/api.php` | Storefront API (prefix `/api/storefront/v1`), Admin API (prefix `/api/admin/v1`), Webhook receiver routes |
specs/09-IMPLEMENTATION-ROADMAP.md:1982:| `WebhookService` | Event dispatch, HMAC signing, delivery management |
specs/06-AUTH-AND-SECURITY.md:222:    [*] --> Created: Admin creates token in Developers section
specs/06-AUTH-AND-SECURITY.md:234:- Tokens are created in the admin panel, under the Developers section.
specs/06-AUTH-AND-SECURITY.md:363:| Apps | List / View | Y | Y | N | N |
specs/06-AUTH-AND-SECURITY.md:364:| Apps | Install / Remove | Y | Y | N | N |
specs/06-AUTH-AND-SECURITY.md:366:| Developers / API | Manage | Y | Y | N | N |
specs/06-AUTH-AND-SECURITY.md:382:| OwnerOrAdmin | Owner, Admin | Settings, themes, staff, developers, navigation, apps, shipping, taxes, search, delete/archive actions |
specs/06-AUTH-AND-SECURITY.md:552:| `manage-developers` | Owner, Admin | Create/revoke API tokens, manage webhooks |
specs/06-AUTH-AND-SECURITY.md:558:| `manage-apps` | Owner, Admin | Install and remove third-party apps |
specs/06-AUTH-AND-SECURITY.md:617:| Webhook routes | `api`, `throttle:webhooks` | Reserved for outbound app webhooks (no inbound payment webhooks - Mock PSP is in-process) |
specs/06-AUTH-AND-SECURITY.md:744:| `webhooks` | 100 per minute | IP address | Outbound app webhook endpoints (reserved; no inbound payment webhooks - Mock PSP is in-process) |
specs/06-AUTH-AND-SECURITY.md:767:| WebhookSubscription | `signing_secret_encrypted` | `encrypted` | Per-subscription signing secrets for outbound webhooks |
specs/06-AUTH-AND-SECURITY.md:933:## 7. Webhook Signature Verification
specs/06-AUTH-AND-SECURITY.md:935:### 7.1 Inbound Payment Webhooks
specs/06-AUTH-AND-SECURITY.md:937:> **Not applicable.** All payment processing happens in-process via the Mock PSP. There are no inbound payment webhooks from external providers (no Stripe, no PayPal). This section is preserved for reference only -- if a real PSP is integrated in the future, inbound webhook verification would follow the patterns described in the outbound webhook section below (HMAC signature verification).
specs/06-AUTH-AND-SECURITY.md:941:### 7.2 Platform Webhooks (Outbound)
specs/06-AUTH-AND-SECURITY.md:943:Service: `App\Services\WebhookDispatcher`
specs/06-AUTH-AND-SECURITY.md:945:When the platform sends webhooks to external subscribers (third-party apps, integrations).
specs/06-AUTH-AND-SECURITY.md:1041:| `webhook.signature_invalid` | Outbound webhook delivery failed signature verification at subscriber | subscriber_url, ip |
specs/06-AUTH-AND-SECURITY.md:1100:| 6 | Rate limiting | Seven named limiters (login, api.admin, api.storefront, checkout, search, analytics, webhooks) | Required |
specs/06-AUTH-AND-SECURITY.md:1108:| 14 | Webhook signature verification | HMAC-SHA256 for outbound platform webhooks (no inbound payment webhooks - Mock PSP is in-process) | Required |
specs/progress.md:10:- Active slice: Phase 7 - apps/webhooks
specs/progress.md:22:7. Search/analytics/apps/webhooks: SQLite search, event aggregation, developer token/webhook UI, delivery jobs, and tests.
specs/progress.md:29:| Database schema | `specs/01-DATABASE-SCHEMA.md` | partial | Phase 1 tenancy/auth tables, Phase 2 catalog tables, Phase 3 theme/content/navigation tables, Phase 4 cart/checkout/pricing tables, Phase 5 order/payment/fulfillment tables, and Phase 7 search/analytics tables are implemented: search_settings, search_queries, SQLite FTS5 `products_fts`, analytics_events, and analytics_daily. App/webhook tables are still missing. |
specs/progress.md:30:| Routes/API | `specs/02-API-ROUTES.md` | partial | Phase 1 auth routes plus catalog/cart/order/customer/discount/content/settings/search/analytics UI routes exist: `/`, `/collections`, `/collections/{handle}`, `/products/{handle}`, `/cart`, `/checkout`, `/checkout/confirmation/{order}`, `/search`, `/pages/{handle}`, `/account`, `/account/orders/{order}`, `/admin/products`, `/admin/products/create`, `/admin/products/{product}/edit`, `/admin/collections`, `/admin/collections/create`, `/admin/collections/{collection}/edit`, `/admin/inventory`, `/admin/orders`, `/admin/orders/{order}`, `/admin/customers`, `/admin/customers/{customer}`, `/admin/discounts`, `/admin/discounts/create`, `/admin/discounts/{discount}/edit`, `/admin/pages`, `/admin/pages/create`, `/admin/pages/{page}/edit`, `/admin/navigation`, `/admin/themes`, `/admin/themes/{theme}/editor`, `/admin/settings`, `/admin/settings/shipping`, `/admin/settings/taxes`, `/admin/search/settings`, `/admin/analytics`. Storefront REST API now includes cart line CRUD, checkout address/shipping/discount/payment-method selection, checkout payment completion, token-gated order lookup, product search, search suggestions, and analytics event ingestion under `/api/storefront/v1`. Admin REST API now includes store-scoped order list/detail, fulfillment creation, and refund creation under `/api/admin/v1/stores/{store}`. App/webhook APIs and broader admin REST surfaces are still missing. |
specs/progress.md:31:| Admin UI | `specs/03-ADMIN-UI.md` | partial | Flux/Livewire admin shell now includes dashboard KPI/date-range reporting, analytics reporting with filters/export, catalog/product/collection/inventory management, order/customer/discount management, page index/form, navigation menu editor, theme grid, theme settings editor with iframe preview, general settings, domain management, shipping zone/rate management, tax settings, and search settings/reindexing with auth protection and store scoping. Apps admin surfaces, checkout/notification settings tabs, advanced theme file editing, and product media upload UI are still missing. |
specs/progress.md:37:| Roadmap phases | `specs/09-IMPLEMENTATION-ROADMAP.md` | in progress | Phase 1 foundation, Phase 2 catalog data/UI, Phase 3 theme/content/navigation data with storefront consumption, Phase 4 cart/checkout/pricing backend foundation, Phase 4 storefront cart/checkout UI through `payment_selected`, the Phase 4 cart/checkout REST API surface, cart-page estimates, Phase 5 order/payment backend foundation, Phase 5 refund/fulfillment services, Phase 5 storefront order completion/customer order surfaces, Phase 5 admin order management, Phase 5 order API surfaces, Phase 6 admin dashboard/customer/discount/content/settings/theme/navigation management, Phase 7 search, and Phase 7 analytics are implemented. Phase 7 apps/webhooks surfaces are next. |
specs/progress.md:278:- App/webhook APIs and broader admin REST endpoints outside order management are still missing.
specs/progress.md:287:Not complete. Phase 1 foundation, Phase 2 catalog data/UI surfaces, Phase 3 storefront theme/content/navigation data, the Phase 4 cart/checkout/pricing backend foundation, cart/checkout storefront UI through order completion, cart-page estimates, cart/checkout REST APIs, Phase 5 order/payment backend foundation, Phase 5 refund/fulfillment services, customer order views, admin dashboard/order/customer/discount/content/settings/theme/navigation/search/analytics management, storefront search and analytics APIs, and order API surfaces are implemented, with known auth/token, media UI, advanced theme/navigation/settings, app/webhook API, browser-test, and database CHECK constraint gaps tracked above.
specs/08-PLAYWRIGHT-E2E-PLAN.md:3167:5. **Payment testing:** All payments are processed in-process via the Mock PSP. For credit card tests, use the magic card number `4242 4242 4242 4242` for success, `4000 0000 0000 0002` for decline, and `4000 0000 0000 9995` for insufficient funds. No external API calls, webhooks, or redirects are involved.
specs/02-API-ROUTES.md:117:#### Apps
specs/02-API-ROUTES.md:121:| GET | `/admin/apps` | Apps directory - list installed and available apps | `Admin\Apps\Index` |
specs/02-API-ROUTES.md:122:| GET | `/admin/apps/{installation}` | Installed app detail - scopes, webhooks, usage | `Admin\Apps\Show` |
specs/02-API-ROUTES.md:124:#### Developers
specs/02-API-ROUTES.md:128:| GET | `/admin/developers` | Developer settings - API keys/tokens, webhook management | `Admin\Developers\Index` |
specs/02-API-ROUTES.md:130:> **Note:** Webhook subscription management (CRUD) is available exclusively through the admin UI at `/admin/developers`. There are no REST API endpoints for programmatic webhook subscription management in the initial implementation.
specs/02-API-ROUTES.md:850:Process payment for a checkout using the selected payment method. All payment processing happens in-process via the Mock PSP - no external API calls, no redirects, no webhooks.
specs/02-API-ROUTES.md:1212:These endpoints provide programmatic access for integrations, CLI tools, and third-party apps. The admin web UI uses Livewire actions directly and does not call these endpoints.
specs/02-API-ROUTES.md:2897:## 4. Webhook Receiver Endpoints
specs/02-API-ROUTES.md:2899:> **Note:** No inbound webhook endpoints are needed. All payment processing happens in-process via the Mock PSP -- there are no external payment provider callbacks. The webhook infrastructure defined in this section is reserved for **outbound** app webhooks (see Section 5, OAuth / App Ecosystem).
specs/02-API-ROUTES.md:2907:These endpoints implement OAuth2 authorization for third-party apps, powered by Laravel Passport.
specs/02-API-ROUTES.md:2979:Third-party apps use OAuth tokens to access store data via scoped endpoints.
specs/02-API-ROUTES.md:2983:| GET | `/api/apps/v1/stores/{storeId}/products` | `read-products` | List products (same schema as admin API) |
specs/02-API-ROUTES.md:2984:| GET | `/api/apps/v1/stores/{storeId}/orders` | `read-orders` | List orders |
specs/02-API-ROUTES.md:2985:| GET | `/api/apps/v1/stores/{storeId}/customers` | `read-customers` | List customers |
specs/02-API-ROUTES.md:3086:| `webhooks` | 100 requests | 1 minute | IP address | Reserved for future outbound app webhook endpoints |
specs/02-API-ROUTES.md:3130:| *(no inbound webhook endpoints - mock PSP)* | - |
specs/02-API-ROUTES.md:3173:- **App API:** Versioned via URL path (`/api/apps/v1/`).
specs/02-API-ROUTES.md:3174:- **Outbound webhook payloads:** Include a `api_version` field in outbound webhook deliveries to app installations. Each app installation pins to a version.
specs/03-ADMIN-UI.md:72:|  [squares-2x2]  Apps          |
specs/03-ADMIN-UI.md:73:|  [code-bracket] Developers    |
specs/03-ADMIN-UI.md:97:Apps                             -> admin.apps.index
specs/03-ADMIN-UI.md:98:Developers                       -> admin.developers.index
specs/03-ADMIN-UI.md:1957:## 15. Apps Page
specs/03-ADMIN-UI.md:1959:**Route:** `GET /admin/apps`
specs/03-ADMIN-UI.md:1961:**Livewire component:** `App\Livewire\Admin\Apps\Index`
specs/03-ADMIN-UI.md:1963:**Blade view:** `resources/views/livewire/admin/apps/index.blade.php`
specs/03-ADMIN-UI.md:1969:| `installedApps` | `Collection` | Apps installed for this store |
specs/03-ADMIN-UI.md:1979:| Apps                                                              |
specs/03-ADMIN-UI.md:2002:- Webhook subscriptions table: event type, URL, status
specs/03-ADMIN-UI.md:2005:**Empty state:** "No apps installed" with descriptive text.
specs/03-ADMIN-UI.md:2009:## 16. Developers Page
specs/03-ADMIN-UI.md:2013:**Livewire component:** `App\Livewire\Admin\Developers\Index`
specs/03-ADMIN-UI.md:2022:| `webhooks` | `Collection` | Webhook subscriptions |
specs/03-ADMIN-UI.md:2025:| `webhookEventType` | `string` | Event type for webhook form |
specs/03-ADMIN-UI.md:2026:| `webhookUrl` | `string` | URL for webhook form |
specs/03-ADMIN-UI.md:2027:| `editingWebhook` | `?WebhookSubscription` | Webhook being edited |
specs/03-ADMIN-UI.md:2033:| `openWebhookModal(?WebhookSubscription $webhook)` | Opens webhook create/edit modal |
specs/03-ADMIN-UI.md:2034:| `saveWebhook()` | Saves webhook subscription |
specs/03-ADMIN-UI.md:2035:| `deleteWebhook(int $webhookId)` | Deletes a webhook |
specs/03-ADMIN-UI.md:2041:| Developers                                                        |
specs/03-ADMIN-UI.md:2061:| Manage webhook subscriptions for real-time event notifications.  |
specs/03-ADMIN-UI.md:2068:| [+ Add webhook]                                                   |
specs/03-ADMIN-UI.md:2093:**Webhooks section:**
specs/03-ADMIN-UI.md:2094:- Heading "Webhooks" (size lg)
specs/03-ADMIN-UI.md:2096:- Webhook table with columns:
specs/03-ADMIN-UI.md:2105:- "Add webhook" button opens modal
specs/03-ADMIN-UI.md:2107:#### Webhook Modal
specs/03-ADMIN-UI.md:2111:| Modal name | `webhook-form` |
specs/03-ADMIN-UI.md:2113:| Heading | "Edit webhook" or "Add webhook" (dynamic) |
specs/03-ADMIN-UI.md:2117:- Endpoint URL input (type: url, placeholder: "https://example.com/webhooks")
specs/03-ADMIN-UI.md:2425:| `App\Livewire\Admin\Apps\Index` | `livewire.admin.apps.index` | `GET /admin/apps` |
specs/03-ADMIN-UI.md:2426:| `App\Livewire\Admin\Developers\Index` | `livewire.admin.developers.index` | `GET /admin/developers` |
specs/05-BUSINESS-LOGIC.md:80:products, collections, customers, orders, carts, checkouts, discounts, shipping_zones, themes, pages, navigation_menus, analytics_events, analytics_daily, webhook_subscriptions, inventory_items, search_queries.
specs/05-BUSINESS-LOGIC.md:96:| Manage apps | Yes | Yes | No | No |
specs/05-BUSINESS-LOGIC.md:933:The platform uses a **Mock Payment Service Provider (PSP)** instead of real payment gateways. All payment processing happens in-process with no external API calls, no webhooks, and no redirects. This enables a fully functional checkout flow for development, testing, and demonstration purposes.
specs/05-BUSINESS-LOGIC.md:1345:## 13. Webhook Delivery System
specs/05-BUSINESS-LOGIC.md:1349:Stores (or app installations) subscribe to event types via `webhook_subscriptions`.
specs/05-BUSINESS-LOGIC.md:1366:**Listener:** `App\Listeners\DispatchWebhooks` - registered to listen for all domain events that have webhook counterparts.
specs/05-BUSINESS-LOGIC.md:1368:**Job:** `App\Jobs\DeliverWebhook`
specs/05-BUSINESS-LOGIC.md:1379:    Listener->>Listener: Query webhook_subscriptions for store_id + event_type where status = active
specs/05-BUSINESS-LOGIC.md:1381:        Listener->>Queue: Dispatch DeliverWebhook job
specs/05-BUSINESS-LOGIC.md:1388:    Job->>Job: Record result in webhook_deliveries
specs/05-BUSINESS-LOGIC.md:1421:- If a subscription's endpoint fails 5 consecutive times: automatically set `webhook_subscriptions.status = 'paused'`.
specs/05-BUSINESS-LOGIC.md:1426:**Service:** `App\Services\WebhookService`
specs/05-BUSINESS-LOGIC.md:1594:| `OrderCreated` | Send confirmation email, dispatch webhooks (`order.created`), log analytics event |
specs/05-BUSINESS-LOGIC.md:1595:| `OrderRefunded` | Send refund notification email, dispatch webhooks (`order.refunded`), optionally restock |
specs/05-BUSINESS-LOGIC.md:1598:| `CheckoutCompleted` | Dispatch webhooks (`checkout.completed`) |
specs/05-BUSINESS-LOGIC.md:1601:| `ProductCreated` | Sync FTS5 index, dispatch webhooks (`product.created`) |
specs/05-BUSINESS-LOGIC.md:1602:| `ProductUpdated` | Sync FTS5 index, dispatch webhooks (`product.updated`) |
specs/05-BUSINESS-LOGIC.md:1603:| `ProductDeleted` | Remove from FTS5 index, dispatch webhooks (`product.deleted`) |
specs/05-BUSINESS-LOGIC.md:1606:| `FulfillmentShipped` | Send shipping notification email with tracking info, dispatch webhooks (`order.fulfilled` if fully fulfilled) |
specs/05-BUSINESS-LOGIC.md:1607:| `FulfillmentDelivered` | Update fulfillment record, dispatch webhooks |
specs/05-BUSINESS-LOGIC.md:1628:| `App\Jobs\DeliverWebhook` | POST webhook payload to subscriber endpoint |
specs/05-BUSINESS-LOGIC.md:1654:| WebhookService | `App\Services` | Webhook delivery, circuit breaker |
specs/05-BUSINESS-LOGIC.md:1690:| WebhookSubscriptionStatus | Active (`active`), Paused (`paused`), Disabled (`disabled`) |
specs/05-BUSINESS-LOGIC.md:1706:| WebhookResult | success (bool), message (string) | Webhook processing result |


output:
Chunk ID: 516b78
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 2389
Output:
| order_line_id | INTEGER | No | - | FK -> order_lines(id) ON DELETE CASCADE | Source order line |
| quantity | INTEGER | No | 1 | - | Units fulfilled from this line |

**Indexes:**

- `idx_fulfillment_lines_fulfillment_id` on (`fulfillment_id`) - FK lookup, listing lines per fulfillment
- `idx_fulfillment_lines_fulfillment_order_line` UNIQUE on (`fulfillment_id`, `order_line_id`) - Prevents duplicate line entries per fulfillment

**Notes:** An order with multiple line items may be partially fulfilled. This table tracks how many units of each line item are included in each shipment.

---

## Epic 7 - Analytics

### analytics_events

Raw event stream for storefront analytics.

| Column | Type | Nullable | Default | Constraints | Notes |
|---|---|---|---|---|---|
| id | INTEGER | No | autoincrement | PRIMARY KEY | |
| store_id | INTEGER | No | - | FK -> stores(id) ON DELETE CASCADE | Owning store |
| type | TEXT | No | - | - | Event type (see notes) |
| session_id | TEXT | Yes | NULL | - | Browser session identifier |
| customer_id | INTEGER | Yes | NULL | FK -> customers(id) ON DELETE SET NULL | Nullable for anonymous visitors |
| properties_json | TEXT | No | '{}' | - | Event-specific data as JSON |
| client_event_id | TEXT | Yes | NULL | UNIQUE per (store_id, client_event_id) | Client-provided idempotency key for deduplication |
| occurred_at | TEXT | Yes | NULL | - | ISO-8601 timestamp of when the event actually occurred on the client |
| created_at | TEXT | Yes | NULL | - | ISO-8601 timestamp |

**Indexes:**

- `idx_analytics_events_store_id` on (`store_id`) - FK lookup
- `idx_analytics_events_store_type` on (`store_id`, `type`) - Filtering events by type per store
- `idx_analytics_events_store_created` on (`store_id`, `created_at`) - Time-range event queries
- `idx_analytics_events_session` on (`session_id`) - Session-based event grouping
- `idx_analytics_events_customer` on (`customer_id`) - Customer activity history
- `idx_analytics_events_client_event` UNIQUE on (`store_id`, `client_event_id`) - Client-side event deduplication

**Notes:** Event types include: `page_view`, `product_view`, `add_to_cart`, `remove_from_cart`, `checkout_started`, `checkout_completed`, `search`. This table will grow fast - consider periodic archiving or partitioning by date for production workloads beyond SQLite.

---

### analytics_daily

Pre-aggregated daily metrics per store. Composite primary key.

| Column | Type | Nullable | Default | Constraints | Notes |
|---|---|---|---|---|---|
| store_id | INTEGER | No | - | PK (composite), FK -> stores(id) ON DELETE CASCADE | |
| date | TEXT | No | - | PK (composite) | ISO-8601 date (YYYY-MM-DD) |
| orders_count | INTEGER | No | 0 | - | Number of orders placed |
| revenue_amount | INTEGER | No | 0 | - | Total revenue in minor units |
| aov_amount | INTEGER | No | 0 | - | Average order value in minor units |
| visits_count | INTEGER | No | 0 | - | Unique sessions |
| add_to_cart_count | INTEGER | No | 0 | - | Add-to-cart events |
| checkout_started_count | INTEGER | No | 0 | - | Checkout-started events |
| checkout_completed_count | INTEGER | No | 0 | - | Checkout-completed events |

**Primary Key:** (`store_id`, `date`)

**Indexes:**

- `idx_analytics_daily_store_date` on (`store_id`, `date`) - Time-range metric queries (covered by PK but explicit for clarity)

**Notes:** `date` is an ISO-8601 date string (YYYY-MM-DD). `revenue_amount` and `aov_amount` (average order value) are in minor units (cents). This table is populated by a scheduled aggregation job.

---

## Epic 8 - Apps and Webhooks

### apps

Third-party or first-party application registry.

| Column | Type | Nullable | Default | Constraints | Notes |
|---|---|---|---|---|---|
| id | INTEGER | No | autoincrement | PRIMARY KEY | |
| name | TEXT | No | - | - | App display name |
| status | TEXT | No | 'active' | CHECK IN ('active', 'disabled') | |
| created_at | TEXT | Yes | NULL | - | ISO-8601 timestamp |

**Indexes:**

- `idx_apps_status` on (`status`) - Filtering apps by status

---

### app_installations

Installation of an app on a specific store.

| Column | Type | Nullable | Default | Constraints | Notes |
|---|---|---|---|---|---|
| id | INTEGER | No | autoincrement | PRIMARY KEY | |
| store_id | INTEGER | No | - | FK -> stores(id) ON DELETE CASCADE | |
| app_id | INTEGER | No | - | FK -> apps(id) ON DELETE CASCADE | |
| scopes_json | TEXT | No | '[]' | - | JSON array of granted permission scopes |
| status | TEXT | No | 'active' | CHECK IN ('active', 'suspended', 'uninstalled') | |
| installed_at | TEXT | Yes | NULL | - | ISO-8601 timestamp |

**Indexes:**

- `idx_app_installations_store_app` UNIQUE on (`store_id`, `app_id`) - One installation per app per store
- `idx_app_installations_store_id` on (`store_id`) - Listing installations per store
- `idx_app_installations_app_id` on (`app_id`) - Finding all stores with an app installed

---

### oauth_clients

OAuth2 client credentials for apps.

| Column | Type | Nullable | Default | Constraints | Notes |
|---|---|---|---|---|---|
| id | INTEGER | No | autoincrement | PRIMARY KEY | |
| app_id | INTEGER | No | - | FK -> apps(id) ON DELETE CASCADE | Parent app |
| client_id | TEXT | No | - | UNIQUE per table | OAuth2 client identifier |
| client_secret_encrypted | TEXT | No | - | - | Encrypted client secret |
| redirect_uris_json | TEXT | No | '[]' | - | JSON array of allowed redirect URIs |

**Indexes:**

- `idx_oauth_clients_client_id` UNIQUE on (`client_id`) - OAuth2 client lookup
- `idx_oauth_clients_app_id` on (`app_id`) - FK lookup, listing clients per app

---

### oauth_tokens

Issued access/refresh tokens for app installations.

| Column | Type | Nullable | Default | Constraints | Notes |
|---|---|---|---|---|---|
| id | INTEGER | No | autoincrement | PRIMARY KEY | |
| installation_id | INTEGER | No | - | FK -> app_installations(id) ON DELETE CASCADE | Parent installation |
| access_token_hash | TEXT | No | - | UNIQUE per table | Hashed access token |
| refresh_token_hash | TEXT | Yes | NULL | - | Hashed refresh token |
| expires_at | TEXT | No | - | - | ISO-8601 expiration timestamp |

**Indexes:**

- `idx_oauth_tokens_installation_id` on (`installation_id`) - FK lookup, listing tokens per installation
- `idx_oauth_tokens_access_hash` UNIQUE on (`access_token_hash`) - Token validation lookup
- `idx_oauth_tokens_expires_at` on (`expires_at`) - Cleanup of expired tokens

---

### webhook_subscriptions

Registered webhook endpoints for event delivery.

| Column | Type | Nullable | Default | Constraints | Notes |
|---|---|---|---|---|---|
| id | INTEGER | No | autoincrement | PRIMARY KEY | |
| store_id | INTEGER | No | - | FK -> stores(id) ON DELETE CASCADE | Owning store |
| app_installation_id | INTEGER | Yes | NULL | FK -> app_installations(id) ON DELETE CASCADE | Nullable for store-level hooks |
| event_type | TEXT | No | - | - | Event to subscribe to |
| target_url | TEXT | No | - | - | Delivery endpoint URL |
| signing_secret_encrypted | TEXT | No | - | - | Encrypted HMAC signing secret |
| status | TEXT | No | 'active' | CHECK IN ('active', 'paused', 'disabled') | |

**Indexes:**

- `idx_webhook_subscriptions_store_id` on (`store_id`) - FK lookup
- `idx_webhook_subscriptions_store_event` on (`store_id`, `event_type`) - Finding subscriptions for a given event
- `idx_webhook_subscriptions_installation` on (`app_installation_id`) - Listing subscriptions per app installation

---

### webhook_deliveries

Delivery log for webhook attempts.

| Column | Type | Nullable | Default | Constraints | Notes |
|---|---|---|---|---|---|
| id | INTEGER | No | autoincrement | PRIMARY KEY | |
| subscription_id | INTEGER | No | - | FK -> webhook_subscriptions(id) ON DELETE CASCADE | Parent subscription |
| event_id | TEXT | No | - | - | Unique event identifier |
| attempt_count | INTEGER | No | 1 | - | Number of delivery attempts |
| status | TEXT | No | 'pending' | CHECK IN ('pending', 'success', 'failed') | |
| last_attempt_at | TEXT | Yes | NULL | - | ISO-8601 timestamp |
| response_code | INTEGER | Yes | NULL | - | HTTP response code |
| response_body_snippet | TEXT | Yes | NULL | - | Truncated response body for debugging |

**Indexes:**

- `idx_webhook_deliveries_subscription_id` on (`subscription_id`) - FK lookup, listing deliveries per subscription
- `idx_webhook_deliveries_event_id` on (`event_id`) - Deduplication and lookup by event
- `idx_webhook_deliveries_status` on (`status`) - Finding pending/failed deliveries for retry
- `idx_webhook_deliveries_last_attempt` on (`last_attempt_at`) - Time-based retry scheduling

---

## Laravel Framework Tables

The following tables are managed by Laravel's built-in migrations and should not be modified.

### sessions

| Column | Type | Nullable | Default | Constraints | Notes |
|---|---|---|---|---|---|
| id | TEXT | No | - | PRIMARY KEY | Session ID |
| user_id | INTEGER | Yes | NULL | - | Associated user |
| ip_address | TEXT | Yes | NULL | - | Client IP |
| user_agent | TEXT | Yes | NULL | - | Client user agent |
| payload | TEXT | No | - | - | Serialized session data |
| last_activity | INTEGER | No | - | - | Unix timestamp |

### cache

| Column | Type | Nullable | Default | Constraints | Notes |
|---|---|---|---|---|---|
| key | TEXT | No | - | PRIMARY KEY | Cache key |
| value | TEXT | No | - | - | Serialized cache value |
| expiration | INTEGER | No | - | - | Unix timestamp |

### cache_locks

| Column | Type | Nullable | Default | Constraints | Notes |
|---|---|---|---|---|---|


output:
Chunk ID: c37c7c
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 1412
Output:

**File to create:** `App\Services\NavigationService`

| Method | Description |
|--------|-------------|
| `buildTree(NavigationMenu $menu): array` | Builds navigation trees from navigation_menus / navigation_items |
| `resolveUrl(NavigationItem $item): string` | Resolves URLs for page, collection, product type items |

Caches per store with 5-minute TTL.

---

### Phase 4: Cart, Checkout, Discounts, Shipping, Taxes

**Priority:** HIGH - core shopping flow.

#### Step 4.1: Migrations (Batch 4-5)

1. `create_carts_table` - FK: `store_id`, `customer_id`
2. `create_cart_lines_table` - FK: `cart_id`, `variant_id`
3. `create_checkouts_table` - FK: `store_id`, `cart_id`, `customer_id`
4. `create_shipping_zones_table` - FK: `store_id`
5. `create_shipping_rates_table` - FK: `zone_id`
6. `create_tax_settings_table` - PK: `store_id`
7. `create_discounts_table` - FK: `store_id`

#### Step 4.2: Models

**Files to create:**

| Type | Name | Purpose |
|------|------|---------|
| Model (with migration, factory, seeder) | `Cart` | Shopping cart entity |
| Model (with migration, factory, seeder) | `CartLine` | Individual cart line item |
| Model (with migration, factory, seeder) | `Checkout` | Checkout session entity |
| Model (with migration, factory, seeder) | `ShippingZone` | Shipping zone configuration |
| Model (with migration, factory, seeder) | `ShippingRate` | Shipping rate within a zone |
| Model (with migration, factory, seeder) | `TaxSettings` | Per-store tax configuration |
| Model (with migration, factory, seeder) | `Discount` | Discount code or automatic discount |

**Relationship definitions:**

| Model | Key Relationships |
|-------|-------------------|
| `Cart` | `belongsTo(Store)`, `belongsTo(Customer)`, `hasMany(CartLine)`, `hasMany(Checkout)` |
| `CartLine` | `belongsTo(Cart)`, `belongsTo(ProductVariant)` |
| `Checkout` | `belongsTo(Store)`, `belongsTo(Cart)`, `belongsTo(Customer)` |
| `ShippingZone` | `belongsTo(Store)`, `hasMany(ShippingRate)` |
| `ShippingRate` | `belongsTo(ShippingZone)` |
| `TaxSettings` | `belongsTo(Store)` |
| `Discount` | `belongsTo(Store)` |

**Enums to create:**

| Enum | Values |
|------|--------|
| `CartStatus` | `Active`, `Converted`, `Abandoned` |
| `CheckoutStatus` | `Started`, `Addressed`, `ShippingSelected`, `PaymentPending`, `Completed`, `Expired` |
| `DiscountType` | `Code`, `Automatic` |
| `DiscountValueType` | `Percent` (`percent`), `Fixed` (`fixed`), `FreeShipping` (`free_shipping`) -- PascalCase cases backed by lowercase string values stored in DB |
| `DiscountStatus` | `Draft`, `Active`, `Expired`, `Disabled` |
| `ShippingRateType` | `Flat`, `Weight`, `Price`, `Carrier` |
| `TaxMode` | `Manual`, `Provider` |

#### Step 4.3: Cart Service

**File to create:** `App\Services\CartService`

| Method | Description |
|--------|-------------|
| `create(Store, ?Customer): Cart` | Creates cart with store currency, version 1 |
| `addLine(Cart, int $variantId, int $qty): CartLine` | Validates variant/product active, inventory, increments existing line or creates new |
| `updateLineQuantity(Cart, int $lineId, int $qty): CartLine` | Validates quantity, recalculates amounts |
| `removeLine(Cart, int $lineId): void` | Deletes line, increments version |
| `getOrCreateForSession(Store, ?Customer): Cart` | Session-based cart binding |
| `mergeOnLogin(Cart $guest, Cart $customer): Cart` | Merges guest cart into customer cart |

All mutations increment `cart_version`. API clients must send `expected_version` or receive 409 Conflict.

#### Step 4.4: Discount Service

**File to create:** `App\Services\DiscountService`

| Method | Description |
|--------|-------------|
| `validate(string $code, Store $store, Cart $cart): Discount` | Validates code exists, is active, dates valid, usage limit not reached, minimum purchase met |
| `calculate(Discount $discount, int $subtotal, array $lines): DiscountResult` | Calculates amount, allocates proportionally across qualifying lines |

Case-insensitive code lookup. Throws `InvalidDiscountException` with specific reason codes.

#### Step 4.5: Shipping Calculator

**File to create:** `App\Services\ShippingCalculator`

| Method | Description |
|--------|-------------|
| `getAvailableRates(Store $store, array $address): Collection` | Matches zones by country/region, returns active rates |
| `calculate(ShippingRate $rate, Cart $cart): int` | Calculates cost based on rate type (flat/weight/price) |

Zone matching: checks `countries_json` for country code, then `regions_json` for region code.

#### Step 4.6: Tax Calculator

**File to create:** `App\Services\TaxCalculator`

| Method | Description |
|--------|-------------|
| `calculate(int $amount, TaxSettings $settings, array $address): TaxResult` | Calculates tax per manual rate or provider |
| `extractInclusive(int $grossAmount, int $rateBasisPoints): int` | Extracts tax from gross (prices-include-tax mode) |
| `addExclusive(int $netAmount, int $rateBasisPoints): int` | Adds tax to net amount |

All math uses integers. Rates stored in basis points (1900 = 19%).

#### Step 4.7: Pricing Engine

**File to create:** `App\Services\PricingEngine`

Method: `calculate(Checkout $checkout): PricingResult`

**Pipeline steps:**
1. Line subtotals: `unit_price * quantity` per line
2. Cart subtotal: sum of line subtotals
3. Discount: apply rules, allocate proportionally
4. Discounted subtotal: subtotal minus discount
5. Shipping: based on selected method/rate
6. Tax: on discounted amounts (plus shipping if taxable)
7. Total: discounted_subtotal + shipping + tax_total

Result stored in `checkouts.totals_json` as snapshot.

**Value objects to create:**


output:
Chunk ID: 146d4c
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 1828
Output:
| `tags` | Nullable, string |
| `handle` | Required, string, max 255, unique (excluding current product) |
| `variants.*.sku` | Nullable, string, max 255 |
| `variants.*.price` | Required, integer, min 0 |
| `variants.*.compareAtPrice` | Nullable, integer, min 0 |
| `variants.*.quantity` | Required, integer, min 0 |

### Two-Column Layout Wireframe

```
+------------------------------------------+-------------------+
|          LEFT COLUMN (2/3)               | RIGHT COLUMN (1/3)|
+------------------------------------------+-------------------+
|                                          |                   |
|  TITLE                                   |  STATUS CARD      |
|  +------------------------------------+  |  Status: [Draft v]|
|  | [Short Sleeve T-Shirt           ]  |  |                   |
|  +------------------------------------+  |  PUBLISHING CARD  |
|                                          |  Published at:    |
|  DESCRIPTION                             |  [datetime input]  |
|  +------------------------------------+  |                   |
|  | Describe your product...           |  |  ORGANIZATION     |
|  |                                    |  |  Vendor:          |
|  |                                    |  |  [Nike          ] |
|  |                                    |  |  Product type:    |
|  +------------------------------------+  |  [T-Shirts      ] |
|                                          |  Tags:            |
|  MEDIA                                   |  [summer, cotton ] |
|  +------------------------------------+  |  (comma-separated)|
|  | +--+ +--+ +--+ +--+               |  |                   |
|  | |  | |  | |  | |  |  (thumbnails) |  |  COLLECTIONS      |
|  | +--+ +--+ +--+ +--+               |  |  [x] Summer Sale  |
|  |                                    |  |  [ ] Winter       |
|  | [--- Drag & drop or click ---]    |  |  [x] New Arrivals |
|  +------------------------------------+  |                   |
|                                          |                   |
|  VARIANTS                                |                   |
|  Option name: [Size    ]                 |                   |
|  Values:      [S, M, L, XL         ]    |                   |
|  [trash]                                 |                   |
|                                          |                   |
|  [+ Add another option]                  |                   |
|                                          |                   |
|  Variant | SKU    | Price | Compare | Qty | Ship |          |
|  S / Red  | [   ] | [   ] | [     ] | [ ] | [x]  |          |
|  M / Red  | [   ] | [   ] | [     ] | [ ] | [x]  |          |
|  L / Red  | [   ] | [   ] | [     ] | [ ] | [x]  |          |
|                                          |                   |
|  SEO (collapsible)                       |                   |
|  > Search engine listing                 |                   |
|    URL handle: [short-sleeve-t-shirt]    |                   |
+------------------------------------------+-------------------+

+==================================================================+
| STICKY SAVE BAR (fixed bottom)                                    |
|                                      [Discard]  [Save]           |
+==================================================================+
```

### Left Column Sections

#### Title Input
- Flux field with label "Title"
- Flux input, placeholder "Short Sleeve T-Shirt"
- Flux error display for validation

#### Description
- Flux field with label "Description"
- Flux textarea, 8 rows, placeholder "Describe your product..."
- Future enhancement: WYSIWYG toolbar via Alpine.js; baseline is plain textarea

#### Media Section
A card with heading "Media" (size md).

**Upload area:** A dashed-border drop zone with upload icon and text "Drag and drop images or click to upload". A hidden file input (multiple, accept images) triggered by clicking the zone.

**Upload progress:** A progress bar shown during file upload.

**Media grid:** A 4-column grid (responsive: 2 on small, 3 on sm, 4 on lg) of thumbnails:
- Each item: square aspect ratio, object-cover, rounded
- On hover overlay shows: delete button (X icon, top-right), alt text edit button (pencil icon, bottom-left)
- Drag-to-reorder supported via Livewire sortable or Alpine.js Sortable

#### Variants Section
A card with heading "Variants" (size md).

**Options builder:** For each option, a row containing:
- "Option name" input field (placeholder: "Size")
- "Values" input field (placeholder: "S, M, L, XL" - comma-separated)
- Trash button to remove the option

Below options: "Add another option" ghost button with plus icon.

When options change, `generateVariants()` is called automatically via lifecycle hook.

**Variants table:** Shown when variants exist. Scrollable table with inline-editable fields:

| Column | Input Type | Description |
|--------|-----------|-------------|
| Variant | Read-only text | Combination name, e.g. "S / Red" |
| SKU | Text input (small) | Stock keeping unit |
| Price | Number input (small) | Price in major units |
| Compare at price | Number input (small) | Original price for sale display |
| Quantity | Number input (small) | On-hand inventory |
| Requires shipping | Checkbox | Whether variant needs shipping |

#### SEO Section
A collapsible section (collapsed by default, expands with animation):
- Toggle button: chevron icon + "Search engine listing" text
- When expanded: URL handle input field with placeholder "short-sleeve-t-shirt" and validation error display

### Right Column Sections

#### Status Card
- Flux field with label "Status"
- Flux select with options: Draft, Active, Archived

#### Publishing Card
- Flux field with label "Published at"
- Datetime-local input

#### Product Organization Card
- Vendor: Flux input, placeholder "Nike"
- Product type: Flux input, placeholder "T-Shirts"
- Tags: Flux input, placeholder "summer, cotton, sale" with description "Separate tags with commas"

#### Collections Card
- Flux field with label "Collections"
- For each available collection: a row with checkbox and collection title text
- Checkbox values bound to `collectionIds`

### Sticky Save Bar

Fixed at the bottom of the viewport. Appears when changes are detected.
- Left-aligned to account for sidebar width on desktop
- "Discard" button (ghost variant) - links back to products index
- "Save" button (primary variant) - calls `save()`, shows "Saving..." text during loading, disabled while loading

### Delete Confirmation Modal (Edit Only)

| Property | Value |
|----------|-------|
| Modal name | `confirm-delete-product` |
| Max width | md |
| Heading | "Delete this product?" |
| Body text | "This product will be archived. Products with existing orders cannot be permanently removed." |
| Cancel button | Ghost variant, closes modal |
| Confirm button | Danger variant, calls `deleteProduct` |

### Flux Component Mapping

| UI Element | Flux Component |
|-----------|----------------|
| Page title | `heading` |
| Section titles | `heading` (size md) |
| All form labels | `field`, label via Flux |
| Text inputs | `input` |
| Textarea | `textarea` |
| Status/type selects | `select` |
| Checkboxes | `checkbox` |
| Validation errors | `field` error display |


message: The docs confirm the right Laravel primitives here: queued jobs with backoff, HTTP client fakes/prevent-stray testing, form requests for nested validation, and Livewire/Flux validation/UI patterns. I’m narrowing the spec now to the minimum coherent apps/webhooks slice: schema, service/job, admin surfaces, and tests.

phase: commentary


type: message

role: assistant

text: The docs confirm the right Laravel primitives here: queued jobs with backoff, HTTP client fakes/prevent-stray testing, form requests for nested validation, and Livewire/Flux validation/UI patterns. I’m narrowing the spec now to the minimum coherent apps/webhooks slice: schema, service/job, admin surfaces, and tests.


cmd: sed -n '912,965p' specs/09-IMPLEMENTATION-ROADMAP.md


cmd: sed -n '1950,2135p' specs/03-ADMIN-UI.md


cmd: sed -n '1340,1445p' specs/05-BUSINESS-LOGIC.md


cmd: sed -n '933,1010p' specs/06-AUTH-AND-SECURITY.md


cmd: sed -n '112,132p' specs/02-API-ROUTES.md


command: sed -n '1950,2135p' specs/03-ADMIN-UI.md

aggregated_output:
- Conditional fields based on type:
  - **Custom link:** URL input (placeholder: "https://...")
  - **Page/Collection/Product:** Resource select dropdown populated with available resources of that type
- Cancel button (ghost) and "Save item" button (primary)

---

## 15. Apps Page

**Route:** `GET /admin/apps`

**Livewire component:** `App\Livewire\Admin\Apps\Index`

**Blade view:** `resources/views/livewire/admin/apps/index.blade.php`

### Component Requirements

| Property | Type | Purpose |
|----------|------|---------|
| `installedApps` | `Collection` | Apps installed for this store |

| Action | Behavior |
|--------|----------|
| `uninstallApp(int $appId)` | Uninstalls an app (with confirmation) |

### Layout Wireframe

```
+==================================================================+
| Apps                                                              |
+==================================================================+
|                                                                   |
| +--------------------------------------------------------------+ |
| | [icon]  My Integration App                         [Active]  | |
| |         Installed 3 months ago                               | |
| +--------------------------------------------------------------+ |
| +--------------------------------------------------------------+ |
| | [icon]  Analytics Plugin                          [Inactive] | |
| |         Installed 1 week ago                                 | |
| +--------------------------------------------------------------+ |
|                                                                   |
+==================================================================+
```

List of installed app cards, each containing:
- App icon placeholder (squares icon in a rounded container)
- App name (heading, size md)
- Install date (relative timestamp, muted text)
- Status badge (active=green, inactive=zinc)

Each app links to a detail page showing:
- Scopes granted (list of permission strings)
- Webhook subscriptions table: event type, URL, status
- Usage stats (API calls count, last call timestamp)

**Empty state:** "No apps installed" with descriptive text.

---

## 16. Developers Page

**Route:** `GET /admin/developers`

**Livewire component:** `App\Livewire\Admin\Developers\Index`

**Blade view:** `resources/views/livewire/admin/developers/index.blade.php`

### Component Requirements

| Property | Type | Purpose |
|----------|------|---------|
| `tokens` | `Collection` | API tokens for the store |
| `webhooks` | `Collection` | Webhook subscriptions |
| `newTokenName` | `string` | Name for new token |
| `generatedToken` | `?string` | Newly generated token (shown once) |
| `webhookEventType` | `string` | Event type for webhook form |
| `webhookUrl` | `string` | URL for webhook form |
| `editingWebhook` | `?WebhookSubscription` | Webhook being edited |

| Action | Behavior |
|--------|----------|
| `generateToken()` | Creates a new API token |
| `revokeToken(int $tokenId)` | Revokes a token |
| `openWebhookModal(?WebhookSubscription $webhook)` | Opens webhook create/edit modal |
| `saveWebhook()` | Saves webhook subscription |
| `deleteWebhook(int $webhookId)` | Deletes a webhook |

### Layout Wireframe

```
+==================================================================+
| Developers                                                        |
+==================================================================+
|                                                                   |
| API TOKENS                                                        |
| Manage personal access tokens for the Admin API.                 |
|                                                                   |
| Name           | Last used      | Created       | Actions        |
|----------------|----------------|---------------|----------------|
| My integration | 2 hours ago    | Jan 1, 2024   | [Revoke]       |
| CI Pipeline    | Never          | Feb 10, 2024  | [Revoke]       |
|                                                                   |
| [Generate new token]                                              |
|                                                                   |
| (after generating, shown once:)                                   |
| [!] Copy this token now. It will not be shown again.             |
| [sk_live_abc123xyz789...]                                        |
|                                                                   |
| [separator]                                                       |
|                                                                   |
| WEBHOOKS                                                          |
| Manage webhook subscriptions for real-time event notifications.  |
|                                                                   |
| Event type           | URL                    | Status  | Act    |
|----------------------|------------------------|---------|--------|
| order.created        | https://ex.com/hook    | Active  | [E][D] |
| product.updated      | https://ex.com/prod    | Failing | [E][D] |
|                                                                   |
| [+ Add webhook]                                                   |
+==================================================================+
```

Two sections separated by a Flux separator.

**API Tokens section:**
- Heading "API tokens" (size lg)
- Description text
- Token table with columns: Name, Last used (relative or "Never"), Created (date), Actions (Revoke button)
- "Generate new token" button opens modal
- When a token is generated: a warning callout is displayed with the token value and a note that it will only be shown once

#### Generate Token Modal

| Property | Value |
|----------|-------|
| Modal name | `generate-token` |
| Max width | md |
| Heading | "Generate API token" |

**Form fields:**
- Token name input (placeholder: "My integration")
- Cancel button (ghost) and "Generate" button (primary)

**Webhooks section:**
- Heading "Webhooks" (size lg)
- Description text
- Webhook table with columns:

| Column | Content |
|--------|---------|
| Event type | e.g., `order.created`, `product.updated` |
| URL | Endpoint URL |
| Status | Flux badge: active=green, failing=red |
| Actions | Edit and Delete buttons |

- "Add webhook" button opens modal

#### Webhook Modal

| Property | Value |
|----------|-------|
| Modal name | `webhook-form` |
| Max width | md |
| Heading | "Edit webhook" or "Add webhook" (dynamic) |

**Form fields:**
- Event type select with options: order.created, order.updated, order.cancelled, product.created, product.updated, product.deleted, customer.created, checkout.completed, fulfillment.created, refund.created
- Endpoint URL input (type: url, placeholder: "https://example.com/webhooks")
- Cancel button (ghost) and "Save" button (primary)

---

## 17. Analytics Page

**Route:** `GET /admin/analytics`

**Livewire component:** `App\Livewire\Admin\Analytics\Index`

**Blade view:** `resources/views/livewire/admin/analytics/index.blade.php`

### Component Requirements

| Property | Type | Purpose |
|----------|------|---------|
| `dateRange` | `string` | Date range preset |
| `customStartDate` | `?string` | Custom start date |


command: sed -n '912,965p' specs/09-IMPLEMENTATION-ROADMAP.md

aggregated_output:
Requirements:
- Runs daily via `routes/console.php`
- Aggregates raw events into `analytics_daily` rows
- Calculates: orders_count, revenue_amount, aov_amount, visits_count, add_to_cart_count, checkout_started_count

---

### Phase 10: Apps and Webhooks

**Priority:** LOW - extensibility.

#### Step 10.1: Migrations

1. `create_apps_table`
2. `create_app_installations_table` - FK: `store_id`, `app_id`
3. `create_oauth_clients_table` - FK: `app_id`
4. `create_oauth_tokens_table` - FK: `installation_id`
5. `create_webhook_subscriptions_table` - FK: `store_id`, `app_installation_id`
6. `create_webhook_deliveries_table` - FK: `subscription_id`

#### Step 10.2: Webhook Service

**File to create:** `App\Services\WebhookService`

| Method | Description |
|--------|-------------|
| `dispatch(Store $store, string $eventType, array $payload): void` | Finds matching subscriptions, queues delivery jobs |
| `sign(string $payload, string $secret): string` | HMAC-SHA256 signature |
| `verify(string $payload, string $signature, string $secret): bool` | Verify incoming webhook signatures |

**File to create:** `App\Jobs\DeliverWebhook`

Requirements:
- HTTP POST to target URL with JSON payload
- HMAC signature in `X-Platform-Signature` header, event type in `X-Platform-Event`, delivery ID in `X-Platform-Delivery-Id`, timestamp in `X-Platform-Timestamp`
- Retry with exponential backoff: 1 min, 5 min, 30 min, 2 h, 12 h (6 total attempts: 1 initial + 5 retries; Laravel backoff config `[60, 300, 1800, 7200, 43200]`)
- Records response in `webhook_deliveries`
- Circuit breaker: after 5 consecutive failures, pause subscription

---

### Phase 11: Polish

**Priority:** LOW but important for completeness.

1. Accessibility audit: skip links on all pages, ARIA labels on interactive elements, focus management in modals
2. Responsive testing: all pages render correctly at sm/md/lg/xl breakpoints
3. Dark mode: `dark:` prefix on all storefront views and admin views
4. Error pages: styled 404 and 503 pages matching the storefront theme
5. Structured logging: JSON channel in `config/logging.php`
6. Seed data: comprehensive seeders for demo stores

---


output:
Chunk ID: 5f96b5
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 540
Output:
Requirements:
- Runs daily via `routes/console.php`
- Aggregates raw events into `analytics_daily` rows
- Calculates: orders_count, revenue_amount, aov_amount, visits_count, add_to_cart_count, checkout_started_count

---

### Phase 10: Apps and Webhooks

**Priority:** LOW - extensibility.

#### Step 10.1: Migrations

1. `create_apps_table`
2. `create_app_installations_table` - FK: `store_id`, `app_id`
3. `create_oauth_clients_table` - FK: `app_id`
4. `create_oauth_tokens_table` - FK: `installation_id`
5. `create_webhook_subscriptions_table` - FK: `store_id`, `app_installation_id`
6. `create_webhook_deliveries_table` - FK: `subscription_id`

#### Step 10.2: Webhook Service

**File to create:** `App\Services\WebhookService`

| Method | Description |
|--------|-------------|
| `dispatch(Store $store, string $eventType, array $payload): void` | Finds matching subscriptions, queues delivery jobs |
| `sign(string $payload, string $secret): string` | HMAC-SHA256 signature |
| `verify(string $payload, string $signature, string $secret): bool` | Verify incoming webhook signatures |

**File to create:** `App\Jobs\DeliverWebhook`

Requirements:
- HTTP POST to target URL with JSON payload
- HMAC signature in `X-Platform-Signature` header, event type in `X-Platform-Event`, delivery ID in `X-Platform-Delivery-Id`, timestamp in `X-Platform-Timestamp`
- Retry with exponential backoff: 1 min, 5 min, 30 min, 2 h, 12 h (6 total attempts: 1 initial + 5 retries; Laravel backoff config `[60, 300, 1800, 7200, 43200]`)
- Records response in `webhook_deliveries`
- Circuit breaker: after 5 consecutive failures, pause subscription

---

### Phase 11: Polish

**Priority:** LOW but important for completeness.

1. Accessibility audit: skip links on all pages, ARIA labels on interactive elements, focus management in modals
2. Responsive testing: all pages render correctly at sm/md/lg/xl breakpoints
3. Dark mode: `dark:` prefix on all storefront views and admin views
4. Error pages: styled 404 and 503 pages matching the storefront theme
5. Structured logging: JSON channel in `config/logging.php`
6. Seed data: comprehensive seeders for demo stores

---


output:
Chunk ID: c3ca94
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 1827
Output:
- Conditional fields based on type:
  - **Custom link:** URL input (placeholder: "https://...")
  - **Page/Collection/Product:** Resource select dropdown populated with available resources of that type
- Cancel button (ghost) and "Save item" button (primary)

---

## 15. Apps Page

**Route:** `GET /admin/apps`

**Livewire component:** `App\Livewire\Admin\Apps\Index`

**Blade view:** `resources/views/livewire/admin/apps/index.blade.php`

### Component Requirements

| Property | Type | Purpose |
|----------|------|---------|
| `installedApps` | `Collection` | Apps installed for this store |

| Action | Behavior |
|--------|----------|
| `uninstallApp(int $appId)` | Uninstalls an app (with confirmation) |

### Layout Wireframe

```
+==================================================================+
| Apps                                                              |
+==================================================================+
|                                                                   |
| +--------------------------------------------------------------+ |
| | [icon]  My Integration App                         [Active]  | |
| |         Installed 3 months ago                               | |
| +--------------------------------------------------------------+ |
| +--------------------------------------------------------------+ |
| | [icon]  Analytics Plugin                          [Inactive] | |
| |         Installed 1 week ago                                 | |
| +--------------------------------------------------------------+ |
|                                                                   |
+==================================================================+
```

List of installed app cards, each containing:
- App icon placeholder (squares icon in a rounded container)
- App name (heading, size md)
- Install date (relative timestamp, muted text)
- Status badge (active=green, inactive=zinc)

Each app links to a detail page showing:
- Scopes granted (list of permission strings)
- Webhook subscriptions table: event type, URL, status
- Usage stats (API calls count, last call timestamp)

**Empty state:** "No apps installed" with descriptive text.

---

## 16. Developers Page

**Route:** `GET /admin/developers`

**Livewire component:** `App\Livewire\Admin\Developers\Index`

**Blade view:** `resources/views/livewire/admin/developers/index.blade.php`

### Component Requirements

| Property | Type | Purpose |
|----------|------|---------|
| `tokens` | `Collection` | API tokens for the store |
| `webhooks` | `Collection` | Webhook subscriptions |
| `newTokenName` | `string` | Name for new token |
| `generatedToken` | `?string` | Newly generated token (shown once) |
| `webhookEventType` | `string` | Event type for webhook form |
| `webhookUrl` | `string` | URL for webhook form |
| `editingWebhook` | `?WebhookSubscription` | Webhook being edited |

| Action | Behavior |
|--------|----------|
| `generateToken()` | Creates a new API token |
| `revokeToken(int $tokenId)` | Revokes a token |
| `openWebhookModal(?WebhookSubscription $webhook)` | Opens webhook create/edit modal |
| `saveWebhook()` | Saves webhook subscription |
| `deleteWebhook(int $webhookId)` | Deletes a webhook |

### Layout Wireframe

```
+==================================================================+
| Developers                                                        |
+==================================================================+
|                                                                   |
| API TOKENS                                                        |
| Manage personal access tokens for the Admin API.                 |
|                                                                   |
| Name           | Last used      | Created       | Actions        |
|----------------|----------------|---------------|----------------|
| My integration | 2 hours ago    | Jan 1, 2024   | [Revoke]       |
| CI Pipeline    | Never          | Feb 10, 2024  | [Revoke]       |
|                                                                   |
| [Generate new token]                                              |
|                                                                   |
| (after generating, shown once:)                                   |
| [!] Copy this token now. It will not be shown again.             |
| [sk_live_abc123xyz789...]                                        |
|                                                                   |
| [separator]                                                       |
|                                                                   |
| WEBHOOKS                                                          |
| Manage webhook subscriptions for real-time event notifications.  |
|                                                                   |
| Event type           | URL                    | Status  | Act    |
|----------------------|------------------------|---------|--------|
| order.created        | https://ex.com/hook    | Active  | [E][D] |
| product.updated      | https://ex.com/prod    | Failing | [E][D] |
|                                                                   |
| [+ Add webhook]                                                   |
+==================================================================+
```

Two sections separated by a Flux separator.

**API Tokens section:**
- Heading "API tokens" (size lg)
- Description text
- Token table with columns: Name, Last used (relative or "Never"), Created (date), Actions (Revoke button)
- "Generate new token" button opens modal
- When a token is generated: a warning callout is displayed with the token value and a note that it will only be shown once

#### Generate Token Modal

| Property | Value |
|----------|-------|
| Modal name | `generate-token` |
| Max width | md |
| Heading | "Generate API token" |

**Form fields:**
- Token name input (placeholder: "My integration")
- Cancel button (ghost) and "Generate" button (primary)

**Webhooks section:**
- Heading "Webhooks" (size lg)
- Description text
- Webhook table with columns:

| Column | Content |
|--------|---------|
| Event type | e.g., `order.created`, `product.updated` |
| URL | Endpoint URL |
| Status | Flux badge: active=green, failing=red |
| Actions | Edit and Delete buttons |

- "Add webhook" button opens modal

#### Webhook Modal

| Property | Value |
|----------|-------|
| Modal name | `webhook-form` |
| Max width | md |
| Heading | "Edit webhook" or "Add webhook" (dynamic) |

**Form fields:**
- Event type select with options: order.created, order.updated, order.cancelled, product.created, product.updated, product.deleted, customer.created, checkout.completed, fulfillment.created, refund.created
- Endpoint URL input (type: url, placeholder: "https://example.com/webhooks")
- Cancel button (ghost) and "Save" button (primary)

---

## 17. Analytics Page

**Route:** `GET /admin/analytics`

**Livewire component:** `App\Livewire\Admin\Analytics\Index`

**Blade view:** `resources/views/livewire/admin/analytics/index.blade.php`

### Component Requirements

| Property | Type | Purpose |
|----------|------|---------|
| `dateRange` | `string` | Date range preset |
| `customStartDate` | `?string` | Custom start date |


command: sed -n '1340,1445p' specs/05-BUSINESS-LOGIC.md

aggregated_output:
    // by setting a password via registration or account-claim flow
```

---

## 13. Webhook Delivery System

### 13.1 Subscription Model

Stores (or app installations) subscribe to event types via `webhook_subscriptions`.

**Supported event types:**

| Event Type | Triggered By |
|---|---|
| `order.created` | OrderCreated event |
| `order.paid` | Payment captured successfully |
| `order.fulfilled` | All order lines fulfilled |
| `order.refunded` | Refund processed |
| `product.created` | New product saved |
| `product.updated` | Product modified |
| `product.deleted` | Product deleted or archived |
| `checkout.completed` | Checkout successfully completed |

### 13.2 Delivery Process

**Listener:** `App\Listeners\DispatchWebhooks` - registered to listen for all domain events that have webhook counterparts.

**Job:** `App\Jobs\DeliverWebhook`

```mermaid
sequenceDiagram
    participant DomainEvent
    participant Listener
    participant Queue
    participant Job
    participant TargetURL

    DomainEvent->>Listener: Event fired (e.g., OrderCreated)
    Listener->>Listener: Query webhook_subscriptions for store_id + event_type where status = active
    loop For each matching subscription
        Listener->>Queue: Dispatch DeliverWebhook job
    end
    Queue->>Job: Process job
    Job->>Job: Serialize event payload as JSON
    Job->>Job: Generate HMAC-SHA256 signature using subscription's signing_secret (decrypted)
    Job->>TargetURL: HTTP POST with headers and payload
    TargetURL-->>Job: Response
    Job->>Job: Record result in webhook_deliveries
```

**Required HTTP headers on delivery:**

| Header | Value |
|---|---|
| `X-Platform-Signature` | HMAC-SHA256 hex digest of the payload |
| `X-Platform-Event` | Event type string (e.g., `order.created`) |
| `X-Platform-Delivery-Id` | Unique UUID for this delivery attempt |
| `X-Platform-Timestamp` | Unix timestamp of the event |
| `Content-Type` | `application/json` |

### 13.3 Retry Logic

On non-2xx response or connection timeout:

| Attempt | Backoff Delay |
|---|---|
| 1 (initial) | Immediate |
| 2 | 1 minute |
| 3 | 5 minutes |
| 4 | 30 minutes |
| 5 | 2 hours |
| 6 (final) | 12 hours |

After 5 retry attempts (6 total), mark the delivery as `failed` (dead letter).

Use Laravel's built-in job retry mechanism with the backoff delays configured as: 60, 300, 1800, 7200, 43200 seconds.

### 13.4 Circuit Breaker

- Track consecutive failures per subscription (across different events).
- If a subscription's endpoint fails 5 consecutive times: automatically set `webhook_subscriptions.status = 'paused'`.
- Log a warning and (future) send an admin notification.
- Manual re-enable required by the merchant through the admin UI.
- On each successful delivery, reset the consecutive failure counter to 0.

**Service:** `App\Services\WebhookService`

---

## 14. Analytics

### 14.1 Event Ingestion

**Endpoint:** `POST /api/storefront/v1/analytics/events`

**Request structure:**

| Field | Type | Required | Description |
|---|---|---|---|
Events are submitted in batches:

```json
{
  "events": [
    {


output:
Chunk ID: 596eb4
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 774
Output:
    // by setting a password via registration or account-claim flow
```

---

## 13. Webhook Delivery System

### 13.1 Subscription Model

Stores (or app installations) subscribe to event types via `webhook_subscriptions`.

**Supported event types:**

| Event Type | Triggered By |
|---|---|
| `order.created` | OrderCreated event |
| `order.paid` | Payment captured successfully |
| `order.fulfilled` | All order lines fulfilled |
| `order.refunded` | Refund processed |
| `product.created` | New product saved |
| `product.updated` | Product modified |
| `product.deleted` | Product deleted or archived |
| `checkout.completed` | Checkout successfully completed |

### 13.2 Delivery Process

**Listener:** `App\Listeners\DispatchWebhooks` - registered to listen for all domain events that have webhook counterparts.

**Job:** `App\Jobs\DeliverWebhook`

```mermaid
sequenceDiagram
    participant DomainEvent
    participant Listener
    participant Queue
    participant Job
    participant TargetURL

    DomainEvent->>Listener: Event fired (e.g., OrderCreated)
    Listener->>Listener: Query webhook_subscriptions for store_id + event_type where status = active
    loop For each matching subscription
        Listener->>Queue: Dispatch DeliverWebhook job
    end
    Queue->>Job: Process job
    Job->>Job: Serialize event payload as JSON
    Job->>Job: Generate HMAC-SHA256 signature using subscription's signing_secret (decrypted)
    Job->>TargetURL: HTTP POST with headers and payload
    TargetURL-->>Job: Response
    Job->>Job: Record result in webhook_deliveries
```

**Required HTTP headers on delivery:**

| Header | Value |
|---|---|
| `X-Platform-Signature` | HMAC-SHA256 hex digest of the payload |
| `X-Platform-Event` | Event type string (e.g., `order.created`) |
| `X-Platform-Delivery-Id` | Unique UUID for this delivery attempt |
| `X-Platform-Timestamp` | Unix timestamp of the event |
| `Content-Type` | `application/json` |

### 13.3 Retry Logic

On non-2xx response or connection timeout:

| Attempt | Backoff Delay |
|---|---|
| 1 (initial) | Immediate |
| 2 | 1 minute |
| 3 | 5 minutes |
| 4 | 30 minutes |
| 5 | 2 hours |
| 6 (final) | 12 hours |

After 5 retry attempts (6 total), mark the delivery as `failed` (dead letter).

Use Laravel's built-in job retry mechanism with the backoff delays configured as: 60, 300, 1800, 7200, 43200 seconds.

### 13.4 Circuit Breaker

- Track consecutive failures per subscription (across different events).
- If a subscription's endpoint fails 5 consecutive times: automatically set `webhook_subscriptions.status = 'paused'`.
- Log a warning and (future) send an admin notification.
- Manual re-enable required by the merchant through the admin UI.
- On each successful delivery, reset the consecutive failure counter to 0.

**Service:** `App\Services\WebhookService`

---

## 14. Analytics

### 14.1 Event Ingestion

**Endpoint:** `POST /api/storefront/v1/analytics/events`

**Request structure:**

| Field | Type | Required | Description |
|---|---|---|---|
Events are submitted in batches:

```json
{
  "events": [
    {


command: sed -n '112,132p' specs/02-API-ROUTES.md

aggregated_output:
| Method | Path | Description | Component |
|--------|------|-------------|-----------|
| GET | `/admin/navigation` | Navigation menus management | `Admin\Navigation\Index` |

#### Apps

| Method | Path | Description | Component |
|--------|------|-------------|-----------|
| GET | `/admin/apps` | Apps directory - list installed and available apps | `Admin\Apps\Index` |
| GET | `/admin/apps/{installation}` | Installed app detail - scopes, webhooks, usage | `Admin\Apps\Show` |

#### Developers

| Method | Path | Description | Component |
|--------|------|-------------|-----------|
| GET | `/admin/developers` | Developer settings - API keys/tokens, webhook management | `Admin\Developers\Index` |

> **Note:** Webhook subscription management (CRUD) is available exclusively through the admin UI at `/admin/developers`. There are no REST API endpoints for programmatic webhook subscription management in the initial implementation.

#### Analytics


command: sed -n '933,1010p' specs/06-AUTH-AND-SECURITY.md

aggregated_output:
## 7. Webhook Signature Verification

### 7.1 Inbound Payment Webhooks

> **Not applicable.** All payment processing happens in-process via the Mock PSP. There are no inbound payment webhooks from external providers (no Stripe, no PayPal). This section is preserved for reference only -- if a real PSP is integrated in the future, inbound webhook verification would follow the patterns described in the outbound webhook section below (HMAC signature verification).

---

### 7.2 Platform Webhooks (Outbound)

Service: `App\Services\WebhookDispatcher`

When the platform sends webhooks to external subscribers (third-party apps, integrations).

```mermaid
sequenceDiagram
    participant App as Platform
    participant Sub as Subscriber URL

    App->>App: Serialize payload as JSON
    App->>App: Generate Unix timestamp
    App->>App: Concatenate: "{timestamp}.{json_body}"
    App->>App: HMAC-SHA256(signing_secret, concatenated_message)
    App->>Sub: POST to subscriber URL
    Note right of Sub: Headers: X-Platform-Signature, X-Platform-Timestamp, Content-Type: application/json
    Sub-->>App: 200 OK (acknowledged)
```

**Requirements:**

1. The payload is serialized as JSON.
2. A Unix timestamp is generated (current time).
3. The message to sign is: `"{timestamp}.{json_body}"` (timestamp, dot, JSON string).
4. The signature is computed as HMAC-SHA256 using the per-subscription `signing_secret` (decrypted from the `signing_secret_encrypted` column).
5. The HTTP request to the subscriber includes three headers:
   - `X-Platform-Signature`: the hex-encoded HMAC digest
   - `X-Platform-Timestamp`: the Unix timestamp as a string
   - `Content-Type`: `application/json`
6. Replay protection guidance for recipients: reject any request where `X-Platform-Timestamp` is more than 5 minutes in the past.

---

## 8. Audit Logging

### 8.1 Logging Channel

A dedicated `audit` channel must be added to `config/logging.php`.

| Setting | Value |
|---------|-------|
| Driver | `daily` (file-based, daily rotation) |
| Path | `storage/logs/audit.log` |
| Level | `info` |
| Retention | 90 days |

---

### 8.2 Log Entry Format

All audit log entries are structured JSON objects with the following schema:

| Field | Type | Required | Description |
|-------|------|:--------:|-------------|
| `timestamp` | string (ISO-8601) | Y | When the event occurred |
| `event` | string | Y | Event identifier (e.g., `product.updated`, `auth.login`) |
| `user_id` | integer or null | N | ID of the admin user who triggered the event |
| `store_id` | integer or null | N | ID of the store context |
| `resource_type` | string or null | N | Type of resource affected (e.g., `product`, `order`) |
| `resource_id` | integer or null | N | ID of the affected resource |
| `ip` | string | Y | Client IP address |
| `user_agent` | string | Y | Client user agent string |
| `changes` | object or null | N | For update events: object mapping field names to `[old_value, new_value]` tuples |
| (additional keys) | varies | N | Event-specific extra data |

---

### 8.3 Events to Log