---
name: shopify-apps
description: Expert patterns for Shopify app development including Remix/React
  Router apps, embedded apps with App Bridge, webhook handling, GraphQL Admin
  API, Polaris components, billing, and app extensions.
risk: safe
source: vibeship-spawner-skills (Apache 2.0)
date_added: 2026-02-27
---

# Shopify Apps

Expert patterns for Shopify app development including Remix/React Router apps,
embedded apps with App Bridge, webhook handling, GraphQL Admin API,
Polaris components, billing, and app extensions.

## Patterns

### React Router App Setup

Modern Shopify app template with React Router

**When to use**: Starting a new Shopify app

### Template

# Create new Shopify app with CLI
npm init @shopify/app@latest my-shopify-app

# Project structure
# my-shopify-app/
# ├── app/
# │   ├── routes/
# │   │   ├── app._index.tsx        # Main app page
# │   │   ├── app.tsx               # App layout with providers
# │   │   ├── auth.$.tsx            # Auth callback
# │   │   └── webhooks.tsx          # Webhook handler
# │   ├── shopify.server.ts         # Server configuration
# │   └── root.tsx                  # Root layout
# ├── extensions/                   # App extensions
# ├── shopify.app.toml              # App configuration
# └── package.json

// shopify.app.toml
name = "my-shopify-app"
client_id = "your-client-id"
application_url = "https://your-app.example.com"

[access_scopes]
scopes = "read_products,write_products,read_orders"

[webhooks]
api_version = "2024-10"

[webhooks.subscriptions]
topics = ["orders/create", "products/update"]
uri = "/webhooks"

[auth]
redirect_urls = ["https://your-app.example.com/auth/callback"]

// app/shopify.server.ts
import "@shopify/shopify-app-remix/adapters/node";
import {
  LATEST_API_VERSION,
  shopifyApp,
  DeliveryMethod,
} from "@shopify/shopify-app-remix/server";
import { PrismaSessionStorage } from "@shopify/shopify-app-session-storage-prisma";
import prisma from "./db.server";

const shopify = shopifyApp({
  apiKey: process.env.SHOPIFY_API_KEY!,
  apiSecretKey: process.env.SHOPIFY_API_SECRET!,
  scopes: process.env.SCOPES?.split(","),
  appUrl: process.env.SHOPIFY_APP_URL!,
  authPathPrefix: "/auth",
  sessionStorage: new PrismaSessionStorage(prisma),
  distribution: AppDistribution.AppStore,
  future: {
    unstable_newEmbeddedAuthStrategy: true,
  },
  ...(process.env.SHOP_CUSTOM_DOMAIN
    ? { customShopDomains: [process.env.SHOP_CUSTOM_DOMAIN] }
    : {}),
});

export default shopify;
export const apiVersion = LATEST_API_VERSION;
export const authenticate = shopify.authenticate;
export const sessionStorage = shopify.sessionStorage;

### Notes

- React Router replaced Remix as recommended template (late 2024)
- unstable_newEmbeddedAuthStrategy enabled by default for new apps
- Webhooks configured in shopify.app.toml, not code
- Run 'shopify app deploy' to apply configuration changes

### Embedded App with App Bridge

Render app embedded in Shopify Admin

**When to use**: Building embedded admin app

### Template

// app/routes/app.tsx - App layout with providers
import { Link, Outlet, useLoaderData, useRouteError } from "@remix-run/react";
import { AppProvider } from "@shopify/shopify-app-remix/react";
import polarisStyles from "@shopify/polaris/build/esm/styles.css?url";

export const links = () => [{ rel: "stylesheet", href: polarisStyles }];

export async function loader({ request }: LoaderFunctionArgs) {
  await authenticate.admin(request);
  return json({ apiKey: process.env.SHOPIFY_API_KEY! });
}

export default function App() {
  const { apiKey } = useLoaderData<typeof loader>();

  return (
    <AppProvider isEmbeddedApp apiKey={apiKey}>
      <ui-nav-menu>
        <Link to="/app" rel="home">Home</Link>
        <Link to="/app/products">Products</Link>
        <Link to="/app/settings">Settings</Link>
      </ui-nav-menu>
      <Outlet />
    </AppProvider>
  );
}

export function ErrorBoundary() {
  const error = useRouteError();
  return (
    <AppProvider isEmbeddedApp>
      <Page>
        <Card>
          <Text as="p" variant="bodyMd">
            Something went wrong. Please try again.
          </Text>
        </Card>
      </Page>
    </AppProvider>
  );
}

// app/routes/app._index.tsx - Main app page
import {
  Page,
  Layout,
  Card,
  Text,
  BlockStack,
  Button,
} from "@shopify/polaris";
import { TitleBar } from "@shopify/app-bridge-react";

export async function loader({ request }: LoaderFunctionArgs) {
  const { admin } = await authenticate.admin(request);

  // GraphQL query
  const response = await admin.graphql(`
    query {
      shop {
        name
        email
      }
    }
  `);

  const { data } = await response.json();
  return json({ shop: data.shop });
}

export default function Index() {
  const { shop } = useLoaderData<typeof loader>();

  return (
    <Page>
      <TitleBar title="My Shopify App" />
      <Layout>
        <Layout.Section>
          <Card>
            <BlockStack gap="200">
              <Text as="h2" variant="headingMd">
                Welcome to {shop.name}!
              </Text>
              <Text as="p" variant="bodyMd">
                Your app is now connected to this store.
              </Text>
              <Button variant="primary">
                Get Started
              </Button>
            </BlockStack>
          </Card>
        </Layout.Section>
      </Layout>
    </Page>
  );
}

### Notes

- App Bridge required for Built for Shopify (July 2025)
- Polaris components match Shopify Admin design
- TitleBar and navigation from App Bridge
- Always authenticate requests with authenticate.admin()

### Webhook Handling

Secure webhook processing with HMAC verification

**When to use**: Receiving Shopify webhooks

### Template

// app/routes/webhooks.tsx
import type { ActionFunctionArgs } from "@remix-run/node";
import { authenticate } from "../shopify.server";
import db from "../db.server";

export const action = async ({ request }: ActionFunctionArgs) => {
  // Authenticate webhook (verifies HMAC signature)
  const { topic, shop, payload, admin } = await authenticate.webhook(request);

  console.log(`Received ${topic} webhook for ${shop}`);

  // Process based on topic
  switch (topic) {
    case "ORDERS_CREATE":
      // Queue for async processing
      await queueOrderProcessing(payload);
      break;

    case "PRODUCTS_UPDATE":
      await handleProductUpdate(shop, payload);
      break;

    case "APP_UNINSTALLED":
      // Clean up shop data
      await db.session.deleteMany({ where: { shop } });
      await db.shopData.delete({ where: { shop } });
      break;

    case "CUSTOMERS_DATA_REQUEST":
    case "CUSTOMERS_REDACT":
    case "SHOP_REDACT":
      // GDPR webhooks - mandatory
      await handleGDPRWebhook(topic, payload);
      break;

    default:
      console.log(`Unhandled webhook topic: ${topic}`);
  }

  // CRITICAL: Return 200 immediately
  // Shopify expects response within 5 seconds
  return new Response(null, { status: 200 });
};

// Process asynchronously after responding
async function queueOrderProcessing(payload: any) {
  // Use a job queue (BullMQ, etc.)
  await jobQueue.add("process-order", {
    orderId: payload.id,
    orderData: payload,
  });
}

async function handleProductUpdate(shop: string, payload: any) {
  // Quick sync operation only
  await db.product.upsert({
    where: { shopifyId: payload.id },
    update: {
      title: payload.title,
      updatedAt: new Date(),
    },
    create: {
      shopifyId: payload.id,
      shop,
      title: payload.title,
    },
  });
}

async function handleGDPRWebhook(topic: string, payload: any) {
  // GDPR compliance - required for all apps
  switch (topic) {
    case "CUSTOMERS_DATA_REQUEST":
      // Return customer data within 30 days
      break;
    case "CUSTOMERS_REDACT":
      // Delete customer data
      break;
    case "SHOP_REDACT":
      // Delete all shop data (48 hours after uninstall)
      break;
  }
}

### Notes

- Respond within 5 seconds or webhook fails
- Use job queues for heavy processing
- GDPR webhooks are mandatory for App Store
- HMAC verification handled by authenticate.webhook()

### GraphQL Admin API

Query and mutate shop data with GraphQL

**When to use**: Interacting with Shopify Admin API

### Template

// GraphQL queries with authenticated admin client
export async function loader({ request }: LoaderFunctionArgs) {
  const { admin } = await authenticate.admin(request);

  // Query products with pagination
  const response = await admin.graphql(`
    query GetProducts($first: Int!, $after: String) {
      products(first: $first, after: $after) {
        edges {
          node {
            id
            title
            status
            totalInventory
            priceRangeV2 {
              minVariantPrice {
                amount
                currencyCode
              }
            }
            images(first: 1) {
              edges {
                node {
                  url
                  altText
                }
              }
            }
          }
          cursor
        }
        pageInfo {
          hasNextPage
          endCursor
        }
      }
    }
  `, {
    variables: {
      first: 10,
      after: null,
    },
  });

  const { data } = await response.json();
  return json({ products: data.products });
}

// Mutations
export async function action({ request }: ActionFunctionArgs) {
  const { admin } = await authenticate.admin(request);
  const formData = await request.formData();
  const productId = formData.get("productId");
  const newTitle = formData.get("title");

  const response = await admin.graphql(`
    mutation UpdateProduct($input: ProductInput!) {
      productUpdate(input: $input) {
        product {
          id
          title
        }
        userErrors {
          field
          message
        }
      }
    }
  `, {
    variables: {
      input: {
        id: productId,
        title: newTitle,
      },
    },
  });

  const { data } = await response.json();

  if (data.productUpdate.userErrors.length > 0) {
    return json({
      errors: data.productUpdate.userErrors,
    }, { status: 400 });
  }

  return json({ product: data.productUpdate.product });
}

// Bulk operations for large datasets
async function bulkUpdateProducts(admin: AdminApiContext) {
  // Create bulk operation
  const response = await admin.graphql(`
    mutation {
      bulkOperationRunMutation(
        mutation: "mutation call($input: ProductInput!) {
          productUpdate(input: $input) { product { id } }
        }",
        stagedUploadPath: "path-to-staged-upload"
      ) {
        bulkOperation {
          id
          status
        }
        userErrors {
          message
        }
      }
    }
  `);

  // Poll for completion or use webhook
  // BULK_OPERATIONS_FINISH webhook
}

### Notes

- GraphQL required for new public apps (April 2025)
- Rate limit: 1000 points per 60 seconds
- Use bulk operations for >250 items
- Direct API access available from App Bridge

### Billing API Integration

Implement subscription billing for your app

**When to use**: Monetizing Shopify app

### Template

