content:
     1→# 06 - Authentication and Security Specification
     2→
     3→> E-commerce platform - Auth, authorization, and security controls for Laravel 12 + Livewire v4 + SQLite
     4→
     5→---
     6→
     7→## 1. Authentication
     8→
     9→The platform supports three distinct authentication mechanisms: session-based admin auth, session-based customer auth (store-scoped), and token-based API auth via Sanctum.
    10→
    11→### 1.1 Admin Authentication
    12→
    13→| Setting | Value |
    14→|---------|-------|
    15→| Guard name | `web` (default Laravel guard) |
    16→| Provider name | `users` (Eloquent driver) |
    17→| Model | `App\Models\User` |
    18→| Driver | Session (cookie-based) |
    19→| Login route | `GET /admin/login` |
    20→| Login form | Livewire component `Admin\Auth\Login` |
    21→
    22→#### Admin Login Flow
    23→
    24→```mermaid
    25→sequenceDiagram
    26→    participant B as Browser
    27→    participant LW as Livewire Component
    28→    participant Auth as Auth Guard (web)
    29→    participant DB as Database
    30→    participant S as Session
    31→
    32→    B->>LW: Submit email + password
    33→    LW->>LW: Validate input (email required, password required)
    34→    LW->>LW: Check rate limiter (5/min per IP)
    35→    alt Rate limit exceeded
    36→        LW-->>B: Flash "Too many attempts. Try again in X seconds."
    37→    end
    38→    LW->>Auth: attempt(credentials, remember)
    39→    Auth->>DB: SELECT from users WHERE email = ?
    40→    Auth->>Auth: Verify bcrypt hash
    41→    alt Credentials valid
    42→        Auth->>S: Regenerate session ID (prevent fixation)
    43→        Auth-->>LW: Success
    44→        LW-->>B: Redirect to /admin (dashboard)
    45→    else Credentials invalid
    46→        Auth-->>LW: Failure
    47→        LW-->>B: Flash "Invalid credentials" (generic, never reveal which field)
    48→    end
    49→```
    50→
    51→**Requirements:**
    52→
    53→- The login form collects `email` and `password`.
    54→- On successful login, the session MUST be regenerated to prevent session fixation attacks.
    55→- On failed login, only the generic message "Invalid credentials" is shown. The system must not indicate whether the email or password was wrong.
    56→- Rate limiting: maximum 5 attempts per minute, keyed by IP address. Uses the `login` rate limiter.
    57→- After login: redirect to `/admin` (the dashboard).
    58→
    59→#### Remember Me
    60→
    61→- An optional "Remember me" checkbox on the login form.
    62→- When enabled, Laravel sets a long-lived remember token cookie.
    63→- The `remember_token` column on the `users` table stores the hashed token.
    64→- Cookie name follows Laravel default convention: `remember_web_{hash}`.
    65→
    66→#### Admin Password Reset Flow
    67→
    68→```mermaid
    69→sequenceDiagram
    70→    participant B as Browser
    71→    participant App as Application
    72→    participant DB as Database
    73→    participant Mail as Mail Service
    74→
    75→    B->>App: GET /admin/forgot-password
    76→    App-->>B: Render forgot-password form
    77→    B->>App: POST /admin/forgot-password (email)
    78→    App->>DB: Look up user by email
    79→    alt User exists
    80→        App->>DB: Store reset token in password_reset_tokens table
    81→        App->>Mail: Send reset link email
    82→    end
    83→    App-->>B: "If that email exists, we sent a reset link." (generic)
    84→    B->>App: GET /admin/reset-password/{token}
    85→    App-->>B: Render reset-password form
    86→    B->>App: POST /admin/reset-password (token, email, password, password_confirmation)
    87→    App->>DB: Validate token (exists, not expired, matches email)
    88→    alt Valid token
    89→        App->>DB: Update user password (bcrypt hash)
    90→        App->>DB: Delete used token
    91→        App-->>B: Redirect to /admin/login with success flash
    92→    else Invalid/expired token
    93→        App-->>B: Error message
    94→    end
    95→```
    96→
    97→**Requirements:**
    98→
    99→- Uses Laravel's built-in password broker (`users` broker).
   100→- Reset tokens stored in the `password_reset_tokens` table.
   101→- Token expiration: 60 minutes (configurable in `config/auth.php` under `passwords.users.expire`).
   102→- Throttle: one reset email per 60 seconds per email address.
   103→- The "forgot password" response must be generic (do not reveal whether email exists).
   104→
   105→#### Admin Logout
   106→
   107→- Route: `POST /admin/logout` (via Livewire action or standard route).
   108→- Steps: invalidate the entire session, regenerate the CSRF token, redirect to `/admin/login`.
   109→- The response must not cache.
   110→
   111→#### Session Lifetime
   112→
   113→| Setting | Value |
   114→|---------|-------|
   115→| Lifetime | 120 minutes (configurable via `SESSION_LIFETIME` env var) |
   116→| Expire on close | No (session persists until lifetime expires) |
   117→
   118→---
   119→
   120→### 1.2 Customer Authentication
   121→
   122→| Setting | Value |
   123→|---------|-------|
   124→| Guard name | `customer` (custom guard) |
   125→| Provider name | `customers` (Eloquent driver) |
   126→| Model | `App\Models\Customer` |
   127→| Driver | Session (cookie-based) |
   128→| Login route | `GET /account/login` |
   129→| Registration route | `GET /account/register` |
   130→
   131→#### Customer Login Flow
   132→
   133→```mermaid
   134→sequenceDiagram
   135→    participant B as Browser
   136→    participant LW as Livewire Component
   137→    participant Auth as Auth Guard (customer)
   138→    participant Store as ResolveStore Middleware
   139→    participant DB as Database
   140→
   141→    Note over Store: Store already resolved from hostname
   142→    B->>LW: Submit email + password
   143→    LW->>LW: Validate input
   144→    LW->>LW: Check rate limiter (5/min per IP)
   145→    LW->>Auth: attempt(credentials + store_id, remember)
   146→    Auth->>DB: SELECT from customers WHERE email = ? AND store_id = ?
   147→    Auth->>Auth: Verify bcrypt hash
   148→    alt Valid
   149→        Auth-->>LW: Success (regenerate session)
   150→        LW-->>B: Redirect to /account (or intended URL)
   151→    else Invalid
   152→        Auth-->>LW: Failure
   153→        LW-->>B: Flash "Invalid credentials"
   154→    end
   155→```
   156→
   157→**Requirements:**
   158→
   159→- Customer login is always scoped to the current store. The store is resolved from the request hostname by the `ResolveStore` middleware before the login attempt.
   160→- The custom `CustomerUserProvider` must inject `store_id` into the credential check so the query filters by both `email` and `store_id`.
   161→- The same email address can exist in different stores (multi-tenant isolation).
   162→- Rate limiting: maximum 5 attempts per minute per IP, using the same `login` rate limiter.
   163→- After login: redirect to `/account` or the intended URL (if the customer was redirected from a protected page).
   164→
   165→#### Customer Registration Flow
   166→
   167→```mermaid
   168→flowchart TD
   169→    A[Customer visits /account/register] --> B[Fill form: name, email, password, password_confirmation, marketing_opt_in]
   170→    B --> C{Validate via RegisterCustomerRequest}
   171→    C -- Invalid --> D[Show validation errors]
   172→    C -- Valid --> E[Create Customer record scoped to current store]
   173→    E --> F[Auto-login via customer guard]
   174→    F --> G[Redirect to /account]
   175→```
   176→
   177→**Registration fields:**
   178→
   179→| Field | Type | Required | Validation |
   180→|-------|------|:--------:|------------|
   181→| name | string | Y | max:255 |
   182→| email | string | Y | valid email, unique per store (composite unique on email + store_id) |
   183→| password | string | Y | min:8, confirmed |
   184→| password_confirmation | string | Y | must match password |
   185→| marketing_opt_in | boolean | N | defaults to false |
   186→
   187→#### Customer Store Scoping
   188→
   189→- The `customers` auth provider requires a custom `CustomerUserProvider` class.
   190→- This provider overrides the default Eloquent provider to always include `store_id` in credential queries.
   191→- The `store_id` is obtained from the container-bound current store (resolved by `ResolveStore` middleware).
   192→- This ensures that `Auth::guard('customer')->attempt()` never crosses store boundaries.
   193→
   194→#### Customer Password Reset
   195→
   196→- Uses a separate password broker: `customers` broker.
   197→- Tokens stored in the `customer_password_reset_tokens` table (separate from admin tokens).
   198→- Token records include `store_id` to scope resets to the correct store.
   199→- Routes: `GET /forgot-password`, `POST /forgot-password`, `GET /reset-password/{token}`, `POST /reset-password`.
   200→- Token expiration: 60 minutes.
   201→- Throttle: one reset email per 60 seconds.
   202→- Generic response on the "forgot password" form (do not reveal whether email exists in the store).
   203→
   204→---
   205→
   206→### 1.3 API Authentication (Sanctum)
   207→
   208→| Setting | Value |
   209→|---------|-------|
   210→| Guard name | `sanctum` |
   211→| Provider | `users` (admin users only) |
   212→| Token type | Personal access tokens |
   213→| Token prefix | `shop_` |
   214→| Hashing | SHA-256 |
   215→| Default expiration | 1 year |
   216→| Storage | `personal_access_tokens` table |
   217→
   218→#### Token Lifecycle
   219→
   220→```mermaid
   221→stateDiagram-v2
   222→    [*] --> Created: Admin creates token in Developers section
   223→    Created --> Active: Plain-text shown once, hash stored
   224→    Active --> InUse: Token sent as Bearer header
   225→    InUse --> Active: Request processed
   226→    Active --> Revoked: Admin revokes token
   227→    Active --> Expired: Expiration date reached
   228→    Revoked --> [*]: Token row deleted
   229→    Expired --> [*]: Token row deleted (cleanup job)
   230→```
   231→
   232→**Requirements:**
   233→
   234→- Tokens are created in the admin panel, under the Developers section.
   235→- Each token has: a human-readable name, an optional expiration date, and a list of abilities (scopes).
   236→- On creation, the plain-text token (prefixed with `shop_`) is displayed exactly once. Only the SHA-256 hash is persisted.
   237→- Tokens are sent as `Authorization: Bearer shop_xxxxxxxx`.
   238→- Sanctum middleware validates the hash and resolves the associated user.
   239→- Ability checks are performed per-endpoint (e.g., the products list endpoint requires `read-products`).
   240→- Revoking a token deletes its row from the `personal_access_tokens` table.
   241→
   242→#### Token Abilities (Scopes)
   243→
   244→| Ability | Description |
   245→|---------|-------------|
   246→| `read-products` | List and view products |
   247→| `write-products` | Create, update, delete products |
   248→| `read-orders` | List and view orders |
   249→| `write-orders` | Update orders, create fulfillments |
   250→| `read-customers` | List and view customers |
   251→| `write-customers` | Update customers |
   252→| `read-collections` | List and view collections |
   253→| `write-collections` | Create, update, delete collections |
   254→| `read-discounts` | List and view discounts |
   255→| `write-discounts` | Create, update, delete discounts |
   256→| `read-analytics` | View analytics data |
   257→| `read-settings` | View store settings (general, shipping, taxes, checkout, notifications) |
   258→| `write-settings` | Update store settings (replaces the legacy `manage-store`, `manage-shipping`, `manage-taxes` scopes) |
   259→| `read-themes` | View themes and theme files |
   260→| `write-themes` | Create, update, delete, publish themes |
   261→| `read-content` | View pages and navigation menus |
   262→| `write-content` | Create, update, delete pages and navigation items |
   263→| `manage-platform` | Platform-level management (super-admin only) |
   264→
   265→Each API endpoint must check the token's abilities before processing the request. If the token lacks the required ability, the endpoint returns `403 Forbidden`.
   266→
   267→---
   268→
   269→### 1.4 Guard Configuration
   270→
   271→File to configure: `config/auth.php`
   272→
   273→#### Guards Data Dictionary
   274→
   275→| Guard Name | Driver | Provider | Purpose |
   276→|------------|--------|----------|---------|
   277→| `web` | session | `users` | Admin panel session auth (default guard) |
   278→| `customer` | session | `customers` | Storefront customer session auth |
   279→| `sanctum` | sanctum | `users` | Admin REST API token auth |
   280→
   281→#### Providers Data Dictionary
   282→
   283→| Provider Name | Driver | Model | Purpose |
   284→|---------------|--------|-------|---------|
   285→| `users` | eloquent | `App\Models\User` | Admin users |
   286→| `customers` | eloquent | `App\Models\Customer` | Storefront customers (store-scoped via custom provider) |
   287→
   288→#### Password Brokers Data Dictionary
   289→
   290→| Broker Name | Provider | Token Table | Expiry | Throttle |
   291→|-------------|----------|-------------|--------|----------|
   292→| `users` | `users` | `password_reset_tokens` | 60 min | 60 sec |
   293→| `customers` | `customers` | `customer_password_reset_tokens` | 60 min | 60 sec |
   294→
   295→**Defaults:** The default guard is `web` and the default password broker is `users`.
   296→
   297→---
   298→
   299→## 2. Authorization
   300→
   301→### 2.1 Role Enum
   302→
   303→File: `app/Enums/StoreUserRole.php`
   304→
   305→A backed string enum with four values:
   306→
   307→| Case | Stored Value | Description |
   308→|------|-------------|-------------|
   309→| Owner | `'owner'` | Full access. Can transfer ownership, manage billing, delete store. One per store. |
   310→| Admin | `'admin'` | Near-full access. Cannot transfer ownership or delete store. |
   311→| Staff | `'staff'` | Operational access. Can manage products, orders, collections, discounts, pages. Cannot access settings, themes, staff management, or developers. |
   312→| Support | `'support'` | Read-only access to products, orders, and customers. For customer service agents. |
   313→
   314→**Constraints:**
   315→
   316→- Each store must have exactly one Owner.
   317→- Ownership transfer is a dedicated action (not a simple role change).
   318→- A User can hold different roles in different stores (via the `store_users` pivot table).
   319→
   320→---
   321→
   322→### 2.2 Permission Matrix
   323→
   324→The `store_users` pivot table links a `User` to a `Store` with a `role`. A user can have different roles in different stores.
   325→
   326→| Resource | Action | Owner | Admin | Staff | Support |
   327→|----------|--------|:-----:|:-----:|:-----:|:-------:|
   328→| Store Settings | View | Y | Y | N | N |
   329→| Store Settings | Update | Y | Y | N | N |
   330→| Store | Delete | Y | N | N | N |
   331→| Products | List / View | Y | Y | Y | Y |
   332→| Products | Create | Y | Y | Y | N |
   333→| Products | Update | Y | Y | Y | N |
   334→| Products | Delete / Archive | Y | Y | N | N |
   335→| Products | Restore | Y | Y | N | N |
   336→| Collections | List / View | Y | Y | Y | Y |
   337→| Collections | Create | Y | Y | Y | N |
   338→| Collections | Update | Y | Y | Y | N |
   339→| Collections | Delete | Y | Y | N | N |
   340→| Orders | List / View | Y | Y | Y | Y |
   341→| Orders | Update | Y | Y | Y | N |
   342→| Orders | Cancel | Y | Y | N | N |
   343→| Fulfillments | Create | Y | Y | Y | N |
   344→| Fulfillments | Update / Cancel | Y | Y | Y | N |
   345→| Refunds | Create | Y | Y | N | N |
   346→| Customers | List / View | Y | Y | Y | Y |
   347→| Customers | Update | Y | Y | Y | N |
   348→| Discounts | List / View | Y | Y | Y | Y |
   349→| Discounts | Create | Y | Y | Y | N |
   350→| Discounts | Update | Y | Y | Y | N |
   351→| Discounts | Delete | Y | Y | N | N |
   352→| Themes | List / View | Y | Y | N | N |
   353→| Themes | Create / Upload | Y | Y | N | N |
   354→| Themes | Customize | Y | Y | N | N |
   355→| Themes | Publish | Y | Y | N | N |
   356→| Themes | Delete | Y | Y | N | N |
   357→| Pages | List / View | Y | Y | Y | N |
   358→| Pages | Create | Y | Y | Y | N |
   359→| Pages | Update | Y | Y | Y | N |
   360→| Pages | Delete | Y | Y | N | N |
   361→| Navigation | List / View | Y | Y | Y | N |
   362→| Navigation | Manage | Y | Y | N | N |
   363→| Apps | List / View | Y | Y | N | N |
   364→| Apps | Install / Remove | Y | Y | N | N |
   365→| Analytics | View | Y | Y | Y | N |
   366→| Developers / API | Manage | Y | Y | N | N |
   367→| Staff | List / View | Y | Y | N | N |
   368→| Staff | Invite | Y | Y | N | N |
   369→| Staff | Update Role | Y | Y | N | N |
   370→| Staff | Remove | Y | Y | N | N |
   371→| Shipping Zones | Manage | Y | Y | N | N |
   372→| Tax Settings | Manage | Y | Y | N | N |
   373→| Search Settings | Manage | Y | Y | N | N |
   374→
   375→#### Role Group Shorthand
   376→
   377→The permission matrix reveals three natural groupings that the authorization layer should use as reusable helpers:
   378→
   379→| Group Name | Roles Included | Used For |
   380→|------------|---------------|----------|
   381→| OwnerOnly | Owner | Store deletion, ownership transfer |
   382→| OwnerOrAdmin | Owner, Admin | Settings, themes, staff, developers, navigation, apps, shipping, taxes, search, delete/archive actions |
   383→| OwnerAdminOrStaff | Owner, Admin, Staff | Create/update products, collections, discounts, pages, fulfillments, order updates |
   384→| AnyRole | Owner, Admin, Staff, Support | Read-only listing/viewing of products, orders, customers, collections, discounts |
   385→
   386→---
   387→
   388→### 2.3 Role Checking Trait
   389→
   390→File: `app/Traits/ChecksStoreRole.php`
   391→
   392→A reusable trait included in all policies and used by gate definitions. Must provide the following methods:
   393→
   394→| Method | Parameters | Returns | Description |
   395→|--------|-----------|---------|-------------|
   396→| `getStoreRole` | User, store_id (int) | StoreUserRole or null | Looks up the user's role for the given store via the `store_users` relationship. Returns null if the user has no role in that store. |
   397→| `hasRole` | User, store_id (int), roles (array of StoreUserRole) | bool | Returns true if the user's role for the store is in the provided list. Returns false if user has no role. |
   398→| `isOwnerOrAdmin` | User, store_id (int) | bool | Shorthand: checks if role is Owner or Admin. |
   399→| `isOwnerAdminOrStaff` | User, store_id (int) | bool | Shorthand: checks if role is Owner, Admin, or Staff. |
   400→| `isAnyRole` | User, store_id (int) | bool | Returns true if the user has any role at all in the store. |
   401→
   402→**Store ID resolution:** When the policy method receives a model instance (e.g., Product), the store_id comes from the model's `store_id` attribute. When the policy method does not receive a model (e.g., `viewAny`, `create`), the store_id is resolved from the container-bound `current_store`.
   403→
   404→---
   405→
   406→### 2.4 Laravel Policies
   407→
   408→All policies use the `ChecksStoreRole` trait. Laravel 12 auto-discovers policies by convention (`App\Policies\{Model}Policy`), so no manual registration is needed.
   409→
   410→Every policy method follows this pattern:
   411→1. Determine the `store_id` (from the model if available, otherwise from the resolved current store).
   412→2. Use the appropriate role-checking helper method.
   413→3. Return a boolean.
   414→
   415→#### ProductPolicy
   416→
   417→File: `app/Policies/ProductPolicy.php`
   418→
   419→| Method | Receives Model? | Required Roles | Store ID Source |
   420→|--------|:--------------:|----------------|-----------------|
   421→| `viewAny(User)` | No | AnyRole | current_store |
   422→| `view(User, Product)` | Yes | AnyRole | product.store_id |
   423→| `create(User)` | No | OwnerAdminOrStaff | current_store |
   424→| `update(User, Product)` | Yes | OwnerAdminOrStaff | product.store_id |
   425→| `delete(User, Product)` | Yes | OwnerOrAdmin | product.store_id |
   426→| `archive(User, Product)` | Yes | OwnerOrAdmin | product.store_id |
   427→| `restore(User, Product)` | Yes | OwnerOrAdmin | product.store_id |
   428→
   429→#### OrderPolicy
   430→
   431→File: `app/Policies/OrderPolicy.php`
   432→
   433→| Method | Receives Model? | Required Roles | Store ID Source |
   434→|--------|:--------------:|----------------|-----------------|
   435→| `viewAny(User)` | No | AnyRole | current_store |
   436→| `view(User, Order)` | Yes | AnyRole | order.store_id |
   437→| `update(User, Order)` | Yes | OwnerAdminOrStaff | order.store_id |
   438→| `cancel(User, Order)` | Yes | OwnerOrAdmin | order.store_id |
   439→| `createFulfillment(User, Order)` | Yes | OwnerAdminOrStaff | order.store_id |
   440→| `createRefund(User, Order)` | Yes | OwnerOrAdmin | order.store_id |
   441→
   442→#### CollectionPolicy
   443→
   444→File: `app/Policies/CollectionPolicy.php`
   445→
   446→| Method | Receives Model? | Required Roles | Store ID Source |
   447→|--------|:--------------:|----------------|-----------------|
   448→| `viewAny(User)` | No | AnyRole | current_store |
   449→| `view(User, Collection)` | Yes | AnyRole | collection.store_id |
   450→| `create(User)` | No | OwnerAdminOrStaff | current_store |
   451→| `update(User, Collection)` | Yes | OwnerAdminOrStaff | collection.store_id |
   452→| `delete(User, Collection)` | Yes | OwnerOrAdmin | collection.store_id |
   453→
   454→#### DiscountPolicy
   455→
   456→File: `app/Policies/DiscountPolicy.php`
   457→
   458→| Method | Receives Model? | Required Roles | Store ID Source |
   459→|--------|:--------------:|----------------|-----------------|
   460→| `viewAny(User)` | No | AnyRole | current_store |
   461→| `view(User, Discount)` | Yes | AnyRole | discount.store_id |
   462→| `create(User)` | No | OwnerAdminOrStaff | current_store |
   463→| `update(User, Discount)` | Yes | OwnerAdminOrStaff | discount.store_id |
   464→| `delete(User, Discount)` | Yes | OwnerOrAdmin | discount.store_id |
   465→
   466→#### CustomerPolicy
   467→
   468→File: `app/Policies/CustomerPolicy.php`
   469→
   470→| Method | Receives Model? | Required Roles | Store ID Source |
   471→|--------|:--------------:|----------------|-----------------|
   472→| `viewAny(User)` | No | AnyRole | current_store |
   473→| `view(User, Customer)` | Yes | AnyRole | customer.store_id |
   474→| `update(User, Customer)` | Yes | OwnerAdminOrStaff | customer.store_id |
   475→
   476→#### ThemePolicy
   477→
   478→File: `app/Policies/ThemePolicy.php`
   479→
   480→| Method | Receives Model? | Required Roles | Store ID Source |
   481→|--------|:--------------:|----------------|-----------------|
   482→| `viewAny(User)` | No | OwnerOrAdmin | current_store |
   483→| `view(User, Theme)` | Yes | OwnerOrAdmin | theme.store_id |
   484→| `create(User)` | No | OwnerOrAdmin | current_store |
   485→| `update(User, Theme)` | Yes | OwnerOrAdmin | theme.store_id |
   486→| `delete(User, Theme)` | Yes | OwnerOrAdmin | theme.store_id |
   487→| `publish(User, Theme)` | Yes | OwnerOrAdmin | theme.store_id |
   488→
   489→#### PagePolicy
   490→
   491→File: `app/Policies/PagePolicy.php`
   492→
   493→| Method | Receives Model? | Required Roles | Store ID Source |
   494→|--------|:--------------:|----------------|-----------------|
   495→| `viewAny(User)` | No | OwnerAdminOrStaff | current_store |
   496→| `view(User, Page)` | Yes | OwnerAdminOrStaff | page.store_id |
   497→| `create(User)` | No | OwnerAdminOrStaff | current_store |
   498→| `update(User, Page)` | Yes | OwnerAdminOrStaff | page.store_id |
   499→| `delete(User, Page)` | Yes | OwnerOrAdmin | page.store_id |
   500→
   501→#### StorePolicy
   502→
   503→File: `app/Policies/StorePolicy.php`
   504→
   505→| Method | Receives Model? | Required Roles | Store ID Source |
   506→|--------|:--------------:|----------------|-----------------|
   507→| `viewSettings(User, Store)` | Yes | OwnerOrAdmin | store.id |
   508→| `updateSettings(User, Store)` | Yes | OwnerOrAdmin | store.id |
   509→| `delete(User, Store)` | Yes | OwnerOnly | store.id |
   510→
   511→#### FulfillmentPolicy
   512→
   513→File: `app/Policies/FulfillmentPolicy.php`
   514→
   515→| Method | Receives Model? | Required Roles | Store ID Source |
   516→|--------|:--------------:|----------------|-----------------|
   517→| `create(User, Order)` | Yes (Order) | OwnerAdminOrStaff | order.store_id |
   518→| `update(User, Fulfillment)` | Yes | OwnerAdminOrStaff | fulfillment -> order.store_id |
   519→| `cancel(User, Fulfillment)` | Yes | OwnerAdminOrStaff | fulfillment -> order.store_id |
   520→
   521→Note: `create` receives the parent Order, not a Fulfillment, since the Fulfillment does not exist yet. The `update` and `cancel` methods navigate through the Fulfillment's order relationship to get the store_id.
   522→
   523→#### RefundPolicy
   524→
   525→File: `app/Policies/RefundPolicy.php`
   526→
   527→| Method | Receives Model? | Required Roles | Store ID Source |
   528→|--------|:--------------:|----------------|-----------------|
   529→| `create(User, Order)` | Yes (Order) | OwnerOrAdmin | order.store_id |
   530→
   531→Note: `create` receives the parent Order for the same reason as FulfillmentPolicy.
   532→
   533→#### NavigationMenuPolicy
   534→
   535→File: `app/Policies/NavigationMenuPolicy.php`
   536→
   537→| Method | Receives Model? | Required Roles | Store ID Source |
   538→|--------|:--------------:|----------------|-----------------|
   539→| `viewAny(User)` | No | OwnerAdminOrStaff | current_store |
   540→| `manage(User)` | No | OwnerOrAdmin | current_store |
   541→
   542→---
   543→
   544→### 2.5 Gates (Non-Model Operations)
   545→
   546→Gates are registered in `AppServiceProvider` (or a dedicated `AuthServiceProvider`). Each gate resolves the current store from the container and checks the user's role via the `store_users` relationship.
   547→
   548→| Gate Name | Required Roles | Purpose |
   549→|-----------|---------------|---------|
   550→| `manage-store-settings` | Owner, Admin | Access store settings pages |
   551→| `manage-staff` | Owner, Admin | Invite, update, remove staff members |
   552→| `manage-developers` | Owner, Admin | Create/revoke API tokens, manage webhooks |
   553→| `view-analytics` | Owner, Admin, Staff | View analytics dashboard and reports |
   554→| `manage-shipping` | Owner, Admin | Manage shipping zones, rates, profiles |
   555→| `manage-taxes` | Owner, Admin | Manage tax settings and rates |
   556→| `manage-search-settings` | Owner, Admin | Configure search weights and synonyms |
   557→| `manage-navigation` | Owner, Admin | Create/edit/delete navigation menus |
   558→| `manage-apps` | Owner, Admin | Install and remove third-party apps |
   559→
   560→**Gate resolution pattern (all gates follow this):**
   561→
   562→1. Resolve the current store from the container
   563→2. Look up the user's `store_users` record for that store
   564→3. If no record exists, return false
   565→4. If the user's role is in the gate's required roles list, return true
   566→5. Otherwise return false
   567→
   568→---
   569→
   570→### 2.6 Policy and Gate Usage Patterns
   571→
   572→#### In Livewire Components
   573→
   574→- List views: call `authorize('viewAny', ModelClass)` in the `mount()` lifecycle method.
   575→- Action methods (delete, update, etc.): call `authorize('action', $modelInstance)` at the start of the action, before performing any mutation.
   576→- If authorization fails, Laravel throws `AuthorizationException`, which renders as 403.
   577→
   578→#### In API Controllers
   579→
   580→- Use `$this->authorize('action', $model)` at the start of each controller method.
   581→- Alternatively, use `can` middleware on routes for simpler checks.
   582→- Form Requests also implement `authorize()` to check the relevant policy or gate before validation runs.
   583→
   584→#### In Blade Templates
   585→
   586→- Use `@can('gate-name')` / `@cannot` directives to conditionally show/hide navigation items and action buttons.
   587→- This is purely cosmetic (UI hiding) - the actual authorization check happens server-side in policies/gates.
   588→
   589→---
   590→
   591→## 3. Middleware Stack
   592→
   593→### 3.1 Middleware Registration
   594→
   595→File: `bootstrap/app.php`
   596→
   597→Three custom middleware aliases must be registered:
   598→
   599→| Alias | Class | Purpose |
   600→|-------|-------|---------|
   601→| `store.resolve` | `App\Http\Middleware\ResolveStore` | Resolves current store from hostname or session |
   602→| `role.check` | `App\Http\Middleware\CheckStoreRole` | Checks user has required role for current store |
   603→| `auth:customer` | `App\Http\Middleware\CustomerAuthenticate` | Ensures customer is authenticated via customer guard |
   604→
   605→---
   606→
   607→### 3.2 Route Middleware Groups
   608→
   609→| Route Group | Middleware Stack | Description |
   610→|-------------|-----------------|-------------|
   611→| Admin web (auth pages) | `web` | Login, forgot-password, reset-password |
   612→| Admin web (authenticated) | `web`, `auth`, `verified`, `store.resolve`, `role.check` | All admin panel pages |
   613→| Admin API | `api`, `auth:sanctum`, `store.resolve`, `throttle:api.admin` | REST API endpoints |
   614→| Storefront web (public) | `web`, `store.resolve` | Public store pages (catalog, product, cart) |
   615→| Storefront web (customer) | `web`, `store.resolve`, `auth:customer` | Customer account pages |
   616→| Storefront API | `api`, `store.resolve`, `throttle:api.storefront` | Cart and checkout API |
   617→| Webhook routes | `api`, `throttle:webhooks` | Reserved for outbound app webhooks (no inbound payment webhooks - Mock PSP is in-process) |
   618→
   619→---
   620→
   621→### 3.3 Custom Middleware Specifications
   622→
   623→#### ResolveStore
   624→
   625→File: `app/Http/Middleware/ResolveStore.php`
   626→
   627→**Purpose:** Resolves the current store from the request hostname and binds it to the service container.
   628→
   629→```mermaid
   630→flowchart TD
   631→    A[Extract hostname from request] --> B{Route type?}
   632→    B -- Storefront --> C{Match hostname in store_domains table?}
   633→    C -- Yes --> F[Store found]
   634→    C -- No --> E[Abort 404: Store not found]
   635→    F --> G{Store status == suspended?}
   636→    G -- Yes --> H[Abort 503: Store unavailable]
   637→    G -- No --> I[Bind Store to container as 'current_store']
   638→    I --> J[Share Store with all views as 'currentStore']
   639→    J --> K[Continue to next middleware]
   640→    B -- Admin --> L[Read store_id from session 'current_store_id']
   641→    L --> M{User has store_users record?}
   642→    M -- No --> N[Abort 403]
   643→    M -- Yes --> I
   644→```
   645→
   646→**Requirements:**
   647→
   648→Storefront requests: Resolve store from the `store_domains` table by matching the request hostname against `store_domains.hostname`. Cache the hostname-to-store mapping using file cache (tag: `store-domains`).
   649→
   650→Admin requests: Store is determined by session key `current_store_id`, set after login or via the store-switcher in the admin panel. Admin routes do NOT use hostname resolution. The `admin.acme-fashion.test` domain in seeders routes requests to the admin panel, but the actual store context within the admin is determined by the session `current_store_id` after login.
   651→
   652→**Shared steps (after store is resolved):**
   653→
   654→1. If the store's `status` column is `'suspended'`: abort with HTTP 503 and message "This store is currently unavailable." (For admin routes: abort 403 for mutations instead.)
   655→2. Bind the resolved Store model instance to the service container with the key `'current_store'`.
   656→3. Share the store with all Blade views under the variable name `currentStore`.
   657→4. Allow the request to proceed.
   658→
   659→#### CheckStoreRole
   660→
   661→File: `app/Http/Middleware/CheckStoreRole.php`
   662→
   663→**Purpose:** Verifies the authenticated user has one of the specified roles for the current store.
   664→
   665→**Route usage:** Applied as `role.check:owner,admin` or `role.check:owner,admin,staff` (comma-separated role values passed as variadic parameters).
   666→
   667→```mermaid
   668→flowchart TD
   669→    A[Resolve current_store from container] --> B[Get authenticated user]
   670→    B --> C[Look up store_users record for user + store]
   671→    C --> D{Record exists?}
   672→    D -- No --> E[Abort 403: No access to this store]
   673→    D -- Yes --> F[Parse allowed roles from middleware parameters]
   674→    F --> G{User's role in allowed list?}
   675→    G -- No --> H[Abort 403: Insufficient permissions]
   676→    G -- Yes --> I[Attach store_user to request attributes]
   677→    I --> J[Continue to next middleware]
   678→```
   679→
   680→**Requirements:**
   681→
   682→1. Resolve the current store from the container.
   683→2. Get the authenticated user from the request.
   684→3. Query the user's `store_users` relationship filtered by `store_id`.
   685→4. If no record exists: abort 403 with message "You do not have access to this store."
   686→5. Convert the middleware parameter strings to `StoreUserRole` enum instances.
   687→6. If the user's role is not in the allowed list: abort 403 with message "Insufficient permissions."
   688→7. Store the resolved `StoreUser` pivot record on the request attributes (key: `store_user`) for downstream use.
   689→8. Allow the request to proceed.
   690→
   691→#### CustomerAuthenticate
   692→
   693→File: `app/Http/Middleware/CustomerAuthenticate.php`
   694→
   695→**Purpose:** Ensures the request is authenticated via the `customer` guard. Redirects unauthenticated customers to login.
   696→
   697→```mermaid
   698→flowchart TD
   699→    A[Check customer guard authentication] --> B{Authenticated?}
   700→    B -- Yes --> C[Continue to next middleware]
   701→    B -- No --> D[Store current URL as intended redirect]
   702→    D --> E[Redirect to /account/login]
   703→```
   704→
   705→**Requirements:**
   706→
   707→1. Check if `Auth::guard('customer')` reports the user as authenticated.
   708→2. If authenticated: allow the request to proceed.
   709→3. If not authenticated: save the current URL in the session as the intended URL (so the customer returns there after login), then redirect to the storefront login route (`/account/login`).
   710→
   711→---
   712→
   713→## 4. Security Controls
   714→
   715→### 4.1 CSRF Protection
   716→
   717→| Context | Mechanism |
   718→|---------|-----------|
   719→| Blade forms | Laravel's `@csrf` directive injects a hidden token field (automatic) |
   720→| Livewire actions | Livewire handles CSRF verification automatically on every request |
   721→| API routes (Sanctum token) | Exempt from CSRF (token-based auth replaces CSRF need) |
   722→| SPA clients | `GET /sanctum/csrf-cookie` sets XSRF-TOKEN cookie for session-based API calls |
   723→
   724→**Requirements:**
   725→
   726→- CSRF verification is enforced globally on all `web` middleware group routes.
   727→- No routes within the `web` group may be excluded from CSRF verification.
   728→- API routes (under `api` middleware group) are inherently exempt since they use token auth.
   729→
   730→---
   731→
   732→### 4.2 Rate Limiting
   733→
   734→All rate limiters are defined in `bootstrap/app.php` (or `AppServiceProvider::boot()`).
   735→
   736→| Limiter Name | Limit | Keyed By | Applied To |
   737→|-------------|-------|----------|------------|
   738→| `login` | 5 per minute | IP address | Admin login, customer login |
   739→| `api.admin` | 60 per minute | User ID (if authenticated), otherwise IP | Admin REST API endpoints |
   740→| `api.storefront` | 120 per minute | IP address | Public storefront API (cart, search) |
   741→| `checkout` | 10 per minute | Session ID | Checkout-related operations |
   742→| `search` | 30 per minute | IP address | Search queries |
   743→| `analytics` | 60 per minute | IP address | Analytics event ingestion |
   744→| `webhooks` | 100 per minute | IP address | Outbound app webhook endpoints (reserved; no inbound payment webhooks - Mock PSP is in-process) |
   745→
   746→**Rate limit response headers** (returned on all API responses by Laravel default):
   747→
   748→| Header | Description |
   749→|--------|-------------|
   750→| `X-RateLimit-Limit` | Maximum requests allowed in the window |
   751→| `X-RateLimit-Remaining` | Requests remaining in the current window |
   752→| `Retry-After` | Seconds until the rate limit resets (only on 429 responses) |
   753→
   754→When a rate limit is exceeded, the response is HTTP 429 Too Many Requests.
   755→
   756→---
   757→
   758→### 4.3 Encryption at Rest
   759→
   760→Encryption algorithm: AES-256-CBC with MAC verification, keyed by the application's `APP_KEY`.
   761→
   762→**Fields requiring encrypted storage (using Laravel's `encrypted` cast):**
   763→
   764→| Model | Column | Cast Type | Purpose |
   765→|-------|--------|-----------|---------|
   766→| OauthClient | `client_secret_encrypted` | `encrypted` | Third-party OAuth client secrets |
   767→| WebhookSubscription | `signing_secret_encrypted` | `encrypted` | Per-subscription signing secrets for outbound webhooks |
   768→| Payment | `raw_json` | `encrypted:array` | Full payment gateway response (may contain sensitive card/account data) |
   769→
   770→**Requirements:**
   771→
   772→- These columns are declared with the `encrypted` (or `encrypted:array`) cast in the model's `casts()` method.
   773→- Data is automatically encrypted before writing to the database and decrypted when reading.
   774→- The encrypted columns store opaque ciphertext (type TEXT in SQLite). They cannot be queried with WHERE clauses at the database level.
   775→
   776→---
   777→
   778→### 4.4 Input Validation
   779→
   780→All write endpoints must use dedicated Form Request classes. Validation rules use string syntax per project convention.
   781→
   782→#### Form Request Inventory
   783→
   784→| Form Request Class | Used By | Key Validation Rules |
   785→|-------------------|---------|---------------------|
   786→| `StoreProductRequest` | Create product | `title`: required, string, max:255. `description_html`: nullable, string. `status`: required, in:active,draft,archived. `vendor`: nullable, string. Price fields: required, integer, min:0. |
   787→| `UpdateProductRequest` | Update product | Same fields as StoreProductRequest but all optional (sometimes rule). |
   788→| `StoreCollectionRequest` | Create collection | `title`: required, string, max:255. `type`: required, in:manual,automated. `rules_json`: required_if:type,automated, valid JSON. |
   789→| `UpdateCollectionRequest` | Update collection | Same as StoreCollectionRequest, all optional. |
   790→| `StoreDiscountRequest` | Create discount | `type`: required, in:code,automatic. `code`: required_if:type,code, string, max:50, unique per store (case-insensitive). `value_type`: required, in:fixed,percent,free_shipping. `value_amount`: required_unless:value_type,free_shipping, integer, min:0; when value_type is `percent`, max:100. `minimum_order_amount`: nullable, integer, min:0. `usage_limit`: nullable, integer, min:1. `starts_at`: nullable, date, ISO-8601. `ends_at`: nullable, date, after:starts_at. |
   791→| `UpdateDiscountRequest` | Update discount | Same as StoreDiscountRequest, code unique ignoring self. |
   792→| `SetCheckoutAddressRequest` | Set shipping/billing address | `first_name`, `last_name`, `address1`, `city`, `country_code`: required. `province_code`: required when applicable. `postal_code`: required. `phone`: nullable. |
   793→| `ApplyDiscountRequest` | Apply discount to checkout | `code`: required, string, must exist in discounts table for current store. |
   794→| `CreateFulfillmentRequest` | Create fulfillment | `line_item_ids`: required, array of existing line item IDs. `tracking_company`: nullable, string. `tracking_number`: nullable, string. `tracking_url`: nullable, valid URL. |
   795→| `CreateRefundRequest` | Create refund | `line_items`: required, array with `id` and `quantity` per item. `reason`: required, string. `amount`: required, integer, min:0, max:original payment amount. |
   796→| `UpdateStoreSettingsRequest` | Update store settings | `name`: required, string. `default_currency`: in allowed currencies. `default_locale`: in allowed locales. `timezone`: valid timezone string. |
   797→| `InviteStaffRequest` | Invite staff member | `email`: required, valid email. `role`: required, in:admin,staff,support (not owner). |
   798→| `RegisterCustomerRequest` | Customer registration | `name`: required. `email`: required, email, unique per store (composite). `password`: required, min:8, confirmed. |
   799→| `LoginRequest` | Admin/customer login | `email`: required, email. `password`: required, string. |
   800→
   801→**Authorization in Form Requests:**
   802→
   803→Each Form Request's `authorize()` method must check the relevant policy or gate. For example, `StoreProductRequest::authorize()` checks whether the user can create a Product. If authorization fails, the request is rejected with 403 before validation even runs.
   804→
   805→---
   806→
   807→### 4.5 XSS Prevention
   808→
   809→| Context | Strategy |
   810→|---------|----------|
   811→| Standard Blade output | Double-curly-brace syntax auto-escapes all output (Laravel default) |
   812→| Rich HTML output | Unescaped syntax used only for trusted HTML fields: product `description_html`, page `body_html` |
   813→| HTML input sanitization | Use HTMLPurifier (or equivalent) on save, before persisting to database |
   814→| Livewire properties | Auto-escaped by default when rendered in Blade |
   815→| JSON API responses | Laravel escapes JSON output by default |
   816→
   817→#### HTML Sanitization Requirements
   818→
   819→An `SanitizeHtml` action class must be created at `app/Actions/SanitizeHtml.php`.
   820→
   821→**Allowed HTML elements and attributes:**
   822→
   823→| Element | Allowed Attributes |
   824→|---------|-------------------|
   825→| `p`, `br`, `strong`, `em`, `u` | none |
   826→| `ol`, `ul`, `li` | none |
   827→| `a` | `href` |
   828→| `img` | `src`, `alt` |
   829→| `h1` through `h6` | none |
   830→| `blockquote` | none |
   831→| `table`, `thead`, `tbody`, `tr`, `th`, `td` | none |
   832→| `div`, `span` | none |
   833→
   834→All other elements and attributes must be stripped. Empty elements should be removed.
   835→
   836→**Where sanitization is applied:** Before persisting product `description_html` and page `body_html` fields. The sanitization runs during the create and update operations for these models.
   837→
   838→---
   839→
   840→### 4.6 SQL Injection Prevention
   841→
   842→**Requirements:**
   843→
   844→- All database queries must use Eloquent ORM with parameterized bindings (Laravel default behavior).
   845→- No raw query strings may contain unsanitized user input.
   846→- Any use of `whereRaw`, `selectRaw`, or `DB::raw` must use parameter bindings (the `?` placeholder syntax).
   847→- FTS5 (full-text search) queries require special handling: user input must be sanitized before building the MATCH expression.
   848→
   849→#### FTS5 Input Sanitization
   850→
   851→Before using user input in an FTS5 MATCH expression:
   852→
   853→1. Strip all characters that are not Unicode letters, Unicode numbers, or whitespace (remove FTS5 special operators like `*`, `AND`, `OR`, `NOT`, `NEAR`, quotes, parentheses, etc.).
   854→2. Trim leading and trailing whitespace.
   855→3. Pass the sanitized string via a parameterized binding (never concatenate into the query string).
   856→
   857→---
   858→
   859→### 4.7 Mass Assignment Protection
   860→
   861→**Requirements:**
   862→
   863→- Every Eloquent model must define a `$fillable` array listing only the columns that are safe for mass assignment.
   864→- The pattern `$guarded = []` is never permitted in any model.
   865→- All controller and Livewire action code must use `$request->validated()` (not `$request->all()` or `$request->input()`) when passing data to model creation or update methods.
   866→
   867→---
   868→
   869→### 4.8 Password Security
   870→
   871→| Setting | Value |
   872→|---------|-------|
   873→| Minimum length | 8 characters (for both admin and customer passwords) |
   874→| Hashing algorithm | Bcrypt (Laravel default) |
   875→| Password in logs | Never logged - use Laravel's built-in log redaction |
   876→| Password in API responses | Never returned - `password` and `remember_token` must be in the model's `$hidden` array |
   877→
   878→**Model requirements:**
   879→
   880→- Both `User` and `Customer` models must include `password` and `remember_token` in their `$hidden` array.
   881→- Password validation rules must enforce minimum 8 characters and use the `confirmed` rule (requiring a matching `password_confirmation` field).
   882→
   883→**Important:** Both the `User` and `Customer` models use `password_hash` as the database column name instead of Laravel's default `password`. Both models must override `getAuthPassword()`:
   884→
   885→```php
   886→public function getAuthPassword(): string
   887→{
   888→    return $this->password_hash;
   889→}
   890→```
   891→
   892→This is required for `Auth::attempt()` and `Hash::check()` to work correctly with the custom column name.
   893→
   894→---
   895→
   896→## 5. Session Configuration
   897→
   898→File: `config/session.php`
   899→
   900→| Setting | Value | Rationale |
   901→|---------|-------|-----------|
   902→| `driver` | `'file'` | Simple, no external dependencies. Compatible with SQLite-based stack without needing Redis. |
   903→| `lifetime` | 120 (minutes) | Configurable via `SESSION_LIFETIME` env var. |
   904→| `expire_on_close` | `false` | Session persists until lifetime expires, not on browser close. |
   905→| `encrypt` | `true` | Session data encrypted at rest on disk. |
   906→| `cookie` | `'shop_session'` | Custom name to avoid generic fingerprinting. Configurable via `SESSION_COOKIE` env var. |
   907→| `path` | `'/'` | Cookie available across entire domain. |
   908→| `domain` | `null` | Laravel determines from request. |
   909→| `secure` | `true` | HTTPS-only cookies in production. Configurable via `SESSION_SECURE_COOKIE` env var (set to false for local dev). |
   910→| `http_only` | `true` | Cookie inaccessible to JavaScript (mitigates XSS-based cookie theft). |
   911→| `same_site` | `'lax'` | Allows navigation-initiated requests while preventing CSRF from external origins. |
   912→| `partitioned` | `false` | Not needed for this use case. |
   913→
   914→---
   915→
   916→## 6. CORS Configuration
   917→
   918→File: `config/cors.php`
   919→
   920→| Setting | Value | Rationale |
   921→|---------|-------|-----------|
   922→| `paths` | `['api/*', 'sanctum/csrf-cookie']` | Only API and Sanctum CSRF cookie endpoints are CORS-enabled. Web routes are not. |
   923→| `allowed_methods` | `['*']` | All HTTP methods allowed for API. |
   924→| `allowed_origins` | `[FRONTEND_URL env var, default '*']` | Restrict to store domain in production. Wildcard for development. |
   925→| `allowed_origins_patterns` | `[]` | No pattern-based origin matching needed. |
   926→| `allowed_headers` | `['*']` | All headers allowed (Authorization, Content-Type, etc.). |
   927→| `exposed_headers` | `['X-RateLimit-Limit', 'X-RateLimit-Remaining', 'Retry-After']` | Expose rate limit headers so API clients can implement backoff. |
   928→| `max_age` | `0` | No preflight caching (conservative default). |
   929→| `supports_credentials` | `true` | Required for Sanctum SPA auth (cookies). |
   930→
   931→---
   932→
   933→## 7. Webhook Signature Verification
   934→
   935→### 7.1 Inbound Payment Webhooks
   936→
   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).
   938→
   939→---
   940→
   941→### 7.2 Platform Webhooks (Outbound)
   942→
   943→Service: `App\Services\WebhookDispatcher`
   944→
   945→When the platform sends webhooks to external subscribers (third-party apps, integrations).
   946→
   947→```mermaid
   948→sequenceDiagram
   949→    participant App as Platform
   950→    participant Sub as Subscriber URL
   951→
   952→    App->>App: Serialize payload as JSON
   953→    App->>App: Generate Unix timestamp
   954→    App->>App: Concatenate: "{timestamp}.{json_body}"
   955→    App->>App: HMAC-SHA256(signing_secret, concatenated_message)
   956→    App->>Sub: POST to subscriber URL
   957→    Note right of Sub: Headers: X-Platform-Signature, X-Platform-Timestamp, Content-Type: application/json
   958→    Sub-->>App: 200 OK (acknowledged)
   959→```
   960→
   961→**Requirements:**
   962→
   963→1. The payload is serialized as JSON.
   964→2. A Unix timestamp is generated (current time).
   965→3. The message to sign is: `"{timestamp}.{json_body}"` (timestamp, dot, JSON string).
   966→4. The signature is computed as HMAC-SHA256 using the per-subscription `signing_secret` (decrypted from the `signing_secret_encrypted` column).
   967→5. The HTTP request to the subscriber includes three headers:
   968→   - `X-Platform-Signature`: the hex-encoded HMAC digest
   969→   - `X-Platform-Timestamp`: the Unix timestamp as a string
   970→   - `Content-Type`: `application/json`
   971→6. Replay protection guidance for recipients: reject any request where `X-Platform-Timestamp` is more than 5 minutes in the past.
   972→
   973→---
   974→
   975→## 8. Audit Logging
   976→
   977→### 8.1 Logging Channel
   978→
   979→A dedicated `audit` channel must be added to `config/logging.php`.
   980→
   981→| Setting | Value |
   982→|---------|-------|
   983→| Driver | `daily` (file-based, daily rotation) |
   984→| Path | `storage/logs/audit.log` |
   985→| Level | `info` |
   986→| Retention | 90 days |
   987→
   988→---
   989→
   990→### 8.2 Log Entry Format
   991→
   992→All audit log entries are structured JSON objects with the following schema:
   993→
   994→| Field | Type | Required | Description |
   995→|-------|------|:--------:|-------------|
   996→| `timestamp` | string (ISO-8601) | Y | When the event occurred |
   997→| `event` | string | Y | Event identifier (e.g., `product.updated`, `auth.login`) |
   998→| `user_id` | integer or null | N | ID of the admin user who triggered the event |
   999→| `store_id` | integer or null | N | ID of the store context |
  1000→| `resource_type` | string or null | N | Type of resource affected (e.g., `product`, `order`) |
  1001→| `resource_id` | integer or null | N | ID of the affected resource |
  1002→| `ip` | string | Y | Client IP address |
  1003→| `user_agent` | string | Y | Client user agent string |
  1004→| `changes` | object or null | N | For update events: object mapping field names to `[old_value, new_value]` tuples |
  1005→| (additional keys) | varies | N | Event-specific extra data |
  1006→
  1007→---
  1008→
  1009→### 8.3 Events to Log
  1010→
  1011→#### Authentication Events
  1012→
  1013→| Event ID | Trigger | Extra Data |
  1014→|----------|---------|------------|
  1015→| `auth.login` | Successful admin login | user_id, ip, user_agent |
  1016→| `auth.logout` | Admin logout | user_id |
  1017→| `auth.failed_login` | Failed admin login attempt | email (never password), ip, user_agent |
  1018→| `customer.login` | Successful customer login | customer_id, store_id, ip |
  1019→| `customer.registered` | New customer registration | customer_id, store_id |
  1020→| `customer.failed_login` | Failed customer login attempt | email, store_id, ip |
  1021→
  1022→#### Admin Resource Events
  1023→
  1024→For each of the following resource types, log `created`, `updated`, `deleted`, and `restored` (where applicable) events:
  1025→
  1026→**Resource types:** product, collection, discount, page, theme, order, fulfillment, refund, navigation_menu, shipping_zone, tax_setting, staff
  1027→
  1028→| Event Pattern | Trigger | Extra Data |
  1029→|---------------|---------|------------|
  1030→| `{resource}.created` | Resource created | resource_id, key attributes of the new resource |
  1031→| `{resource}.updated` | Resource updated | resource_id, `changes` object with old/new values for each changed field |
  1032→| `{resource}.deleted` | Resource soft-deleted or archived | resource_id |
  1033→| `{resource}.restored` | Resource restored from soft-delete | resource_id |
  1034→
  1035→#### Security Events
  1036→
  1037→| Event ID | Trigger | Extra Data |
  1038→|----------|---------|------------|
  1039→| `api_token.created` | New API token issued | token_name, abilities list, user_id |
  1040→| `api_token.revoked` | API token revoked | token_name, user_id |
  1041→| `webhook.signature_invalid` | Outbound webhook delivery failed signature verification at subscriber | subscriber_url, ip |
  1042→| `store.settings_changed` | Store settings updated | changed fields with old/new values |
  1043→
  1044→---
  1045→
  1046→### 8.4 Audit Logger Service
  1047→
  1048→File: `app/Services/AuditLogger.php`
  1049→
  1050→A service class that encapsulates audit log writing.
  1051→
  1052→**Method signature:**
  1053→
  1054→| Parameter | Type | Required | Description |
  1055→|-----------|------|:--------:|-------------|
  1056→| `event` | string | Y | Event identifier |
  1057→| `userId` | int or null | N | Acting user ID |
  1058→| `storeId` | int or null | N | Store context ID |
  1059→| `resourceType` | string or null | N | Affected resource type |
  1060→| `resourceId` | int or null | N | Affected resource ID |
  1061→| `extra` | associative array | N | Additional key-value pairs to include in the log entry |
  1062→
  1063→**Behavior:**
  1064→
  1065→1. Write to the `audit` logging channel at the `info` level.
  1066→2. Automatically include `timestamp` (ISO-8601), `ip` (from request), and `user_agent` (from request) in every entry.
  1067→3. Merge any `extra` data into the log context.
  1068→
  1069→---
  1070→
  1071→### 8.5 Integration with Laravel Events
  1072→
  1073→The audit logger hooks into Laravel's built-in authentication events and custom model observers.
  1074→
  1075→#### Auth Event Listeners
  1076→
  1077→Register listeners (in `AppServiceProvider` or `EventServiceProvider`) for these Laravel events:
  1078→
  1079→| Laravel Event | Audit Event ID | Data Captured |
  1080→|--------------|----------------|---------------|
  1081→| `Illuminate\Auth\Events\Login` | `auth.login` | user_id from event |
  1082→| `Illuminate\Auth\Events\Failed` | `auth.failed_login` | email from credentials (never password) |
  1083→| `Illuminate\Auth\Events\Logout` | `auth.logout` | user_id from event |
  1084→
  1085→#### Model Observers
  1086→
  1087→For each auditable resource model, create an observer (or use a shared trait) that logs `created`, `updated`, and `deleted` events. The `updated` observer should capture the changed fields (dirty attributes) with their original and new values.
  1088→
  1089→---
  1090→
  1091→## 9. Security Checklist Summary
  1092→
  1093→| # | Control | Implementation Approach | Priority |
  1094→|---|---------|------------------------|----------|
  1095→| 1 | Session-based auth (admin) | Laravel `web` guard with bcrypt, session regeneration on login | Required |
  1096→| 2 | Session-based auth (customer) | Custom `customer` guard, store-scoped via custom user provider | Required |
  1097→| 3 | API token auth | Sanctum personal access tokens with per-token abilities | Required |
  1098→| 4 | Role-based access control | `StoreUserRole` enum, Laravel policies on all models, gates for non-model ops | Required |
  1099→| 5 | CSRF protection | `@csrf` on all session forms, Livewire auto-handles, API routes exempt | Required |
  1100→| 6 | Rate limiting | Seven named limiters (login, api.admin, api.storefront, checkout, search, analytics, webhooks) | Required |
  1101→| 7 | Input validation | Dedicated Form Request class for every write endpoint | Required |
  1102→| 8 | XSS prevention | Blade auto-escaping by default, HTMLPurifier for rich content fields | Required |
  1103→| 9 | SQL injection prevention | Eloquent ORM with parameterized queries, FTS5 input sanitization | Required |
  1104→| 10 | Mass assignment protection | `$fillable` on all models, always use `$request->validated()` | Required |
  1105→| 11 | Encryption at rest | Laravel `encrypted` cast for secrets and sensitive JSON columns | Required |
  1106→| 12 | Password security | Bcrypt, min 8 characters, `$hidden` on User and Customer models | Required |
  1107→| 13 | Session security | Encrypted sessions, HTTP-only cookies, secure flag, SameSite=Lax | Required |
  1108→| 14 | Webhook signature verification | HMAC-SHA256 for outbound platform webhooks (no inbound payment webhooks - Mock PSP is in-process) | Required |
  1109→| 15 | Audit logging | Structured JSON to dedicated daily-rotation log channel, 90-day retention | Required |
  1110→| 16 | CORS | Restricted to `api/*` paths, credentials supported, rate limit headers exposed | Required |
  1111→

