Augur

Migration runbook

Bring an existing Augur site up to the platform standard: the order of work, dependencies, tsconfig, ESLint, next.config, vitest, pre-commit hooks and the proxy.

Migration runbook

How to bring an existing site up to the platform standard, and the gotchas on the way. The migration guide covers replacing imports; augur-doctor shows what is still left.

Order of work

Each step depends on the one before.

  1. Align package.json (by hand, then pnpm install).
  2. Extend augur-config/tsconfig/next.
  3. Switch to augur-config/eslint/next.
  4. Align next.config (serverExternalPackages, headers, optimizePackageImports).
  5. Replace imports (migration guide).
  6. Set up vitest.
  7. Run the whole quality gate: typecheck, lint, test, build.

Package dependencies

Sites MUST NOT bump augur packages with pnpm update or pnpm add -L. Both re-resolve transitive dependencies, which can change how shared ones such as React resolve; vitest then loads two Reacts and fails with "Invalid hook call", which is hard to trace.

  1. Edit the versions in package.json by hand.
  2. Run pnpm install.
  3. Check pnpm ls react shows one copy.
  4. Run pnpm augur-doctor .: each augur package should be on its own latest release.

Every augur package, augur-api included, is CalVer (YYYY.M.seq) and released on its own, so each one is upgraded on its own. Peer dependencies (React, TanStack Query, Next.js, NextAuth, Zustand) SHOULD satisfy the ranges in each package's peerDependencies.

Icons

The standard is react-icons/lu (Lucide through react-icons). Sites MUST NOT use lucide-react or @heroicons/react:

- import { ShoppingCartIcon } from "@heroicons/react/24/outline";
+ import { LuShoppingCart } from "react-icons/lu";

Tailwind 4

A site on Tailwind 3 upgrades to 4:

  1. Install tailwindcss@^4 and tailwindcss-animate@^1.0.7.
  2. Delete tailwind.config.ts: Tailwind 4 is configured in CSS and finds content itself.
  3. Replace the stylesheet's head:
/* Before (Tailwind 3) */
@tailwind base;
@tailwind components;
@tailwind utilities;

/* After (Tailwind 4) */
@import "tailwindcss";
@import "@simpleapps-com/augur-tailwind/base.css";
@plugin "tailwindcss-animate";
  1. Move theme extensions from the JS config into @theme blocks.

tsconfig

{
  "extends": "@simpleapps-com/augur-config/tsconfig/next",
  "compilerOptions": {
    "noUncheckedIndexedAccess": false,
    "declaration": false,
    "declarationMap": false,
    "sourceMap": false,
    "paths": { "@/*": ["./*"] }
  },
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", ".next/dev/types/**/*.ts"],
  "exclude": ["node_modules"]
}

The preset gives strict mode, bundler resolution, isolatedModules, JSX, DOM libs, incremental and the Next.js plugin (all presets). Turning noUncheckedIndexedAccess off is a stopgap for a site with many index reads; leave it on when you can. Map @/* to ./* or ./src/*, whichever the site already uses, consistently.

ESLint

// eslint.config.mjs
import augurNext from "@simpleapps-com/augur-config/eslint/next";

export default [
  ...augurNext,
  {
    files: ["**/*.{ts,tsx,js,jsx}"],
    rules: {
      // site-specific overrides only
    },
  },
  { ignores: ["out/**"] },
];

The preset brings @typescript-eslint, react-hooks and @next/next (recommended and Core Web Vitals). A leftover .eslintrc.json or .eslintrc.js MUST be deleted: flat config ignores it, so its rules silently stop applying.

next.config

serverExternalPackages

List native Node modules that can't be bundled: ioredis, isomorphic-dompurify, jsdom, dompurify. It MUST NOT list @simpleapps-com/augur-server, which is plain JavaScript that Next.js has to bundle; externalized, it fails at runtime with "module not found".