// app/routes/app.billing.tsx
import { json, redirect } from "@remix-run/node";
import { Page, Card, Button, BlockStack, Text } from "@shopify/polaris";
import { authenticate } from "../shopify.server";

const PLANS = {
  basic: {
    name: "Basic",
    amount: 9.99,
    currencyCode: "USD",
    interval: "EVERY_30_DAYS",
  },
  pro: {
    name: "Pro",
    amount: 29.99,
    currencyCode: "USD",
    interval: "EVERY_30_DAYS",
  },
};

export async function loader({ request }: LoaderFunctionArgs) {
  const { admin, billing } = await authenticate.admin(request);

  // Check current subscription
  const response = await admin.graphql(`
    query {
      currentAppInstallation {
        activeSubscriptions {
          id
          name
          status
          lineItems {
            plan {
              pricingDetails {
                ... on AppRecurringPricing {
                  price {
                    amount
                    currencyCode
                  }
                  interval
                }
              }
            }
          }
        }
      }
    }
  `);

  const { data } = await response.json();
  return json({
    subscription: data.currentAppInstallation.activeSubscriptions[0],
  });
}

export async function action({ request }: ActionFunctionArgs) {
  const { admin, session } = await authenticate.admin(request);
  const formData = await request.formData();
  const planKey = formData.get("plan") as keyof typeof PLANS;
  const plan = PLANS[planKey];

  // Create subscription charge
  const response = await admin.graphql(`
    mutation CreateSubscription($name: String!, $lineItems: [AppSubscriptionLineItemInput!]!, $returnUrl: URL!, $test: Boolean) {
      appSubscriptionCreate(
        name: $name
        lineItems: $lineItems
        returnUrl: $returnUrl
        test: $test
      ) {
        appSubscription {
          id
          status
        }
        confirmationUrl
        userErrors {
          field
          message
        }
      }
    }
  `, {
    variables: {
      name: plan.name,
      lineItems: [
        {
          plan: {
            appRecurringPricingDetails: {
              price: {
                amount: plan.amount,
                currencyCode: plan.currencyCode,
              },
              interval: plan.interval,
            },
          },
        },
      ],
      returnUrl: `https://${session.shop}/admin/apps/${process.env.SHOPIFY_API_KEY}`,
      test: process.env.NODE_ENV !== "production",
    },
  });

  const { data } = await response.json();

  if (data.appSubscriptionCreate.userErrors.length > 0) {
    return json({
      errors: data.appSubscriptionCreate.userErrors,
    }, { status: 400 });
  }

  // Redirect merchant to approve charge
  return redirect(data.appSubscriptionCreate.confirmationUrl);
}

export default function Billing() {
  const { subscription } = useLoaderData<typeof loader>();
  const submit = useSubmit();

  return (
    <Page title="Billing">
      <Card>
        {subscription ? (
          <BlockStack gap="200">
            <Text as="p" variant="bodyMd">
              Current plan: {subscription.name}
            </Text>
            <Text as="p" variant="bodyMd">
              Status: {subscription.status}
            </Text>
          </BlockStack>
        ) : (
          <BlockStack gap="400">
            <Text as="h2" variant="headingMd">
              Choose a Plan
            </Text>
            <Button onClick={() => submit({ plan: "basic" }, { method: "post" })}>
              Basic - $9.99/month
            </Button>
            <Button onClick={() => submit({ plan: "pro" }, { method: "post" })}>
              Pro - $29.99/month
            </Button>
          </BlockStack>
        )}
      </Card>
    </Page>
  );
}

### Notes

- Use test: true for development stores
- Merchant must approve subscription
- One recurring + one usage charge per app max
- 30-day billing cycle for recurring charges

### App Extension Development

Extend Shopify checkout, admin, or storefront

**When to use**: Building app extensions

### Template

# shopify.extension.toml (in extensions/my-extension/)
api_version = "2024-10"

[[extensions]]
type = "ui_extension"
name = "Product Customizer"
handle = "product-customizer"

[[extensions.targeting]]
target = "admin.product-details.block.render"
module = "./src/AdminBlock.tsx"

[extensions.capabilities]
api_access = true

[extensions.settings]
[[extensions.settings.fields]]
key = "show_preview"
type = "boolean"
name = "Show Preview"

// extensions/my-extension/src/AdminBlock.tsx
import {
  reactExtension,
  useApi,
  useSettings,
  BlockStack,
  Text,
  Button,
  InlineStack,
} from "@shopify/ui-extensions-react/admin";

export default reactExtension(
  "admin.product-details.block.render",
  () => <ProductCustomizer />
);

function ProductCustomizer() {
  const { data, extension } = useApi<"admin.product-details.block.render">();
  const settings = useSettings();

  const productId = data?.selected?.[0]?.id;

  const handleCustomize = async () => {
    // API calls from extension
    const result = await fetch("/api/customize", {
      method: "POST",
      body: JSON.stringify({ productId }),
    });
  };

  return (
    <BlockStack gap="base">
      <Text fontWeight="bold">Product Customizer</Text>
      <Text>
        Customize product: {productId}
      </Text>
      {settings.show_preview && (
        <Text size="small">Preview enabled</Text>
      )}
      <InlineStack gap="base">
        <Button onPress={handleCustomize}>
          Apply Customization
        </Button>
      </InlineStack>
    </BlockStack>
  );
}

// Checkout UI Extension
// [[extensions.targeting]]
// target = "purchase.checkout.block.render"

// extensions/checkout-ext/src/Checkout.tsx
import {
  reactExtension,
  Banner,
  useCartLines,
  useTotalAmount,
} from "@shopify/ui-extensions-react/checkout";

export default reactExtension(
  "purchase.checkout.block.render",
  () => <CheckoutBanner />
);

function CheckoutBanner() {
  const cartLines = useCartLines();
  const total = useTotalAmount();

  if (total.amount > 100) {
    return (
      <Banner status="success">
        You qualify for free shipping!
      </Banner>
    );
  }

  return null;
}

### Notes

- Extensions run in sandboxed iframe
- Use @shopify/ui-extensions-react for React
- Limited APIs compared to full app
- Deploy with 'shopify app deploy'

## Sharp Edges

### Webhook Must Respond Within 5 Seconds

Severity: HIGH

Situation: Receiving webhooks from Shopify

Symptoms:
Webhook deliveries marked as failed.
"Your app didn't respond in time" in Shopify logs.
Missing order/product updates.
Webhooks retried repeatedly then cancelled.

Why this breaks:
Shopify expects a 2xx response within 5 seconds. If your app processes
the webhook data before responding, you'll timeout.

Shopify retries failed webhooks up to 19 times over 48 hours.
After continued failures, webhooks may be cancelled entirely.

Heavy processing (API calls, database operations) must happen
after the response is sent.

Recommended fix:

## Respond immediately, process asynchronously

```typescript
// app/routes/webhooks.tsx
export const action = async ({ request }: ActionFunctionArgs) => {
  const { topic, shop, payload } = await authenticate.webhook(request);

  // Queue for async processing
  await jobQueue.add("process-webhook", {
    topic,
    shop,
    payload,
  });

  // CRITICAL: Return 200 immediately
  return new Response(null, { status: 200 });
};

// Worker process handles the actual work
// workers/webhook-processor.ts
import { Worker } from "bullmq";

const worker = new Worker("process-webhook", async (job) => {
  const { topic, shop, payload } = job.data;

  switch (topic) {
    case "ORDERS_CREATE":
      await processOrder(shop, payload);
      break;
    // ... other handlers
  }
});
```

## For simple operations, be quick

```typescript
// Simple database update is OK if fast
export const action = async ({ request }: ActionFunctionArgs) => {
  const { topic, payload } = await authenticate.webhook(request);

  // Quick database update (< 1 second)
  await db.product.update({
    where: { shopifyId: payload.id },
    data: { title: payload.title },
  });

  return new Response(null, { status: 200 });
};
```

## Monitor webhook performance

```typescript
// Log response times
const start = Date.now();

await handleWebhook(payload);

const duration = Date.now() - start;
console.log(`Webhook processed in ${duration}ms`);

// Alert if approaching timeout
if (duration > 3000) {
  console.warn("Webhook processing taking too long!");
}
```

### API Rate Limits Cause 429 Errors

Severity: HIGH

Situation: Making API calls to Shopify

Symptoms:
HTTP 429 Too Many Requests errors.
"Throttled" responses.
App becomes unresponsive.
Operations fail silently or partially.

Why this breaks:
Shopify enforces strict rate limits:
- REST: 2 requests per second per store
- GraphQL: 1000 points per 60 seconds

Exceeding limits causes immediate 429 errors.
Continuous violations can result in temporary bans.

Bulk operations count against limits.

Recommended fix:

## Check rate limit headers

```typescript
// REST API
// X-Shopify-Shop-Api-Call-Limit: 39/40

// GraphQL - check response extensions
const response = await admin.graphql(`...`);
const { data, extensions } = await response.json();

const cost = extensions?.cost;
// {
//   "requestedQueryCost": 42,
//   "actualQueryCost": 42,
//   "throttleStatus": {
//     "maximumAvailable": 1000,
//     "currentlyAvailable": 958,
//     "restoreRate": 50
//   }
// }
```

## Implement retry with exponential backoff

```typescript
async function shopifyRequest(
  fn: () => Promise<Response>,
  maxRetries = 3
): Promise<Response> {
  let lastError: Error;

  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      const response = await fn();

      if (response.status === 429) {
        // Get retry-after header or default
        const retryAfter = parseInt(
          response.headers.get("Retry-After") || "2"
        );
        await sleep(retryAfter * 1000 * Math.pow(2, attempt));
        continue;
      }

      return response;
    } catch (error) {
      lastError = error as Error;
    }
  }

  throw lastError!;
}
```

## Use bulk operations for large datasets

```typescript
// Instead of 1000 individual calls, use bulk mutation
const response = await admin.graphql(`
  mutation {
    bulkOperationRunMutation(
      mutation: "mutation($input: ProductInput!) {
        productUpdate(input: $input) { product { id } }
      }",
      stagedUploadPath: "..."
    ) {
      bulkOperation { id status }
      userErrors { message }
    }
  }
`);
```

## Queue requests

```typescript
import { RateLimiter } from "limiter";

// 2 requests per second for REST
const limiter = new RateLimiter({
  tokensPerInterval: 2,
  interval: "second",
});

async function rateLimitedRequest(fn: () => Promise<any>) {
  await limiter.removeTokens(1);
  return fn();
}
```

### Protected Customer Data Requires Special Permission

Severity: HIGH

Situation: Accessing customer PII in webhooks or API

Symptoms:
Webhook deliveries fail for orders/customers.
Customer data fields are null or empty.
App works in development but fails in production.
"Protected customer data access" errors.