prompt:
Read ALL the specification files in /Users/fabianwesner/Herd/shop/specs/ directory. I need the full content of:
1. specs/01-DATABASE-SCHEMA.md (read in chunks if needed)
2. specs/02-API-ROUTES.md (read in chunks if needed)
3. specs/03-ADMIN-UI.md (read in chunks if needed)
4. specs/04-STOREFRONT-UI.md (read in chunks if needed)
5. specs/05-BUSINESS-LOGIC.md (read in chunks if needed)
6. specs/06-AUTH-AND-SECURITY.md (read in chunks if needed)
7. specs/07-SEEDERS-AND-TEST-DATA.md (read in chunks if needed)
8. specs/08-PLAYWRIGHT-E2E-PLAN.md (read in chunks if needed)

For each file provide a comprehensive summary including:
- All table/column definitions
- All API endpoints
- All UI components and their specifications
- All business rules
- All test specifications
- All seeder data definitions

This is critical - I need ALL the details, not just summaries. Extract every table name, column, relationship, route, component, and test case.


userType: external

slug: iterative-seeking-pine

type: progress

type: assistant

type: message

role: assistant

Bash

description: List spec files

service_tier: standard

prompt:
Explore the current state of the Laravel project at /Users/fabianwesner/Herd/shop. I need to understand:

