# Spec Audit: A11y, Performance, i18n/RTL, Security Gaps

**Scope:** All 156 active specs evaluated against four cross-cutting dimensions.  
**Output:** Targeted edits to existing specs — no new specs proposed.  
**Priority:** Security > A11y > i18n/RTL > Performance

---

## 1. Security Gaps

### 1.1 CSP `connect-src` missing WebSocket origin
**Edit:** `2026-05-30-foundation-monorepo.md` → Security Standards → HTTP Response Security Headers

The current `connect-src` directive includes `https://api.zync.is` and Firebase URLs but omits the WebSocket endpoint for `TenantRealtimeDO` (spec 45). A strict CSP blocks `wss://` separately from `https://`.

**Add to `connect-src`:**
```
"connect-src 'self' https://api.zync.is wss://api.zync.is https://*.firebaseapp.com https://firebasestorage.googleapis.com",
```

---

### 1.2 HMAC comparisons must use timing-safe equality
**Edit:** `2026-05-30-foundation-monorepo.md` → Security Standards → add new subsection  
**Also applies to:** specs 27 (webhooks), 49 (payment gateways), 13 (magic links), 39 (API keys)

No spec mandates timing-safe comparison for HMAC/token verification. Naive `===` leaks timing info.

**Add subsection "Cryptographic Comparison Mandate" to spec 1:**
```ts
// packages/auth/src/crypto.ts — use for ALL token/HMAC comparisons
export function timingSafeEqual(a: string, b: string): boolean {
  const encoder = new TextEncoder()
  const bufA = encoder.encode(a)
  const bufB = encoder.encode(b)
  if (bufA.byteLength !== bufB.byteLength) return false
  return crypto.subtle.timingSafeEqual(bufA, bufB)  // available in Cloudflare Workers runtime
}
```

Enforcement: ESLint rule `no-string-equality-for-tokens` — flag direct `===` comparisons on variables named `*token`, `*hmac`, `*hash`, `*signature`, `*secret`.

---

### 1.3 R2 key: filename component sanitization
**Edit:** `2026-05-31-unified-attachments.md` → R2 Key Convention

Spec 41 already uses a UUID-prefixed key format (`{tenantId}/{module}/{entityId}/{uuid}-{filename}`) which mitigates cross-tenant traversal. However, the `{filename}` component is not explicitly sanitized — a filename containing bidi override characters or null bytes can corrupt logs and admin UIs.

**Add to spec 41 → R2 Key Convention:**
```ts
function sanitizeFilenameComponent(original: string): string {
  return original
    .replace(/[/\\]/g, '_')          // strip path separators (defense in depth)
    .replace(/\0/g, '')              // strip null bytes
    .replace(/[‪-‮⁦-⁩]/g, '')  // strip bidi override chars
    .slice(0, 200)                   // cap length for key legibility
}

// Key construction (mandatory for all entity types):
const r2Key = `${tenantId}/${module}/${entityId}/${randomUUID()}-${sanitizeFilenameComponent(filename)}`
```

---

### 1.4 API key failed-auth rate limiting
**Edit:** `2026-05-31-tenant-public-api.md` → Authentication → Auth Flow

Spec 39 has a 100 req/min rate limit per key but no specific rate limit for requests with *invalid* API keys. An attacker can brute-force valid keys without limit from many IPs.

**Add to Rate Limiting section:**
- Invalid API key responses (`401 invalid_api_key`) tracked in KV: `api_key_fail:{sha256(prefix)}:{1-min-window}`
- After 10 failures for the same key prefix in 1 minute → `429` with 5-minute backoff
- IP-based limit: 50 invalid key attempts per IP per minute across all keys → `429`
- Both limits use `RATE_LIMITER_AUTH` binding (reuse existing)

---

### 1.5 Email template variable injection (XSS in sent email)
**Edit:** `2026-05-31-email-template-editor.md` → Variable Interpolation section

Spec 66 sanitizes the template HTML on save (strips `<script>`, `<iframe>`, `on*` attributes) but does not HTML-escape variable *values* at send time. A customer named `<img src=x onerror=alert(1)>` would inject into the rendered email after save-time sanitization has already run.

