# Spec Architecture Review

**Date:** 2026-05-31  
**Scope:** `docs/specs/` only (34 written specs + index; no application code)  
**Goal:** Surface architectural friction in the spec corpus *before* implementation, so the first codebase inherits deep modules and clear seams rather than inheriting spec debt.

---

## Executive Summary

The Zync spec corpus is unusually mature for a greenfield project: layered index, foundation deltas registry, adapter patterns, and consistent module spec skeletons (Overview → Data Model → Features → Permissions → API → Architecture Decisions). That structure already earns **locality** for implementers working inside a single domain (Invoices, Tasks, Marketing).

The main friction is at **seams between specs**. Cross-cutting platform behavior — outbound events, credential storage, attachments, tier entitlements, realtime — is defined in multiple places with small but load-bearing divergences. Several module specs are **shallow**: their interfaces (tables, routes, permissions, webhook event names) are nearly as large as the behavior they describe, and callers must bounce across 3–5 documents to understand one user journey.

**Recommendation:** Before writing application code, invest in 5–7 *platform* specs that deepen cross-cutting seams, plus a `CONTEXT.md` domain glossary and a enforced spec template. Resolve 6 documented contradictions. This is cheaper now than reconciling 34 specs against a running monorepo.

---

## Corpus Overview

| Metric | Value |
|--------|-------|
| Written spec files | 34 (+ `00-index.md`, + `tax-rates-seed-data.md` orphan) |
| Index-declared total | 43 (9 planned, not yet written) |
| Layer model | 7 layers (Foundation → Enterprise) in `00-index.md` |
| Specs with `Architecture Decisions` | 33 / 34 written |
| Specs with `Foundation Deltas` section | 8 (plus consolidated block in index) |
| Specs with `Permissions` + `API Endpoints` | 21 module-style specs |
| Specs with acceptance / verification criteria | ~1 (`tax-rates-seed-data.md` only) |

### Layer distribution (from index)

```
Layer 0  Foundation (3)     — monorepo, design-system, auth-rbac
Layer 1  System Core (3)     — i18n, communications, AI
Layer 2  App Shells (7+3 planned)
Layer 3  Product Modules (13)
Layer 4  Marketing (2)
Layer 5  Cross-cutting (6+5 planned)
Layer 6  Enterprise (2)
```

### Document structure patterns (what repeats)

Most product module specs follow:

1. Header metadata (`Date`, `Status`, `Depends on`, `Referenced by`)
2. Overview
3. Data Model (inline SQL)
4. Features / screens
5. Permissions
6. API Endpoints
7. Webhooks (optional, module-local event list)
8. Architecture Decisions

Foundation and system specs vary more (design tokens, auth flows, adapter interfaces). Newer specs (`2026-05-31-*`) add `Frontend Implementation Notes` and richer UI sections.

---

## What Works Well

These patterns should be preserved and extended, not replaced.

1. **Layered index with dependency graph** (`00-index.md`) — gives implementers a write order and a single place to see infra accumulation (bindings, secrets, crons, queues, tables).

2. **Foundation Deltas as a feedback loop** — module specs append infra requirements; index aggregates them back into `foundation-monorepo`. The *idea* is correct even if the execution is duplicated today.

3. **Adapter interfaces at seams** — `InvoiceAdapter`, `CommsAdapter`, `ZyncPaymentAdapter`, country adapters in i18n. These are **deep module** sketches: small interfaces, behavior behind them.

4. **Split invoice core vs adapters** — domain law and lifecycle in `invoices-core`; provider push in `invoices-adapters`. Clean seam; billing-module can depend on adapters without re-learning IL tax rules.

5. **Module management as a registry** (`module-management`) — canonical module IDs, hard/soft dependency matrix, enable/disable semantics. This is the right shape for a platform seam.

6. **Architecture Decisions tables** — nearly every spec records *why*, not just *what*. Valuable for ADR extraction later.

7. **Retroactive cross-cutting constraints called out explicitly** — e.g. audit-in-same-transaction (spec 28) flagged as applying to specs 9–27. Honest about ordering debt.