1. What files already exist (models, controllers, migrations, routes, views, tests, etc.)
2. Current state of composer.json dependencies
3. Current state of package.json
4. Current .env configuration
5. Current config files (auth.php, database.php, etc.)
6. Current routes/web.php, routes/api.php, routes/console.php
7. Current app directory structure
8. Current tests directory structure
9. Current database/migrations directory
10. Current resources/views directory

Give me a comprehensive overview of what already exists vs what needs to be built.


type: user

role: user

content:
total 1456
drwxr-xr-x  11 fabianwesner  staff     352 Feb 13 18:02 .
drwxr-xr-x@ 39 fabianwesner  staff    1248 Feb 13 18:15 ..
-rw-r--r--@  1 fabianwesner  staff   69521 Feb 11 19:43 01-DATABASE-SCHEMA.md
-rw-r--r--@  1 fabianwesner  staff   89763 Feb 12 19:35 02-API-ROUTES.md
-rw-r--r--@  1 fabianwesner  staff  102829 Feb 12 19:35 03-ADMIN-UI.md
-rw-r--r--@  1 fabianwesner  staff   74582 Feb 12 19:35 04-STOREFRONT-UI.md
-rw-r--r--@  1 fabianwesner  staff   68936 Feb 12 19:35 05-BUSINESS-LOGIC.md
-rw-r--r--@  1 fabianwesner  staff   50342 Feb 12 19:35 06-AUTH-AND-SECURITY.md
-rw-r--r--@  1 fabianwesner  staff   69428 Feb 12 19:35 07-SEEDERS-AND-TEST-DATA.md
-rw-r--r--@  1 fabianwesner  staff   88099 Feb 12 19:35 08-PLAYWRIGHT-E2E-PLAN.md
-rw-r--r--@  1 fabianwesner  staff  116810 Feb 12 19:35 09-IMPLEMENTATION-ROADMAP.md