**Add to spec 66 → Variable Interpolation:**
```ts
function interpolateTemplate(html: string, vars: Record<string, string>): string {
  return html.replace(/\{\{(\w+)\}\}/g, (_, key) => {
    const val = vars[key] ?? ''
    return val
      .replace(/&/g, '&amp;')
      .replace(/</g, '&lt;')
      .replace(/>/g, '&gt;')
      .replace(/"/g, '&quot;')
      .replace(/'/g, '&#x27;')
  })
}
```

Raw HTML slots (where tenant intentionally embeds HTML) use separate `{{{rawSlot}}}` syntax with an explicit allowlist of permitted tags via DOMPurify.

---

### 1.6 Custom domain: dangling CNAME / subdomain takeover
**Edit:** `2026-06-01-custom-domain-settings-ui.md` → DNS Instructions  
**Edit:** `2026-05-30-white-label-api.md` → Domain verification

Spec 137 says "DNS instructions" but no spec defines the verification flow before CF custom hostname activation. A tenant could point a CNAME at `zync.is` without completing DNS verification, and if the CF custom hostname is pre-provisioned, it's a dangling record.

**Add to both specs:**
1. CF custom hostname is NOT provisioned until `tenant_domains.status = 'verified'`
2. `domain-verify` cron (every 15 min) calls CF API `GET /zones/{zone}/custom_hostnames/{hostnameId}` — only marks verified when `ssl.status = 'active'` AND `status = 'active'`
3. If a domain remains unverified > 48h after creation, soft-delete the `tenant_domains` record and release the CF custom hostname
4. On domain removal, CF custom hostname deleted immediately (prevents squatting)

---

### 1.7 Webhook signature verification — return 200 on bad signature
**Edit:** `2026-05-31-payment-gateway-adapters.md` → Webhook endpoint security  
**Edit:** `2026-05-31-webhook-endpoint-detail.md` (inbound webhooks)

Spec 49 correctly notes "return 200 (don't leak verification failure to attacker)" for payment webhooks but spec 27/105 (outbound webhook delivery + tenant-registered webhooks) doesn't specify behavior for *inbound* webhook signature failures.

**Add to spec 105 (webhook-endpoint-detail) → Inbound Webhook Security:**
- Invalid HMAC signature: respond `200 OK` with empty body; log to `webhook_delivery_log` with `status = 'rejected_invalid_signature'`
- Rate limit: `RATE_LIMITER_WEBHOOK` (already defined)
- Log the raw body for forensics regardless of signature outcome (truncated to 10KB)

---

## 2. Accessibility (A11y) Gaps

### 2.1 Timeline / Gantt view has no keyboard accessibility spec
**Edit:** `2026-05-30-tasks-board-engine.md` → Views → Timeline View

The Kanban view has a detailed keyboard accessibility spec (with dnd-kit `KeyboardSensor`). The Timeline/Gantt view section says only "Gantt" with no a11y spec. The Timeline is complex (resize handles, date scrubbing, row reordering).

**Add to Timeline View section:**
```
Keyboard accessibility for Timeline:
- Gantt bars: focusable via Tab. Aria: role="button" aria-label="Task '{title}': {startDate} to {endDate}"
- Left/Right Arrow: extend/shrink duration by 1 day when bar is focused
- Enter/Space: open task detail
- Row reordering: same KeyboardSensor pattern as Kanban (Space to pick up, Arrow keys, Space/Enter to drop)
- Horizontal date scroll: Left/Right Arrow on the timeline grid container (not while a bar is focused)
- Screen reader summary row: visually hidden <caption> on Gantt table: "{n} tasks, spanning {startDate} to {endDate}"
- No drag-only interactions: all resize/move operations have keyboard equivalents
```

---

### 2.2 Tiptap rich text editor: ARIA and keyboard requirements
**Edit:** `2026-05-30-tasks-detail-communication.md`, `2026-05-31-proposal-editor.md`, `2026-05-31-contracts-esignature.md`