---

## Architectural Friction

### 1. No domain glossary (`CONTEXT.md` missing)

Terms like **Tenant**, **Module**, **Tier**, **Portal**, **Adapter**, **Proforma**, **Lead**, **Webhook** (four different meanings) appear across specs without a single authoritative definition file. The index uses "module" in two senses (product module vs spec file). **Tier** pricing lives in `zync-subscription`; **Tier** entitlements live in `foundation-auth-rbac`.

**Impact:** AI and human navigators cannot rely on consistent vocabulary. Every new spec re-explains concepts → shallow modules at the documentation layer.

### 2. Outbound webhook gateway defined twice (contradictory)

| Aspect | `system-communications-notifications` §3 | `white-label-api` § Webhook Gateway |
|--------|------------------------------------------|-------------------------------------|
| Tier gate | "White Label tier" (inline) | Full spec owner |
| Column name | `active BOOLEAN` | `is_active BOOLEAN` |
| Delivery columns | no `tenant_id`, no `next_retry_at` | has both |
| Retry policy | max 3 retries | 5 failures, exponential backoff table |
| Secret storage | tenant-set plaintext in schema comment | AES-256-GCM via `INTEGRATION_ENCRYPTION_KEY` |
| Event catalog | "see Zync.txt lines 476–514" | Full inline table (~30 events) |
| UI | not specified | `/settings/webhooks` |

Both specs claim ownership of `webhook_endpoints` / `webhook_deliveries` and `webhook.deliver` queue.

**Deletion test:** Removing either spec section leaves the other incomplete; complexity does not concentrate — it **conflicts**.

### 3. Credential storage: two table names, one seam

- `system-communications-notifications` → `integration_credentials`
- `invoices-adapters`, `settings-module`, `crm-support-center` → `adapter_credentials`
- `onboarding` references `integration_credentials`; settings references `adapter_credentials` for the same encrypted store pattern

Same AES-256-GCM seam, two interfaces. Implementers must guess whether email OAuth and Morning API keys share a table.

### 4. Telegram bot token: explicit contradiction

- `00-index.md` + `foundation-monorepo`: `TELEGRAM_BOT_TOKEN` **REMOVED** — per-tenant tokens in DB (spec 14)
- `system-communications-notifications`: still documents global `TELEGRAM_BOT_TOKEN` env secret and global bot
- `crm-support-center`: per-tenant bot in `adapter_credentials` (aligns with index)

**Impact:** First implementer of comms spec will build the wrong adapter topology.

### 5. Tier entitlements split across commerce and auth (leaky seam)

| Concern | Owner spec |
|---------|------------|
| `TenantTier` enum, `requireTier`, `useTierGate` | `foundation-auth-rbac` |
| Entitlement matrix (feature × tier) | `foundation-auth-rbac` |
| Metered quotas (`usage_counters`, OCR limits) | `foundation-auth-rbac` |
| Plan pricing, checkout, trial, `/settings/plan` UI | `zync-subscription` |
| Upgrade modal UI + triggers | `zync-subscription` (+ planned `upgrade-upsell-modal.md`) |
| Nav upgrade badges | `app-shell` |

No single spec defines the **Tier interface** end-to-end: what changes when `zync_subscriptions.status` flips vs when JWT `tier` field changes vs when admin overrides.

### 6. Attachments: repeated shallow implementation, planned unification too late

R2 key patterns, signed URL TTL, MIME allowlists appear independently in:

- `tasks-detail-communication` (full detail)
- `kb-module`, `expenses-module`, `crm-support-center` (partial)
- Planned `unified-attachments.md` (Layer 5, priority Low)

**Impact:** Four modules will ship four upload handlers before the shared seam exists. The planned spec should be Layer 1 and **blocking** for any module with file upload.

### 7. Realtime (`TenantRealtimeDO`) owned by a feature spec