prompt:
Explore the current state of the Laravel project at /Users/fabianwesner/Herd/shop. I need to understand:

1. What files already exist (models, controllers, migrations, routes, views, tests, etc.)
2. Current state of composer.json dependencies
3. Current state of package.json
4. Current .env configuration
5. Current config files (auth.php, database.php, etc.)
6. Current routes/web.php, routes/api.php, routes/console.php
7. Current app directory structure
8. Current tests directory structure
9. Current database/migrations directory
10. Current resources/views directory

Give me a comprehensive overview of what already exists vs what needs to be built.


prompt:
Explore the current state of the Laravel project at /Users/fabianwesner/Herd/shop. I need to understand:

1. What files already exist (models, controllers, migrations, routes, views, tests, etc.)
2. Current state of composer.json dependencies
3. Current state of package.json
4. Current .env configuration
5. Current config files (auth.php, database.php, etc.)
6. Current routes/web.php, routes/api.php, routes/console.php
7. Current app directory structure
8. Current tests directory structure
9. Current database/migrations directory
10. Current resources/views directory

Give me a comprehensive overview of what already exists vs what needs to be built.


content:
     1→# 09 - Implementation Roadmap and Pest Test Specification
     2→
     3→> E-commerce platform - Step-by-step build order and exhaustive test plan for Laravel 12 + Livewire v4 + Flux UI Free + SQLite + Tailwind CSS v4
     4→
     5→---
     6→
     7→## 1. Technology Decisions (Final)
     8→
     9→These decisions adapt the original PDF spec for a self-contained, benchmarkable Laravel application. All decisions are final and must not be revisited.
    10→
    11→| Concern | PDF Default | Our Decision | Rationale |
    12→|---------|-------------|--------------|-----------|
    13→| Database | PostgreSQL | SQLite | Self-contained, no external DB, zero config |
    14→| Cache | Redis | File (Array for tests) | No Redis dependency |
    15→| Session | Redis | File | No Redis dependency |
    16→| Queue | Redis + Horizon | Sync (dev) / Database (prod) | No Redis dependency, sync for immediate feedback |
    17→| Search | Elasticsearch + Scout | SQLite FTS5 | Self-contained, no external service |
    18→| Storage | S3 | Local filesystem (storage/app/public) | No AWS dependency |
    19→| Admin UI | React/Vue SPA | Livewire v4 + Flux UI Free | Server-rendered, matches project stack |
    20→| Storefront | Blade SSR + Vite | Blade + Livewire + Tailwind v4 | Server-rendered, matches project stack |
    21→| Auth (admin) | Sanctum SPA | Laravel session auth | Standard Livewire auth pattern |
    22→| Auth (API) | Sanctum tokens | Sanctum personal access tokens | For programmatic API access |
    23→| Auth (customer) | Separate guard | Custom `customer` guard | Storefront customer auth |
    24→| OAuth (apps) | Passport | Deferred/Stubbed | Simplifies initial build, can add later |
    25→| Payments | Stripe + PayPal | Mock PSP (in-process) | No external API calls. MockPaymentProvider handles credit card, PayPal, bank transfer with magic card numbers for testing. |
    26→| Email | SMTP/SES | Log driver (dev) / SMTP | Log for benchmark, SMTP configurable |
    27→| Monitoring | OpenTelemetry | Laravel Log (structured JSON) | Simplified for benchmark |
    28→| CI/CD | GitHub Actions | Not implemented | Out of scope for one-shot benchmark |
    29→| Docker | Multi-stage | Not implemented | App served by Laravel Herd locally |
    30→
    31→---
    32→
    33→## 2. Implementation Phases (Build Order)
    34→
    35→Each phase builds on the previous. A coding agent should execute these in strict sequential order. Tests are written alongside each phase (see Section 3 for the full test specification).
    36→
    37→### Phase Dependency Graph
    38→
    39→```mermaid
    40→flowchart TD
    41→    P1[Phase 1: Foundation]
    42→    P2[Phase 2: Catalog]
    43→    P3[Phase 3: Themes and Storefront Layout]
    44→    P4[Phase 4: Cart, Checkout, Discounts, Shipping, Taxes]
    45→    P5[Phase 5: Payments, Orders, Fulfillment]
    46→    P6[Phase 6: Customer Accounts]
    47→    P7[Phase 7: Admin Panel]
    48→    P8[Phase 8: Search]
    49→    P9[Phase 9: Analytics]
    50→    P10[Phase 10: Apps and Webhooks]
    51→    P11[Phase 11: Polish]
    52→    P12[Phase 12: Full Test Suite]
    53→
    54→    P1 --> P2
    55→    P1 --> P3
    56→    P2 --> P4
    57→    P3 --> P4
    58→    P4 --> P5
    59→    P5 --> P6
    60→    P5 --> P7
    61→    P2 --> P8
    62→    P5 --> P9
    63→    P5 --> P10
    64→    P6 --> P11
    65→    P7 --> P11
    66→    P8 --> P11
    67→    P9 --> P11
    68→    P10 --> P11
    69→    P11 --> P12
    70→```
    71→
    72→---
    73→
    74→### Phase 1: Foundation (Migrations, Models, Middleware, Auth)
    75→
    76→**Priority:** CRITICAL - everything depends on this.
    77→
    78→#### Step 1.1: Environment and Config
    79→
    80→| Config File | Setting | Value | Purpose |
    81→|-------------|---------|-------|---------|
    82→| `.env` | `DB_CONNECTION` | `sqlite` | Use SQLite as database |
    83→| `.env` | `DB_DATABASE` | `database/database.sqlite` | SQLite database file path |
    84→| `.env` | `CACHE_STORE` | `file` | File-based caching |
    85→| `.env` | `SESSION_DRIVER` | `file` | File-based sessions |
    86→| `.env` | `QUEUE_CONNECTION` | `sync` | Synchronous queue for dev |
    87→| `.env` | `MAIL_MAILER` | `log` | Log-based mail for dev |
    88→| `.env` | `FILESYSTEM_DISK` | `local` | Local filesystem storage |
    89→| `config/database.php` | SQLite connection | WAL mode, foreign keys enabled, busy_timeout=5000, synchronous=normal | Optimized SQLite configuration |
    90→| `config/session.php` | `driver` | `file` | Session driver |
    91→| `config/session.php` | `lifetime` | `120` | Session lifetime in minutes |
    92→| `config/cache.php` | `default` | `file` | Cache driver |
    93→| `config/queue.php` | `default` | `sync` | Queue driver |
    94→| `config/auth.php` | Guards | Add `customer` guard with `customers` provider | Customer storefront authentication |
    95→| `config/auth.php` | Providers | Add `customers` provider pointing to Customer model | Customer user provider |
    96→| `config/auth.php` | Passwords | Add `customers` password broker | Customer password reset |
    97→| `config/filesystems.php` | `public` disk | Ensure configured for local storage | Media file storage |
    98→| `config/logging.php` | Channels | Add structured JSON channel | Structured logging |
    99→
   100→#### Step 1.2: Core Migrations (Batch 1-2)
   101→
   102→Create in this dependency order:
   103→
   104→1. `create_organizations_table` - no dependencies
   105→2. `create_stores_table` - FK: `organization_id`
   106→3. `create_store_domains_table` - FK: `store_id`
   107→4. Modify default `users` migration - add `status`, `last_login_at`, `two_factor_*` columns; rename `password` to `password_hash` internally
   108→5. `create_store_users_table` - composite PK: `store_id` + `user_id`, with `role` column
   109→6. `create_store_settings_table` - PK: `store_id`
   110→
   111→All monetary amounts stored as INTEGER in minor units (cents). All enums stored as TEXT with CHECK constraints. All timestamps as TEXT (ISO-8601) via Laravel.
   112→
   113→#### Step 1.3: Core Models
   114→
   115→**Files to create:**
   116→
   117→| Type | Name | Purpose |
   118→|------|------|---------|
   119→| Model (with migration, factory, seeder) | `Organization` | Top-level tenant entity |
   120→| Model (with migration, factory, seeder) | `Store` | Individual store within an organization |
   121→| Model (with migration, factory, seeder) | `StoreDomain` | Domain mapping for a store |
   122→| Model (pivot) | `StoreUser` | Pivot model with `role` attribute, custom Pivot class |
   123→| Model (with migration, factory, seeder) | `StoreSettings` | Per-store configuration |
   124→
   125→**Relationship definitions:**
   126→
   127→| Model | Key Relationships |
   128→|-------|-------------------|
   129→| `Organization` | `hasMany(Store::class)` |
   130→| `Store` | `belongsTo(Organization::class)`, `hasMany(StoreDomain::class)`, `belongsToMany(User::class)` through `store_users`, `hasOne(StoreSettings::class)` |
   131→| `StoreDomain` | `belongsTo(Store::class)` |
   132→| `User` | `belongsToMany(Store::class)` through `store_users` |
   133→| `StoreUser` | Pivot model with `role` attribute, custom `Pivot` class |
   134→| `StoreSettings` | `belongsTo(Store::class)` |
   135→
   136→Each model must have:
   137→- Explicit return type declarations on all methods
   138→- Proper `$fillable` or `$guarded` arrays
   139→- `casts()` method for JSON/enum columns
   140→- Factory with meaningful default values
   141→- Seeder that creates sample data
   142→
   143→#### Step 1.4: Enums
   144→
   145→**Files to create in `app/Enums/`:**
   146→
   147→| Enum | Values |
   148→|------|--------|
   149→| `StoreStatus` | `Active`, `Suspended` |
   150→| `StoreUserRole` | `Owner`, `Admin`, `Staff`, `Support` |