`kb-article-editor` (spec 101) already specifies `role="textbox"`, `aria-multiline`, and toolbar `aria-label`. The three remaining Tiptap specs have no a11y requirements for the editor.

**Add "Accessibility" subsection to each affected spec:**
```
Editor a11y requirements:
- Editor div: role="textbox" aria-multiline="true" aria-label="{context-label}"
  (e.g. "Task description editor", "Contract body editor")
- Toolbar: role="toolbar" aria-label="Text formatting"
- Toolbar buttons: aria-pressed for toggle states (Bold, Italic, etc.)
- Slash command menu: role="listbox" with aria-activedescendant tracking focused option
- Keyboard: full keyboard navigation without mouse (formatting via ⌘B/⌘I/⌘U shortcuts)
- Focus: Tab enters editor; Escape exits to last focused element outside editor
- @-mention dropdown: role="listbox", keyboard navigable, Escape dismisses
- Image insertion: aria-label="Insert image" on trigger; alt text required field in insert modal
```

---

### 2.3 Data tables: missing grid accessibility
**Edit:** `2026-05-31-bulk-operations.md` → DataTable integration  
**Edit:** `2026-05-30-invoices-core.md` → Invoice list

Specs reference `DataTable` for list views and bulk-select but no spec defines the ARIA grid pattern for complex data tables with sortable columns, row selection, and bulk actions.

**Add to spec 42 (bulk-operations) as a "DataTable A11y Contract" section:**
```
All DataTable instances across modules must:
- Table element: role="grid" when rows are interactive (clickable/selectable); role="table" for read-only
- Column headers with sorting: aria-sort="ascending|descending|none"
- Row selection checkbox: aria-label="Select {entity name}" (not just "Select")
- Select-all checkbox: aria-label="Select all {n} {entities}"
- Bulk action toolbar (appears on selection): role="toolbar" aria-label="Bulk actions"
  aria-live="polite" announcing "{n} items selected"
- Empty state row: role="row" with single cell spanning all columns (no role="gridcell" mismatch)
- Sort change: aria-live="polite" region announces "Sorted by {column} {direction}"
```

---

### 2.4 Charts (Recharts): accessible fallback data tables
**Edit:** `2026-05-31-revenue-forecasting.md`, `2026-05-31-profitability-reports.md`, `2026-05-31-expense-reports-ui.md`

`reports-analytics` (spec 24) already defines the accessible chart pattern (aria-label on containers + visually-hidden data table fallback). The three remaining report specs use Recharts but don't reference or replicate this pattern, leaving those charts inaccessible.

**Add to each affected spec under the chart component definition, adopting spec 24's established pattern:**
```
Chart accessibility requirements (same pattern as spec 24):
1. Wrapper: role="figure" aria-labelledby="{chart-id}-title"
2. <figcaption id="{chart-id}-title"> matches visual chart heading
3. Visually-hidden <table> sibling to every chart, containing the same data
   in tabular form. Toggle visible with "Show data table" button adjacent to chart.
4. SVG charts: <title> + <desc> elements on the root <svg>
   <title>: one-line summary (e.g., "Monthly revenue: Jan ₪12,400 — Dec ₪31,200")
   <desc>: trend description (e.g., "Revenue grew 152% over the year")
5. Tooltip: aria-live="polite" region that echoes tooltip content on hover/focus
6. Interactive charts (clickable segments): focusable via Tab, activated via Enter/Space
```

---

### 2.5 Timer display: aria-live for running timer
**Edit:** `2026-05-30-time-management.md` → Timer toggle section

The running timer shows a live HH:MM:SS counter. Screen readers won't announce live DOM updates without an `aria-live` region.

**Add to time management spec:**
```
Timer display accessibility:
- Timer container: aria-live="off" by default (announcing every second would be unusable)
- aria-label on the timer element updated every 10 seconds for polling screen readers:
  aria-label="Timer running: {h} hours {m} minutes"
- Start/Stop button: aria-label changes with state ("Start timer" / "Stop timer")
- aria-pressed on Start/Stop button: true when timer is running
- When timer stops: role="status" region announces "Timer stopped at {duration}"
```