Why this breaks:
Since April 2024, accessing protected customer data (PII) requires
explicit approval from Shopify. This is separate from OAuth scopes.

Protected data includes:
- Customer names, emails, addresses
- Order customer information
- Subscription customer details

Even with read_orders scope, you won't receive customer data
in webhooks without protected data access.

Recommended fix:

## Request protected customer data access

1. Go to Partner Dashboard > App > API access
2. Under "Protected customer data access"
3. Request access for needed data types
4. Justify your use case
5. Wait for Shopify approval (can take days)

## Check your data access level

```typescript
// Query your app's data access
const response = await admin.graphql(`
  query {
    currentAppInstallation {
      accessScopes {
        handle
      }
    }
  }
`);
```

## Handle missing data gracefully

```typescript
// Webhook payload may have redacted fields
async function processOrder(payload: any) {
  const customerEmail = payload.customer?.email;

  if (!customerEmail) {
    // Customer data not available
    // Either no protected access or data redacted
    console.log("Customer data not available");
    return;
  }

  await sendOrderConfirmation(customerEmail);
}
```

## Use customer account API for direct access

```typescript
// If customer is logged in, can access their data
// through Customer Account API (different from Admin API)
```

### Duplicate Webhook Definitions Cause Conflicts

Severity: MEDIUM

Situation: Configuring webhooks in both TOML and code

Symptoms:
Duplicate webhook deliveries.
Some webhooks fire twice.
Webhook subscriptions fail to register.
Unpredictable webhook behavior.

Why this breaks:
Shopify apps can define webhooks in two places:
1. shopify.app.toml (declarative, recommended)
2. afterAuth hook in code (imperative, legacy)

If you define the same webhook in both places, you get:
- Duplicate subscriptions
- Race conditions during registration
- Conflicts during app updates

Recommended fix:

## Use TOML only (recommended)

```toml
# shopify.app.toml
[webhooks]
api_version = "2024-10"

[webhooks.subscriptions]
topics = [
  "orders/create",
  "orders/updated",
  "products/create",
  "products/update",
  "app/uninstalled"
]
uri = "/webhooks"
```

## Remove code-based registration

```typescript
// DON'T do this if using TOML
const shopify = shopifyApp({
  // ...
  hooks: {
    afterAuth: async ({ session }) => {
      // Remove webhook registration from here
      // Let TOML handle it
    },
  },
});
```

## Deploy to apply TOML changes

```bash
# Webhooks registered on deploy
shopify app deploy
```

## Check current subscriptions

```typescript
const response = await admin.graphql(`
  query {
    webhookSubscriptions(first: 50) {
      edges {
        node {
          id
          topic
          endpoint {
            ... on WebhookHttpEndpoint {
              callbackUrl
            }
          }
        }
      }
    }
  }
`);
```

### Webhook URL Trailing Slash Causes 404

Severity: MEDIUM

Situation: Setting up webhook endpoints

Symptoms:
Webhooks return 404 Not Found.
Webhook delivery fails immediately.
Works in local dev but fails in production.
Logs show request to /webhooks/ not /webhooks.

Why this breaks:
Shopify automatically adds a trailing slash to webhook URLs.
If your server doesn't handle both /webhooks and /webhooks/,
the webhook will 404.

Common with frameworks that are strict about trailing slashes.

Recommended fix:

## Handle both URL formats

```typescript
// Remix/React Router - both work by default
// app/routes/webhooks.tsx handles /webhooks

// Express - add middleware
app.use((req, res, next) => {
  if (req.path.endsWith('/') && req.path.length > 1) {
    const query = req.url.slice(req.path.length);
    const safePath = req.path.slice(0, -1);
    res.redirect(301, safePath + query);
  }
  next();
});
```

## Configure web server

```nginx
# Nginx - strip trailing slashes
location ~ ^(.+)/$ {
  return 301 $1;
}

# Or rewrite to handler
location /webhooks {
  try_files $uri $uri/ @webhooks;
}
location @webhooks {
  proxy_pass http://app:3000/webhooks;
}
```

## Test both formats

```bash
# Test without slash
curl -X POST https://your-app.com/webhooks

# Test with slash
curl -X POST https://your-app.com/webhooks/
```

### REST API Required Migration to GraphQL (April 2025)

Severity: HIGH

Situation: Building new public apps or maintaining existing

Symptoms:
App store submission rejected for REST API usage.
Deprecation warnings in console.
Some REST endpoints stop working.
Missing features only in GraphQL.

Why this breaks:
As of October 2024, REST Admin API is legacy.
Starting April 2025, new public apps MUST use GraphQL.

REST endpoints will continue working for existing apps,
but new features are GraphQL-only.

Metafields, bulk operations, and many new features
require GraphQL.

Recommended fix:

## Use GraphQL for all new code

```typescript
// REST (legacy)
const response = await fetch(
  `https://${shop}/admin/api/2024-10/products.json`,
  {
    headers: { "X-Shopify-Access-Token": token },
  }
);

// GraphQL (recommended)
const response = await admin.graphql(`
  query {
    products(first: 10) {
      edges {
        node {
          id
          title
        }
      }
    }
  }
`);
```

## Migrate existing REST calls

```typescript
// REST: GET /products/{id}.json
// GraphQL equivalent:
const response = await admin.graphql(`
  query GetProduct($id: ID!) {
    product(id: $id) {
      id
      title
      status
      variants(first: 10) {
        edges {
          node {
            id
            price
            inventoryQuantity
          }
        }
      }
    }
  }
`, {
  variables: { id: `gid://shopify/Product/${productId}` },
});
```

## Use GraphQL for webhooks too

```toml
# shopify.app.toml
[webhooks]
api_version = "2024-10"  # Use latest GraphQL version
```

### App Bridge Required for Built for Shopify (July 2025)

Severity: HIGH

Situation: Building embedded Shopify apps

Symptoms:
App rejected from "Built for Shopify" program.
App not appearing correctly in admin.
Navigation and chrome issues.
Warning about App Bridge version.

Why this breaks:
Effective July 2025, all apps seeking "Built for Shopify" status
must use the latest version of App Bridge and be embedded.

Apps using old App Bridge versions or not embedded will
lose built for Shopify benefits (better placement, badges).

Shopify now serves App Bridge and Polaris via unversioned
script tags that auto-update.

Recommended fix:

## Use latest App Bridge via script tag

```html
<!-- Automatically stays up to date -->
<script src="https://cdn.shopify.com/shopifycloud/app-bridge.js"></script>
```

## Use AppProvider in React

```typescript
// app/routes/app.tsx
import { AppProvider } from "@shopify/shopify-app-remix/react";

export default function App() {
  return (
    <AppProvider isEmbeddedApp apiKey={apiKey}>
      <Outlet />
    </AppProvider>
  );
}
```

## Enable embedded auth strategy

```typescript
// shopify.server.ts
const shopify = shopifyApp({
  // ...
  future: {
    unstable_newEmbeddedAuthStrategy: true,
  },
});
```

## Check embedded status

```typescript
import { useAppBridge } from "@shopify/app-bridge-react";

function MyComponent() {
  const app = useAppBridge();
  const isEmbedded = app.hostOrigin !== window.location.origin;
}
```

### Missing GDPR Webhooks Block App Store Approval

Severity: HIGH

Situation: Submitting app to Shopify App Store

Symptoms:
App submission rejected.
"GDPR webhooks not implemented" error.
Manual review fails for compliance.
Data request webhooks not handled.

Why this breaks:
Shopify requires all apps to handle three GDPR webhooks:
1. customers/data_request - Provide customer data
2. customers/redact - Delete customer data
3. shop/redact - Delete all shop data

These are automatically subscribed when you create an app.
You MUST implement handlers even if you don't store data.

Recommended fix:

## Implement all GDPR handlers

```typescript
// app/routes/webhooks.tsx
export const action = async ({ request }: ActionFunctionArgs) => {
  const { topic, payload, shop } = await authenticate.webhook(request);

  switch (topic) {
    case "CUSTOMERS_DATA_REQUEST":
      await handleDataRequest(shop, payload);
      break;

    case "CUSTOMERS_REDACT":
      await handleCustomerRedact(shop, payload);
      break;

    case "SHOP_REDACT":
      await handleShopRedact(shop, payload);
      break;
  }

  return new Response(null, { status: 200 });
};

async function handleDataRequest(shop: string, payload: any) {
  const customerId = payload.customer.id;

  // Return customer data within 30 days
  // Usually send to data_request.destination_url
  const customerData = await db.customer.findUnique({
    where: { shopifyId: customerId, shop },
  });

  if (customerData) {
    // Send to provided URL or email
    await sendDataToMerchant(payload.data_request, customerData);
  }
}

async function handleCustomerRedact(shop: string, payload: any) {
  const customerId = payload.customer.id;

  // Delete customer's personal data
  await db.customer.deleteMany({
    where: { shopifyId: customerId, shop },
  });

  await db.order.updateMany({
    where: { customerId, shop },
    data: { customerEmail: null, customerName: null },
  });
}

async function handleShopRedact(shop: string, payload: any) {
  // Shop uninstalled 48+ hours ago
  // Delete ALL data for this shop
  await db.session.deleteMany({ where: { shop } });
  await db.customer.deleteMany({ where: { shop } });
  await db.order.deleteMany({ where: { shop } });
  await db.settings.deleteMany({ where: { shop } });
}
```

## Even if you store nothing

```typescript
// You must still respond 200
case "CUSTOMERS_DATA_REQUEST":
case "CUSTOMERS_REDACT":
case "SHOP_REDACT":
  // No data stored, but must acknowledge
  console.log(`GDPR ${topic} for ${shop} - no data stored`);
  break;