prompt:
Explore the current state of the Laravel project at /Users/fabianwesner/Herd/shop. I need to understand:

1. What files already exist (models, controllers, migrations, routes, views, tests, etc.)
2. Current state of composer.json dependencies
3. Current state of package.json
4. Current .env configuration
5. Current config files (auth.php, database.php, etc.)
6. Current routes/web.php, routes/api.php, routes/console.php
7. Current app directory structure
8. Current tests directory structure
9. Current database/migrations directory
10. Current resources/views directory

Give me a comprehensive overview of what already exists vs what needs to be built.


prompt:
Explore the current state of the Laravel project at /Users/fabianwesner/Herd/shop. I need to understand:

1. What files already exist (models, controllers, migrations, routes, views, tests, etc.)
2. Current state of composer.json dependencies
3. Current state of package.json
4. Current .env configuration
5. Current config files (auth.php, database.php, etc.)
6. Current routes/web.php, routes/api.php, routes/console.php
7. Current app directory structure
8. Current tests directory structure
9. Current database/migrations directory
10. Current resources/views directory

Give me a comprehensive overview of what already exists vs what needs to be built.


content:
     1→# 01 - Database Schema Specification
     2→
     3→> E-commerce platform - SQLite database schema for Laravel 12
     4→
     5→---
     6→
     7→## Conventions
     8→
     9→| Convention | Detail |
    10→|---|---|
    11→| Primary keys | Auto-incrementing integer primary key on every table (column: `id`) |
    12→| Tenant isolation | `store_id` foreign key on every tenant-scoped table |
    13→| Monetary amounts | Stored as **integer in minor units** (cents). Column names end in `_amount`. |
    14→| Booleans | Integer, not null, default 0 (0 = false, 1 = true) |
    15→| JSON data | Text column - Laravel casts to/from JSON at the application layer |
    16→| Enumerations | Check constraints on text columns |
    17→| Timestamps | Text (ISO-8601) via Laravel's `created_at` / `updated_at` |
    18→| Soft deletes | Only where explicitly noted (`deleted_at` text, nullable) |
    19→| Encrypted columns | Suffixed `_encrypted` - encrypted at application layer, stored as text |
    20→
    21→---
    22→
    23→## Epic 1 - Foundation and Multi-Store Tenancy
    24→
    25→### organizations
    26→
    27→Billing entity that owns one or more stores.
    28→
    29→| Column | Type | Nullable | Default | Constraints | Notes |
    30→|---|---|---|---|---|---|
    31→| id | INTEGER | No | autoincrement | PRIMARY KEY | |
    32→| name | TEXT | No | - | - | Organization display name |
    33→| billing_email | TEXT | No | - | - | Primary billing contact |
    34→| created_at | TEXT | Yes | NULL | - | ISO-8601 timestamp |
    35→| updated_at | TEXT | Yes | NULL | - | ISO-8601 timestamp |
    36→
    37→**Indexes:**
    38→
    39→- `idx_organizations_billing_email` on (`billing_email`) - Supports lookup by billing email
    40→
    41→---
    42→
    43→### stores
    44→
    45→Individual storefront belonging to an organization.
    46→
    47→| Column | Type | Nullable | Default | Constraints | Notes |
    48→|---|---|---|---|---|---|
    49→| id | INTEGER | No | autoincrement | PRIMARY KEY | |
    50→| organization_id | INTEGER | No | - | FK -> organizations(id) ON DELETE CASCADE | Parent organization |
    51→| name | TEXT | No | - | - | Store display name |
    52→| handle | TEXT | No | - | UNIQUE per table | URL-friendly slug |
    53→| status | TEXT | No | 'active' | CHECK IN ('active', 'suspended') | |
    54→| default_currency | TEXT | No | 'USD' | - | ISO 4217 currency code |
    55→| default_locale | TEXT | No | 'en' | - | IETF locale tag |
    56→| timezone | TEXT | No | 'UTC' | - | IANA timezone |
    57→| created_at | TEXT | Yes | NULL | - | ISO-8601 timestamp |
    58→| updated_at | TEXT | Yes | NULL | - | ISO-8601 timestamp |
    59→
    60→**Indexes:**
    61→
    62→- `idx_stores_handle` UNIQUE on (`handle`) - Ensures globally unique store handles for URL routing
    63→- `idx_stores_organization_id` on (`organization_id`) - FK lookup and listing stores by org
    64→- `idx_stores_status` on (`status`) - Filtering stores by active/suspended status
    65→
    66→---
    67→
    68→### store_domains
    69→
    70→Custom domains attached to a store.
    71→
    72→| Column | Type | Nullable | Default | Constraints | Notes |
    73→|---|---|---|---|---|---|
    74→| id | INTEGER | No | autoincrement | PRIMARY KEY | |
    75→| store_id | INTEGER | No | - | FK -> stores(id) ON DELETE CASCADE | Owning store |
    76→| hostname | TEXT | No | - | UNIQUE per table | e.g. "shop.example.com" |
    77→| type | TEXT | No | 'storefront' | CHECK IN ('storefront', 'admin', 'api') | Domain purpose |
    78→| is_primary | INTEGER | No | 0 | - | Boolean: primary domain flag |
    79→| tls_mode | TEXT | No | 'managed' | CHECK IN ('managed', 'bring_your_own') | TLS certificate mode |
    80→| created_at | TEXT | Yes | NULL | - | ISO-8601 timestamp |
    81→
    82→**Indexes:**
    83→
    84→- `idx_store_domains_hostname` UNIQUE on (`hostname`) - Globally unique hostname for routing
    85→- `idx_store_domains_store_id` on (`store_id`) - FK lookup, listing domains per store
    86→- `idx_store_domains_store_primary` on (`store_id`, `is_primary`) - Quick lookup of a store's primary domain
    87→
    88→---
    89→
    90→### users
    91→
    92→Admin/staff users who manage stores (not storefront customers).
    93→
    94→| Column | Type | Nullable | Default | Constraints | Notes |
    95→|---|---|---|---|---|---|
    96→| id | INTEGER | No | autoincrement | PRIMARY KEY | |
    97→| email | TEXT | No | - | UNIQUE per table | Login email |
    98→| password_hash | TEXT | No | - | - | bcrypt/argon2 hash; maps to Laravel's `password` field |
    99→| name | TEXT | No | - | - | Display name |
   100→| status | TEXT | No | 'active' | CHECK IN ('active', 'disabled') | |