`tasks-detail-communication` defines the Durable Object contract, event types, and broadcast path. `app-shell` notification bell and future modules will need the same seam. Index assigns `DO_REALTIME` to specs 5 and 12 but only spec 12 specifies the DO interface.

### 8. Event catalog fragmented across modules

Outbound events listed in:

- `white-label-api` (master catalog)
- Per-module `## Webhooks` / `## Outbound Webhook Events` sections (7 modules)
- `system-communications-notifications` (pointer to Zync.txt)

Event naming is mostly consistent (`lead.created`, `invoice.paid`) but payload shapes are module-local. No spec defines the **Event interface** (envelope, versioning, idempotency key, tenant scoping).

### 9. Tax rate domain split across four artifacts

| Artifact | Role |
|----------|------|
| `system-i18n` | `vat_rates` table + IL VAT history |
| `admin-dashboard` | `tax_rates` table + admin UI + lookup helper |
| `tax-rates-seed-data.md` | SQL seeds (not in index) |
| `invoices-core`, `reports-analytics`, `contractor-payouts` | consumers |

Index says `tax_rates` owned by "spec 8" (app-shell) — **wrong**; app-shell has no tax content. Actual owner is `admin-dashboard`.

### 10. Foundation Deltas triple-maintained

Infra additions appear in:

1. Each module's `Foundation Deltas` section (8 specs)
2. `00-index.md` consolidated tables (~130 lines)
3. `foundation-monorepo` bindings/secrets/crons/queues tables

Drift risk is already visible (telegram secret, webhook retry counts). **Locality** for infra changes is poor: a billing cron edit may require three file touches.

### 11. Dependency metadata inconsistent

`Depends on` lines mix formats:

- Backtick slug: `` `foundation-auth-rbac` ``
- Filename: `2026-05-30-foundation-design-system`
- Unquoted slug: `foundation-auth-rbac, settings-module, app-shell`

No machine-parseable graph. Circular risk: `white-label-api` → `system-communications-notifications`; comms spec embeds webhook gateway that white-label owns.

### 12. Permissions duplicated 21 times

`foundation-auth-rbac` lists all permission keys. Each module spec repeats a subset under `## Permissions`. No spec is the **registry owner** for module-scoped permissions; changes require editing auth spec + N module specs.

### 13. No verification seam

Except seed-data verification notes, specs lack:

- Acceptance criteria
- Edge-case matrices
- "Must not" behavioral tests
- Cross-spec integration checkpoints

**The interface is the test surface** — but interfaces are not yet written to be testable as contracts.

### 14. Planned spec overlap

| Planned | Overlaps with |
|---------|---------------|
| `upgrade-upsell-modal.md` | `zync-subscription` § Upgrade Modal (full UI spec already written) |
| `notification-center.md` | `app-shell` § Notification Dropdown + comms § In-App Notifications |
| `search-completeness.md` | `app-shell` § Global Search Modal entity table |

Risk of two specs diverging on the same UI surface.

### 15. `module-management` layered too late

Index places it in Layer 5 (Cross-cutting), but:

- `onboarding` depends on it
- `app-shell` nav visibility depends on `tenant_modules`
- Every module spec should declare its module ID and hard/soft deps *by reference* to the registry

It behaves like **Layer 1 System Core**, not a late cross-cutting concern.

---

## Recommendations

Numbered **deepening opportunities** for the spec architecture. Each follows: Problem → Solution → Benefits (locality, leverage, testability).

---

### 1. Create `docs/CONTEXT.md` (domain glossary)

**Files:** New; referenced by `00-index.md` and every spec header  
**Problem:** Domain terms defined implicitly across 34 files. "Webhook" means inbound provider callback, inbound lead capture, outbound tenant delivery, and payment provider notification.  
**Solution:** Single glossary: Tenant, User, Module (product), Tier, Portal (customer vs staff), Adapter, Event (outbound), Webhook (inbound), Invoice lifecycle states, Lead vs Customer. Link from each spec's Overview: "Domain terms: CONTEXT.md".  
**Benefits:** **Leverage** — one read orients across all specs. **Locality** — term drift fixed in one place. Enables consistent naming in code packages.