```

## Validation Checks

### Hardcoded Shopify API Secret

Severity: ERROR

API secrets must never be hardcoded

Message: Hardcoded Shopify API secret. Use environment variables.

### Hardcoded Shopify API Key

Severity: ERROR

API keys should use environment variables

Message: Hardcoded Shopify API key. Use environment variables.

### Missing HMAC Verification

Severity: ERROR

Webhook endpoints must verify HMAC signature

Message: Webhook handler without HMAC verification. Use authenticate.webhook().

### Synchronous Webhook Processing

Severity: WARNING

Webhook handlers should respond quickly

Message: Multiple await calls in webhook handler. Consider async processing.

### Missing Webhook Response

Severity: ERROR

Webhooks must return 200 status

Message: Webhook handler may not return proper response.

### Duplicate Webhook Registration

Severity: WARNING

Webhooks should be defined in TOML only

Message: Code-based webhook registration. Define webhooks in shopify.app.toml.

### REST API Usage

Severity: INFO

REST API is deprecated, use GraphQL

Message: REST API usage detected. Consider migrating to GraphQL.

### Missing Rate Limit Handling

Severity: WARNING

API calls should handle 429 responses

Message: API call without rate limit handling. Implement retry logic.

### In-Memory Session Storage

Severity: WARNING

In-memory sessions don't scale

Message: In-memory session storage. Use PrismaSessionStorage or similar.

### Missing Session Validation

Severity: ERROR

Routes should validate session

Message: Loader without authentication. Use authenticate.admin(request).

## Collaboration

### Delegation Triggers

- user needs payment processing -> stripe-integration (Shopify Payments or Stripe integration)
- user needs custom authentication -> auth-specialist (Beyond Shopify OAuth)
- user needs email/SMS notifications -> twilio-communications (Customer notifications outside Shopify)
- user needs AI features -> llm-architect (Product descriptions, chatbots)
- user needs serverless deployment -> aws-serverless (Lambda or Vercel deployment)

## When to Use
- User mentions or implies: shopify app
- User mentions or implies: shopify
- User mentions or implies: embedded app
- User mentions or implies: polaris
- User mentions or implies: app bridge
- User mentions or implies: shopify webhook

## Limitations
- Use this skill only when the task clearly matches the scope described above.
- Do not treat the output as a substitute for environment-specific validation, testing, or expert review.
- Stop and ask for clarification if required inputs, permissions, safety boundaries, or success criteria are missing.


---
name: shopify-automation
description: "Automate Shopify tasks via Rube MCP (Composio): products, orders, customers, inventory, collections. Always search tools first for current schemas."
risk: critical
source: community
date_added: "2026-02-27"
---

# Shopify Automation via Rube MCP

Automate Shopify operations through Composio's Shopify toolkit via Rube MCP.

## Prerequisites

- Rube MCP must be connected (RUBE_SEARCH_TOOLS available)
- Active Shopify connection via `RUBE_MANAGE_CONNECTIONS` with toolkit `shopify`
- Always call `RUBE_SEARCH_TOOLS` first to get current tool schemas

## Setup

**Get Rube MCP**: Add `https://rube.app/mcp` as an MCP server in your client configuration. No API keys needed — just add the endpoint and it works.

1. Verify Rube MCP is available by confirming `RUBE_SEARCH_TOOLS` responds
2. Call `RUBE_MANAGE_CONNECTIONS` with toolkit `shopify`
3. If connection is not ACTIVE, follow the returned auth link to complete Shopify OAuth
4. Confirm connection status shows ACTIVE before running any workflows

## Core Workflows

### 1. Manage Products

**When to use**: User wants to list, search, create, or manage products

**Tool sequence**:
1. `SHOPIFY_GET_PRODUCTS` / `SHOPIFY_GET_PRODUCTS_PAGINATED` - List products [Optional]
2. `SHOPIFY_GET_PRODUCT` - Get single product details [Optional]
3. `SHOPIFY_BULK_CREATE_PRODUCTS` - Create products in bulk [Optional]
4. `SHOPIFY_GET_PRODUCTS_COUNT` - Get product count [Optional]

**Key parameters**:
- `product_id`: Product ID for single retrieval
- `title`: Product title
- `vendor`: Product vendor
- `status`: 'active', 'draft', or 'archived'

**Pitfalls**:
- Paginated results require cursor-based pagination for large catalogs
- Product variants are nested within the product object

### 2. Manage Orders

**When to use**: User wants to list, search, or inspect orders

**Tool sequence**:
1. `SHOPIFY_GET_ORDERS_WITH_FILTERS` - List orders with filters [Required]
2. `SHOPIFY_GET_ORDER` - Get single order details [Optional]
3. `SHOPIFY_GET_FULFILLMENT` - Get fulfillment details [Optional]
4. `SHOPIFY_GET_FULFILLMENT_EVENTS` - Track fulfillment events [Optional]

**Key parameters**:
- `status`: Order status filter ('any', 'open', 'closed', 'cancelled')
- `financial_status`: Payment status filter
- `fulfillment_status`: Fulfillment status filter
- `order_id`: Order ID for single retrieval
- `created_at_min`/`created_at_max`: Date range filters

**Pitfalls**:
- Order IDs are numeric; use string format for API calls
- Default order listing may not include all statuses; specify 'any' for all

### 3. Manage Customers

**When to use**: User wants to list or search customers

**Tool sequence**:
1. `SHOPIFY_GET_ALL_CUSTOMERS` - List all customers [Required]

**Key parameters**:
- `limit`: Number of customers per page
- `since_id`: Pagination cursor

**Pitfalls**:
- Customer data includes order count and total spent
- Large customer lists require pagination

### 4. Manage Collections

**When to use**: User wants to manage product collections

**Tool sequence**:
1. `SHOPIFY_GET_SMART_COLLECTIONS` - List smart collections [Optional]
2. `SHOPIFY_GET_SMART_COLLECTION_BY_ID` - Get collection details [Optional]
3. `SHOPIFY_CREATE_SMART_COLLECTIONS` - Create a smart collection [Optional]
4. `SHOPIFY_ADD_PRODUCT_TO_COLLECTION` - Add product to collection [Optional]
5. `SHOPIFY_GET_PRODUCTS_IN_COLLECTION` - List products in collection [Optional]

**Key parameters**:
- `collection_id`: Collection ID
- `product_id`: Product ID for adding to collection
- `rules`: Smart collection rules for automatic inclusion

**Pitfalls**:
- Smart collections auto-populate based on rules; manual collections use custom collections API
- Collection count endpoints provide approximate counts

### 5. Manage Inventory

**When to use**: User wants to check or manage inventory levels

**Tool sequence**:
1. `SHOPIFY_GET_INVENTORY_LEVELS` / `SHOPIFY_RETRIEVES_A_LIST_OF_INVENTORY_LEVELS` - Check stock [Required]
2. `SHOPIFY_LIST_LOCATION` - List store locations [Optional]

**Key parameters**:
- `inventory_item_ids`: Inventory item IDs to check
- `location_ids`: Location IDs to filter by

**Pitfalls**:
- Inventory is tracked per variant per location
- Location IDs are required for multi-location stores

## Common Patterns

### Pagination

- Use `limit` and `page_info` cursor for paginated results
- Check response for `next` link header
- Continue until no more pages available

### GraphQL Queries

For advanced operations:
```
1. Call SHOPIFY_GRAPH_QL_QUERY with custom query
2. Parse response from data object
```

## Known Pitfalls

**API Versioning**:
- Shopify REST API has versioned endpoints
- Some features require specific API versions

**Rate Limits**:
- REST API: 2 requests/second for standard plans
- GraphQL: 1000 cost points per second

## Quick Reference

| Task | Tool Slug | Key Params |
|------|-----------|------------|
| List products | SHOPIFY_GET_PRODUCTS | (filters) |
| Get product | SHOPIFY_GET_PRODUCT | product_id |
| Products paginated | SHOPIFY_GET_PRODUCTS_PAGINATED | limit, page_info |
| Bulk create | SHOPIFY_BULK_CREATE_PRODUCTS | products |
| Product count | SHOPIFY_GET_PRODUCTS_COUNT | (none) |
| List orders | SHOPIFY_GET_ORDERS_WITH_FILTERS | status, financial_status |
| Get order | SHOPIFY_GET_ORDER | order_id |
| List customers | SHOPIFY_GET_ALL_CUSTOMERS | limit |
| Shop details | SHOPIFY_GET_SHOP_DETAILS | (none) |
| Validate access | SHOPIFY_VALIDATE_ACCESS | (none) |
| Smart collections | SHOPIFY_GET_SMART_COLLECTIONS | (none) |
| Products in collection | SHOPIFY_GET_PRODUCTS_IN_COLLECTION | collection_id |
| Inventory levels | SHOPIFY_GET_INVENTORY_LEVELS | inventory_item_ids |
| Locations | SHOPIFY_LIST_LOCATION | (none) |
| Fulfillment | SHOPIFY_GET_FULFILLMENT | order_id, fulfillment_id |
| GraphQL | SHOPIFY_GRAPH_QL_QUERY | query |
| Bulk query | SHOPIFY_BULK_QUERY_OPERATION | query |

## When to Use
This skill is applicable to execute the workflow or actions described in the overview.

## Limitations
- Use this skill only when the task clearly matches the scope described above.
- Do not treat the output as a substitute for environment-specific validation, testing, or expert review.
- Stop and ask for clarification if required inputs, permissions, safety boundaries, or success criteria are missing.


---
name: shopify-development
description: Build Shopify apps, extensions, themes using GraphQL Admin API, Shopify CLI, Polaris UI, and Liquid.
risk: critical
source: community
date_added: '2026-02-27'
---

# Shopify Development Skill

Use this skill when the user asks about:

- Building Shopify apps or extensions
- Creating checkout/admin/POS UI customizations
- Developing themes with Liquid templating
- Integrating with Shopify GraphQL or REST APIs
- Implementing webhooks or billing
- Working with metafields or Shopify Functions

---

## ROUTING: What to Build

**IF user wants to integrate external services OR build merchant tools OR charge for features:**
→ Build an **App** (see `references/app-development.md`)

**IF user wants to customize checkout OR add admin UI OR create POS actions OR implement discount rules:**
→ Build an **Extension** (see `references/extensions.md`)

**IF user wants to customize storefront design OR modify product/collection pages:**
→ Build a **Theme** (see `references/themes.md`)

**IF user needs both backend logic AND storefront UI:**
→ Build **App + Theme Extension** combination

---

## Shopify CLI Commands

Install CLI:

```bash
npm install -g @shopify/cli@latest
```

Create and run app:

```bash
shopify app init          # Create new app
shopify app dev           # Start dev server with tunnel
shopify app deploy        # Build and upload to Shopify
```

Generate extension:

```bash
shopify app generate extension --type checkout_ui_extension
shopify app generate extension --type admin_action
shopify app generate extension --type admin_block
shopify app generate extension --type pos_ui_extension
shopify app generate extension --type function
```

Theme development:

```bash
shopify theme init        # Create new theme
shopify theme dev         # Start local preview at localhost:9292
shopify theme pull --live # Pull live theme
shopify theme push --development  # Push to dev theme
```

---

## Access Scopes

Configure in `shopify.app.toml`:

```toml
[access_scopes]
scopes = "read_products,write_products,read_orders,write_orders,read_customers"
```

Common scopes:

- `read_products`, `write_products` - Product catalog access
- `read_orders`, `write_orders` - Order management
- `read_customers`, `write_customers` - Customer data
- `read_inventory`, `write_inventory` - Stock levels
- `read_fulfillments`, `write_fulfillments` - Order fulfillment

---

## GraphQL Patterns (Validated against API 2026-01)

### Query Products

```graphql
query GetProducts($first: Int!, $query: String) {
  products(first: $first, query: $query) {
    edges {
      node {
        id
        title
        handle
        status
        variants(first: 5) {
          edges {
            node {
              id
              price
              inventoryQuantity
            }
          }
        }
      }
    }
    pageInfo {
      hasNextPage
      endCursor
    }
  }
}
```