---

### 2.6 File upload drag-and-drop zone: keyboard + screen reader
**Edit:** `2026-05-30-expenses-module.md` → Features → Upload

The upload dropzone is mentioned as "Drag-and-drop zone + file picker" but has no a11y spec.

**Add to Upload section:**
```
Upload zone a11y:
- Zone element: role="button" tabindex="0" aria-label="Upload receipts. Accepts JPG, PNG, HEIC, PDF up to 10MB."
- Enter/Space on focused zone: opens file picker (same as click)
- Drag state: aria-describedby pointing to a status region
- Upload progress: role="progressbar" aria-valuenow="{pct}" aria-valuemin="0" aria-valuemax="100"
  aria-label="Uploading {filename}: {pct}% complete"
- Upload success/failure: role="status" for success, role="alert" for error
- Batch upload queue: list with role="list", each item role="listitem" with status chip
```

---

### 2.7 Date picker: keyboard navigation requirements
**Edit:** `2026-05-31-hebrew-locale-dates.md` → Date Picker Library Locale Configuration

Spec 117 configures locale for the date picker but specifies no a11y requirements for the calendar grid.

**Add subsection "Date Picker Accessibility":**
```
Date picker keyboard requirements (applies to whatever date picker library is used):
- Calendar grid: role="grid" with aria-label="Choose date" (or contextual label)
- Day cells: role="gridcell" aria-label="{weekday}, {day} {month} {year}"
  aria-selected="true" for selected date, aria-disabled for out-of-range dates
- Navigation buttons (prev/next month): aria-label="Previous month" / "Next month"
- Month/year header: aria-live="polite" (announces when month changes)
- Keyboard: Left/Right Arrow navigate days; Up/Down Arrow navigate weeks;
  Page Up/Down navigate months; Home/End go to start/end of week
- Focus trap: when calendar is open, Tab cycles through focusable elements within calendar
- Escape: closes calendar, returns focus to trigger input
- Input field: aria-haspopup="dialog" aria-expanded="true|false"
```

---

### 2.8 WCAG module checklist: add missing patterns
**Edit:** `2026-05-31-wcag-accessibility.md` → Module A11y Checklist

Current checklist omits: charts, rich text editors, drag-and-drop interactions, file uploads, and timer displays.

**Add to the module checklist:**
```
Additional checklist items:
- [ ] Charts have role="figure" + visually-hidden data table alternative
- [ ] Rich text editors (Tiptap): role="textbox" aria-multiline, toolbar role="toolbar"
- [ ] File upload zones: role="button", keyboard operable, progress announced via aria-live
- [ ] Drag-and-drop: KeyboardSensor configured, DndContext announcements defined
- [ ] Live-updating counters (timers, counts): aria-label updated periodically; NOT aria-live="assertive"
- [ ] Data tables with sort: aria-sort on column headers, sort change announced
- [ ] Date pickers: full keyboard navigation, aria-label on each day cell
```

---

## 3. i18n / RTL Gaps

### 3.1 Hebrew pluralization: 5 forms, not 2
**Edit:** `2026-05-30-system-i18n.md` → Architecture → react-i18next section

Hebrew has 5 CLDR plural categories: `zero`, `one`, `two`, `many`, `other`. English has 2 (`one`, `other`). The spec doesn't define the plural key convention, so developers default to the English 2-form pattern, producing ungrammatical Hebrew strings for numbers in the "many" range (10–19, 100, 1000…).

**Add to spec 4:**
```ts
// Hebrew plural forms in he.json — 5 keys required (react-i18next uses CLDR rules):
// zero, one, two, many (10-19, multiples of 100/1000), other (3-9, 20-99, etc.)
{
  "tasks_count": {
    "zero":  "אין משימות",
    "one":   "משימה אחת",
    "two":   "שתי משימות",
    "many":  "{{count}} משימות",   // 10–19, 100, 1000, …
    "other": "{{count}} משימות"    // 3–9, 20–99, …  (same string, different CLDR bucket)
  }
}

// English (2 forms as usual — flat key form):
{
  "tasks_count_one":   "1 task",
  "tasks_count_other": "{{count}} tasks"
}

// Usage (same for both locales):
t('tasks_count', { count: n })
```