---

### 2. Extract `system-outbound-events.md` (deepen the Event seam)

**Files:** Absorb §3 from `system-communications-notifications`; § Webhook Gateway from `white-label-api`; per-module `## Webhooks` event rows  
**Problem:** Two owners, conflicting schemas and retry policies; event payloads scattered.  
**Solution:** One platform spec owning:

- Event envelope interface (`event`, `tenantId`, `timestamp`, `data`, `idempotencyKey`)
- `webhook_endpoints` / `webhook_deliveries` canonical schema (pick white-label version)
- Delivery adapter interface + retry policy
- Full event catalog with payload types
- Tier gate: `requireTier('white_label')` for endpoint CRUD only; events always emitted internally

Module specs shrink to: "Emits: `invoice.paid` — see system-outbound-events § Finance".  
**Benefits:** **Depth** at the event seam. **Locality** for retry/HMAC/versioning. Tests target one delivery module.

---

### 3. Extract `system-integration-credentials.md` (unify credential seam)

**Files:** Merge `integration_credentials` / `adapter_credentials` references across comms, settings, invoices-adapters, CRM, onboarding  
**Problem:** Two table names for one encryption seam; onboarding vs settings disagree.  
**Solution:** One spec, one table name (`integration_credentials` or `adapter_credentials` — pick one), one TypeScript interface:

```ts
interface IntegrationCredentialStore {
  save(tenantId, adapterId, credentials): Promise<void>
  get(tenantId, adapterId): Promise<DecryptedCredentials>
  delete(tenantId, adapterId): Promise<void>
  test(tenantId, adapterId): Promise<TestResult>
}
```

Module specs reference adapter IDs only.  
**Benefits:** **Deletion test** passes — removing per-module credential SQL concentrates complexity here. Security audits have **locality**.

---

### 4. Promote `unified-attachments.md` to Layer 1 (blocking)

**Files:** New/promoted; trim attachment sections from tasks, kb, expenses, crm  
**Problem:** Four upload implementations planned before shared seam.  
**Solution:** Write now as `system-storage-attachments.md`:

- R2 key convention `{tenantId}/{domain}/{entityId}/{uuid}-{filename}`
- Upload API interface (presigned POST, max size, MIME policy hook)
- Signed URL TTL policy
- Virus scan hook (future stub)
- Domain-specific metadata tables stay in module specs; bytes on disk do not

**Benefits:** **Leverage** — one adapter for R2. Module specs become **deeper** (attachment behavior without S3 details).

---

### 5. Extract `system-realtime.md` (TenantRealtimeDO seam)

**Files:** Move DO contract from `tasks-detail-communication`; reference from comms, app-shell  
**Problem:** Realtime is platform infrastructure owned by a task feature spec.  
**Solution:** Platform spec for DO naming, broadcast command, connection auth, event type registry (extensible). Task spec says: "Broadcasts `task.updated` per system-realtime § Task events".  
**Benefits:** Notification live-updates and future modules share one **interface**. DO tests live in one place.

---

### 6. Split Tier into interface spec + two adapters

**Files:** New `system-tier-entitlements.md`; slim `foundation-auth-rbac`; slim `zync-subscription`  
**Problem:** Tier commerce and tier enforcement are coupled across JWT, DB subscription row, and admin override with no state machine.  
**Solution:**

| Module | Owns |
|--------|------|
| `system-tier-entitlements` | Tier enum, entitlement matrix, quota keys, `resolveEffectiveTier(tenantId)` state machine (subscription + trial + admin override + JWT cache rules) |
| `foundation-auth-rbac` | `requireTier` middleware calling resolver; permissions unchanged |
| `zync-subscription` | Payment adapter, checkout, invoice history — *writes* subscription rows consumed by resolver |

Document: "JWT `tier` is cache; resolver is source of truth on refresh."  
**Benefits:** **Depth** — callers use `resolveEffectiveTier`, not three documents. Cancel planned `upgrade-upsell-modal.md` or reduce it to a pointer into `zync-subscription`.

