QA Checklist — Run Before Declaring Work Complete
Purpose: Every agent must run through this checklist after completing implementation and before presenting work as done. This catches the 9 categories of issues that have repeatedly shipped without detection.
Updated: 2026-03-30
How to Use
After finishing your implementation:
- Switch mental mode: "I am now a senior QA engineer who finds bugs others miss"
- Run through each applicable section below
- Fix ALL issues found — critical, important, AND minor
- Never defer with "we can address this later"
1. Expected Output Compliance (if you touched customer-facing or tier-gated UI)
- Identified all affected tiers — which tier+channel combos does this change affect?
- Read the acceptance spec — checked
knowledge/acceptance/<tier>.mdfor each affected tier - Spec exists for every affected touchpoint — if missing, flagged to founder before proceeding
- Code matches spec — every "Should See" item is visible, every "Should NOT See" item is hidden
- No tier bleed — features from higher tiers don't leak into lower tiers
- Upgrade CTAs present — locked features show appropriate upgrade messaging per spec
Quick check: For each tier affected, open the admin dashboard as that tier and visually verify against the spec.
2. Database Queries
- No
select('*')or bareselect()— grep your changed files for these patterns - Every query has
.limit()or.range()— no unbounded result sets - No user input interpolated into
.or()or.rpc()strings — use parameterized methods - Loop operations are batched — no
awaitinsideforloops for DB calls - Multi-step mutations are idempotent — use upsert, not insert; include dedup keys
Quick check: grep -n "select\('\*'\)\|\.select()\|\.select(\"" <your-files>
3. Security
- No sensitive fields in client component props — admin_token, access_token, stripe_*, *_secret, *_api_key
- Mutation endpoints have CSRF check —
verifyCsrf(request)at top of POST/PATCH/PUT/DELETE - Auth matches route purpose — founder routes check FOUNDER_TOKEN, admin routes check resolveTokenOrHeaders
- No hardcoded secrets — all API keys, tokens, passwords from env vars
- Webhooks verify signatures — Stripe: constructEvent, Twilio: validateRequest
Quick check: grep -n "admin_token\|access_token\|pco_secret\|cal_api_key\|sk_live\|sk_test" <your-files>
4. Client/Server Boundary
- Server-only files have
import 'server-only'— any file importing supabase admin client -
'use client'components only import from*-shared.tsortier-config.ts— never from*-queries.ts - No
SUPABASE_SERVICE_ROLE_KEYreferenced in client code
Quick check: grep -rn "from.*queries" <your-client-components> — should return nothing
5. SEO & Metadata (if you touched any page)
- Page has
alternates: { canonical: '...' }— full absolute URL - Title uses
{ absolute: '...' }for sub-brand pages — SermonWise, ShareWiseAI -
openGraph.siteNameset for sub-brand pages — not inheriting "ChurchWiseAI" - JSON-LD uses
JSON.stringify()— never raw HTML interpolation - Sitemap entries match actual pages — no phantom URLs
Quick check: Search your page file for canonical, siteName, absolute
6. Content Accuracy (if you touched marketing copy)
- Tool counts match canonical values — 39 total (Starter=12, Pro=35, Suite=39)
- Agent count is 2 — Care + Coordinator (not 4)
- Tradition count is 17 — theological lenses
- Church count is 218K+ visible — always filter
directory_visible=true; 261K total rows - Illustration count is 53K+ — not 30K, not 320K
- Pricing matches PRICING.md — every dollar amount verified
- No false social proof — no "join churches that switched" without real customers
- "Coming Soon" products are gated — no active signup/checkout flows
Quick check: grep -rn "33 tool\|4 agent\|320K\|240K\|261K\|30,000\|Trusted by church" <your-files>
7. UI/UX (if you touched components)
- Gold text NOT used on light backgrounds — use #8B6914 or #a07d1c instead of #D4AF37
- Card pattern followed —
bg-white rounded-xl border border-stone-200 shadow-sm - Container is
max-w-7xl— not max-w-6xl - Animations respect
prefers-reduced-motion - Images have descriptive alt text
7a. UI Interaction Click-Through (MANDATORY for every UI flow) ⚠️
If code you changed touches ANY clickable element, this section is not optional. Code-path tests, type checks, and unit tests CANNOT catch interaction bugs. WebFetch cannot click buttons. Only actually clicking does.
Rule of thumb: If you haven't clicked the button, it isn't tested.
- Every new or changed button has a Playwright click test asserting the side effect (navigation, tab switch, modal open, form submit, state update). Placed in
e2e/{feature}-clickthrough.spec.ts - Every new or changed form has a submit test covering success path AND at least one validation failure path
- Every hash-anchor deep link (e.g.
#website-logo) has a test asserting both that the right tab becomes active AND the target element is in the viewport after scroll - Every route that uses
window.location.assignorwindow.location.hashhas a regression test for hash-change handling (AdminDashboard must listen forhashchange, not only read hash on mount) - Every wizard/multi-step flow has a spec that walks the entire sequence and asserts progress indicator updates, state persists, celebration fires
- Every email-link landing page has a spec that opens it (token-gated or not) and asserts expected content — no 404, no redirect to
/?auth=invalid - Mobile viewport (iPhone 12, 375×812) — all tap targets measured ≥ 44×44px via
locator.boundingBox(). No text-link buttons with no padding - Tested against production or a preview deploy — NOT localhost alone. WebFetch results do not satisfy this check
- Opened the real page in a real browser at least once before claiming the feature works
Canonical failure mode (2026-04-14 wizard bug):
window.location.assign(samepath#hash) is a soft hash change. Browsers do NOT re-mount the tree — they fire a hashchange event. If the consuming component only reads window.location.hash on initial mount, the tab never switches and nothing visible happens. Always pair programmatic hash navigation with a hashchange listener and a click-through test.
Quick check:
# In the changed repo
pnpm exec playwright test e2e/*-clickthrough.spec.ts --project=desktop --project=iphone
# Then manually open the deployed URL on your phone and tap each button
See also: feedback_click_through_tests_mandatory.md (memory), feedback_playwright_production_testing.md (memory).
8. Multi-Property Branding (if you touched shared components or sub-brand pages)
- LayoutShell excludes sub-brand paths —
/social,/sermonsare bare (no CWA header/footer) - No regex in
'use client'components — use string methods (formatters strip backslash escapes) - Sub-brand auth pages show correct branding — not ChurchWiseAI defaults
- Stripe redirect URLs detect hostname — sermonwise.ai users stay on sermonwise.ai
9. Build Verification
-
pnpm buildpasses — zero TypeScript errors, zero compilation failures - No new warnings introduced — check build output
- Commit message is clear — describes what AND why
10. Product Knowledge (if you changed features, pricing, or flows)
- Updated
product_knowledgetable — chatbot and voice agent query this at runtime - Updated PRICING.md — if any price or Stripe ID changed
- Updated CLAUDE.md — if architecture or ownership changed
11. Payment Flow Integrity
Every checkout flow must follow the payment-first principle: no database records until Stripe confirms payment.
- No pre-payment DB writes: Submitting onboard/claim forms creates NO premium_churches, churches, identities, or organization_settings records
- Webhook creates all records: After checkout.session.completed, verify: premium_churches (status=active), churches row, identities, organization_settings, church_team_members all exist
- Failed/abandoned checkout = clean DB: Abandon checkout mid-flow, verify no orphan records remain
- Welcome email post-payment only: No emails sent until webhook confirms payment
- MailerLite sync post-payment only: Subscriber added only after webhook, respecting marketing_opt_in
- Return page handles webhook delay: Return page polls for record, shows spinner, handles 30s timeout gracefully
- Webhook idempotency: Duplicate webhook events don't create duplicate records (webhook_events table prevents this)
- USD currency enforced: All checkout sessions force currency: 'usd' regardless of customer location
- Stripe metadata complete: All customer data passed via Stripe session metadata (church_name, contact_name, email, phone, tier, etc.)
- Upgrade flow preserved: Existing customers can upgrade plans via admin dashboard without breaking
Automated Enforcement (Future)
These checks will be automated via:
- Claude Code hooks — PreToolUse blocks SELECT *, PostToolUse runs type check
- Custom ESLint rules — client/server boundary, NEXT_PUBLIC_ audit
- Playwright SEO suite — canonical URLs, OG tags, sitemap validation per domain
- Gitleaks pre-commit — secret scanning before commit
- Health endpoint — runtime Supabase + Stripe connectivity check