All count-bearing translation keys must define all 5 Hebrew forms. Translation governance (same-PR rule) extends to plural key completeness — a PR that adds an English count key without the 5 Hebrew forms is rejected in review.

---

### 3.2 Tiptap: RTL configuration required
**Edit:** `2026-05-31-kb-article-editor.md`, `2026-05-31-proposal-editor.md`, `2026-05-31-contracts-esignature.md`, `2026-05-30-tasks-detail-communication.md`

Tiptap doesn't automatically mirror its editor direction when `<html dir="rtl">` is set. Without explicit RTL configuration, the editor renders LTR even in Hebrew mode.

**Add "RTL Configuration" to each affected editor spec:**
```ts
// In editor extensions config:
import { Direction } from '@tiptap/extension-text-direction'

const extensions = [
  // ... other extensions
  Direction.configure({
    defaultDirection: locale === 'he-IL' ? 'rtl' : 'ltr',
    // Allows per-paragraph direction override via toolbar button
  }),
]

// The direction extension adds:
// - Toolbar button to toggle paragraph direction (↔ icon)
// - Persists as data-text-align / dir attribute on paragraph nodes in JSONB
// - When locale = he-IL: new paragraphs default RTL
// - When locale = en-US: new paragraphs default LTR
```

---

### 3.3 Recharts: RTL axis mirroring
**Edit:** `2026-05-30-reports-analytics.md`, `2026-05-31-revenue-forecasting.md`, `2026-05-31-profitability-reports.md`, `2026-05-31-expense-reports-ui.md`

Spec 24's RTL support covers only Excel export (`Views: [{ RTL: true }]`). Recharts chart *rendering* in RTL has no spec in any report spec: Y-axis renders on left (LTR convention), tooltip anchoring breaks, and bar progression direction is wrong.

**Add "RTL Chart Configuration" to each affected report spec:**
```tsx
// Wrap all Recharts with locale-aware RTL config:
function Chart({ locale }: { locale: string }) {
  const isRtl = locale === 'he-IL'
  return (
    <ResponsiveContainer>
      <BarChart layout={isRtl ? 'vertical' : 'horizontal'}>
        {/* Y-axis: flip orientation in RTL */}
        <YAxis orientation={isRtl ? 'right' : 'left'} />
        <XAxis orientation="bottom" />
        {/* Tooltip: anchor end-of-bar in RTL */}
        <Tooltip position={{ x: isRtl ? 'left' : 'right' }} />
      </BarChart>
    </ResponsiveContainer>
  )
}
```

For stacked bar charts: `stackOffset="expand"` is direction-agnostic. For line charts: `dot` positions are coordinate-based and don't need RTL adjustment. Document per-chart type in each spec.

---

### 3.4 Number/amount input fields: force `dir="ltr"` in RTL
**Edit:** `2026-05-31-rtl-hebrew-ui.md` → Form Fields section

Spec 81 says "numbers always LTR-aligned" for display columns but doesn't address `<input type="number">` or `<input type="text">` used for ILS amounts. In RTL, a numeric input field inherits `dir="rtl"` and the cursor, caret, and digit entry behave unexpectedly.

**Add to spec 81 → Form Fields:**
```
Numeric input fields:
- All inputs for: amounts (ILS), percentages, hours, invoice numbers, phone numbers, tax IDs (ח.פ./ע.מ.) 
  must have dir="ltr" explicitly, regardless of document direction.
- Pattern: <input type="text" dir="ltr" inputMode="decimal" />
  (inputMode="decimal" shows numeric keyboard on mobile; type="number" suppresses arrow keys in some browsers)
- Label for the field remains in document direction (RTL for Hebrew)
- Placeholder text in numeric fields: always LTR ("0.00", "1234567890")
- Helper text / error message below field: document direction (RTL)
```

---