---

### 7. Adopt a three-layer spec template (Interface / Seam / Surface)

**Files:** `docs/specs/_TEMPLATE.md`; retrofit foundation + system specs first  
**Problem:** Module specs mix SQL, REST routes, React routes, and business rules in one flat doc → shallow interfaces.  
**Solution:** Mandatory sections:

```markdown
## Interface
Behavior callers depend on: invariants, state machines, error modes, idempotency.

## Seams
Adapters, events emitted/consumed, integration points (link to platform specs).

## Surface
UI routes, API route list, schema DDL — implementation detail.

## Verification
Acceptance criteria, edge cases, cross-spec integration checks.
```

**Benefits:** Implementers read Interface first; testers write against Verification. AI agents navigate predictably.

---

### 8. Single owner for Foundation Deltas + generated index section

**Files:** `foundation-monorepo.md` (owner); `00-index.md` (aggregator)  
**Problem:** Triple maintenance of bindings/secrets/crons/queues/tables.  
**Solution:**

- Only `foundation-monorepo` holds full infra tables
- Module specs use one line: `Foundation delta: adds RATE_LIMITER_LEAD_FORM — see foundation-monorepo § Rate Limiters`
- Index Foundation Deltas section becomes a checklist ("pending deltas not yet merged into monorepo spec") not a duplicate copy

Optional later: script to validate index against monorepo spec.  
**Benefits:** **Locality** for infra. Reduces merge conflicts during parallel spec edits.

---

### 9. Create `system-permissions-registry.md`

**Files:** Extract permission keys from `foundation-auth-rbac`; module specs reference slices  
**Problem:** 21 duplicate Permissions sections; auth spec has monolithic list.  
**Solution:** Registry spec:

| Permission | Module | Min role | Tier gate |
|------------|--------|----------|-----------|

Module specs: "Permissions: see registry § Invoices".  
**Benefits:** RBAC changes are **local**. ESLint/route guards generated from one table (future).

---

### 10. Normalize dependency metadata

**Files:** All spec headers; optional `docs/specs/deps.json`  
**Problem:** Inconsistent `Depends on` format; index spec numbers don't match filenames.  
**Solution:**

- Standard: `` **Depends on:** `slug-a`, `slug-b` `` (slug = filename without date prefix)
- Add `**Spec ID:**` matching index # (e.g. `Spec ID: 15`)
- Fix index: `tax_rates` owner → admin-dashboard; register `tax-rates-seed-data.md` as appendix to admin-dashboard or i18n

**Benefits:** Parseable dependency graph; CI can detect cycles before implementation.

---

### 11. Resolve contradictions before any Layer 3 implementation

| # | Contradiction | Resolution |
|---|---------------|------------|
| 1 | Telegram global vs per-tenant bot | Align comms spec with CRM + index: remove global token; per-tenant only |
| 2 | Webhook schema + retry | Adopt white-label schema; remove comms §3 body |
| 3 | `integration_credentials` vs `adapter_credentials` | Pick one table name in platform spec #3 |
| 4 | `tax_rates` index owner "spec 8" | Correct to admin-dashboard (spec 8 in index = app-shell) |
| 5 | Upgrade modal planned vs written | Drop planned #36 or mark "merged into zync-subscription" |
| 6 | Password hashing: auth says Argon2id; settings profile says PBKDF2 | Pick one in auth spec; fix settings reference |

---

### 12. Add Verification section to every spec (minimum bar)

**Files:** All 34 specs  
**Problem:** No acceptance criteria → specs aren't test surfaces.  
**Solution:** Each spec ends with:

- 5–10 Given/When/Then bullets for core flows
- Explicit "Out of scope" (some specs already have this — standardize)
- Cross-spec checkpoints (e.g. "Disable invoices module → billing hard-dep cascade per module-management matrix")

**Benefits:** **The interface is the test surface** becomes literal. E2E plan can trace to spec IDs.

---

### 13. Re-layer platform specs in the index

