> Meticulous records real user sessions from a web app and replays them against each commit to catch visual regressions.
>
> Full AI-readable docs index (complete project setup journey + every page as markdown): https://app.meticulous.ai/llms.txt

# Companion Assets (Advanced)

> **info: Companion assets are only relevant to the cloud-compute (tunnel) workflow**
> If you can build your app as a Docker image, prefer the `upload-container` action — Meticulous serves your app directly from the uploaded image, so there is no tunnel and no need for companion assets. The companion-assets pattern below only applies when you're using the tunnel-based `cloud-compute` action.

## What are Companion Assets?

Companion assets allow you to serve static files alongside your running application during Meticulous tests. When a request matches a specific pattern, Meticulous serves the file from a local folder instead of proxying it through the secure tunnel.

This is an advanced feature that solves specific performance and deployment challenges.

---

## When to Use Companion Assets

### Next.js Applications

**Problem**: Next.js apps generate static assets in the `.next/static/` folder during build. These assets include JavaScript bundles, CSS files, and other resources. When testing a Next.js app locally, these assets need to be available at the correct paths.

**Solution**: Upload the `.next/static/` folder as companion assets and configure Meticulous to serve requests to `/_next/static/` from this folder.

### Large Static Assets

**Problem**: Your app has large static assets (images, fonts, videos) that slow down the secure tunnel. Transferring large files through the tunnel adds latency and can timeout.

**Solution**: Upload these static assets as companion assets so Meticulous serves them directly, bypassing the tunnel.

### CDN-Hosted Assets During Recording

**Problem**: During session recording, your app loads assets from a CDN (e.g., `https://cdn.example.com/assets/`). During testing, you want to serve these assets locally instead of from the CDN.

**Solution**: Download the CDN assets locally, upload them as companion assets, and configure Meticulous to intercept CDN requests and serve them from your local folder.

### Multi-Server Applications

**Problem**: Your application is served by multiple servers (e.g., main app on `:3000`, assets server on `:8080`). During tests, you want to consolidate asset serving.

**Solution**: Use companion assets to serve assets that would normally come from a separate server.

---

## How Companion Assets Work

When you configure companion assets, Meticulous:

1. **Uploads** the specified local folder to cloud storage
2. **Starts** a static file server in the test environment with your assets
3. **Intercepts** requests matching the regex pattern
4. **Redirects** matching requests to the local asset server instead of proxying through the tunnel

### Request Routing Example

Without companion assets:
```
Browser → /_next/static/chunks/main.js → Tunnel → Your App (localhost:3000)
```

With companion assets:
```
Browser → /_next/static/chunks/main.js → Companion Assets Server → Uploaded files
```

---

## Configuration

Companion assets require **two parameters**, and you must provide **both or neither**:

### 1. `companion-assets-folder`

The path to a local folder containing the static files to upload.

- Must be a relative or absolute path on your CI runner
- The entire folder contents are uploaded
- Folder structure is preserved

### 2. `companion-assets-regex`

A regular expression pattern to match request URLs that should be served from companion assets.

- Matches against the request **pathname** (e.g., `/_next/static/chunks/main.js`)
- Should typically start with `^` to match from the beginning
- Only GET and HEAD requests are intercepted
- Case-sensitive by default

> **warning: Both Parameters Required**
> You must provide both `companion-assets-folder` and `companion-assets-regex`, or neither. Providing only one will result in an error.

---

## Complete Examples

### Next.js Application

Next.js apps are the most common use case for companion assets.

#### GitHub Actions Workflow

