Recova Project Skill
Stack
- Next.js 16.2.4 App Router, React 19, TypeScript 5, Tailwind CSS v4
- Supabase (SSR client via
@supabase/ssr, admin client via lib/supabase/admin.ts)
- Stripe (platform account + Connect), Resend, Anthropic Claude
- Vercel hosting, Sentry error tracking
Critical conventions
No em dashes
Never use em dashes anywhere, not in code, copy, or comments. Use a regular hyphen or rewrite the sentence. Recovery emails have a hard scrubber for AI-generated dashes; the rest of the codebase follows the same rule by convention.
Field names (blog/admin)
- Blog post body field is named
body not content
- Blog post published flag is
published (boolean) not is_published
- Blog post author is a free-text
author string field not a FK
Auth patterns
- Admin routes (server components / pages):
requireAdminAudited() from lib/supabase/require-admin-audited.ts
- Admin+operator API routes:
requireAdminOrOperator() from lib/supabase/require-admin-or-operator.ts
- Admin-email check:
isAdminEmail(email) from lib/account.ts (reads comma-separated ADMIN_EMAIL)
- Never roll a custom auth check, use the helpers
Vault paths
- Files in the Recova Vault are at repo-relative paths
- e.g.
read_file("app/admin/blog/BlogPostForm.tsx") reads from the repo root
- Session state files (TASK.md, backlog.md etc) are stored in the Vault KV
CANONICAL_TAGS for blog
payment-recovery, decline-codes, disputes, subscription-intelligence, comparisons
Defined twice: as an array of {slug, label} in app/(marketing)/blog/archive/page.tsx, and as a Set<string> in components/admin/blog/TagPills.tsx. Free-form tags remain valid, but only these slugs roll up into the indexed cluster pages.
Database / Supabase
- Admin client:
import { createAdminClient } from '@/lib/supabase/admin' then createAdminClient() (zero args)
- Never inline:
createClient(URL, SERVICE_ROLE_KEY, { auth: { persistSession: false } }). The helper bakes the option preset.
- Helper exclusions:
lib/intelligence/customer-context-ssr.ts has its own adminSupabase() (predates the migration), lib/supabase/require-admin-audited.ts uses both createServerClient and createClient for an audit-logging flow, leave both alone
- Supabase MCP is NOT configured for Recova (deliberate security decision per docs/CLAUDE.md, production holds encrypted Stripe tokens + PII)
- Service-role key bypasses RLS, server-only paths only
Design tokens (Tailwind v4 via @theme inline in app/globals.css)
Text (four-stop scale, all defined as --text / --text-secondary etc):
text-text (primary, 15.2:1 AAA on bg) — headers, stat values, currently-selected nav
text-text-secondary (subheadings, column headers)
text-text-tertiary (labels, captions, metadata, most common non-primary tier)
text-text-subtle (footer, empty states, decorative)
text-text-primary does NOT exist, use text-text
text-muted is deprecated, migrate to text-text-tertiary or text-text-subtle
Surfaces:
bg-bg (page background)
bg-surface (cards, panels)
bg-surface2 (hover rows, secondary surface)
- Elevation ladder:
--bg -> --surface -> --surface2, never skip
Borders:
border-border (card outlines)
border-border-sub (table row dividers)
border-border-i (input outlines, stronger than --border)
border-amber-border-interactive (ghost button border, WCAG 1.4.11 compliant)
border-border-secondary does NOT exist
Amber (brand):
bg-amber / text-amber (CTAs, logo, primary metrics — one per card max)
text-amber-text (inline amber text on dark/light surfaces, AA-safe variant)
bg-amber-bg (alert fills, decorative)
text-on-amber (always-dark overlay for text on amber buttons)
bg-amber-dark (button hover state)
Status (text variants are contrast-tuned, use them for inline text on tinted backgrounds):
- Inline text:
text-success-text, text-error-text, text-warning-text, text-cobalt-text
- Raw chart/icon colors:
text-green, text-red, text-orange, text-blue
text-success, text-error, text-warning do NOT exist
- Backgrounds:
bg-green-bg, bg-red-bg, bg-orange-bg, bg-blue-bg, bg-amber-bg, bg-cobalt-bg
Special:
text-purple / bg-purple-bg: Stripe Connect UI and win-back attribution only, no other use
text-on-amber: always-dark overlay for amber buttons
Semantic font scale (added 2026-06-05, partial migration):
text-display (1.75rem, hero stat values)
text-title (1.25rem, page titles, PageHeader)
text-body (1rem)
text-label (0.8125rem)
text-micro (0.6875rem)
Hard rules:
- Never hardcode hex in TSX (no
bg-[#222420], no style={{color: '#xxx'}})
- Never use
style={{ color, background, border }} for finite-enum values, map to className
- Inline style is acceptable only for runtime-dynamic numeric values (computed widths, chart dims)
- Amber on active state: only the 2px
border-b-2 underline on primary nav links, never as text or background fill
Cron pattern (every cron must follow this exactly)
- Verify
Authorization: Bearer ${process.env.CRON_SECRET}, return 401 if wrong
acquireLock(supabase, 'cron-name', CRON_LOCK_TTL_SECONDS) from lib/cron-lock.ts, return 200 with {skipped: 'already running'} if locked
- Main logic in try/catch
recordCronSuccess(...) on success, notifyCronFailure(...) on error (both in lib/cron-monitor.ts)
releaseLock(...) in finally block
- Return JSON with counts: processed, skipped, errors
Demo accounts are excluded by DEMO_STRIPE_PREFIX filter in lib/constants.ts, never write processing logic that touches demo accounts.
tsc before every push
Run npx tsc --noEmit before every git push. Fix all type errors. typescript.ignoreBuildErrors: true in next.config.ts means type errors do not block builds, but we still fix them.
Button styles
- Primary CTA:
btn-primary class (defined in app/globals.css)
- Ghost/secondary:
btn-ghost class
- Subtle secondary:
btn-ghost-subtle class
- Danger:
text-red border-red treatment
- Stripe Connect only: purple (
bg-purple etc)
Workflow gotchas
git add after a git rm silently skips the rest of the add-list, never include a git rm'd path in the subsequent git add
- No AI attribution in commits or user-facing surfaces (no
Co-Authored-By: Claude, no "AI" markers in PRs)
- Never name Claude or Anthropic in user-facing copy, always say "AI"
- Vercel CLI is not installed on this machine, use Vercel MCP for deploy verification (
list_deployments, get_runtime_logs)
- Sentry MCP is read-only (search_issues, get_sentry_resource), resolution stays manual in Sentry UI
- Recova Admin MCP
get_cron_log requires an account_id parameter even for platform-level queries, pass DEMO_ACCOUNT_ID_1 (00000000-0000-0000-0000-000000000001) to satisfy the wrapper
Key files to read at session start
docs/CLAUDE.md always first
docs/context.md current product status
vault/priorities.md (via Recova Vault) what is blocked and in progress
docs/decisions.md what has already been decided
docs/AGENTS.md confirmed findings that prevent repeated mistakes
docs/reference/design.md for any UI work
docs/reference/database.md for any schema work
lib/constants.ts for fee percentages, timing windows, model names
lib/products.ts for product definitions and pricing
1---2name: recova3description: Project-specific conventions for the Recova codebase. Encodes design tokens, auth helpers, cron pattern, and field-name gotchas so sessions start with correct context. Triggers on session start when working in the recova repo, when writing TSX with design tokens, when writing crons, or when touching the admin/blog area.4---56# Recova Project Skill78## Stack9- Next.js 16.2.4 App Router, React 19, TypeScript 5, Tailwind CSS v410- Supabase (SSR client via `@supabase/ssr`, admin client via `lib/supabase/admin.ts`)11- Stripe (platform account + Connect), Resend, Anthropic Claude12- Vercel hosting, Sentry error tracking1314## Critical conventions1516### No em dashes17Never use em dashes anywhere, not in code, copy, or comments. Use a regular hyphen or rewrite the sentence. Recovery emails have a hard scrubber for AI-generated dashes; the rest of the codebase follows the same rule by convention.1819### Field names (blog/admin)20- Blog post body field is named `body` not `content`21- Blog post published flag is `published` (boolean) not `is_published`22- Blog post author is a free-text `author` string field not a FK2324### Auth patterns25- Admin routes (server components / pages): `requireAdminAudited()` from `lib/supabase/require-admin-audited.ts`26- Admin+operator API routes: `requireAdminOrOperator()` from `lib/supabase/require-admin-or-operator.ts`27- Admin-email check: `isAdminEmail(email)` from `lib/account.ts` (reads comma-separated `ADMIN_EMAIL`)28- Never roll a custom auth check, use the helpers2930### Vault paths31- Files in the Recova Vault are at repo-relative paths32- e.g. `read_file("app/admin/blog/BlogPostForm.tsx")` reads from the repo root33- Session state files (TASK.md, backlog.md etc) are stored in the Vault KV3435### CANONICAL_TAGS for blog36`payment-recovery`, `decline-codes`, `disputes`, `subscription-intelligence`, `comparisons`3738Defined twice: as an array of `{slug, label}` in `app/(marketing)/blog/archive/page.tsx`, and as a `Set<string>` in `components/admin/blog/TagPills.tsx`. Free-form tags remain valid, but only these slugs roll up into the indexed cluster pages.3940### Database / Supabase41- Admin client: `import { createAdminClient } from '@/lib/supabase/admin'` then `createAdminClient()` (zero args)42- Never inline: `createClient(URL, SERVICE_ROLE_KEY, { auth: { persistSession: false } })`. The helper bakes the option preset.43- Helper exclusions: `lib/intelligence/customer-context-ssr.ts` has its own `adminSupabase()` (predates the migration), `lib/supabase/require-admin-audited.ts` uses both `createServerClient` and `createClient` for an audit-logging flow, leave both alone44- Supabase MCP is NOT configured for Recova (deliberate security decision per docs/CLAUDE.md, production holds encrypted Stripe tokens + PII)45- Service-role key bypasses RLS, server-only paths only4647### Design tokens (Tailwind v4 via `@theme inline` in `app/globals.css`)4849**Text** (four-stop scale, all defined as `--text` / `--text-secondary` etc):50- `text-text` (primary, 15.2:1 AAA on bg) — headers, stat values, currently-selected nav51- `text-text-secondary` (subheadings, column headers)52- `text-text-tertiary` (labels, captions, metadata, most common non-primary tier)53- `text-text-subtle` (footer, empty states, decorative)54- `text-text-primary` does NOT exist, use `text-text`55- `text-muted` is deprecated, migrate to `text-text-tertiary` or `text-text-subtle`5657**Surfaces:**58- `bg-bg` (page background)59- `bg-surface` (cards, panels)60- `bg-surface2` (hover rows, secondary surface)61- Elevation ladder: `--bg` -> `--surface` -> `--surface2`, never skip6263**Borders:**64- `border-border` (card outlines)65- `border-border-sub` (table row dividers)66- `border-border-i` (input outlines, stronger than `--border`)67- `border-amber-border-interactive` (ghost button border, WCAG 1.4.11 compliant)68- `border-border-secondary` does NOT exist6970**Amber (brand):**71- `bg-amber` / `text-amber` (CTAs, logo, primary metrics — one per card max)72- `text-amber-text` (inline amber text on dark/light surfaces, AA-safe variant)73- `bg-amber-bg` (alert fills, decorative)74- `text-on-amber` (always-dark overlay for text on amber buttons)75- `bg-amber-dark` (button hover state)7677**Status (text variants are contrast-tuned, use them for inline text on tinted backgrounds):**78- Inline text: `text-success-text`, `text-error-text`, `text-warning-text`, `text-cobalt-text`79- Raw chart/icon colors: `text-green`, `text-red`, `text-orange`, `text-blue`80- `text-success`, `text-error`, `text-warning` do NOT exist81- Backgrounds: `bg-green-bg`, `bg-red-bg`, `bg-orange-bg`, `bg-blue-bg`, `bg-amber-bg`, `bg-cobalt-bg`8283**Special:**84- `text-purple` / `bg-purple-bg`: Stripe Connect UI and win-back attribution only, no other use85- `text-on-amber`: always-dark overlay for amber buttons8687**Semantic font scale** (added 2026-06-05, partial migration):88- `text-display` (1.75rem, hero stat values)89- `text-title` (1.25rem, page titles, PageHeader)90- `text-body` (1rem)91- `text-label` (0.8125rem)92- `text-micro` (0.6875rem)9394**Hard rules:**95- Never hardcode hex in TSX (no `bg-[#222420]`, no `style={{color: '#xxx'}}`)96- Never use `style={{ color, background, border }}` for finite-enum values, map to className97- Inline style is acceptable only for runtime-dynamic numeric values (computed widths, chart dims)98- Amber on active state: only the 2px `border-b-2` underline on primary nav links, never as text or background fill99100### Cron pattern (every cron must follow this exactly)1011. Verify `Authorization: Bearer ${process.env.CRON_SECRET}`, return 401 if wrong1022. `acquireLock(supabase, 'cron-name', CRON_LOCK_TTL_SECONDS)` from `lib/cron-lock.ts`, return 200 with `{skipped: 'already running'}` if locked1033. Main logic in try/catch1044. `recordCronSuccess(...)` on success, `notifyCronFailure(...)` on error (both in `lib/cron-monitor.ts`)1055. `releaseLock(...)` in finally block1066. Return JSON with counts: processed, skipped, errors107108Demo accounts are excluded by `DEMO_STRIPE_PREFIX` filter in `lib/constants.ts`, never write processing logic that touches demo accounts.109110### tsc before every push111Run `npx tsc --noEmit` before every git push. Fix all type errors. `typescript.ignoreBuildErrors: true` in `next.config.ts` means type errors do not block builds, but we still fix them.112113### Button styles114- Primary CTA: `btn-primary` class (defined in `app/globals.css`)115- Ghost/secondary: `btn-ghost` class116- Subtle secondary: `btn-ghost-subtle` class117- Danger: `text-red border-red` treatment118- Stripe Connect only: purple (`bg-purple` etc)119120### Workflow gotchas121- `git add` after a `git rm` silently skips the rest of the add-list, never include a `git rm`'d path in the subsequent `git add`122- No AI attribution in commits or user-facing surfaces (no `Co-Authored-By: Claude`, no "AI" markers in PRs)123- Never name Claude or Anthropic in user-facing copy, always say "AI"124- Vercel CLI is not installed on this machine, use Vercel MCP for deploy verification (`list_deployments`, `get_runtime_logs`)125- Sentry MCP is read-only (search_issues, get_sentry_resource), resolution stays manual in Sentry UI126- Recova Admin MCP `get_cron_log` requires an `account_id` parameter even for platform-level queries, pass `DEMO_ACCOUNT_ID_1` (`00000000-0000-0000-0000-000000000001`) to satisfy the wrapper127128## Key files to read at session start129- `docs/CLAUDE.md` always first130- `docs/context.md` current product status131- `vault/priorities.md` (via Recova Vault) what is blocked and in progress132- `docs/decisions.md` what has already been decided133- `docs/AGENTS.md` confirmed findings that prevent repeated mistakes134- `docs/reference/design.md` for any UI work135- `docs/reference/database.md` for any schema work136- `lib/constants.ts` for fee percentages, timing windows, model names137- `lib/products.ts` for product definitions and pricing