**Proposed index order change:**

```
Layer 0  Foundation (unchanged)
Layer 1  System Core — ADD: module-management, outbound-events, integration-credentials,
                        storage-attachments, realtime, tier-entitlements, permissions-registry
         KEEP: i18n, communications (trimmed), AI
Layer 2  App Shells (unchanged)
Layer 3+ Product modules (unchanged)
```

Write new Layer 1 platform specs **before** implementing modules that duplicate their seams.

---

## Proposed Spec Dependency Graph (target state)

```
CONTEXT.md
    │
Foundation (monorepo, design-system, auth-rbac)
    │
System Core ─────────────────────────────────────────┐
    ├── i18n                                         │
    ├── integration-credentials                      │
    ├── outbound-events                              │
    ├── storage-attachments                          │
    ├── realtime                                     │
    ├── tier-entitlements                            │
    ├── permissions-registry                         │
    ├── module-management                            │
    ├── communications (email/notifications only)    │
    └── ai-assistant                                 │
    │                                                │
App Shells ◄─────────────────────────────────────────┘
    │
Product Modules (reference platform specs by link, not re-definition)
```

---

## Contradictions & Index Errors (action list)

| Item | Location | Fix |
|------|----------|-----|
| Global Telegram bot | `system-communications-notifications` L64, L279 | Remove; point to CRM per-tenant pattern |
| Webhook gateway duplicate | comms §3 vs white-label | Extract to `system-outbound-events`; delete duplicates |
| `tax_rates` owner "spec 8" | `00-index.md` L209 | Change to admin-dashboard |
| `tax-rates-seed-data.md` orphan | not in index | Appendix under admin-dashboard or i18n |
| PBKDF2 vs Argon2id | `settings-module` L149 vs auth | Align with auth spec |
| Index count 43 vs 34 files | index header | Reconcile planned vs written |
| Circular deps | white-label ↔ comms | Break via outbound-events owner |

---

## Patterns to Keep (do not refactor away)

1. **Core + adapters split** (invoices, payments, comms channels) — extend to other provider families, don't collapse.
2. **Architecture Decisions tables** — migrate key rows to `docs/adr/` over time, but keep per-spec summaries.
3. **Module registry with hard/soft deps** — deepen with link from each product spec's header: `Module ID: invoices`.
4. **Foundation advisor batch review** — apply same batch review to new Layer 1 platform specs before module implementation.
5. **Inline SQL in specs** — acceptable in Surface section once template split exists; consider generating Drizzle from spec later.

---

## Suggested Next Steps

1. **Immediate (before code):** Fix contradictions table (#11) — half-day editorial pass.
2. **Week 1:** Write `CONTEXT.md` + `_TEMPLATE.md` + re-layer index.
3. **Week 1–2:** Author 5 platform specs (#2–6, #9) by *relocating* existing content, not rewriting from scratch.
4. **Week 2:** Add Verification sections to foundation + system specs; propagate template to 2–3 pilot modules (invoices, tasks).
5. **Ongoing:** Module implementation blocked until its platform seams are linked, not duplicated.

---

## Which opportunities to explore first?

If prioritizing for maximum **leverage** with minimum writing:

| Priority | Opportunity | Why first |
|----------|-------------|-----------|
| P0 | #11 Contradiction fixes | Blocks correct first implementation |
| P0 | #1 CONTEXT.md | Cheap; improves all other work |
| P1 | #2 Outbound events | Highest conflict surface |
| P1 | #3 Integration credentials | Onboarding + settings + adapters all blocked |
| P1 | #4 Storage attachments | Prevents four duplicate upload paths |
| P2 | #6 Tier entitlements | Needed before subscription + shell upgrade UX ship |
| P2 | #7 Spec template | Scales quality across remaining modules |
| P3 | #8 Foundation deltas consolidation | Reduces ongoing maintenance pain |

---

*Generated from a full scan of `docs/specs/`. No application code was reviewed. ADRs do not exist yet; Architecture Decisions tables in individual specs are the de facto decision log.*
