Skip to content

TECH.md — ID-21: TanStack workspace-types migration (Path c)

TECH.md — ID-21: TanStack workspace-types migration (Path c)

Section titled “TECH.md — ID-21: TanStack workspace-types migration (Path c)”

Status: ratified-ready · awaiting Subtask PLAN.md breakdown Audience: Knowledge Hub engineering (Liam + future Executor agent) Scope: Refactor only — no behavioural change. No PRODUCT.md required (Liam ratification S251).

ReferenceLocator
Backlog item ID-21 (full notes + Path c)docs/reference/product-backlog.json (search "id": "ID-21")
S248 T4 procurement umbrella rename closeout99dfc5fd 51b6cc8c f9261904 (the trio that landed the rename + workspace-types comment)
S249 GitNexus impact investigation95b660ef (refactor(s249-id-23) — same commit notes the 132-symbol HIGH-risk finding)
T2 application_types migrationsupabase/migrations/20260520120828_t2_combined_pr_intel_shape_b_form_type_split.sql §1.1-1.2
Generated DB types (source of truth)supabase/types/database.types.ts (application_types row at L17)
Static registry being retiredlib/workspace-types.ts
Sync constraint site (preserved)lib/validation/schemas.ts:535
Reference TanStack patternhooks/intelligence/use-company-profiles.ts + app/api/intelligence/profiles/route.ts
CLAUDE.md gotchas referencedanon-EXECUTE pattern (S250 W1b), SET search_path = public, extensions, sandbox CLI rule
  • Author hooks/workspaces/use-application-types.ts exporting a useApplicationTypes() TanStack Query hook against the application_types table.
  • Author a server-side fetcher route or extend an existing one so the hook has a typed JSON contract to consume (sibling-only — does not depend on schema mutability work).
  • Migrate the 3 UI consumer files to the hook:
    • app/workspaces/workspaces-content.tsx
    • components/workspace/workspace-card.tsx
    • components/workspace/workspace-create-dialog.tsx
  • Delete the static registry surface in lib/workspace-types.ts: WORKSPACE_TYPE_REGISTRY, registerType(), the 3 inline registerType({…}) call blocks, getWorkspaceType(), getAllWorkspaceTypes(), getLauncherTypes(), formatTypeCount(), and the WorkspaceTypeConfig interface (moved into the hook module per §4).
  • Resolve the metadata-location open question per §4 (recommendation: Option C).
  • Align __tests__/lib/workspace-types.test.ts to the new shape and add __tests__/hooks/use-application-types.test.ts.
  • APPLICATION_TYPE_KEYS sync tuple (lib/workspace-types.ts:166-173) — kept exactly as-is. This is the sync constraint and is the whole point of Path c.
  • getValidTypeValues() — kept exactly as-is. Sync-callable, returns the hardcoded tuple.
  • lib/validation/schemas.ts:535 — kept exactly as-is. Continues constructing z.enum(getValidTypeValues()) at module load.
  • Admin UI for mutating application_types rows — deferred per Q-OQR1-13 to v1.1. This refactor reads from the table; it does not gate it behind an admin editor.
  • Adding new application types to the seed — none of the 6 existing seed rows change. Future type additions are a separate change (sync tuple + application_types INSERT in lockstep, same constraint as today).
  • Dead-code at app/api/workspaces/route.ts:107 legacy 'bid' → 'procurement' ternary — already removed S248 T4 (Liam ratification S251 verified).

lib/workspace-types.ts predates the T2 application_types table (S246 migration). It hardcodes UI metadata (label, icon, route, default colour, features) for 3 types in a client-side WORKSPACE_TYPE_REGISTRY populated via inline registerType({…}) calls at module-init time.

T2 introduced the canonical 6-row application_types table. T4 (S248) and ID-23 (S249) completed the bid → procurement rename and updated the static registry comment to note that a full TanStack Query migration of UI helpers is a backlog item rather than a T4 blocker.

A naive “migrate everything to TanStack” framing failed S249 impact analysis: gitnexus_impact on getValidTypeValues returned HIGH risk + 132 impacted symbols, because the value of getValidTypeValues() is consumed synchronously at module-load time by Zod schema construction (lib/validation/schemas.ts:535). TanStack useQuery is async; you cannot build a Zod enum from useQuery() data.

Path c — decouple sync from metadata — preserves the sync seam.

Two distinct surfaces, two distinct contracts:

SurfaceContractMechanismPath c posture
Identity (the closed set of valid keys)Sync, module-load-timeAPPLICATION_TYPE_KEYS hardcoded tupleKept as-is
Metadata (label, icon, route, colour)Async, render-time, may need datauseApplicationTypes() TanStack Query hook (new)Migrated

The constraint that previously coupled them — both surfaces hanging off a single static WORKSPACE_TYPE_REGISTRY — is what made the naive framing non-viable. Path c keeps the tuple and migrates only the registry side.

Each P-N maps to a verifiable acceptance criterion in §5 testing. Numbered to match the dispatch brief.

P-1 — useApplicationTypes() hook signature + cache key + fetcher contract

Section titled “P-1 — useApplicationTypes() hook signature + cache key + fetcher contract”

New file: hooks/workspaces/use-application-types.ts

import { useQuery } from '@tanstack/react-query';
import { queryKeys } from '@/lib/query/query-keys';
import { fetchJson } from '@/lib/query/fetchers';
import type { LucideIcon } from 'lucide-react';
/** Shape returned by GET /api/application-types. Mirrors WorkspaceTypeConfig
* pre-refactor with one delta: `icon` arrives as a string name (DB column)
* and is resolved to a LucideIcon at hook layer per §4 Option C. */
export interface ApplicationTypeRow {
readonly key: string; // = workspaces.application_types.key
readonly label: string;
readonly labelPlural: string;
readonly description: string;
readonly defaultIcon: string; // DB column application_types.default_icon
readonly defaultColour: string; // DB column application_types.default_colour
}
export interface WorkspaceTypeConfig extends ApplicationTypeRow {
/** Resolved LucideIcon (looked up from defaultIcon via a static
* icon-name → LucideIcon map maintained in the hook module). */
readonly icon: LucideIcon;
/** Routing target — static client config per §4 Option C. */
readonly route: string | null;
/** Whether the type is renderable today — static client config per §4. */
readonly available: boolean;
/** Whether the type has a custom creation flow — static client config. */
readonly hasCustomCreation: boolean;
/** Feature flags — static client config per §4. */
readonly features: {
readonly hasStatus: boolean;
readonly hasContentAssignment: boolean;
readonly hasDomainMetadata: boolean;
};
}
export function useApplicationTypes() {
return useQuery({
queryKey: queryKeys.applicationTypes.list,
queryFn: () => fetchJson<ApplicationTypeRow[]>('/api/application-types'),
select: (rows) => rows.map(toWorkspaceTypeConfig), // join static config + Lucide icon
staleTime: 5 * 60_000, // 5 min — closed-list reference data
});
}

New API route: app/api/application-types/route.tsGET only. Authenticated (any role). Reads application_types via getAuthenticatedClient() with the order-by label. Returns ApplicationTypeRow[] JSON.

Cache key: new applicationTypes namespace in lib/query/query-keys.ts:

applicationTypes: {
all: ['application-types'] as const,
list: ['application-types', 'list'] as const,
},

P-2 — Hook return shape (strongly typed)

Section titled “P-2 — Hook return shape (strongly typed)”

The hook returns UseQueryResult<WorkspaceTypeConfig[]>. Three sync helpers replace the three deleted static functions; each is a pure-function selector over the array (no new state, no new lookup primitives):