### 3.5 Invoice and outbound emails: Hebrew subject + body
**Edit:** `2026-05-30-invoices-core.md` → Invoice HTML generation  
**Edit:** `2026-05-30-system-communications-notifications.md` → Email adapter

No spec specifies the locale of outgoing email subjects and bodies. Invoices sent to customers of Hebrew-locale tenants should have Hebrew subject lines and body text.

**Add to invoices-core.md:**
```ts
// Email subject and body are locale-aware:
const subject = locale === 'he-IL'
  ? `חשבונית מס ${invoice.taxInvoiceNumber} מ-${tenant.businessName}`
  : `Invoice ${invoice.taxInvoiceNumber} from ${tenant.businessName}`

// Email body template key: 'invoice_sent_he' / 'invoice_sent_en'
// Both templates defined in system email templates (spec 66)
// dir="rtl" on the email <html> element for Hebrew

// Reminder emails (spec 79) follow same pattern — locale from tenant settings
```

**Add to communications spec:** The `sendEmail` function signature must accept `locale` param. Adapter resolves template + subject using locale. Default: tenant's `settings.locale`.

---

### 3.6 `lang` attribute on mixed-direction inline content
**Edit:** `2026-05-31-rtl-hebrew-ui.md` → Typography section

When displaying data that may be in a language different from the document direction (e.g., an English customer name in a Hebrew-RTL UI), there's no spec for inline `lang` + `dir` attributes. Screen readers need `lang` to pronounce text correctly.

**Add to spec 81 → Typography:**
```
Mixed-language inline content:
- Entity names from DB (customer name, vendor name, project name): these are user-generated
  and may be in either Hebrew or English regardless of UI locale.
- Detect direction heuristically: if >50% of characters are Hebrew Unicode (U+0590–U+05FF, U+FB1D–U+FDFF),
  treat as RTL; otherwise LTR.
- Apply inline: <span dir="auto" lang="he|en"> around user-generated string fields
  where display language is uncertain.
- dir="auto" is the safe default — browser detects direction from first strong character.
- Do NOT apply to: fixed strings (status labels, category names, dates) — these are always translated.
```

---

### 3.7 Calendar module: RTL layout requirements
**Edit:** `2026-05-30-calendar-module.md`  
**Edit:** `2026-05-31-calendar-event-detail.md`

The calendar module (Google/Outlook sync, event view) has no RTL-specific requirements. In Hebrew, week grid columns (Sun–Sat) should render right-to-left (Sat on left, Sun on right in the Israeli standard).

**Add "RTL Calendar Layout" to spec 19:**
```
Hebrew calendar grid:
- Week starts on Sunday (Israel standard) — already noted as locale adapter
- Column order in RTL: renders same logical order (Sun first) but CSS grid columns
  are reversed visually via dir="rtl" on the grid container — leftmost column = Sat, rightmost = Sun
- Time labels (left sidebar in LTR): in RTL, time labels appear on RIGHT side
  Achieved via: CSS logical property `inset-inline-start: 0` on time gutter
- Event blocks: text within event block always follows event's own language dir
- Navigation arrows (prev/next week): mirror in RTL (< becomes >, > becomes <)
  Use CSS transform: scaleX(-1) or logical margin on arrow icons per spec 81 icon mirroring rules
```

---

## 4. Performance Gaps

### 4.1 Internal list views: pagination and virtual scrolling
**Edit:** `2026-05-30-invoices-core.md` → Invoice list  
**Edit:** `2026-05-30-customers-module.md` → Customer list  
**Edit:** `2026-05-31-marketing-leads-pipeline.md` → Lead list  
**Edit:** `2026-05-30-expenses-module.md` → Expense list

The public API (spec 39) correctly uses cursor-based pagination, but the internal app list views don't specify their pagination strategy. A tenant with 10,000 invoices loading all rows in one query will cause Neon query timeouts and Workers memory exhaustion (128MB limit).