### Query Orders

```graphql
query GetOrders($first: Int!) {
  orders(first: $first) {
    edges {
      node {
        id
        name
        createdAt
        displayFinancialStatus
        totalPriceSet {
          shopMoney {
            amount
            currencyCode
          }
        }
      }
    }
  }
}
```

### Set Metafields

```graphql
mutation SetMetafields($metafields: [MetafieldsSetInput!]!) {
  metafieldsSet(metafields: $metafields) {
    metafields {
      id
      namespace
      key
      value
    }
    userErrors {
      field
      message
    }
  }
}
```

Variables example:

```json
{
  "metafields": [
    {
      "ownerId": "gid://shopify/Product/123",
      "namespace": "custom",
      "key": "care_instructions",
      "value": "Handle with care",
      "type": "single_line_text_field"
    }
  ]
}
```

---

## Checkout Extension Example

```tsx
import {
  reactExtension,
  BlockStack,
  TextField,
  Checkbox,
  useApplyAttributeChange,
} from "@shopify/ui-extensions-react/checkout";

export default reactExtension("purchase.checkout.block.render", () => (
  <GiftMessage />
));

function GiftMessage() {
  const [isGift, setIsGift] = useState(false);
  const [message, setMessage] = useState("");
  const applyAttributeChange = useApplyAttributeChange();

  useEffect(() => {
    if (isGift && message) {
      applyAttributeChange({
        type: "updateAttribute",
        key: "gift_message",
        value: message,
      });
    }
  }, [isGift, message]);

  return (
    <BlockStack spacing="loose">
      <Checkbox checked={isGift} onChange={setIsGift}>
        This is a gift
      </Checkbox>
      {isGift && (
        <TextField
          label="Gift Message"
          value={message}
          onChange={setMessage}
          multiline={3}
        />
      )}
    </BlockStack>
  );
}
```

---

## Liquid Template Example

```liquid
{% comment %} Product Card Snippet {% endcomment %}
<div class="product-card">
  <a href="{{ product.url }}">
    {% if product.featured_image %}
      <img
        src="{{ product.featured_image | img_url: 'medium' }}"
        alt="{{ product.title | escape }}"
        loading="lazy"
      >
    {% endif %}
    <h3>{{ product.title }}</h3>
    <p class="price">{{ product.price | money }}</p>
    {% if product.compare_at_price > product.price %}
      <p class="sale-badge">Sale</p>
    {% endif %}
  </a>
</div>
```

---

## Webhook Configuration

In `shopify.app.toml`:

```toml
[webhooks]
api_version = "2026-01"

[[webhooks.subscriptions]]
topics = ["orders/create", "orders/updated"]
uri = "/webhooks/orders"

[[webhooks.subscriptions]]
topics = ["products/update"]
uri = "/webhooks/products"

# GDPR mandatory webhooks (required for app approval)
[webhooks.privacy_compliance]
customer_data_request_url = "/webhooks/gdpr/data-request"
customer_deletion_url = "/webhooks/gdpr/customer-deletion"
shop_deletion_url = "/webhooks/gdpr/shop-deletion"
```

---

## Best Practices

### API Usage

- Use GraphQL over REST for new development
- Request only fields you need (reduces query cost)
- Implement cursor-based pagination with `pageInfo.endCursor`
- Use bulk operations for processing more than 250 items
- Handle rate limits with exponential backoff

### Security

- Store API credentials in environment variables
- Always verify webhook HMAC signatures before processing
- Validate OAuth state parameter to prevent CSRF
- Request minimal access scopes
- Use session tokens for embedded apps

### Performance

- Cache API responses when data doesn't change frequently
- Use lazy loading in extensions
- Optimize images in themes using `img_url` filter
- Monitor GraphQL query costs via response headers

---

## Troubleshooting

**IF you see rate limit errors:**
→ Implement exponential backoff retry logic
→ Switch to bulk operations for large datasets
→ Monitor `X-Shopify-Shop-Api-Call-Limit` header

**IF authentication fails:**
→ Verify the access token is still valid
→ Check that all required scopes were granted
→ Ensure OAuth flow completed successfully

**IF extension is not appearing:**
→ Verify the extension target is correct
→ Check that extension is published via `shopify app deploy`
→ Confirm the app is installed on the test store

**IF webhook is not receiving events:**
→ Verify the webhook URL is publicly accessible
→ Check HMAC signature validation logic
→ Review webhook logs in Partner Dashboard

**IF GraphQL query fails:**
→ Validate query against schema (use GraphiQL explorer)
→ Check for deprecated fields in error message
→ Verify you have required access scopes

---

## Reference Files

For detailed implementation guides, read these files:

- `references/app-development.md` - OAuth authentication flow, GraphQL mutations for products/orders/billing, webhook handlers, billing API integration
- `references/extensions.md` - Checkout UI components, Admin UI extensions, POS extensions, Shopify Functions for discounts/payment/delivery
- `references/themes.md` - Liquid syntax reference, theme directory structure, sections and snippets, common patterns

---

## Scripts

- `scripts/shopify_init.py` - Interactive project scaffolding. Run: `python scripts/shopify_init.py`
- `scripts/shopify_graphql.py` - GraphQL utilities with query templates, pagination, rate limiting. Import: `from shopify_graphql import ShopifyGraphQL`

---

## Official Documentation Links

- Shopify Developer Docs: https://shopify.dev/docs
- GraphQL Admin API Reference: https://shopify.dev/docs/api/admin-graphql
- Shopify CLI Reference: https://shopify.dev/docs/api/shopify-cli
- Polaris Design System: https://polaris.shopify.com

API Version: 2026-01 (quarterly releases, 12-month deprecation window)

## When to Use
This skill is applicable to execute the workflow or actions described in the overview.

## Limitations
- Use this skill only when the task clearly matches the scope described above.
- Do not treat the output as a substitute for environment-specific validation, testing, or expert review.
- Stop and ask for clarification if required inputs, permissions, safety boundaries, or success criteria are missing.


---
name: shopify-review-triage
description: "Turn public 1-3-star Shopify App Store review rows into a P0-P3 triage brief: incident risk, repeated friction, pricing confusion, feature requests, and an explicit needs-human-read bucket."
category: product
risk: none
source: community
source_repo: alfredtech2026/shopify-app-review-brief
source_type: community
date_added: "2026-08-03"
author: alfredtech2026
tags: [shopify, app-store-reviews, customer-feedback, triage, product-management, support]
tools: [claude, cursor, codex, gemini, antigravity]
license: "MIT"
license_source: "https://github.com/alfredtech2026/shopify-app-review-brief/blob/main/LICENSE"
---

# Shopify Review Triage — public low-star reviews to a P0–P3 brief

## Overview

Takes rows of **public** Shopify App Store review text and produces one prioritized brief a
product or support owner can act on: what kind of problem each review describes, how badly it
can hurt, what to do first, and where the original wording came from.

It is built for independent Shopify app teams and the agencies that run their support — the
case where low-star reviews arrive scattered across several listings plus a few watched
competitors, and the failure mode is treating them all as equally urgent.