Deleted static functionReplacement (in use-application-types.ts)
getWorkspaceType(t)useWorkspaceType(type: string) — wraps useApplicationTypes(), .find(c => c.key === type)
getLauncherTypes()useLauncherTypes() — wraps useApplicationTypes(), filters `route !== null
formatTypeCount(t, n)formatTypeCount(config: WorkspaceTypeConfig | undefined, n: number) — sync utility, no hook
getAllWorkspaceTypes()useApplicationTypes() itself — data is already the full list

getWorkspaceType and formatTypeCount are called from non-hook contexts in the current code (the dialog useEffect, the card render path) — the hook-based replacement keeps these in render contexts only; the formatTypeCount utility accepts a WorkspaceTypeConfig | undefined so the caller’s useQuery result feeds it directly.

The 3 consumer files migrate as follows:

Today (L4, L11, L28):

import { getLauncherTypes, formatTypeCount } from '@/lib/workspace-types';
// …
const launcherTypes = getLauncherTypes();
// …
const countText = formatTypeCount(wt.type, count);

Target:

import { useLauncherTypes, formatTypeCount } from '@/hooks/workspaces/use-application-types';
// …
const { data: launcherTypes = [], isLoading } = useLauncherTypes();
// In the render path, accept the loading state (render the existing skeleton/empty grid).
// …
const countText = formatTypeCount(wt, count); // pass the config directly

Loading-state handling: the current page is a server component (app/workspaces/page.tsx) that fetches counts on the server and hands them to the 'use client' WorkspacesContent. The hook adds a render-time async fetch (max 1 round-trip on first paint, served from cache afterwards). On first paint, render an empty <div> (same layout container) until the hook resolves. Acceptance criterion gates this in §5 (AC-3a).

3b. components/workspace/workspace-card.tsx

Section titled “3b. components/workspace/workspace-card.tsx”

Today (L18, L46):

import { getWorkspaceType } from '@/lib/workspace-types';
// …
const typeConfig = workspace.type ? getWorkspaceType(workspace.type) : null;

Target:

import { useWorkspaceType } from '@/hooks/workspaces/use-application-types';
// …
const { data: typeConfig = null } = useWorkspaceType(workspace.type ?? '');

useWorkspaceType returns WorkspaceTypeConfig | undefined; the existing null-coalesce + typeConfig && JSX guard handles the loading frame the same way it handles the “joined row absent” case today.

3c. components/workspace/workspace-create-dialog.tsx

Section titled “3c. components/workspace/workspace-create-dialog.tsx”

Today (L19, L50, L54):

import { getWorkspaceType } from '@/lib/workspace-types';
// …
const typeConfig = getWorkspaceType(type);
// …
if (open && typeConfig?.hasCustomCreation) { … }

Target:

import { useWorkspaceType } from '@/hooks/workspaces/use-application-types';
// …
const { data: typeConfig } = useWorkspaceType(type);
// …
// useEffect deps unchanged — typeConfig identity will change when data
// resolves, which already triggers the effect.
if (open && typeConfig?.hasCustomCreation) { … }

Hook caveat: the dialog opens via a controlled open prop. On first open it may render once with typeConfig === undefined before the hook resolves. The dialog’s existing dialog-shell renders fine in this frame (the typeConfig?.label ?? 'Workspace' fallback at L126 covers it).

After the 3 consumer migrations land, delete the following from lib/workspace-types.ts:

SymbolLines (current)Deletion gate
WorkspaceTypeConfig interface18-61Moved into use-application-types.ts
WORKSPACE_TYPE_REGISTRY const64Zero remaining importers
registerType()67-73Zero remaining callers (all 3 inline)
Inline registerType({…}) ×381-136Zero remaining callers
getWorkspaceType()141-145Replaced by useWorkspaceType() hook
getAllWorkspaceTypes()148-150Subsumed by useApplicationTypes().data
getLauncherTypes()153-157Replaced by useLauncherTypes() hook
formatTypeCount()186-192Re-exported from use-application-types.ts
logger import3Deleted with registerType()
Lucide icon imports + LucideIcon1-2Moved into icon-name map in hook module

Retained verbatim:

  • APPLICATION_TYPE_KEYS tuple (L166-173)
  • getValidTypeValues() (L181-183)
  • Top-of-file comment block (L1-11) — updated to reflect Path c completion rather than “follow-up backlog item”

Post-deletion, lib/workspace-types.ts is ~25 lines of sync constraint plumbing — no UI metadata. The file’s purpose collapses to “the sync source of truth for application_types keys, paired with the DB seed.”

Zero-callers verification gate: before deleting any symbol, the Executor must run an ast-dataflow rename-sweep equivalent or a grep -rn across app/, components/, lib/, hooks/, contexts/, __tests__/ confirming zero remaining call sites for each deleted symbol. This is the same posture as S248 T4 and S249 ID-23.

Modified: __tests__/lib/workspace-types.test.ts

Today the file tests the 5 deleted public functions. Post-refactor it should test only what lib/workspace-types.ts retains: APPLICATION_TYPE_KEYS shape + getValidTypeValues() tuple invariants. Delete the describe('getWorkspaceType'), describe('getAllWorkspaceTypes'), describe('getLauncherTypes'), describe('formatTypeCount') blocks; keep the describe('getValidTypeValues') block.

New: __tests__/hooks/use-application-types.test.ts

Mirror the existing TanStack hook test patterns in __tests__/hooks/ (e.g. how use-company-profiles is exercised — look at __tests__/hooks/intelligence/ or __tests__/api/intelligence/ for the closest existing pattern). Coverage targets:

  1. Hook returns 6 rows when application_types table is mocked with 6 rows.
  2. useWorkspaceType('procurement') resolves to a config with label === 'Procurement', route === '/procurement', available === true, hasCustomCreation === true, icon === Briefcase (resolved from string).
  3. useWorkspaceType('unknown_key') resolves to undefined (preserves the current getWorkspaceType() contract).
  4. useLauncherTypes() filters out types where route === null && available === true (preserves current getLauncherTypes() semantics).
  5. formatTypeCount(undefined, 0) falls back to '0 active workspaces' (preserves current undefined-config branch in formatTypeCount()).
  6. formatTypeCount(intelligenceConfig, 3) returns '3 active intelligence streams' (preserves singular/plural logic).

Mock strategy: createMockSupabaseClient() from __tests__/helpers/mock-supabase.ts, returning the 6 seed rows verbatim with the Option C metadata grafted on (matches what the select: (rows) => rows.map(toWorkspaceTypeConfig) selector produces).

§4 Metadata location — Option C (hybrid) recommendation

Section titled “§4 Metadata location — Option C (hybrid) recommendation”

The 8 metadata fields decompose into two groups by editability requirement:

FieldEditable by client admin?Lives where (Option C)
keyNo (immutable identity)DB: application_types.key (today)
labelYes (rebranding)DB: application_types.label (today)
defaultIconYes (admin theming)DB: application_types.default_icon (column exists; NULL)
defaultColourYes (admin theming)DB: application_types.default_colour (column exists; NULL)
labelPluralYes (rebranding)DB: NEW column application_types.label_plural (per §6)
descriptionYes (admin copywriting)DB: NEW column application_types.description (per §6)
routeNo (code-side routing)Static client map in use-application-types.ts
availableNo (code-side feature flag)Static client map in use-application-types.ts
hasCustomCreationNo (code-side dispatch)Static client map in use-application-types.ts
features.*No (code-side feature flag)Static client map in use-application-types.ts

Why Option C over A or B:

OptionProsCons
A — all DBSingle source of truthSchema migration adds 5 columns; route/available/features are code-side concerns the DB cannot enforce; admin UI gate (Q-OQR1-13)
B — all static clientNo migration, no admin UI gateStatic registry survives in a thinner form — not a meaningful step forward from today
C — hybridEditable copy/branding in DB (label, labelPlural, description, defaultIcon, defaultColour); code-side concerns (route, available, hasCustomCreation, features) stay in code where they can be PR-reviewed; matches the existing default_icon / default_colour precedentTwo-source registry, but the boundary is clean (admin-editable ↔ developer-editable)

Rationale for the split:

  • route is a Next.js path — the static client map is the only place it can meaningfully be enforced.
  • available and features.* gate code paths (e.g. available: false surfaces “Coming soon”); they are code-side feature flags that should land via PR review, not via an admin UI mutation.
  • label, labelPlural, description, defaultIcon, defaultColour are user-visible strings — they are exactly the surface a future client-admin UI would want to edit (Q-OQR1-13 v1.1 ratification).

The static client map in use-application-types.ts is keyed by application_types.key and contributes only the 4 code-side fields. Joining it with the DB row happens in the select: selector — the hook output is a single flat WorkspaceTypeConfig indistinguishable from today’s shape.

Sequential land order (each step blocks the next):

  1. Land migration (§6) — adds label_plural, description columns to application_types, backfills the 6 seed rows with values matching the current static registry’s labels for procurement, intelligence, proposal and reasonable defaults (or NULL fallback handled in code) for the 3 other seed rows that don’t have a current UI surface (sales_proposal, product_guide, competitor_research, training_onboarding). Also backfills default_icon + default_colour from the current static registry values for the 3 rendered types.
  2. Regen DB typesbun run … supabase gen types typescript … per CLAUDE.md command table; the regen extends application_types Row/Insert/Update with the 2 new column triples.
  3. Land hook + route + queryKey namespaceuseApplicationTypes() + GET /api/application-types + queryKeys.applicationTypes.list. At this point the hook coexists with the static registry; no consumer uses it yet.
  4. Migrate 3 UI consumers in any order (independent files).
  5. Delete static registry surface in lib/workspace-types.ts per §3 P-4. Run the zero-callers verification gate before each deletion.
  6. Land test changes per §3 P-5 (modify existing test + add new test).
  7. Run regression suitebun run test + bun lint + an E2E smoke on the /workspaces launcher page.

Chicken-and-egg note: the hook (step 3) cannot return useful data until the migration (step 1) lands. The Path-c-correct sequence is migration → regen → hook → consumers → deletion. Reversing 1↔3 would either ship a hook returning NULL-filled rows or require code-side fallback logic that this spec specifically avoids.

Migration filename: supabase/migrations/<timestamp>_id_21_application_types_metadata_columns.sql

-- =============================================================================
-- ID-21 — application_types metadata columns (label_plural, description)
-- =============================================================================
-- Adds the two metadata columns that currently live in the static
-- `WORKSPACE_TYPE_REGISTRY` (lib/workspace-types.ts). Backfills the 6 seed
-- rows. No data loss — the static registry remains the source of truth
-- until §5 step 5 deletes it; this migration writes the same values the
-- registry holds so the hook output matches today's UI output byte-for-byte
-- for the 3 currently-rendered types.
--
-- Path c posture: see docs/specs/id-29-tanstack-workspace-types/TECH.md
-- =============================================================================
SET search_path = public, extensions;
BEGIN;
ALTER TABLE public.application_types
ADD COLUMN label_plural text NULL,
ADD COLUMN description text NULL;
-- Backfill from current static registry values.
-- procurement / intelligence / sales_proposal (= "proposal" in registry)
-- get exact labels. Other 3 seed rows take reasonable defaults derived from
-- the `label` column; the UI does not render them today so cosmetic drift
-- is acceptable (Q-OQR1-13 admin UI will let clients fix in v1.1).
UPDATE public.application_types SET
label_plural = 'Procurements',
description = 'Manage bid responses and tender submissions using your knowledge base',
default_icon = 'briefcase',
default_colour = '#d4880f'
WHERE key = 'procurement';
UPDATE public.application_types SET
label_plural = 'Intelligence Streams',
description = 'Sector and competitor news feeds tailored to your company profile.',
default_icon = 'newspaper',
default_colour = '#059669'
WHERE key = 'intelligence';
UPDATE public.application_types SET
label_plural = 'Sales Proposals',
description = 'Draft and manage sales proposals drawing on your knowledge base',
default_icon = 'file-signature',
default_colour = '#0d9488'
WHERE key = 'sales_proposal';
UPDATE public.application_types SET
label_plural = label || 's',
description = label
WHERE key IN ('product_guide', 'competitor_research', 'training_onboarding')
AND label_plural IS NULL;
-- Post-check: verify the 3 currently-rendered types are fully backfilled.
DO $$
DECLARE
v_unbackfilled int;
BEGIN
SELECT count(*) INTO v_unbackfilled
FROM public.application_types
WHERE key IN ('procurement', 'intelligence', 'sales_proposal')
AND (label_plural IS NULL OR description IS NULL
OR default_icon IS NULL OR default_colour IS NULL);
IF v_unbackfilled <> 0 THEN
RAISE EXCEPTION
'ID-21 backfill incomplete: % rows in render-set still NULL', v_unbackfilled;
END IF;
END $$;
COMMIT;

This migration adds columns and runs an UPDATE — no new public.*() functions are created. The CLAUDE.md S250 W1b gotcha (anon-EXECUTE auto-grant) applies only to CREATE OR REPLACE FUNCTION public.*. The anonymous DO $$ block executes inline and does not register as a callable function. No REVOKE/GRANT is required.

The application_types table already has RLS enabled and the application_types_select_all FOR SELECT USING (true) policy from T2 (migration 20260520120828 §1.1). New columns inherit the policy without modification. The GET /api/application-types route reads via getAuthenticatedClient() (any role); anonymous reads are blocked by the proxy not being public.

Per CLAUDE.md Gotchas:

  • Sandbox-disable required for supabase migration new, db push, gen types.
  • cat supabase/.temp/project-ref before each push (staging-first apply).
  • Staging-apply first (turayklvaunphgbgscat), Liam ratification gate, then prod (rovrymhhffssilaftdwd).
  • Regenerate supabase/types/database.types.ts post-apply.
#RiskMitigation
R-1Loading-state flicker on /workspaces launcher between server-rendered count grid and client-resolved metadataFirst paint renders the count grid skeleton (existing layout container) until useApplicationTypes() resolves (~50ms)
R-2Hook returns undefined for a workspace whose application_types.key is unknown (cross-env drift)The 3 consumer files already null-coalesce — current getWorkspaceType() returns undefined for unknown keys, same posture
R-3Icon-name → LucideIcon resolution may drift if a new icon-name is seeded that the static map doesn’t carrytoWorkspaceTypeConfig() selector falls back to a default Folder icon when the icon-name map misses (matches today’s card-render path L40)
R-4application_types table is read on every workspace render — N+1 risk if uncachedstaleTime: 5 * 60_000 (5 min) on the hook; closed-list reference data, no invalidation triggers exist
R-5Test mocks for the hook need a Supabase client mock; the __tests__/helpers/mock-supabase.ts pattern is well-troddenUse createMockSupabaseClient(); mirror use-company-profiles test setup

Open ratification gates (for Liam before PLAN.md)

Section titled “Open ratification gates (for Liam before PLAN.md)”
Q-NQuestionDefault if not answered
Q-1Recommendation in §4 is Option C (hybrid). Ratify, or request A (all DB) / B (all static client)?Option C
Q-2The migration backfills sales_proposal description verbatim from the current proposal registry entry (“proposal” key in registry maps to sales_proposal DB key — verified S246 T2 §1.2 seed). OK?Yes — text matches static today
Q-3Should useApplicationTypes() be authenticated (current spec) or unauthenticated (closed-list reference data, no PII)? Authenticated is the safer default per pattern.Authenticated (any role)
Q-4Do we need a feature flag / kill-switch to roll back to the static registry if the hook misbehaves in prod?No — the deletion of the static registry is the point of the work; rollback is a git revert
Q-5Should the icon-name → LucideIcon static map live in use-application-types.ts (per §4) or in lib/workspace-types.ts (next to APPLICATION_TYPE_KEYS)?In use-application-types.ts (UI metadata, hook scope)
Q-NRatification
Q-1Option C ratified, with the additional stipulation that docs/specs/id-31-0.9-canonical-pipeline/PRODUCT.md Q-OQR1-13 bullet is amended to record: (a) what v1.0 ships via this spec (the hybrid DB-columns + static-client-map split), and (b) what v1.1 must close to restore a single source of truth (collapse the static map into DB columns once admin UI mutation is safe). Cross-ref edit landed alongside this TECH.md cherry-pick.
Q-2Yes — verbatim backfill of sales_proposal description from the current static proposal registry entry.
Q-3Authenticated (any role) — standard auth middleware pattern; no need to add to publicRoutes in proxy.ts.
Q-4No — feature flag / kill-switch not required; rollback is git revert of the registry-deletion commit.
Q-5hooks/use-application-types.ts — icon-name → LucideIcon static map co-located with the consumer of the icon.
  • Backlog item ID-21docs/reference/product-backlog.json (Path c reframe + GitNexus 132-symbol HIGH-risk finding).
  • S248 T4 procurement umbrella rename — commits 99dfc5fd, 51b6cc8c, f9261904; specifically the header-comment block added to lib/workspace-types.ts:1-11.
  • S249 ID-23 closeout — commit 95b660ef; referenced as the gitnexus_impact investigation that ratified Path c.
  • T2 application_types migration — supabase/migrations/20260520120828_t2_combined_pr_intel_shape_b_form_type_split.sql §1.1-1.2 (table + 6 seed rows), §1.3 (FK on workspaces), §application_types_select_all policy.
  • Generated DB types — supabase/types/database.types.ts L15-52 (application_types Row/Insert/Update with default_icon / default_colour already nullable).
  • Existing TanStack hook pattern — hooks/intelligence/use-company-profiles.ts (L33-47 = single-fetch hook shape mirrored here).
  • Existing API route pattern — app/api/intelligence/profiles/route.ts (GET-only authenticated read).
  • TanStack query-key pattern — lib/query/query-keys.ts (intelligence / workspaces / changeReports namespaces as the shape exemplar).
  • CLAUDE.md gotchas: (a) anon-EXECUTE auto-grant pattern (S250 W1b), (b) SET search_path = public, extensions, (c) sandbox-disable for Supabase CLI, (d) cat supabase/.temp/project-ref before push, (e) RLS auto-enable event trigger (P-1 RLS already covers application_types via T2).