```yaml
name: Meticulous
on:
  push:
    branches: [main]
  pull_request: {}
  workflow_dispatch:
    inputs:
      meticulous-commit-sha:
        description: Commit Meticulous has asked this run to build. Defaults to the branch head.
        required: false

permissions:
  actions: write
  contents: read
  issues: write
  pull-requests: write
  statuses: read

env:
  # Prefer the dispatched commit when set; otherwise the PR head. On pull_request github.sha is the merge commit,
  # not the PR head SHA that Meticulous looks up.
  METICULOUS_COMMIT_SHA: ${{ github.event.inputs['meticulous-commit-sha'] || (github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha) }}

jobs:
  test:
    name: Meticulous
    runs-on: ubuntu-latest

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4
        with:
          ref: ${{ env.METICULOUS_COMMIT_SHA }}

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: "24"
          cache: pnpm

      - name: Install dependencies
        run: pnpm install --frozen-lockfile

      - name: Build Next.js app
        run: pnpm build

      - name: Prepare companion assets
        run: |
          # Create companion assets folder
          mkdir -p companion-assets/_next
          # Copy Next.js static assets
          cp -r .next/static companion-assets/_next/

      - name: Serve Next.js app
        run: |
          pnpm start &
          sleep 5

      - name: Run Meticulous tests
        uses: alwaysmeticulous/report-diffs-action/cloud-compute@v1
        with:
          api-token: ${{ secrets.METICULOUS_API_TOKEN }}
          app-url: "http://localhost:3000"
          companion-assets-folder: "companion-assets"
          companion-assets-regex: "^/_next/static/"
```

**Key Points:**
- Build the Next.js app first (`pnpm build`)
- Copy `.next/static/` to `companion-assets/_next/` (preserves path structure)
- Regex `^/_next/static/` matches all requests starting with `/_next/static/`
- Folder structure in `companion-assets` matches URL structure

#### CLI Usage

```bash
# Build the app
pnpm build

# Prepare companion assets
mkdir -p companion-assets/_next
cp -r .next/static companion-assets/_next/

# Start the app
pnpm start &

# Run tests with companion assets
npx @alwaysmeticulous/cli ci run-with-tunnel \
  --apiToken="$METICULOUS_API_TOKEN" \
  --appUrl="http://localhost:3000" \
  --companionAssetsFolder="companion-assets" \
  --companionAssetsRegex="^/_next/static/"
```

### Vite App with CDN Assets

If your Vite app loads assets from a CDN during production but you want to test with local assets:

```yaml
      - name: Build Vite app
        run: pnpm build

      - name: Prepare companion assets
        run: |
          # Copy built assets
          mkdir -p companion-assets/assets
          cp -r dist/assets/* companion-assets/assets/

      - name: Serve app
        run: |
          pnpm preview &
          sleep 3

      - name: Run Meticulous tests
        uses: alwaysmeticulous/report-diffs-action/cloud-compute@v1
        with:
          api-token: ${{ secrets.METICULOUS_API_TOKEN }}
          app-url: "http://localhost:4173"
          companion-assets-folder: "companion-assets"
          companion-assets-regex: "^/assets/"
```

### Multiple Asset Patterns

If you need to serve assets from multiple paths:

```yaml
      - name: Prepare companion assets
        run: |
          mkdir -p companion-assets
          # Copy static assets
          cp -r public/static companion-assets/static
          # Copy built bundles
          cp -r dist/bundles companion-assets/bundles

      - name: Run Meticulous tests
        uses: alwaysmeticulous/report-diffs-action/cloud-compute@v1
        with:
          api-token: ${{ secrets.METICULOUS_API_TOKEN }}
          app-url: "http://localhost:3000"
          companion-assets-folder: "companion-assets"
          # Match /static/ OR /bundles/
          companion-assets-regex: "^/(static|bundles)/"
```

### React App (Create React App)

```yaml
      - name: Build React app
        run: pnpm build

      - name: Prepare companion assets
        run: |
          mkdir -p companion-assets
          # Copy static files from build output
          cp -r build/static companion-assets/static

      - name: Serve app
        run: |
          npx serve -s build -p 3000 &
          sleep 3

      - name: Run Meticulous tests
        uses: alwaysmeticulous/report-diffs-action/cloud-compute@v1
        with:
          api-token: ${{ secrets.METICULOUS_API_TOKEN }}
          app-url: "http://localhost:3000"
          companion-assets-folder: "companion-assets"
          companion-assets-regex: "^/static/"
```

---

## Folder Structure Requirements

The folder structure inside `companion-assets-folder` must match the URL paths.

### Example 1: Next.js

**Request URL**: `http://localhost:3000/_next/static/chunks/main.js`

**Regex**: `^/_next/static/`

**Folder structure**:
```
companion-assets/
└── _next/
    └── static/
        └── chunks/
            └── main.js
```

The pathname `/_next/static/chunks/main.js` maps to `companion-assets/_next/static/chunks/main.js`.