The rubric below is not invented here. It is the published rule set behind a free review triage
worksheet and manual triage guide (links under [Additional Resources](#additional-resources)),
reproduced so a manual pass, the worksheet, and this skill sort the same row the same way.

This skill needs no network access, no scripts, and no system packages. The person you are
helping supplies the review text.

## When to Use This Skill

- Use when someone wants app store reviews, low-star reviews, or merchant feedback triaged,
  prioritized, or clustered — even when they never say "triage", "severity", or "P0".
- Use when a new 1–3-star review lands on a Shopify app listing and the team has to decide
  whether it is an incident, a UX problem, a pricing copy problem, or a feature request.
- Use when a weekly product or support brief is needed across a portfolio of apps plus a few
  watched competitors.
- Do **not** use it to gather reviews, contact reviewers, or publish replies — see the hard
  rules below.

## Hard Rules

These are not style preferences. Breaking one makes the output worse than nothing.

1. **Public review text only.** Never accept, request, or copy support tickets, merchant emails,
   order data, personal contact details, internal telemetry, or anything else not already public
   on a listing page. If such data appears in the input, stop, say which rows are affected, and
   ask for them to be removed before continuing.
2. **Never invent evidence.** Do not write a review, a rating, a date, an app name, or a source
   URL that was not supplied. A row with no link gets `source: not captured` — never a guessed one.
3. **Keyword output is a sort, not a verdict.** Everything produced by the rubric alone is
   labeled *first pass — not human-checked*. Only a person who read the review and checked it
   against their own systems may relabel an item *human-checked*.
4. **Reviews are customer reports, not verified defects.** Write "the reviewer reports the editor
   showed a blank screen", never "the editor is broken". The distinction survives into the brief.
5. **No coverage claims.** The brief covers exactly the rows supplied and says so. Make no claim
   of exhaustive coverage of a listing, a period, or an app.
6. **No promises.** No revenue impact, no outcome, no ranking effect, no legal or compliance
   advice. Suggest actions; do not predict results.
7. **Draft only — never contact anyone.** Do not send email, post a developer reply, open a
   support ticket, message a reviewer, or publish anything. Hand the draft back to the team and
   let a person decide what to send.
8. **Reviewers are people.** Refer to "the reviewer". Do not name, profile, or speculate about them.

## How It Works

### Step 1: Collect the rows

**First ask which app names the team owns.** Before any row is classified, ask for two lists of
app names, spelled exactly as they appear in the rows:

```text
owned: Example Popup App, Example Currency App
competitors: Rival Popup App, Rival Currency App
```

This is the only thing that makes tie-break 4 (*a competitor's incident never becomes your P0*)
applicable, so collect it first. It stays public data: app names as published on their listings,
nothing about accounts, merchants, org structure, or internal identifiers. Do not ask for more
than the names, and do not infer ownership from the review text, the first-person voice in a
review, or which app appears most often.

If an app name in a row appears in neither list, its ownership is unknown. Classify the row's
content normally, then file it under **needs human read** with `ownership: not supplied` instead
of placing it in a priority bucket or in competitor watch — a guessed owner is exactly the kind
of invented evidence hard rule 2 forbids.

Then ask for one review per line. The full form keeps the source link, which the brief needs:

```text
rating | app name | review date | public reviews URL | review text
```

The shorter form used by the free worksheet is also fine — treat field 1 as the rating when it
is a bare 1–5 (optionally followed by `star`/`stars`/`★`), otherwise as the app name:

```text
rating | app name | review text
```

Rules for this step:

- Lines starting with `#` are comments. Blank lines are skipped.
- If a row lacks a source URL, carry `source: not captured` through to the brief. Do not drop
  the row and do not fabricate a link.
- Do not go and fetch anything yourself. This skill needs no network access; the person you are
  helping pastes the public rows they already opened.
- The trigger this rubric is tuned for is a **new 1–3-star review**. Higher-rated rows still
  classify correctly (a 5★ review often lands in feature requests or needs-human-read), so keep
  them if they were supplied, but never present them as low-star signal.

### Step 2: First pass — apply the rubric

Lower-case the review text and normalize curly apostrophes (`’` → `'`) before matching, so a
pasted "won’t load" still matches `won't load`.

Five buckets. Each row gets exactly **one primary** bucket — the first dimension below, in this
order, with any matching keyword. Further matches are recorded as **secondary**, never as a
second brief item.

#### P0 · Incident risk

The purchase path, app activation, or merchant data may be at stake right now. Left alone it
costs the merchant money and the team installs.

**Suggested action.** Try to reproduce on a test store today. If confirmed, treat it as an incident: fix or mitigate first, then reply to the reviewer with what changed.

**Signal keywords.** `won't load`, `wont load`, `won't open`, `wont open`, `can't close`, `cannot close`, `won't close`, `blank screen`, `broken`, `crash`, `stopped working`, `not working`, `doesn't work`, `does not work`, `checkout`, `losing sales`, `lost sales`, `error`

#### P1 · Repeated friction

The product works, but the same struggle keeps showing up across reviews or against an open
support theme. Repetition is the signal, not volume of adjectives.

**Suggested action.** Log it against the matching support theme. If the same complaint repeats across rows, schedule a UX fix ahead of new feature work.

**Signal keywords.** `confusing`, `unclear`, `hard to`, `difficult`, `complicated`, `clunky`, `slow`, `couldn't figure`, `could not figure`, `annoying`, `had to contact support`, `setup took`, `too many steps`

#### P2 · Pricing confusion

What the merchant expected to pay and what happened diverged. Usually a copy problem in the
listing, the plan limits, or the upgrade prompts — not a code problem.

**Suggested action.** Compare what the reviewer expected with the listing's pricing section and in-app upgrade prompts; clarify the copy where they diverge.

**Signal keywords.** `pricing`, `price`, `charged`, `charge`, `billing`, `billed`, `expensive`, `free plan`, `trial`, `refund`, `hidden fee`, `hidden cost`, `paywall`

#### P3 · Feature request

The merchant wants something the app does not do, or could not find. Valuable as a log entry,
rarely urgent on its own.

**Suggested action.** Add it to the feature-request log with a link to the review. If the capability already exists, reply to the reviewer with where to find it.

**Signal keywords.** `wish`, `would be great`, `would love`, `please add`, `feature request`, `missing`, `if only`, `would like`, `no option to`, `needs an option`, `hope you add`, `add support for`

#### Needs human read

No keyword matched. Vague frustration, sarcasm, mixed praise, or a story that needs context.

**Suggested action.** No keyword matched. Read the full review yourself and file it manually — the heuristic makes no guess here.

**Priority.** The worksheet labels this bucket `P2` and sorts it last. Treat that label as
provisional placement in the queue, not as a severity judgment — nothing has been judged yet.

#### Tie-breaks and escalation

1. **Most severe wins.** A row naming both a broken checkout and a billing surprise files under
   P0 with pricing noted as secondary. Never split one review across two brief items.
2. **Repetition escalates.** If the same friction or pricing theme appears in three or more
   reviews within about 60 days, move it up one level and say how many rows drove the change.
3. **Age discounts.** A review older than a year is background, not evidence of a current
   problem, unless a recent row corroborates it. Cite it as context, never as the headline.
4. **Competitor reviews never create a P0 for you.** Resolve the row's app name against the
   ownership lists from step 1: `owned` keeps its rubric bucket, `competitors` moves to the
   competitor watch section whatever its keywords matched, and a name in neither list goes to
   needs human read with `ownership: not supplied`. A competitor's incident is roadmap,
   positioning, or copy input — never your P0.
5. **When unsure, choose needs human read.** The bucket exists so the rubric never launders
   uncertainty into a priority label.

### Step 3: Human pass — verify before you promote anything

The first pass is where this skill stops being able to help on its own. Before any item is
presented as more than a keyword match, a person on the team has to:

- read the full original review at its source link;
- for P0 candidates, attempt to reproduce on a development store and check the error tracker and
  support inbox for matching signals from the same period;
- record the outcome as *reproduced*, *not reproduced*, or *attempted — notes attached*.

Ask for these outcomes rather than assuming them. Until you have them, every item stays labeled
*first pass — not human-checked*, including in the summary line. An unverified P0 is a candidate,
not an incident.

Known limits to state plainly when they apply: keyword matching is English-only, misses sarcasm
and context, can misfile a review that mentions "checkout" in passing, and sees only the rows
supplied.

### Step 4: Write the brief

One document per portfolio, sections in rubric order, every item carrying an owner, a next
action, and a source link. An item without an owner is a note, not a brief entry.

<!-- brief-template -->
```markdown
# Low-star review brief — {portfolio or team name} — week of {YYYY-MM-DD}

Scope: {apps monitored} · {competitors watched} · {N} rows supplied, {date range}.
Covers only the rows supplied — no claim of exhaustive coverage.
Reviews are customer reports, not verified defects. Items marked "first pass" are
unverified keyword matches; "human-checked" means a person read the review and checked it.

## P0 — Incident risk
- **{App} — {signal in a few words}** ({rating}★, {review date}, source: {public reviews URL or not captured})
  - Reviewer reports: {one sentence, in their words where possible}
  - Status: first pass — not human-checked / human-checked
  - Reproduced: {yes / no / attempted — notes}
  - Next action: {action} — owner {name}, due {date}

## P1 — Repeated friction
- **{App} — {theme}** ({rating}★, {date}, source: {public reviews URL or not captured}; also seen: {where})
  - Status: first pass — not human-checked / human-checked
  - Next action: {UX or docs change} — owner {name}, due {date}

## P2 — Pricing confusion
- **{App} — {signal}** ({rating}★, {date}, source: {public reviews URL or not captured})
  - Expected vs. actual: {one line}
  - Status: first pass — not human-checked / human-checked
  - Next action: {copy or prompt change} — owner {name}, due {date}

## P3 — Feature requests
- **{App} — {request}** ({rating}★, {date}, source: {public reviews URL or not captured}) — {log it / already exists → reply with where to find it}

## Needs human read
- **{App}** ({rating}★, {date}, source: {public reviews URL or not captured}) — {no keyword matched; what a human should look for}{, or: ownership: not supplied — app name on neither list}

## Competitor watch
- **{Competitor} — {signal}**: {what it implies for our roadmap, copy, or positioning}

## Decisions this week
- {one decision or experiment, with the row(s) that motivated it}
```

Open the summary line with the counts, e.g. *"Triaged 8 rows supplied: 3 incident risk,
2 repeated friction, 1 pricing confusion, 1 feature request, 1 needs human read — first pass,
not human-checked."*

### Step 5: Self-check before you hand it over

Refuse to deliver until every line is true:

- [ ] Every item names its bucket and priority from the rubric above, and nothing else.
- [ ] Every item carries a source link or an explicit `source: not captured`.
- [ ] Every P0–P3 item is an app on the `owned` list; every competitor row sits in competitor
      watch; every unlisted app name says `ownership: not supplied` under needs human read.
- [ ] No review text, rating, date, app name, or URL appears that was not supplied.
- [ ] Every unverified item says *first pass — not human-checked*; nothing claims a human check
      that did not happen.
- [ ] Claims are phrased as reports ("the reviewer reports…"), not as findings about the code.
- [ ] The scope line says how many rows were supplied and makes no coverage claim.
- [ ] No promise about revenue, ratings, outcomes, or compliance appears anywhere.
- [ ] No private data survived into the output.
- [ ] Nothing was sent, posted, or published — the brief is a draft for the team.

## Examples

### Example 1: Worked example — eight rows in, first pass out

These eight fictional rows are the worksheet's own example set, so the two tools can be compared
directly. Two of them are deliberately 4★ and 5★, to exercise the feature-request and
needs-human-read buckets.

Ownership context, collected before any of it is classified:

```text
owned: Example Popup App, Example Currency App, Example Reviews App
competitors: (none supplied)
```

```text
1 | Example Popup App | The editor shows a blank screen and the popup won't load. We are losing sales every day.
2 | Example Popup App | The overlay can't close on mobile and it blocks the checkout button.
1 | Example Currency App | Conversion is broken at checkout and we were still billed for the month.
3 | Example Currency App | Setup took hours and the settings screen is confusing. Support was slow to reply.
3 | Example Reviews App | The widget looks fine but the template editor is confusing and hard to use on a tablet.
2 | Example Currency App | We kept getting charged after uninstalling, and the pricing page never mentioned this.
4 | Example Reviews App | Great app, but I wish it could export reviews to CSV. Please add filtering by country.
5 | Example Reviews App | Does what it promises and support replied the same day.
```

First pass over those rows:

```text
row 1 → P0 incident risk
row 2 → P0 incident risk
row 3 → P0 incident risk (secondary: pricing confusion)
row 4 → P1 repeated friction
row 5 → P1 repeated friction
row 6 → P2 pricing confusion
row 7 → P3 feature request
row 8 → needs human read
```

**Explanation:** Rows 4 and 5 both matched `confusing`, so they are flagged as a repeated theme —
two rows, which is a cluster to watch, not yet the three that trigger escalation. Row 3 is a
single P0 item with pricing recorded as secondary, never two items. Row 8 matched nothing and
stays unjudged. All three app names are on the `owned` list, so every bucket above is the team's
own queue and competitor watch is empty; had `Example Reviews App` been listed as a competitor
instead, rows 5, 7, and 8 would move there and none of them could become a P0. None of these rows
carried a source URL, so each item would read `source: not captured` until the team supplies the
listing links.

### Example 2: A row that carries its source link

```text
1 | Example Popup App | 2026-07-28 | https://apps.shopify.com/example-popup-app/reviews?ratings%5B%5D=1 | The editor shows a blank screen and the popup won't load. We are losing sales every day.
```

Rendered into the brief:

```markdown
## P0 — Incident risk
- **Example Popup App — editor reported blank, popup reported not loading** (1★, 2026-07-28, [source](https://apps.shopify.com/example-popup-app/reviews?ratings%5B%5D=1))
  - Reviewer reports: the editor shows a blank screen, the popup does not load, and they are losing sales daily.
  - Status: first pass — not human-checked
  - Reproduced: not yet attempted
  - Next action: attempt reproduction on a development store today — owner {name}, due {date}
```

**Explanation:** It files as a P0 only because `Example Popup App` is on the `owned` list; the
same row from a competitor listing would render under competitor watch instead. The wording stays
a report ("the reviewer reports"), the status stays *first pass — not human-checked* until a
person verifies it, and the source link is the listing's public reviews page with the rating
filter kept — the App Store has no per-review permalink.

## Best Practices

- ✅ **Do:** keep one review in exactly one bucket, and record extra matches as secondary notes.
- ✅ **Do:** carry `source: not captured` forward when a row has no link, so the gap is visible.
- ✅ **Do:** label every unverified item *first pass — not human-checked*, including in the summary.
- ✅ **Do:** phrase every finding as a customer report, not as a confirmed defect.
- ✅ **Do:** state how many rows were supplied and refuse any coverage claim beyond them.
- ❌ **Don't:** fetch reviews, scrape listings, or ask for support tickets, emails, or order data.
- ❌ **Don't:** invent a rating, date, app name, or URL that was not supplied.
- ❌ **Don't:** send, post, or publish anything — including a developer reply to a reviewer.
- ❌ **Don't:** promise a revenue, ratings, or compliance outcome from any suggested action.

## Limitations

- Keyword matching is **English-only**. Non-English reviews match nothing and land in
  needs-human-read; that is the correct outcome, not a bug to work around by translating first.
- The rubric misses sarcasm, irony, and context, and can misfile a review that mentions
  "checkout" or "missing" in passing.
- It sees only the rows the person supplies. It cannot know a listing's full review history, the
  team's error tracker, or their support inbox.
- It cannot verify anything. Every P0 it produces is a *candidate*, not a confirmed incident,
  until a person reproduces it.
- It does not replace environment-specific validation, testing, or expert review. Stop and ask
  for clarification if required inputs, permissions, or safety boundaries are missing.

## Security & Safety Notes

- **No commands, no network, no credentials.** This skill runs on pasted text only. It must not
  fetch listings, call APIs, or read files outside what the person supplies.
- **Private data is a stop condition.** If support tickets, merchant emails, order records,
  personal contact details, or internal telemetry appear in the input, stop, name the affected
  rows, and ask for them to be removed before continuing.
- **No outbound messaging.** The output is a draft handed back to the team. Sending email,
  posting a public developer reply, opening a ticket, or contacting a reviewer is out of scope
  under every circumstance (hard rule 7).
- **Reviewers are people.** Do not name, profile, or speculate about a reviewer; refer to
  "the reviewer".
- **No promises.** No revenue, ratings, ranking, legal, or compliance claims belong in a brief.

## Common Pitfalls

- **Problem:** The Shopify App Store has no stable per-review permalink.
  **Solution:** Cite the listing's public reviews page, keep the rating filter if one was used
  (`…/reviews?ratings%5B%5D=1`), and pin the item with the review date plus the reviewer's first
  few words so a human can find it again.
- **Problem:** `checkout` is the noisiest keyword in the set — it fires on "we love the checkout
  upsell".
  **Solution:** A P0 whose only evidence is the word `checkout` is a needs-human-read row wearing
  a P0 badge. Say so instead of promoting it.
- **Problem:** `missing` and `error` cross buckets — "missing a dark mode" is P3, "settings page
  errors out" is P0.
  **Solution:** Primary-bucket order resolves the collision mechanically; the human pass fixes
  the ones where it guessed wrong.
- **Problem:** A competitor's incident looks worse than anything on the team's own listings.
  **Solution:** It still goes to competitor watch. A competitor's P0 is never yours.
- **Problem:** One review gets split across two sections, double-counting the same merchant and
  inflating every count in the summary line.
  **Solution:** One review, one item. Secondary matches are annotations.
- **Problem:** The free in-browser worksheet parses three fields and folds everything after the
  second `|` into the review text, so a five-field row displays its date and URL inside the quote.
  **Solution:** Paste the short form into the worksheet and keep the long form here.

## Related Skills

- `@customer-research` — when the goal is broader voice-of-customer synthesis rather than
  prioritizing a specific set of low-star review rows.
- `@shopify-apps` — when the next step is actually building or fixing the Shopify app behavior a
  triaged P0 points at.
- `@before-you-build` — when a P3 feature request needs product-risk review before it becomes
  roadmap work.

## Additional Resources

This skill packages the public rubric behind **Shopify App Review Brief**, an independent
open-source project that is not affiliated with or endorsed by Shopify Inc. or any app developer.
The same four dimensions, priorities, keyword lists, and suggested actions are published in three
places:

- [Manual guide, tie-break rules, and brief template](https://alfredtech2026.github.io/shopify-app-review-brief/guides/shopify-app-review-triage.html)
- [Free in-browser worksheet that automates the first pass](https://alfredtech2026.github.io/shopify-app-review-brief/tools/review-triage-worksheet.html)
- [Two worked sample briefs over real public reviews](https://alfredtech2026.github.io/shopify-app-review-brief/#samples)

Upstream source repository: [alfredtech2026/shopify-app-review-brief](https://github.com/alfredtech2026/shopify-app-review-brief) (MIT).


---
name: short
description: "Rewrite the previous response more briefly while preserving the substance."
category: writing
risk: safe
source: community
source_repo: davidondrej/skills
source_type: community
date_added: "2026-07-07"
author: davidondrej
tags: [writing, editing, concise]
tools: [claude, codex]
license: "MIT"
license_source: "https://github.com/davidondrej/skills/blob/main/LICENSE"
disable-model-invocation: true
---

rewrite your last response to be simpler & shorter. do not do anything else.

## When to Use

- Use when the user asks for a shorter, simpler, or TLDR version of the previous response.
- Use when the current answer should be compressed without changing the substance.

## Limitations

- Adapted from `davidondrej/skills`; verify local paths, tools, credentials, and agent features before acting.
- For commands, remote access, scheduling, browser automation, or file-changing workflows, get explicit user approval and confirm the target environment first.


---
name: signup-flow-cro
description: "You are an expert in optimizing signup and registration flows. Your goal is to reduce friction, increase completion rates, and set users up for successful activation."
risk: safe
source: community
date_added: "2026-02-27"
---

# Signup Flow CRO

You are an expert in optimizing signup and registration flows. Your goal is to reduce friction, increase completion rates, and set users up for successful activation.

## Initial Assessment

Before providing recommendations, understand:

1. **Flow Type**
   - Free trial signup
   - Freemium account creation
   - Paid account creation
   - Waitlist/early access signup
   - B2B vs B2C

2. **Current State**
   - How many steps/screens?
   - What fields are required?
   - What's the current completion rate?
   - Where do users drop off?

3. **Business Constraints**
   - What data is genuinely needed at signup?
   - Are there compliance requirements?
   - What happens immediately after signup?

---

## Core Principles

### 1. Minimize Required Fields
Every field reduces conversion. For each field, ask:
- Do we absolutely need this before they can use the product?
- Can we collect this later through progressive profiling?
- Can we infer this from other data?

**Typical field priority:**
- Essential: Email (or phone), Password
- Often needed: Name
- Usually deferrable: Company, Role, Team size, Phone, Address

### 2. Show Value Before Asking for Commitment
- What can you show/give before requiring signup?
- Can they experience the product before creating an account?
- Reverse the order: value first, signup second

### 3. Reduce Perceived Effort
- Show progress if multi-step
- Group related fields
- Use smart defaults
- Pre-fill when possible

### 4. Remove Uncertainty
- Clear expectations ("Takes 30 seconds")
- Show what happens after signup
- No surprises (hidden requirements, unexpected steps)

---

## Field-by-Field Optimization

### Email Field
- Single field (no email confirmation field)
- Inline validation for format
- Check for common typos (gmial.com → gmail.com)
- Clear error messages

### Password Field
- Show password toggle (eye icon)
- Show requirements upfront, not after failure
- Consider passphrase hints for strength
- Update requirement indicators in real-time

**Better password UX:**
- Allow paste (don't disable)
- Show strength meter instead of rigid rules
- Consider passwordless options

### Name Field
- Single "Full name" field vs. First/Last split (test this)
- Only require if immediately used (personalization)
- Consider making optional

### Social Auth Options
- Place prominently (often higher conversion than email)
- Show most relevant options for your audience
  - B2C: Google, Apple, Facebook
  - B2B: Google, Microsoft, SSO
- Clear visual separation from email signup
- Consider "Sign up with Google" as primary

### Phone Number
- Defer unless essential (SMS verification, calling leads)
- If required, explain why
- Use proper input type with country code handling
- Format as they type

### Company/Organization
- Defer if possible
- Auto-suggest as they type
- Infer from email domain when possible

### Use Case / Role Questions
- Defer to onboarding if possible
- If needed at signup, keep to one question
- Use progressive disclosure (don't show all options at once)

---

## Single-Step vs. Multi-Step

### Single-Step Works When:
- 3 or fewer fields
- Simple B2C products
- High-intent visitors (from ads, waitlist)

### Multi-Step Works When:
- More than 3-4 fields needed
- Complex B2B products needing segmentation
- You need to collect different types of info

### Multi-Step Best Practices
- Show progress indicator
- Lead with easy questions (name, email)
- Put harder questions later (after psychological commitment)
- Each step should feel completable in seconds
- Allow back navigation
- Save progress (don't lose data on refresh)

**Progressive commitment pattern:**
1. Email only (lowest barrier)
2. Password + name
3. Customization questions (optional)

---

## Trust and Friction Reduction

### At the Form Level
- "No credit card required" (if true)
- "Free forever" or "14-day free trial"
- Privacy note: "We'll never share your email"
- Security badges if relevant
- Testimonial near signup form

### Error Handling
- Inline validation (not just on submit)
- Specific error messages ("Email already registered" + recovery path)
- Don't clear the form on error
- Focus on the problem field

### Microcopy
- Placeholder text: Use for examples, not labels
- Labels: Always visible (not just placeholders)
- Help text: Only when needed, placed close to field

---

## Mobile Signup Optimization

- Larger touch targets (44px+ height)
- Appropriate keyboard types (email, tel, etc.)
- Autofill support
- Reduce typing (social auth, pre-fill)
- Single column layout
- Sticky CTA button
- Test with actual devices

---

## Post-Submit Experience

### Success State
- Clear confirmation
- Immediate next step
- If email verification required:
  - Explain what to do
  - Easy resend option
  - Check spam reminder
  - Option to change email if wrong

### Verification Flows
- Consider delaying verification until necessary
- Magic link as alternative to password
- Let users explore while awaiting verification
- Clear re-engagement if verification stalls

---

## Measurement

### Key Metrics
- Form start rate (landed → started filling)
- Form completion rate (started → submitted)
- Field-level drop-off (which fields lose people)
- Time to complete
- Error rate by field
- Mobile vs. desktop completion

### What to Track
- Each field interaction (focus, blur, error)
- Step progression in multi-step
- Social auth vs. email signup ratio
- Time between steps

---

## Output Format

### Audit Findings
For each issue found:
- **Issue**: What's wrong
- **Impact**: Why it matters (with estimated impact if possible)
- **Fix**: Specific recommendation
- **Priority**: High/Medium/Low

### Recommended Changes
Organized by:
1. Quick wins (same-day fixes)
2. High-impact changes (week-level effort)
3. Test hypotheses (things to A/B test)

### Form Redesign (if requested)
- Recommended field set with rationale
- Field order
- Copy for labels, placeholders, buttons, errors
- Visual layout suggestions

---

## Common Signup Flow Patterns

### B2B SaaS Trial
1. Email + Password (or Google auth)
2. Name + Company (optional: role)
3. → Onboarding flow

### B2C App
1. Google/Apple auth OR Email
2. → Product experience
3. Profile completion later

### Waitlist/Early Access
1. Email only
2. Optional: Role/use case question
3. → Waitlist confirmation

### E-commerce Account
1. Guest checkout as default
2. Account creation optional post-purchase
3. OR Social auth with single click

---

## Experiment Ideas

### Form Design Experiments

**Layout & Structure**
- Single-step vs. multi-step signup flow
- Multi-step with progress bar vs. without
- 1-column vs. 2-column field layout
- Form embedded on page vs. separate signup page
- Horizontal vs. vertical field alignment

**Field Optimization**
- Reduce to minimum fields (email + password only)
- Add or remove phone number field
- Single "Name" field vs. "First/Last" split
- Add or remove company/organization field
- Test required vs. optional field balance

**Authentication Options**
- Add SSO options (Google, Microsoft, GitHub, LinkedIn)
- SSO prominent vs. email form prominent
- Test which SSO options resonate (varies by audience)
- SSO-only vs. SSO + email option

**Visual Design**
- Test button colors and sizes for CTA prominence
- Plain background vs. product-related visuals
- Test form container styling (card vs. minimal)
- Mobile-optimized layout testing

---

### Copy & Messaging Experiments

**Headlines & CTAs**
- Test headline variations above signup form
- CTA button text: "Create Account" vs. "Start Free Trial" vs. "Get Started"
- Add clarity around trial length in CTA
- Test value proposition emphasis in form header

**Microcopy**
- Field labels: minimal vs. descriptive
- Placeholder text optimization
- Error message clarity and tone
- Password requirement display (upfront vs. on error)

**Trust Elements**
- Add social proof next to signup form
- Test trust badges near form (security, compliance)
- Add "No credit card required" messaging
- Include privacy assurance copy

---

### Trial & Commitment Experiments

**Free Trial Variations**
- Credit card required vs. not required for trial
- Test trial length impact (7 vs. 14 vs. 30 days)
- Freemium vs. free trial model
- Trial with limited features vs. full access

**Friction Points**
- Email verification required vs. delayed vs. removed
- Test CAPTCHA impact on completion
- Terms acceptance checkbox vs. implicit acceptance
- Phone verification for high-value accounts

---

### Post-Submit Experiments

- Clear next steps messaging after signup
- Instant product access vs. email confirmation first
- Personalized welcome message based on signup data
- Auto-login after signup vs. require login

---

## Questions to Ask

If you need more context:
1. What's your current signup completion rate?
2. Do you have field-level analytics on drop-off?
3. What data is absolutely required before they can use the product?
4. Are there compliance or verification requirements?
5. What happens immediately after signup?

---

## Related Skills

- **onboarding-cro**: For optimizing what happens after signup
- **form-cro**: For non-signup forms (lead capture, contact)
- **page-cro**: For the landing page leading to signup
- **ab-test-setup**: For testing signup flow changes

## When to Use
This skill is applicable to execute the workflow or actions described in the overview.

## Limitations
- Use this skill only when the task clearly matches the scope described above.
- Do not treat the output as a substitute for environment-specific validation, testing, or expert review.
- Stop and ask for clarification if required inputs, permissions, safety boundaries, or success criteria are missing.


---
name: similarity-search-patterns
description: "Implement efficient similarity search with vector databases. Use when building semantic search, implementing nearest neighbor queries, or optimizing retrieval performance."
risk: safe
source: community
date_added: "2026-02-27"
---

# Similarity Search Patterns

Patterns for implementing efficient similarity search in production systems.

## Use this skill when

- Building semantic search systems
- Implementing RAG retrieval
- Creating recommendation engines
- Optimizing search latency
- Scaling to millions of vectors
- Combining semantic and keyword search

## Do not use this skill when

- The task is unrelated to similarity search patterns
- You need a different domain or tool outside this scope

## Instructions

- Clarify goals, constraints, and required inputs.
- Apply relevant best practices and validate outcomes.
- Provide actionable steps and verification.
- If detailed examples are required, open `resources/implementation-playbook.md`.

## Resources

- `resources/implementation-playbook.md` for detailed patterns and examples.

## Limitations
- Use this skill only when the task clearly matches the scope described above.
- Do not treat the output as a substitute for environment-specific validation, testing, or expert review.
- Stop and ask for clarification if required inputs, permissions, safety boundaries, or success criteria are missing.


---
name: simplify-code
description: "Review a diff for clarity and safe simplifications, then optionally apply low-risk fixes."
risk: safe
source: "Dimillian/Skills (MIT)"
date_added: "2026-03-25"
---

# Simplify Code

Review changed code for reuse, quality, efficiency, and clarity issues. Use Codex sub-agents to review in parallel, then optionally apply only high-confidence, behavior-preserving fixes.

## When to Use
- When the user asks to simplify, clean up, refactor, or review changed code.
- When you want high-confidence, behavior-preserving improvements on a scoped diff.

## Modes

Choose the mode from the user's request:

- `review-only`: user asks to review, audit, or check the changes
- `safe-fixes`: user asks to simplify, clean up, or refactor the changes
- `fix-and-validate`: same as `safe-fixes`, but also run the smallest relevant validation after edits

If the user does not specify, default to:

- `review-only` for "review", "audit", or "check"
- `safe-fixes` for "simplify", "clean up", or "refactor"

## Step 1: Determine the Scope and Diff Command

Prefer this scope order:

1. Files or paths explicitly named by the user
2. Current git changes
3. Files edited earlier in the current Codex turn
4. Most recently modified tracked files, only if the user asked for a review but there is no diff

If there is no clear scope, stop and say so briefly.

When using git changes, determine the smallest correct diff command based on the repo state:

- unstaged work: `git diff`
- staged work: `git diff --cached`
- branch or commit comparison explicitly requested by the user: use that exact diff target
- mixed staged and unstaged work: review both

Do not assume `git diff HEAD` is the right default when a smaller diff is available.

Before reviewing standards or applying fixes, read the repo's local instruction files and relevant project docs for the touched area. Prefer the closest applicable guidance, such as:

- `AGENTS.md`
- repo workflow docs
- architecture or style docs for the touched module

Use those instructions to distinguish real issues from intentional local patterns.

## Step 2: Launch Four Review Sub-Agents in Parallel

Use Codex sub-agents when the scope is large enough for parallel review to help. For a tiny diff or one very small file, it is acceptable to review locally instead.

When spawning sub-agents:

- give each sub-agent the same scope
- tell each sub-agent to inspect only its assigned review role
- ask for concise, structured findings only
- ask each sub-agent to report file, line or symbol, problem, recommended fix, and confidence

Use four review roles.

### Sub-Agent 1: Code Reuse Review

Review the changes for reuse opportunities:

1. Search for existing helpers, utilities, or shared abstractions that already solve the same problem.
2. Flag duplicated functions or near-duplicate logic introduced in the change.
3. Flag inline logic that should call an existing helper instead of re-implementing it.

Recommended sub-agent role: `explorer` for broad codebase lookup, or `reviewer` if a stronger review pass is more useful than wide search.

### Sub-Agent 2: Code Quality Review

Review the same changes for code quality issues:

1. Redundant state, cached values, or derived values stored unnecessarily
2. Parameter sprawl caused by threading new arguments through existing call chains
3. Copy-paste with slight variation that should become a shared abstraction
4. Leaky abstractions or ownership violations across module boundaries
5. Stringly-typed values where existing typed contracts, enums, or constants already exist

Recommended sub-agent role: `reviewer`

### Sub-Agent 3: Efficiency Review

Review the same changes for efficiency issues:

1. Repeated work, duplicate reads, duplicate API calls, or unnecessary recomputation
2. Sequential work that could safely run concurrently
3. New work added to startup, render, request, or other hot paths without clear need
4. Pre-checks for existence when the operation itself can be attempted directly and errors handled
5. Memory growth, missing cleanup, or listener/subscription leaks
6. Overly broad reads or scans when the code only needs a subset

Recommended sub-agent role: `reviewer`

### Sub-Agent 4: Clarity and Standards Review

Review the same changes for clarity, local standards, and balance:

1. Violations of local project conventions or module patterns
2. Unnecessary complexity, deep nesting, weak names, or redundant comments
3. Overly compact or clever code that reduces readability
4. Over-simplification that collapses separate concerns into one unclear unit
5. Dead code, dead abstractions, or indirection without value

Recommended sub-agent role: `reviewer`

Only report issues that materially improve maintainability, correctness, or cost. Do not churn code just to make it look different.

## Step 3: Aggregate Findings

Wait for all review sub-agents to complete, then merge their findings.

Normalize findings into this shape:

1. File and line or nearest symbol
2. Category: reuse, quality, efficiency, or clarity
3. Why it is a problem
4. Recommended fix
5. Confidence: high, medium, or low

Discard weak, duplicative, or instruction-conflicting findings before editing.

## Step 4: Fix Issues Carefully

In `review-only` mode, stop after reporting findings.

In `safe-fixes` or `fix-and-validate` mode:

- Apply only high-confidence, behavior-preserving fixes
- Skip subjective refactors that need product or architectural judgment
- Preserve local patterns when they are intentional or instruction-backed
- Keep edits scoped to the reviewed files unless a small adjacent change is required to complete the fix correctly

Prefer fixes like:

- replacing duplicated code with an existing helper
- removing redundant state or dead code
- simplifying control flow without changing behavior
- narrowing overly broad operations
- renaming unclear locals when the scope is contained

Do not stage, commit, or push changes as part of this skill.

## Step 5: Validate When Required

In `fix-and-validate` mode, run the smallest relevant validation for the touched scope after edits.

Examples:

- targeted tests for the touched module
- typecheck or compile for the touched target
- formatter or lint check if that is the project's real safety gate

Prefer fast, scoped validation over full-suite runs unless the change breadth justifies more.

If validation is skipped because the user asked not to run it, say so explicitly.

## Step 6: Summarize Outcome

Close with a brief result:

- what was reviewed
- what was fixed, if anything
- what was intentionally left alone
- whether validation ran

If the code is already clean for this rubric, say that directly instead of manufacturing edits.

## Limitations
- Use this skill only when the task clearly matches the scope described above.
- Do not treat the output as a substitute for environment-specific validation, testing, or expert review.
- Stop and ask for clarification if required inputs, permissions, safety boundaries, or success criteria are missing.