**Add "Pagination Contract" to each list spec:**
```
Internal list API endpoints must implement keyset pagination:
- Default page size: 50 rows (internal UI; heavier rows than public API)
- Maximum: 200 rows per request
- Cursor: `{ id, created_at }` base64-encoded (same pattern as public API)
- Frontend: TanStack Query `useInfiniteQuery` — loads first page, "Load more" button or
  IntersectionObserver-triggered infinite scroll
- Virtual scrolling: required when visible list height > 600px AND dataset may exceed 500 rows.
  Use TanStack Virtual (`@tanstack/react-virtual`) for windowed rendering.
- COUNT(*) queries: never on list endpoints. Total count is approximate via Postgres 
  `pg_class.reltuples` for display (e.g., "~12,400 invoices"); exact count only on explicit export.
```

---

### 4.2 Route-based code splitting strategy
**Edit:** `2026-05-30-app-shell.md` → Route Structure  
**Edit:** `2026-05-30-foundation-monorepo.md` → Apps → zync-app

No spec defines the code splitting strategy for the SPA. Without splits, the full bundle loads on first paint, adding 2–5 seconds for heavy modules (Recharts, Tiptap, dnd-kit).

**Add to spec 7 (app-shell) → Route Structure:**
```
Code splitting — mandatory for all module routes:
- Every top-level route in `src/routes/` uses React.lazy() + Suspense
- Boundary: one chunk per module (invoices, tasks, expenses, reports, etc.)
- Eager loads: app shell, auth routes, home dashboard
- Chart-heavy pages (reports/*): separate dynamic import for Recharts
  import('recharts') — loaded only when reports route activates
- Editor-heavy pages (KB, proposals, contracts): separate dynamic import for Tiptap
  import('@tiptap/react') — loaded only when editor route activates
- dnd-kit: dynamic import on tasks board; not included in main chunk
- Estimated split budget: main chunk <200KB gzipped; per-module chunks <100KB gzipped

Route-level Suspense fallback: <PageSkeleton /> (spec 31 loading skeleton)
```

---

### 4.3 GIN indexes on JSONB columns
**Edit:** `2026-05-30-foundation-monorepo.md` → Multi-tenant Isolation Model → add "JSONB Index Mandate"  
**Also edit:** `2026-05-30-reports-analytics.md`, `2026-05-30-tasks-board-engine.md`

Multiple specs add JSONB columns that are queried (e.g., `proposals.content`, `tenant_settings.lead_stage_probabilities`, `dashboard_widgets.config`). Without GIN indexes, JSONB queries do full table scans.

**Add to spec 1 → Schema Standards:**
```
JSONB index mandate:
- Any JSONB column used in a WHERE clause or ORDER BY must have a GIN index:
  CREATE INDEX CONCURRENTLY ON table USING GIN (column jsonb_path_ops);
- jsonb_path_ops: smaller index, faster for containment queries (@>, @@)
  jsonb_ops: larger index, supports all operators (use only when key-existence query needed)
- Required GIN indexes not specified in current specs:
  - proposals.content (for proposal search by product/service)
  - tasks.custom_fields JSONB (if added by field-permissions spec)
  - tenant_settings columns queried server-side: use separate typed columns instead of JSONB keys
    where possible — JSONB is for user-extensible config, not for server-filtered queries
```

---

### 4.4 Reconnect debounce for WebSocket
**Edit:** `2026-05-31-real-time-infrastructure.md` → Client Connection → Client-side manager

Spec 45 has exponential backoff (1s → 30s) for reconnects, but if many browser tabs all reconnect after a network blip (e.g., laptop wakes from sleep), all tabs reconnect simultaneously, causing a thundering herd to the DO.

**Add to spec 45 → Client-side manager:**
```ts
// Add jitter to reconnect delay to spread thundering herd:
private nextReconnectDelay(): number {
  const base = Math.min(this.reconnectDelay * 2, 30_000)
  const jitter = Math.random() * 1_000  // 0–1s jitter
  return base + jitter
}

// Also: deduplicate connections across tabs via BroadcastChannel
// One "leader" tab holds the WebSocket; other tabs receive events via BroadcastChannel
// Leadership election: first tab to set localStorage key 'ws_leader_{tenantId}' with a TTL wins
// On leader tab close: next tab in BroadcastChannel takes over within 1s
// This reduces DO connection count from N tabs to 1 per user session
```