### Example 2: Generic Assets

**Request URL**: `http://localhost:3000/assets/images/logo.png`

**Regex**: `^/assets/`

**Folder structure**:
```
companion-assets/
└── assets/
    └── images/
        └── logo.png
```

### Example 3: Root-Level Assets

**Request URL**: `http://localhost:3000/public/font.woff2`

**Regex**: `^/public/`

**Folder structure**:
```
companion-assets/
└── public/
    └── font.woff2
```

---

## Regex Pattern Guide

### Basic Patterns

| Use Case | Regex | Matches |
|----------|-------|---------|
| Next.js static assets | `^/_next/static/` | `/_next/static/chunks/main.js`<br/>`/_next/static/css/app.css` |
| Assets folder | `^/assets/` | `/assets/images/logo.png`<br/>`/assets/fonts/font.woff` |
| Static folder | `^/static/` | `/static/js/bundle.js`<br/>`/static/css/main.css` |
| Multiple folders | `^/(assets|static)/` | `/assets/logo.png`<br/>`/static/main.js` |
| Specific file types | `^/.*\.(png|jpg|woff2)$` | `/images/logo.png`<br/>`/fonts/font.woff2` |

### Advanced Patterns

**Match all .js files in /dist/**:
```
^/dist/.*\.js$
```

**Match versioned assets**:
```
^/assets/v[0-9]+/
```
Matches: `/assets/v1/main.js`, `/assets/v2/app.css`

**Match specific subdirectories**:
```
^/static/(js|css|media)/
```
Matches: `/static/js/main.js`, `/static/css/app.css`, `/static/media/logo.png`

---

## Troubleshooting

### Assets Not Loading

**Symptom**: Your app shows missing resources or broken styles during tests.

**Possible Causes**:

1. **Regex doesn't match requests**
   - Check the browser network tab in test results
   - Verify the exact pathname being requested
   - Test your regex pattern at [regex101.com](https://regex101.com)

   ```bash
   # Example: If requests are for /_next/static/chunks/main-abc123.js
   # This regex won't match (missing trailing slash):
   companion-assets-regex: "^/_next/static"

   # This will match:
   companion-assets-regex: "^/_next/static/"
   ```

2. **Folder structure doesn't match URL structure**
   - Request: `/_next/static/main.js`
   - Correct: `companion-assets/_next/static/main.js`
   - Incorrect: `companion-assets/static/main.js`

3. **Files not copied to companion assets folder**
   - Verify files exist: `ls -la companion-assets/_next/static/`
   - Check your build output directory
   - Ensure copy commands run after build

### Wrong Files Served

**Symptom**: Meticulous serves incorrect or outdated files.

**Solution**: Ensure you're copying the correct build output.

```bash
# Bad: Copies from source instead of build output
cp -r src/assets companion-assets/

# Good: Copies from build output
cp -r .next/static companion-assets/_next/
```

### Assets Upload Too Large

**Symptom**: Companion assets upload times out or fails.

**Solutions**:

1. **Exclude unnecessary files**:
   ```bash
   # Only copy necessary files
   cp -r .next/static companion-assets/_next/
   # Don't copy source maps in production
   find companion-assets -name "*.map" -delete
   ```

2. **Use more specific regex**:
   ```yaml
   # Instead of matching all assets
   companion-assets-regex: "^/assets/"

   # Match only large files (images, fonts)
   companion-assets-regex: "^/assets/.*\.(png|jpg|woff2|woff|ttf)$"
   ```

### Debugging Request Matching

To see which requests are being intercepted, you can add the `--printRequests` flag when using the CLI:

```bash
npx @alwaysmeticulous/cli ci run-with-tunnel \
  --apiToken="$METICULOUS_API_TOKEN" \
  --appUrl="http://localhost:3000" \
  --companionAssetsFolder="companion-assets" \
  --companionAssetsRegex="^/_next/static/" \
  --printRequests
```

For GitHub Actions, you can add the `meticulous-debug` label to your PR to keep the secure tunnel open and access detailed logs.

---

## Performance Considerations

### When Companion Assets Help

- **Large files**: Images, videos, fonts (>100KB)
- **Many files**: Hundreds of small assets loaded per page
- **Slow builds**: Next.js apps with large static asset folders
- **Bandwidth limits**: CI environments with limited egress

### When Companion Assets May Not Help

- **Small apps**: Apps with minimal static assets (<10MB total)
- **Fast tunnels**: If tunnel performance is already good
- **Dynamic assets**: Assets that change based on runtime logic

### Measuring Impact

Compare test run times with and without companion assets:

1. Run tests without companion assets
2. Note the total test run duration
3. Enable companion assets
4. Compare the new duration

Typical improvements: 10-30% faster test runs for Next.js apps with large static folders.

---

## CLI Reference

### `ci run-with-tunnel`

```bash
npx @alwaysmeticulous/cli ci run-with-tunnel \
  --apiToken="<token>" \
  --appUrl="<url>" \
  --companionAssetsFolder="<folder>" \
  --companionAssetsRegex="<regex>"
```

**Parameters**:
- `--companionAssetsFolder`: Path to local folder (relative or absolute)
- `--companionAssetsRegex`: Regex pattern (must be properly escaped)

### `ci start-tunnel`

When testing tunnel setup locally:

```bash
npx @alwaysmeticulous/cli ci start-tunnel --port=3000
```

---

## GitHub Actions Reference

### `cloud-compute` Action

```yaml
- uses: alwaysmeticulous/report-diffs-action/cloud-compute@v1
  with:
    api-token: ${{ secrets.METICULOUS_API_TOKEN }}
    app-url: "http://localhost:3000"
    companion-assets-folder: "companion-assets"  # Required if regex provided
    companion-assets-regex: "^/_next/static/"    # Required if folder provided
```

**Input Types**:
- `companion-assets-folder`: String (path)
- `companion-assets-regex`: String (regex pattern)

**Defaults**: Both default to empty string (feature disabled)

---

## Common Patterns

### Monorepo with Multiple Next.js Apps

```yaml
      - name: Build apps
        run: |
          pnpm build:app1
          pnpm build:app2

      - name: Prepare companion assets for both apps
        run: |
          mkdir -p companion-assets/app1/_next
          mkdir -p companion-assets/app2/_next
          cp -r apps/app1/.next/static companion-assets/app1/_next/
          cp -r apps/app2/.next/static companion-assets/app2/_next/

      - name: Run tests for App 1
        uses: alwaysmeticulous/report-diffs-action/cloud-compute@v1
        with:
          api-token: ${{ secrets.APP1_API_TOKEN }}
          app-url: "http://localhost:3000"
          companion-assets-folder: "companion-assets/app1"
          companion-assets-regex: "^/_next/static/"

      - name: Run tests for App 2
        uses: alwaysmeticulous/report-diffs-action/cloud-compute@v1
        with:
          api-token: ${{ secrets.APP2_API_TOKEN }}
          app-url: "http://localhost:4000"
          companion-assets-folder: "companion-assets/app2"
          companion-assets-regex: "^/_next/static/"
```

### Conditional Companion Assets

```yaml
      - name: Prepare companion assets (only for Next.js)
        if: ${{ env.FRAMEWORK == 'nextjs' }}
        run: |
          mkdir -p companion-assets/_next
          cp -r .next/static companion-assets/_next/

      - name: Run Meticulous tests
        uses: alwaysmeticulous/report-diffs-action/cloud-compute@v1
        with:
          api-token: ${{ secrets.METICULOUS_API_TOKEN }}
          app-url: "http://localhost:3000"
          companion-assets-folder: ${{ env.FRAMEWORK == 'nextjs' && 'companion-assets' || '' }}
          companion-assets-regex: ${{ env.FRAMEWORK == 'nextjs' && '^/_next/static/' || '' }}
```

---

## Summary

**Use companion assets when**:
- You have a Next.js application
- You have large static assets that slow down the tunnel
- Assets are served from a CDN during recording but should be local during tests
- You need to consolidate assets from multiple servers

**Configuration requirements**:
- Both `companion-assets-folder` and `companion-assets-regex` must be provided
- Folder structure must match URL path structure
- Regex must accurately match the requests you want to intercept

**Common mistakes to avoid**:
- Mismatched folder structure and URL paths
- Regex that doesn't match actual request paths
- Forgetting to build the app before copying assets
- Copying source files instead of build output