prompt:
Explore the current state of the Laravel project at /Users/fabianwesner/Herd/shop. I need to understand:

1. What files already exist (models, controllers, migrations, routes, views, tests, etc.)
2. Current state of composer.json dependencies
3. Current state of package.json
4. Current .env configuration
5. Current config files (auth.php, database.php, etc.)
6. Current routes/web.php, routes/api.php, routes/console.php
7. Current app directory structure
8. Current tests directory structure
9. Current database/migrations directory
10. Current resources/views directory

Give me a comprehensive overview of what already exists vs what needs to be built.


prompt:
Read ALL the specification files in /Users/fabianwesner/Herd/shop/specs/ directory. I need the full content of:
1. specs/01-DATABASE-SCHEMA.md (read in chunks if needed)
2. specs/02-API-ROUTES.md (read in chunks if needed)
3. specs/03-ADMIN-UI.md (read in chunks if needed)
4. specs/04-STOREFRONT-UI.md (read in chunks if needed)
5. specs/05-BUSINESS-LOGIC.md (read in chunks if needed)
6. specs/06-AUTH-AND-SECURITY.md (read in chunks if needed)
7. specs/07-SEEDERS-AND-TEST-DATA.md (read in chunks if needed)
8. specs/08-PLAYWRIGHT-E2E-PLAN.md (read in chunks if needed)

For each file provide a comprehensive summary including:
- All table/column definitions
- All API endpoints
- All UI components and their specifications
- All business rules
- All test specifications
- All seeder data definitions

This is critical - I need ALL the details, not just summaries. Extract every table name, column, relationship, route, component, and test case.


prompt:
Explore the current state of the Laravel project at /Users/fabianwesner/Herd/shop. I need to understand:

1. What files already exist (models, controllers, migrations, routes, views, tests, etc.)
2. Current state of composer.json dependencies
3. Current state of package.json
4. Current .env configuration
5. Current config files (auth.php, database.php, etc.)
6. Current routes/web.php, routes/api.php, routes/console.php
7. Current app directory structure
8. Current tests directory structure
9. Current database/migrations directory
10. Current resources/views directory

Give me a comprehensive overview of what already exists vs what needs to be built.