serverExternalPackages: ["ioredis", "isomorphic-dompurify", "jsdom", "dompurify"],

optimizePackageImports

experimental: {
  optimizePackageImports: [
    "@simpleapps-com/augur-core",
    "@simpleapps-com/augur-hooks",
    "@simpleapps-com/augur-server",
    "@simpleapps-com/augur-utils",
    "react-icons",
    "zustand",
  ],
},

Security headers

headers: async () => [
  {
    source: "/:path*",
    headers: [
      { key: "X-DNS-Prefetch-Control", value: "on" },
      { key: "Strict-Transport-Security", value: "max-age=31536000; includeSubDomains" },
      { key: "X-Frame-Options", value: "SAMEORIGIN" },
      { key: "X-Content-Type-Options", value: "nosniff" },
      { key: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
      { key: "Permissions-Policy", value: "camera=(), microphone=(), geolocation=()" },
    ],
  },
  {
    source: "/_next/static/:path*",
    headers: [{ key: "Cache-Control", value: "public, max-age=31536000, immutable" }],
  },
],

Also set reactStrictMode: true, and list agr.media in images.remotePatterns for product images.

Vitest

// vitest.config.ts
import { createRequire } from "node:module";
import path from "node:path";
import { defineConfig } from "vitest/config";

const require = createRequire(import.meta.url);

export default defineConfig({
  resolve: {
    alias: {
      "@": path.resolve(__dirname),
      // one copy of augur-api; /testing MUST come before the main entry
      "@simpleapps-com/augur-api/testing": require.resolve("@simpleapps-com/augur-api/testing"),
      "@simpleapps-com/augur-api": require.resolve("@simpleapps-com/augur-api"),
    },
    dedupe: ["react", "react-dom"],
  },
  test: {
    environment: "node",
    include: ["__tests__/**/*.test.ts"],
    setupFiles: ["__tests__/setup.ts"],
    server: { deps: { inline: [/@simpleapps-com/] } },
    env: {
      AUGUR_SITE_ID: "example-site",
      AUGUR_JWT: "test-jwt-token",
      AUTH_SECRET: "test-auth-secret",
      DEFAULT_CUSTOMER_ID: "99999",
      DEFAULT_CONTACT_ID: "0",
    },
  },
});
Setting Why
server.deps.inline: [/@simpleapps-com/] The augur packages are ESM; vitest needs to inline them to resolve their exports
The augur-api aliases Force one copy, or type guards fail across two; aliases match by prefix, so /testing goes first
dedupe: ["react", "react-dom"] One React, or hooks fail with "Invalid hook call"
environment: "node" Server action tests don't need a DOM; use jsdom only for component tests
env Server code reads these at import time

Global mocks

// __tests__/setup.ts
import { vi } from "vitest";

vi.mock("next/headers", () => ({
  cookies: () => ({ get: vi.fn(), set: vi.fn(), delete: vi.fn() }),
  headers: () => new Headers(),
}));

vi.mock("ioredis", () => {
  const RedisMock = vi.fn(() => ({
    get: vi.fn().mockResolvedValue(null),
    set: vi.fn().mockResolvedValue("OK"),
    del: vi.fn().mockResolvedValue(1),
    on: vi.fn(),
    status: "ready",
  }));
  return { default: RedisMock, Redis: RedisMock };
});

SDK contract tests (__tests__/sdk-contracts/, which augur-doctor looks for) call the real SDK through createMockClient; if a global mock covers augur-api, get the real one with vi.importActual:

const { createMockClient } = await vi.importActual<typeof import("@simpleapps-com/augur-api/testing")>(
  "@simpleapps-com/augur-api/testing",
);

For component tests, mock @simpleapps-com/augur-hooks (what components call), not augur-api: mocking the SDK still runs the hooks, which then need React Query and the providers.

Pre-commit hooks

The standard is lefthook with lint-staged. It isn't required; augur-doctor suggests it when missing.

pnpm add -D lefthook lint-staged
pnpm exec lefthook install
# lefthook.yml
pre-commit:
  parallel: true
  jobs:
    - name: typecheck
      run: pnpm run typecheck
    - name: lint-staged
      run: pnpm exec lint-staged
    - name: lockfile-sync
      glob: "**/package.json"
      run: pnpm install --frozen-lockfile
{
  "lint-staged": {
    "*.{ts,tsx,js,jsx}": ["eslint --fix", "prettier --write"],
    "*.{json,md,css,scss}": ["prettier --write"]
  }
}

(or the same in .lintstagedrc.json). Lefthook is one binary, runs jobs in parallel and filters by glob; a site already on husky can keep it.

Proxy migration

Replace local React Query hooks (in lib/augur.ts: useItemPrice, useInvMastDoc, …) with createServerQueryProxy. The site needs createServerSite() in lib/augur-site.ts and an augur-api release with typed responses (2026.3.7 or later, which augur-doctor checks).

  1. Create lib/augur-query.ts (recipe).
  2. Replace one hook at a time, checking the page each time: useItemPrice(itemId, customerId, 1) becomes useQuery({ ...q.pricing.priceEngine.list({ itemId, customerId, quantity: 1 }), enabled: Boolean(itemId) }).
  3. Change the server prefetch for that query to createAugurOptions in the same commit: the proxy's keys follow the SDK path, so an old prefetch key no longer matches and the page fetches twice.
- import { getItemPriceOptions } from "@simpleapps-com/augur-hooks/server";
- await queryClient.prefetchQuery(getItemPriceOptions(api, itemId, customerId));
+ import { createAugurOptions } from "@simpleapps-com/augur-hooks/server";
+ await queryClient.prefetchQuery(createAugurOptions(site.client).pricing.priceEngine.list({ itemId, customerId }));
  1. When every hook is replaced, delete the local hook file, the old option factories and their tests.
Old key Proxy key
["itemPrice", itemId, customerId, qty] ["pricing", "priceEngine", "list", { customerId, itemId, quantity }]
["invMastDoc", uid, itemId] ["items", "invMast", "doc", "get", uid, { itemId }]
["itemCategory", uid] ["items", "itemCategory", "get", uid]
["categoryItems", uid, filters] ["items", "categories", "items", "list", uid, filters]
["searchSuggestions", query] ["openSearch", "suggestions", "suggest", "list", { q: query }]

A proxy key is the SDK path followed by the arguments, each object's keys sorted; autocomplete on q shows the path. enabled, initialData, select and debouncing stay at the call site, after the spread.

Old cache entries are simply abandoned: the first visit after the switch fetches again. To avoid even that, ship the server prefetch change first and the client change second.

Debugging

Symptom Cause Fix
"Invalid hook call" in vitest Two Reacts resolve.dedupe: ["react", "react-dom"]; don't use pnpm update
"Module not found: augur-server" at runtime augur-server in serverExternalPackages Remove it
augur-web classes missing Tailwind didn't scan the package @source "../node_modules/@simpleapps-com/augur-web/dist";
ESLint rules not firing A leftover .eslintrc* Delete it; use eslint.config.mjs
"Cannot find module augur-api/testing" in vitest Alias order Put the /testing alias first
Hook data undefined in component tests The SDK is mocked instead of the hooks Mock augur-hooks
Type errors after the tsconfig change The site relied on looser options Fix them, or override the option in compilerOptions
pnpm install changed unrelated dependencies pnpm update was used Revert the lockfile, edit package.json, run pnpm install
SSR data doesn't hydrate Server and client keys differ Build both through a proxy
Proxy autocomplete shows unknown An augur-api release without typed responses Upgrade augur-api
"No API instance provided" from a queryFn No api and no registered query action Import @simpleapps-com/augur-server/hooks (or pass api)