---

### 4.5 Session query index
**Edit:** `2026-05-31-session-security.md` → Schema Delta

The `user_sessions` table is queried frequently (every auth middleware request checks for revocation, idle cleanup cron). No indexes are specified.

**Add to spec 122 → Schema Delta:**
```sql
-- Required indexes on user_sessions:
CREATE INDEX ON user_sessions (user_id, tenant_id)
  WHERE revoked_at IS NULL;               -- active sessions per user per tenant

CREATE INDEX ON user_sessions (token_hash)
  WHERE revoked_at IS NULL;               -- fast revocation check (blocklist lookup)

CREATE INDEX ON user_sessions (last_active_at)
  WHERE revoked_at IS NULL;               -- idle cleanup cron (ORDER BY last_active_at ASC)
```

---

## Architecture Decisions

| Decision | Choice | Reason |
|----------|--------|--------|
| Edit existing specs, not create new ones | Edit-in-place | Avoids index fragmentation; keeps related rules co-located with their feature spec |
| Security gaps are highest priority | Address first | CSP/WebSocket, HMAC timing safety are exploitable before any UI is built |
| Virtual scrolling threshold: 500 rows | Not 100, not 1000 | Workers 128MB memory cap; 500 rows × ~5KB rendered DOM = ~2.5MB — safe margin |
| `dir="auto"` for user-generated content | Not heuristic detection | Browser's auto detection is faster and handles mixed scripts correctly; server-side detection would require ICU libraries |
| Recharts RTL: axis orientation flip | Not full chart library swap | Recharts is already specced; axis flip is low-effort; full swap would require re-speccing all report views |
| BroadcastChannel for WS tab dedup | Not SharedWorker | BroadcastChannel is universally supported in target browsers; SharedWorker has Safari restrictions |
| Removed: Tiptap DOMPurify (was 1.4) | Already specced | All 4 Tiptap specs (48, 101, 12, 130) already mandate DOMPurify via `renderContractHTML`/`renderKbHtml` helpers |
| Removed: magic-byte validation (was 1.2) | Already specced | Spec 41 (unified-attachments) uses `file-type` library for magic-byte detection on all uploads |
| Removed: R2 signed URL caching (was 4.3) | Already specced | Spec 41 implements KV caching with 3300s TTL and 55-min safety margin |
| Narrowed: spec 24 excluded from 2.4 | Already specced | Spec 24 (reports-analytics) already defines accessible chart pattern; gap applies to 3 remaining report specs |

---

## Implementation Order

**Phase 1 — Security (before any module implementation):**
1. spec 1: CSP WebSocket + timing-safe mandate
2. spec 41: R2 filename component sanitization
3. spec 66: email template variable HTML-encoding
4. spec 39: API key failed-auth rate limiting
5. spec 137 + spec 27: custom domain dangling CNAME fix
6. spec 105: inbound webhook 200-on-bad-signature

**Phase 2 — A11y + i18n (before UI build begins):**
7. spec 115 (WCAG): extend module checklist
8. spec 4 (i18n): Hebrew pluralization 5-form convention
9. spec 81 (RTL): numeric input dir, lang attribute
10. spec 11 (tasks): Timeline/Gantt a11y
11. Tiptap specs (48, 130, 12): RTL config + a11y contract
12. spec 117 (dates): date picker keyboard nav
13. spec 19 (calendar): RTL layout
14. Report specs (116, 92, 57): accessible chart pattern (adopt spec 24)
15. spec 13 (time-management): timer aria-live
16. spec 42 (bulk-ops): DataTable a11y contract

**Phase 3 — Performance (before production load):**
17. spec 7 (app-shell): code splitting strategy
18. spec 1 (monorepo): JSONB index mandate
19. List view specs: pagination contract + virtual scroll
20. spec 45 (realtime): reconnect jitter + tab dedup
21. spec 122 (sessions): add DB indexes
