references/COMPONENT-SKILLS.md
# Component Skills: Packaging Research as Agent Skills
Every non-trivial technical component identified during design gets its own Agent Skill. This file specifies **naming, directory layout, templates, and discovery semantics** so the skills are actually loaded when implementation begins.
## Why an Agent Skill (not a doc section)
A section inside `specs/design-{feature}.md` is read once — during planning. A file under `skills/tech-{component}/` is **auto-discovered** whenever the skill's `description` matches the current task. That means the research pays off on every future implementation, review, debug, or refactor that touches the component, not just on the day the design was written.
Trade-off: one more file per component. Acceptable, because the design doc stays under a reasonable size and each skill is independently updatable.
## Naming
```
skills/tech-{component}/SKILL.md
```
- Prefix `tech-` groups all component research digests.
- `{component}` is lowercase, hyphens only, reflects the user-facing name:
- `tech-redis-streams` (not `tech-redis`)
- `tech-fcm-android`
- `tech-apns-ios`
- `tech-sqlc`
- `tech-postgres-partial-indexes` — when the *feature*, not the whole product, is the point
- If multiple features share the same component, one skill is enough. Keep a single `tech-{component}` and update it; do not fork.
Name rules (inherited from the Agent Skill spec): lowercase, hyphens, no leading/trailing hyphen, no `--`, max 64 chars.
## Directory Layout
```
skills/tech-{component}/
├── SKILL.md # Required: ~300 lines max, core digest (L3')
└── references/ # Optional: deep reference (L4')
├── API.md # Full API surface if the SKILL.md subset is not enough
├── BENCHMARKS.md # Measurements collected during research
└── MIGRATION.md # Version-upgrade notes, if relevant
```
Keep `SKILL.md` focused on what an agent needs to **write correct code right now**. Anything needed only for review / debugging / migration goes to `references/`.
## SKILL.md Template
```markdown
---
name: tech-{component}
description: {Component} research digest. Use when implementing, reviewing, or debugging code that calls {component} — e.g., files under {path-pattern}, or tasks mentioning {keyword-pattern}.
---
# {Component Name} — Research Digest
## TL;DR
{2-3 sentences. What this component is, how we use it, the single most important trade-off or constraint.}
## Identity
- **Version**: {x.y.z} (verified YYYY-MM-DD)
- **License**: {SPDX id}
- **Docs**: {URL to authoritative reference}
- **Source**: {repo URL}
- **Minimum runtime**: {e.g., Go 1.22+}
## API We Use
The subset we actually call — not the whole library.
```{lang}
// exact signatures, copied from godoc / typedoc / official reference
```
## Operational Notes
- **Throughput**: {number or "not characterized — plan to measure"}
- **Latency**: {number + percentile}
- **Failure modes**: {what breaks, how it surfaces}
- **Retry semantics**: {what the component does vs. what we add}
- **Resource cost**: {memory / connections / file handles}
## Pitfalls
- **{Gotcha}** → {avoidance rule}. *Source: {doc anchor / issue URL}*.
- **{Gotcha}** → {avoidance rule}.
## Integration Pattern
```{lang}
// idiomatic wiring into the project's stack
```
Explain in 1-2 sentences why this shape, not another.
## Alternatives Considered
- **{Alternative}** — rejected because {reason}. (One line each; full rationale lives in the design doc's Alternatives section.)
## Confidence
- **High**: {what we trust fully, and why}
- **Medium**: {what we believe but could not fully verify}
- **Low**: {what we are guessing at; flag for staging verification}
## References
- [references/API.md](references/API.md) — extended API surface
- [references/BENCHMARKS.md](references/BENCHMARKS.md) — measurements
```
## Writing the `description` (Discovery-Critical)
The `description` field decides whether Claude loads this skill on a future task. Two things must be present:
1. **What the component is** — so the reader knows the skill is about `redis-streams`, not `redis` generally.
2. **When to use it** — either a path pattern (`files under internal/notification/sender/`) or a topic pattern (`tasks mentioning push notifications, FCM, or APNs`).
Good:
```yaml
description: Redis Streams research digest (v7.2). Covers XADD, XREADGROUP, XACK, XAUTOCLAIM, idle-pending recovery, and MAXLEN trimming. Use when implementing producers or consumers under internal/notification/consumer/ or any code calling go-redis Stream* methods.
```
Bad:
```yaml
description: Info about Redis. # Too vague — will not be discovered reliably.
```
Always third-person (the description is injected into the system prompt).
## Progressive Disclosure Policy
- **SKILL.md body**: ≤ ~300 lines. Agent implementation context. Loaded when the skill matches.
- **references/\*.md**: loaded on demand. Use for extended API tables, benchmark CSVs, migration playbooks.
- If SKILL.md grows past 300 lines, move sections to `references/` and replace them with a one-line pointer. The digest is a map, not the territory.
## Keeping Digests Fresh
Every SKILL.md has `Version` and `verified YYYY-MM-DD` in the Identity block. Treat as stale when:
- The component releases a new major version.
- A CVE is issued against the pinned version.
- The `verified` date is more than ~9 months old.
- An implementation agent observes behavior that contradicts the digest.
**Refresh in place** — update the existing skill rather than creating `tech-foo-v2`. Bump the `verified` date and, if behavior changed, add a "Changed since last review" subsection at the top.
## Linking From the Design Doc
The design doc's frontmatter enumerates all component skills:
```yaml
---
title: "Push Notifications - Technical Design"
component-skills:
- skills/tech-redis-streams/SKILL.md
- skills/tech-fcm-android/SKILL.md
- skills/tech-apns-ios/SKILL.md
---
```
The Decision Summary table cites the skill in the `Research` column, and each Component Overview entry has a `Research:` line pointing to its skill. This means reviewers can follow the trail: *design decision → research digest → primary source*.
## Linking From L2 Rules
When extracting L2 coding constraints, cite the component skill so agents can reach the rationale in one hop:
```markdown
<!-- .claude/rules/notification-service.md -->
---
paths:
- "internal/notification/**/*.go"
---
Notification service patterns:
- Queue via Redis Streams, never send inline in an HTTP handler.
Research: skills/tech-redis-streams/SKILL.md
- Use firebase-admin-go's Messaging client, not raw HTTP to FCM.
Research: skills/tech-fcm-android/SKILL.md
```
## Anti-Patterns
- **Writing a digest without a version.** Future agents cannot tell whether the API signatures are still current.
- **Copying the entire official docs.** The digest is the *subset we use*, with pitfalls we identified. A mirror of the docs is worse than a link to the docs.
- **Skipping the Confidence block.** Unstated uncertainty becomes stated certainty the next time someone reads it.
- **One skill per feature instead of per component.** If `feature A` and `feature B` both use Redis Streams, one `tech-redis-streams` skill serves both.
- **Prose where code belongs.** API signatures, integration snippets, and DDL all go in as code, not prose descriptions.
references/CONTEXT-LAYERS.md
# Technical Design Context Layer Mapping
Detailed guide on distributing technical design information across context layers.
## Layer Distribution for Design Documents
```
Token Cost
Layer Frequency Per Request Purpose
====================================================================
L1 Every req ~30 tokens Tech stack + feature ref
L2 Path match ~150 tokens Component-local patterns
L3 On demand ~800 tokens Architecture decisions (design doc)
L3' Skill match ~400 tokens Per-component research digest
(skills/tech-{component}/SKILL.md)
L4 Explicit ~500 tokens Rationale & alternatives (design doc)
L4' Explicit varies Component deep reference
(skills/tech-{component}/references/)
====================================================================
```
**L3 vs. L3':** L3 holds *architecture* — how the components fit together for *this feature*. L3' holds *component knowledge* — how a given technology works in general, verified against current docs. The design doc stays feature-scoped; the component skill stays technology-scoped. Both are auto-loadable without loading the other.
## L1: Tech Stack in Constitution
**What goes here:** Technology choices as single lines. The agent needs to know the stack to make consistent decisions.
```markdown
## Tech Stack
- Language: Go 1.23
- HTTP: Echo v4
- Database: PostgreSQL 16 via sqlc
- Queue: Redis Streams
- Frontend: templ + htmx
```
**Plus** a one-line reference to the design doc:
```markdown
## Active Specs
- `specs/design-notifications.md` - Notification architecture
```
**Note:** The PRD is an Agent Skill (`skills/prd-{feature}/SKILL.md`) and is auto-discovered via skill metadata — no L1 entry needed for it. Only the design doc needs a constitution reference.
**Why in L1:** Tech stack choices affect every coding decision. An agent must know "use sqlc, not raw SQL" on every request.
## L2: Component Patterns as Path Rules
**What goes here:** Architecture patterns that apply when editing specific code areas.
**Extraction process:**
1. Read the design doc's Component Overview
2. For each component with clear patterns, create an L2 rule
3. Include only actionable coding constraints, not rationale
**Example extraction:**
Design doc says:
```markdown
### Notification Sender
- **Responsibility**: Delivers push notifications via FCM/APNs
- **Location**: `internal/notification/sender/`
- **Interface**: `Send(ctx, userID, Notification) error`
- **Depends on**: `internal/notification/template/`, FCM SDK
```
Decision Summary says:
```markdown
| Queue | Redis Streams | Already in stack, sufficient throughput |
```
Extracted L2 rule:
```markdown
<!-- .claude/rules/notification-service.md -->
---
paths:
- "internal/notification/**/*.go"
---
Architecture patterns (specs/design-notifications.md):
- All notifications go through NotificationSender interface
- Never call FCM/APNs SDKs directly outside sender/
- Queue via Redis Streams; never send synchronously
- Use structured logging (slog) for all operations
- Template rendering in template/ package, not in sender/
```
### What to Extract vs. Keep in L3
| Extract to L2 | Keep in L3 |
|----------------|------------|
| "Use interface X" | Interface definition (code) |
| "Never call Y directly" | Why Y is abstracted |
| "Queue via Z" | Queue configuration details |
| "Pattern: repository" | Full component diagram |
| "Log with slog" | Logging strategy rationale |
**Rule of thumb:** L2 rules are **imperative commands** the agent follows while coding. L3 content is **reference material** the agent consults when planning.
## L3': Component Research Skills
Each non-trivial technical component identified during design lives at `skills/tech-{component}/SKILL.md`. Claude auto-discovers these via the skill `description` field — no explicit import needed when editing code that matches the skill's path / topic pattern.
**What goes in a component skill (L3'):**
- Identity: version, license, authoritative doc URL, verified date
- API subset the design actually calls (code, not prose)
- Operational characteristics (throughput, latency, failure modes)
- Pitfalls with avoidance rules
- Idiomatic integration snippet for the project's stack
- Confidence block (High / Medium / Low with reasons)
**What does NOT go in a component skill:**
- Feature-specific architecture (that is L3 in the design doc)
- Imperative path-conditional rules (those are L2)
- Whole-library documentation (link to upstream docs instead)
See [COMPONENT-SKILLS.md](COMPONENT-SKILLS.md) for the template, naming, and discovery rules.
**Why auto-discovery matters:** the design doc is read once when planning. The component skill is loaded every time the agent edits code in that component's area. L3' research therefore amortizes across every implementation session, review, and debug session for the lifetime of the component.
## L3: Design Body (Core Content)
**Ordering optimized for agent consumption:**
```
1. TL;DR (100 tokens)
Architecture approach in 2-3 sentences. Agent decides relevance.
2. Decision Summary Table (200 tokens)
Technology/pattern choices with one-phrase rationale.
Agent's PRIMARY reference when starting implementation.
3. Component Overview + Diagram (300 tokens)
What pieces exist, where they live, how they connect.
Agent uses Location field to find code.
4. Interface Contracts (300 tokens)
Actual code: type definitions, function signatures.
Agent implements directly against these.
5. Data Model (200 tokens)
DDL with constraints. Agent creates migrations from this.
6. Open Questions (50 tokens)
Unresolved decisions. Agent checks before asking user.
```
**Total L3 budget:** ~1150 tokens. Loaded once when agent starts working on the feature's architecture.
## L4: Deep Reference
**What goes here:**
- Alternatives considered (why NOT other approaches)
- Migration/rollout plan
- ADR (Architecture Decision Record) log
- Performance analysis
- Security considerations
**When agent loads L4:**
- Questioning a decision from the summary table
- Planning a migration strategy
- Encountering a performance issue
- Security review
## Decision Flow: Which Layer for Design Info?
```
Does the agent need this for ALL coding tasks?
├─ Yes → L1 (tech stack summary)
│ One line per technology choice
└─ No
│
Does this constrain how specific code is written?
├─ Yes → L2 (component pattern rules)
│ Imperative coding constraints
└─ No
│
Is this feature-specific architecture (how pieces fit together)?
├─ Yes → L3 (design body)
│ Decisions, components, interfaces, data model
└─ No
│
Is this per-technology knowledge verified via research?
├─ Yes → L3' (skills/tech-{component}/SKILL.md)
│ API subset, pitfalls, integration snippet
└─ No → L4 / L4' (deep reference)
Alternatives, migration, ADR, extended API tables
```
## Interface Contracts: The L3 Sweet Spot
Interface contracts are the most valuable L3 content. They bridge design decisions and implementation:
```go
// This is L3 content: the agent implements against this.
// It must be in the design doc, not extracted to L2.
type NotificationSender interface {
// Send delivers a notification to a single user.
// Returns ErrUserOptedOut if user disabled notifications.
// Returns ErrInvalidToken if device token is expired.
Send(ctx context.Context, userID string, n Notification) error
}
type Notification struct {
Title string // Short title (max 65 chars for APNs)
Body string // Message body (max 256 chars)
Data map[string]string // Custom key-value payload
Priority Priority // high | normal
}
```
**Why not L2?** Interface definitions are too detailed for path-conditional rules. They're reference material consulted during implementation, not constraints applied while editing.
**Why not L4?** They're the primary implementation specification. The agent needs them to write code, not just for clarification.
## Data Model: DDL as L3 Content
```sql
-- L3: Agent uses this to create migration files
CREATE TABLE notifications (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id),
title TEXT NOT NULL,
body TEXT NOT NULL,
channel TEXT NOT NULL CHECK (channel IN ('push', 'email')),
status TEXT NOT NULL DEFAULT 'pending'
CHECK (status IN ('pending', 'sent', 'failed', 'read')),
sent_at TIMESTAMPTZ,
read_at TIMESTAMPTZ,
error_msg TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- Include index strategy: agent needs this for query optimization
CREATE INDEX idx_notifications_user_unread
ON notifications(user_id, created_at DESC)
WHERE read_at IS NULL;
```
**L2 derivative:**
```markdown
<!-- .claude/rules/notification-queries.md -->
---
paths:
- "internal/notification/repository/**/*.go"
---
DB patterns (specs/design-notifications.md):
- Use sqlc-generated code; no hand-written SQL
- Use partial index idx_notifications_user_unread for unread queries
- All queries must accept context for cancellation
```
references/DEEP-RESEARCH.md
# Deep Research Methodology
How to investigate each technical component before committing to it in a design doc. The goal is a **verifiable, version-stamped digest** per component, not a blog-post pastiche.
## Principles
1. **Primary sources first.** Official docs, source repos, RFCs, specs. Blog posts and Q&A sites are accepted only as evidence of observed behavior or a specific workaround — never as the sole basis for a design decision.
2. **Version-stamp every claim.** A behavior that was true in `v1.4.0` may have changed by `v2.0.0`. A claim without a version has a hidden expiration date.
3. **Research what you will actually touch.** Do not document the whole library. Document the subset the design relies on, plus its nearest pitfalls.
4. **Record the date.** Include `as of YYYY-MM-DD` in each digest so later readers can judge freshness.
5. **Parallelize.** Components are independent — research them in parallel via Agent subagents.
## Source Priority
```
Tier 1 (trust, cite) Tier 2 (cross-check) Tier 3 (evidence only)
=====================================================================================
Official documentation Release notes / changelogs Blog posts
Source code / godoc RFCs / IETF specs Stack Overflow answers
Maintainer-written READMEs Vendor advisories Conference talks
Test suites in the repo Well-known benchmark suites Community tutorials
```
When Tier 1 and Tier 3 disagree, trust Tier 1. When Tier 1 is silent on a detail, Tier 3 is acceptable **provided** the digest marks the claim as "observed, not specified."
## Per-Category Research Checklists
### Library / Framework (Go, Node, Python, ...)
- [ ] Latest stable version + release date
- [ ] License (compatible with the project?)
- [ ] Maintenance signal: last release, open-vs-closed issues, active maintainers
- [ ] Minimum supported runtime version (Go `go` directive, Node engines, Python `requires-python`)
- [ ] Public API surface we will call — record *actual* signatures from godoc/typedoc
- [ ] Error model (sentinel errors? typed errors? panics?)
- [ ] Concurrency model (goroutine-safe? thread-safe? re-entrant?)
- [ ] Extension points (interfaces / hooks we plan to implement)
- [ ] Known CVEs in the version range we target
- [ ] Deprecated APIs to avoid
- [ ] Idiomatic setup snippet (what does "hello world" look like in *this* codebase's stack?)
### Database / Storage Engine
- [ ] Server version + the SQL / query feature we rely on (partial indexes, GIN, JSONB ops, window funcs, ...)
- [ ] When that feature was introduced (so L1 tech stack can pin a minimum version)
- [ ] Transaction / isolation behavior for the pattern we use
- [ ] Indexing strategy for the queries in the Data Model section
- [ ] Lock-table or long-query considerations (online DDL? pg_repack? ...)
- [ ] Backup / replication implications
- [ ] Extension dependencies (pgcrypto for `gen_random_uuid()`, etc.)
### Protocol / External Service (FCM, APNs, OAuth, payment, ...)
- [ ] Current protocol version / API version we target
- [ ] Auth model (service account, OAuth2, mTLS, HMAC signing, ...)
- [ ] Rate limits and quotas — actual numbers, not "has rate limits"
- [ ] Retry policy mandated or recommended by the provider
- [ ] Error taxonomy: which errors are retryable? which are permanent?
- [ ] Payload size limits
- [ ] Regional / data-residency constraints (GDPR, SOC2, ...)
- [ ] Deprecation schedule of the endpoint we use
### Infrastructure Primitive (Redis data structure, Kafka topic, ...)
- [ ] Server version + data structure's semantic guarantees (ordering, at-least-once, ...)
- [ ] Memory / disk footprint model
- [ ] Persistence guarantees (AOF, RDB, replication, ...)
- [ ] Failure modes: what happens on node loss, split brain, full disk?
- [ ] Monitoring surface (what metric tells us it is unhealthy?)
- [ ] Capacity planning: back-of-envelope for our load
### Language Feature (Go 1.23 range-over-func, Python 3.12 PEP-695, ...)
- [ ] Version introduced and stability status (experimental? stable?)
- [ ] Behavior differences vs. the prior idiom
- [ ] Tooling support (linter, formatter, IDE)
- [ ] Performance delta if claimed
## Research Tool Playbook
### WebSearch
- Always include the current year in the query (`"redis streams XAUTOCLAIM 2026"`).
- Search for version-specific docs: `site:pkg.go.dev/sideshow/apns2`.
- Use the `allowed_domains` parameter for known-good sources when available.
### WebFetch
- Use it on the **specific** doc page you already located, not on a homepage.
- Give the fetch prompt a concrete extraction goal: "Return the HTTP status codes FCM returns for permanent failures, verbatim from this page." Generic prompts yield generic answers.
- For pages behind auth (GitHub private repos, Confluence), prefer the appropriate MCP tool or `gh api` instead.
### Agent (general-purpose)
Delegate one research subagent per component when they are independent. Launch them in a single message so they run in parallel.
Prompt template for a component-research subagent:
```
Research {component} for use in {feature-area} on {stack}.
Goal: produce a digest suitable for skills/tech-{component}/SKILL.md.
Cover:
1. Current stable version + release date + doc URL.
2. The subset of the API we will call: {list of operations, e.g. "publish to stream, read via consumer group, ack, claim idle pending"}.
3. Operational characteristics relevant to our scale: {throughput, latency, failure modes}.
4. Top 3 pitfalls with concrete avoidance advice.
5. Idiomatic integration snippet for {language / framework}.
Constraints:
- Primary sources only (official docs, source repo). Mark any Tier-3 claim as "observed".
- Version-stamp every claim.
- Return in the skills/tech-{component}/SKILL.md template structure — do not write the file yet.
- Under 600 words in the digest body.
```
### Explore (subagent)
Use when the research is *inside the codebase*: "do we already wire up a Redis client? where?". This prevents re-researching patterns the repo already enforces.
## When to Skip Research
Not every mention of a technology needs its own digest. Skip when:
- The component is already in L1 tech stack and the feature uses it in the ordinary way (no new API surface).
- Usage is trivial glue (a `strings.TrimSpace`, a `context.WithTimeout`).
- An existing `skills/tech-{component}/SKILL.md` is still fresh (verify the `last-updated` frontmatter — treat stale after ~9 months or on a major version bump).
When a digest exists but is stale, **refresh it** — do not fork a second digest.
## Handling Contradictions
If two sources disagree:
1. If one is Tier 1 and the other is Tier 3, trust Tier 1 and move on.
2. If both are Tier 1 (e.g., docs say X, source code does Y), treat it as a blocker and call it out in the digest's "Pitfalls" section. Agents implementing later need to know the documented behavior is not the actual behavior.
3. If the disagreement is about performance / ops characteristics, prefer the one that includes a measurement over the one that does not.
## Recording Uncertainty
Every digest has a Confidence block. Example:
```markdown
## Confidence
- High: version, API signatures, auth model (from official docs).
- Medium: throughput number (from maintainer blog, no independent benchmark).
- Low: behavior under network partition (no primary source found — plan to verify in staging).
```
Do not pretend to know what the sources did not tell you. A clearly marked "Low" is more useful to future agents than a confident but wrong claim.
references/EXAMPLES.md
# Technical Design Examples
## Example: Notification System Architecture
```markdown
---
title: "Push Notifications - Technical Design"
status: approved
prd: skills/prd-notifications/SKILL.md
component-skills:
- skills/tech-redis-streams/SKILL.md
- skills/tech-fcm-android/SKILL.md
- skills/tech-apns-ios/SKILL.md
- skills/tech-sqlc/SKILL.md
last-updated: 2026-03-01
---
# Push Notifications - Technical Design
## TL;DR
Event-driven architecture using Redis Streams for async notification
delivery. Order status changes publish events; a background consumer
processes them through FCM/APNs. Chosen for simplicity (Redis already
in stack) over adding a dedicated message broker.
## Decision Summary
| Decision | Choice | Rationale | Research |
|----------|--------|-----------|----------|
| Async mechanism | Redis Streams | Already in infrastructure, XREADGROUP for consumer groups | `skills/tech-redis-streams/` |
| Android push | FCM via firebase-admin-go | Official SDK, reliable delivery | `skills/tech-fcm-android/` |
| iOS push | APNs via sideshow/apns2 | Lightweight, well-maintained | `skills/tech-apns-ios/` |
| Token storage | New device_tokens table | Normalized, supports multi-device | — (no external component) |
| Retry strategy | Exponential backoff, max 3 | Prevents thundering herd on provider outage | `skills/tech-fcm-android/`, `skills/tech-apns-ios/` |
| Template engine | Go text/template | Simple, no external dependency needed | — (stdlib) |
## Component Overview
```text
┌─────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ Order │══> │ Redis Stream │══> │ Notification │
│ Handler │ │ "notifications" │ │ Consumer │
└─────────────┘ └──────────────────┘ └────────┬────────┘
│
┌────────▼────────┐
│ Notification │
│ Sender │
├─────────────────┤
│ ┌─────┐ ┌─────┐ │
│ │ FCM │ │APNs │ │
│ └─────┘ └─────┘ │
└────────┬────────┘
│
┌────────▼────────┐
│ PostgreSQL │
│ (notifications) │
└─────────────────┘
```
### Order Handler (existing, modified)
- **Responsibility**: Publishes status change events to Redis Stream
- **Location**: `internal/handler/order.go`
- **Changes**: Add event publishing after status update
- **Depends on**: Redis client
### Notification Consumer (new)
- **Responsibility**: Reads events from stream, orchestrates delivery
- **Location**: `internal/notification/consumer/`
- **Interface**: `Run(ctx context.Context) error` (blocking, long-lived)
- **Depends on**: NotificationSender, NotificationRepository
- **Research**: `skills/tech-redis-streams/SKILL.md`
### Notification Sender (new)
- **Responsibility**: Delivers notifications via FCM/APNs
- **Location**: `internal/notification/sender/`
- **Interface**: See Interface Contracts below
- **Depends on**: FCM SDK, APNs client, DeviceTokenRepository
- **Research**: `skills/tech-fcm-android/SKILL.md`, `skills/tech-apns-ios/SKILL.md`
### Notification Repository (new)
- **Responsibility**: CRUD for notification records
- **Location**: `internal/notification/repository/`
- **Interface**: sqlc-generated from queries
- **Depends on**: PostgreSQL
- **Research**: `skills/tech-sqlc/SKILL.md`
### Device Token Repository (new)
- **Responsibility**: Manage user device tokens
- **Location**: `internal/notification/device/`
- **Interface**: sqlc-generated from queries
- **Depends on**: PostgreSQL
- **Research**: `skills/tech-sqlc/SKILL.md`
## Interface Contracts
### NotificationSender
```go
package sender
type Sender interface {
Send(ctx context.Context, userID string, n Notification) error
SendBatch(ctx context.Context, reqs []SendRequest) []Result
}
type Notification struct {
Title string
Body string
Data map[string]string
Priority Priority
}
type Priority string
const (
PriorityHigh Priority = "high"
PriorityNormal Priority = "normal"
)
type SendRequest struct {
UserID string
Notification Notification
}
type Result struct {
UserID string
Error error
}
```
### Event Schema (Redis Stream)
```json
{
"event_id": "string (UUID)",
"event_type": "order.status_changed",
"order_id": "string (UUID)",
"user_id": "string (UUID)",
"old_status": "string",
"new_status": "string",
"changed_at": "string (ISO8601)",
"metadata": {
"tracking_number": "string (optional)"
}
}
```
### Consumer -> Sender Flow
```go
// Pseudocode for consumer processing loop
func (c *Consumer) processEvent(ctx context.Context, event Event) error {
// 1. Check user preferences
prefs, err := c.userRepo.GetNotificationPrefs(ctx, event.UserID)
if err != nil { return err }
if !prefs.PushEnabled { return nil } // skip silently
// 2. Render notification from template
notif, err := c.tmpl.Render(event.EventType, event)
if err != nil { return err }
// 3. Send via sender (handles FCM/APNs routing)
err = c.sender.Send(ctx, event.UserID, notif)
// 4. Record result
return c.notifRepo.Create(ctx, ...)
}
```
## Data Model
```sql
-- Device tokens for push notification delivery
CREATE TABLE device_tokens (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id),
token TEXT NOT NULL,
platform TEXT NOT NULL CHECK (platform IN ('android', 'ios')),
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE (user_id, token)
);
CREATE INDEX idx_device_tokens_user ON device_tokens(user_id);
-- Notification delivery records
CREATE TABLE notifications (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id),
event_id UUID NOT NULL,
title TEXT NOT NULL,
body TEXT NOT NULL,
channel TEXT NOT NULL CHECK (channel IN ('push')),
status TEXT NOT NULL DEFAULT 'pending'
CHECK (status IN ('pending', 'sent', 'failed')),
sent_at TIMESTAMPTZ,
error_msg TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX idx_notifications_user ON notifications(user_id, created_at DESC);
CREATE INDEX idx_notifications_pending ON notifications(status, created_at)
WHERE status = 'pending';
```
### Entity Relationships
```text
User 1──* DeviceToken
User 1──* Order 1──* StatusChangeEvent
StatusChangeEvent 1──* Notification
```
## Open Questions
- [ ] Should we add a dead letter stream for failed notifications
after max retries? (@backend-lead)
- [x] Consumer group name? -> Decision: "notification-workers"
- [x] Stream MAXLEN? -> Decision: 100000 (auto-trimmed)
---
<!-- Below this line = L4 (deep reference) -->
## Alternatives Considered
### Alternative 1: PostgreSQL LISTEN/NOTIFY
- **Approach**: Use PG notifications instead of Redis Streams
- **Pros**: No additional infrastructure, transactional consistency
- **Cons**: No persistence of events, lost on crash, no consumer groups
- **Rejected because**: Need durable event processing with retry capability
### Alternative 2: Dedicated Message Broker (RabbitMQ/NATS)
- **Approach**: Add a message broker for event-driven processing
- **Pros**: Purpose-built, rich routing, better monitoring
- **Cons**: New infrastructure to maintain, operational overhead
- **Rejected because**: Redis Streams sufficient for current scale (< 1000 events/sec),
avoids adding infrastructure complexity
### Alternative 3: Synchronous with Goroutine Pool
- **Approach**: Fire-and-forget goroutines in HTTP handler
- **Pros**: Simplest implementation, no queue
- **Cons**: Lost notifications on server restart, no retry, hard to monitor
- **Rejected because**: Violates FR-1 (delivery guarantee) and NFR-2 (reliability)
## Migration Plan
1. Create database tables (rollback: drop tables)
2. Deploy notification consumer as background goroutine in existing server
(rollback: remove consumer startup)
3. Add event publishing to order handler behind feature flag
(rollback: disable flag)
4. Enable feature flag in staging, verify end-to-end
(rollback: disable flag)
5. Enable in production with monitoring alerts
(rollback: disable flag)
## ADR Log
| Date | Decision | Context | Consequences |
|------|----------|---------|-------------|
| 2026-02-15 | Redis Streams over PG LISTEN/NOTIFY | Need durable events | Depends on Redis availability |
| 2026-02-18 | Separate device_tokens table | Multi-device support | Additional table to maintain |
| 2026-02-20 | Feature flag for rollout | Risk mitigation | Flag cleanup needed after full rollout |
```
## Example: Component Research Skill (`skills/tech-redis-streams/SKILL.md`)
This is what a digest produced by the Deep Research phase looks like. The design doc above references this skill; Claude auto-loads it when editing files under `internal/notification/consumer/` or anywhere that calls `go-redis` Stream methods.
```markdown
---
name: tech-redis-streams
description: Redis Streams research digest (server v7.2, go-redis/v9). Covers XADD, XREADGROUP, XACK, XAUTOCLAIM, idle-pending recovery, MAXLEN trimming, and consumer-group failure modes. Use when implementing or reviewing producers / consumers under internal/notification/consumer/ or any code calling go-redis Stream* methods.
---
# Redis Streams — Research Digest
## TL;DR
Redis Streams provides durable, ordered, at-least-once delivery with
consumer groups via XREADGROUP. For our push-notification pipeline it
replaces a message broker at zero infra cost. Main trade-off: at-least-once
means consumers must be idempotent, and pending entries require periodic
XAUTOCLAIM sweeps to survive consumer crashes.
## Identity
- **Version**: Redis server 7.2.x; go-redis v9.5.x (verified 2026-02-28)
- **License**: Redis is under RSALv2/SSPLv1 (7.4+); 7.2 under BSD. go-redis: BSD-2.
- **Docs**: https://redis.io/docs/latest/develop/data-types/streams/
- **Source**: https://github.com/redis/go-redis
- **Minimum runtime**: Go 1.22+ for go-redis v9.5
## API We Use
```go
// Producer
xadd := rdb.XAdd(ctx, &redis.XAddArgs{
Stream: "notifications",
MaxLen: 100_000, // approximate trim; use `~` semantics
Approx: true,
Values: map[string]any{"event": payloadJSON},
})
// Consumer group bootstrap (idempotent)
_ = rdb.XGroupCreateMkStream(ctx, "notifications", "notification-workers", "$").Err()
// Consumer read
res, err := rdb.XReadGroup(ctx, &redis.XReadGroupArgs{
Group: "notification-workers",
Consumer: consumerID,
Streams: []string{"notifications", ">"},
Count: 16,
Block: 5 * time.Second,
}).Result()
// Ack after processing
_ = rdb.XAck(ctx, "notifications", "notification-workers", entry.ID).Err()
// Recover entries abandoned by crashed consumers
claimed, _, err := rdb.XAutoClaim(ctx, &redis.XAutoClaimArgs{
Stream: "notifications",
Group: "notification-workers",
Consumer: consumerID,
MinIdle: 60 * time.Second,
Start: "0-0",
Count: 50,
}).Result()
```
## Operational Notes
- **Throughput**: ~1M ops/sec on a single-node 7.2 (Redis official benchmark).
Our target: <1k events/sec — three orders of magnitude of headroom.
- **Latency**: sub-millisecond XADD/XACK locally; p99 ~2ms across VPC.
- **Failure modes**:
- Consumer crash → entries stay in PEL (Pending Entries List) until
XAUTOCLAIM picks them up. Run sweeper every 30s with MinIdle=60s.
- Redis failover → in-flight XREADGROUP returns with partial data;
consumer must tolerate duplicate delivery (idempotency key = event_id).
- **Retry semantics**: none built-in. Our consumer increments a delivery
counter per entry; after 3 attempts it moves the entry to a dead-letter
stream via XADD + XACK on the original.
- **Resource cost**: ~100 bytes/entry + field overhead. With MaxLen=100k,
budget ~30 MB steady-state.
## Pitfalls
- **MAXLEN without `~` is O(N).** Always set `Approx: true` (`MAXLEN ~ N`)
for amortized O(1) trimming. *Source: Redis XADD docs, "Capped streams" section.*
- **`$` on XGroupCreate only reads new messages.** If the stream exists
and has backlog you want to process, use `"0"` instead of `"$"` on first
bootstrap. *Source: Redis XGROUP CREATE semantics.*
- **XREADGROUP `>` vs. explicit ID.** `>` reads only never-delivered
entries. Using an explicit ID re-reads from PEL — useful for recovery,
but easy to confuse with the normal path.
- **PEL growth under persistent consumer failure.** Monitor
XPENDING summary; alert when idle PEL > threshold.
- **go-redis v9 renamed several XAdd options.** If porting v8 code,
`MaxLenApprox` → `MaxLen + Approx: true`.
## Integration Pattern
```go
// internal/notification/consumer/consumer.go
type Consumer struct {
rdb redis.UniversalClient
sender sender.Sender
notifRepo NotificationRepository
id string // e.g., hostname + pid
}
func (c *Consumer) Run(ctx context.Context) error {
if err := c.ensureGroup(ctx); err != nil { return err }
go c.sweepStale(ctx) // XAUTOCLAIM loop
for {
if err := ctx.Err(); err != nil { return err }
if err := c.readOnce(ctx); err != nil {
slog.Error("stream read failed", "err", err)
time.Sleep(time.Second) // backoff
}
}
}
```
Consumers run as goroutines inside the main server binary. We considered
a separate worker binary; rejected for ops simplicity at current scale.
## Alternatives Considered
- **PostgreSQL LISTEN/NOTIFY** — no durable backlog; events lost on crash.
- **RabbitMQ / NATS** — new infra to operate; overkill at <1k events/sec.
- **Redis Pub/Sub** — fire-and-forget; no persistence; wrong primitive.
## Confidence
- **High**: API signatures, MAXLEN semantics, PEL behavior — all confirmed
against official docs and go-redis v9.5 source.
- **Medium**: VPC-local p99 latency figure — from a single staging
measurement, not a sustained benchmark.
- **Low**: behavior under Redis cluster failover — we run single-node in
prod today; plan to re-verify if we ever move to cluster mode.
## References
- [references/PEL-RECOVERY.md](references/PEL-RECOVERY.md) — sweeper tuning
and measured recovery times.
- [references/FAILOVER-NOTES.md](references/FAILOVER-NOTES.md) — behavior
under RDB snapshot + replica promotion.
```
The corresponding L2 rule that extracts the imperative constraints:
```markdown
<!-- .claude/rules/notification-consumer.md -->
---
paths:
- "internal/notification/consumer/**/*.go"
---
Notification consumer patterns (see skills/tech-redis-streams/):
- Always pass `Approx: true` when setting XAdd MaxLen.
- Use `">"` in XReadGroup for normal path; explicit ID only for PEL recovery.
- Run XAutoClaim sweeper every 30s with MinIdle=60s.
- Treat delivery as at-least-once: consumer handlers must be idempotent
(key on event_id from the payload).
- After 3 attempts, XAdd to `notifications-dlq` then XAck the original.
```
SKILL.md
---
name: writing-technical-design
description: Creates agent-optimized technical design documents backed by deep research of every technical component. Each component (library, framework, protocol, service) is investigated via web search / official docs and distilled into its own Agent Skill under skills/tech-{component}/, so future implementation sessions auto-load the relevant knowledge. Use when writing technical designs, architecture docs, defining system components, or making technology choices for spec-driven development.
---
# Writing Technical Design Documents
Create a technical design doc **plus** a set of per-component Agent Skills that capture the deep-research findings used to justify each technology choice. Implementation-time agents then auto-discover only the component skills relevant to the file they are editing.
**Use this skill when** designing how to build a feature, documenting architecture decisions, or making technology choices for spec-driven development.
**Supporting files:**
- [DEEP-RESEARCH.md](references/DEEP-RESEARCH.md) — research methodology, sources, per-category checklists.
- [COMPONENT-SKILLS.md](references/COMPONENT-SKILLS.md) — how to package each researched component as an Agent Skill.
- [CONTEXT-LAYERS.md](references/CONTEXT-LAYERS.md) — layer mapping details.
- [EXAMPLES.md](references/EXAMPLES.md) — complete design doc + component skill examples.
## Outputs
Running this skill produces **two kinds of artifacts**:
```
specs/design-{feature}.md # The design doc (L3 core + L4 rationale)
skills/tech-{component-1}/SKILL.md # Auto-discovered research digest per component
skills/tech-{component-2}/SKILL.md
skills/tech-{component-n}/SKILL.md
```
Each `tech-{component}` skill is a **first-class Agent Skill** — Claude loads it automatically when the implementation task touches that component. The design doc itself stays slim: it points to the component skills instead of inlining their contents.
## Context Layer Distribution
```
Layer What goes here File location
============================================================================
L1 Tech stack summary + feature ref CLAUDE.md / AGENTS.md (constitution)
"Go 1.23, Echo v4, PostgreSQL 16, sqlc"
"specs/design-notifications.md - Notification architecture"
L2 Component-local coding constraints .claude/rules/ or .github/instructions/
"Handlers in this dir use async sender interface"
L3 Design body (this doc) specs/design-{feature}.md
Decision summary, component overview, interfaces, data model
L3' Component research digests skills/tech-{component}/SKILL.md
Auto-discovered via skill metadata when editing related code
L4 Deep reference specs/design-{feature}.md (lower sections)
Alternatives considered, migration plan, ADR rationale
L4' Component deep reference skills/tech-{component}/references/*.md
API surface, edge cases, benchmark notes
============================================================================
```
## Workflow
```
1. Read the approved feature spec (skills/prd-{feature}/SKILL.md)
2. Draft the Decision Summary: list candidate technologies per decision area
3. DEEP RESEARCH each candidate and each confirmed component
→ see "Deep Research Phase" below
4. Record findings as skills/tech-{component}/SKILL.md (one skill per component)
5. Write the design doc (specs/design-{feature}.md) referencing those skills
6. Extract L2 coding constraints to .claude/rules/
7. Set status: "draft" → review → "approved"
```
## Deep Research Phase
This is the part that distinguishes this skill from a plain "write an architecture doc" prompt. **Do not skip it.** A design that names technologies without verifying their current behavior has an expiry date measured in months.
### Step 1 — Enumerate components to research
From the draft Decision Summary, extract every non-trivial technical element:
- Languages / runtimes (only if a version-specific feature is load-bearing)
- Frameworks and libraries (HTTP, ORM/query builder, template, queue client, ...)
- Databases and storage engines (features, version-specific SQL, index types)
- Protocols / external services (FCM, APNs, OAuth providers, payment gateways)
- Cross-cutting infrastructure (tracing, logging, feature flags)
Skip: generic primitives already covered by L1 tech stack, trivial glue code, anything the team has deep institutional knowledge of.
### Step 2 — Research each component
For each component, answer the checklist in [DEEP-RESEARCH.md](references/DEEP-RESEARCH.md). In short:
1. **Identity & version** — current stable version, release date, support status.
2. **Authoritative docs** — fetch the official reference, not blog posts.
3. **API surface you will actually use** — function/struct names, options, error types.
4. **Operational characteristics** — throughput, latency, memory, failure modes.
5. **Known pitfalls** — deprecated patterns, breaking changes, surprising defaults.
6. **Integration pattern** — idiomatic way to wire it into the stack chosen at L1.
7. **Alternatives rejected** — what else was on the table and why not.
Tool usage:
- Use **WebSearch** to locate current docs and recent release notes (always query with the current year).
- Use **WebFetch** to pull specific pages (official docs, RFCs, godoc, pkg pages) and summarize.
- Use the **Agent** tool (`subagent_type: general-purpose`) to parallelize research across components — launch one agent per component in a single message when they are independent. Instruct each agent to return a structured summary matching the skill template below.
- Prefer primary sources (official docs, source repos, RFCs) over secondary ones (blog posts, Stack Overflow). Blog posts are only acceptable when they describe measured behavior or a bug workaround.
- If a claim depends on version, state the version explicitly. Version-less claims rot.
### Step 3 — Package each component as an Agent Skill
Write one skill per researched component at `skills/tech-{component}/SKILL.md`. The detailed template and naming rules live in [COMPONENT-SKILLS.md](references/COMPONENT-SKILLS.md); minimal shape:
```markdown
---
name: tech-{component}
description: {One sentence on what the component is + when Claude should load it — e.g., "Use when implementing code under internal/notification/sender/ or any FCM/APNs delivery path."}
---
# {Component} — Research Digest
## TL;DR
{2-3 sentences on how we use it here and the single biggest trade-off.}
## Version & Source
- Version: {x.y.z} (as of YYYY-MM-DD)
- Docs: {URL}
- Repo: {URL}
## API We Use
{Code block: the exact functions / types / options we plan to call.}
## Operational Notes
- Throughput / latency expectations
- Failure modes and retry semantics
- Resource costs
## Pitfalls
- {Concrete gotcha} → {how we avoid it}
## Integration Pattern
{Snippet showing how it plugs into our stack.}
## References
- [references/API.md](references/API.md) — extended API surface (L4)
- [references/BENCHMARKS.md](references/BENCHMARKS.md) — measurements, if collected
```
**Why a skill, not a section in the design doc:** the design doc is read once during planning. The component skill is auto-loaded every time an agent edits code in that component's area, so the research pays off on every future implementation session.
## Template: `specs/design-{feature-name}.md`
The design doc itself stays lean. It **links to** the component skills instead of re-stating their contents.
```markdown
---
title: "Feature Name - Technical Design"
status: draft | review | approved | implementing | done
prd: skills/prd-feature-name/SKILL.md
component-skills:
- skills/tech-redis-streams/SKILL.md
- skills/tech-fcm-android/SKILL.md
- skills/tech-apns-ios/SKILL.md
last-updated: YYYY-MM-DD
---
# Feature Name - Technical Design
## TL;DR
[2-3 sentences: Architecture approach and key trade-off.]
## Decision Summary
| Decision | Choice | Rationale | Research |
|----------|--------|-----------|----------|
| Async mechanism | Redis Streams | Already in stack, consumer groups | skills/tech-redis-streams/ |
| Android push | FCM (firebase-admin-go) | Official SDK | skills/tech-fcm-android/ |
| iOS push | APNs (sideshow/apns2) | Lightweight, maintained | skills/tech-apns-ios/ |
## Component Overview
```text
[ASCII diagram: components and data flow]
Arrows: ──> sync ══> async ··> optional
```
### [Component Name]
- **Responsibility**: [Single sentence]
- **Location**: `path/to/component/`
- **Interface**: [Key method signatures]
- **Depends on**: [Other components]
- **Research**: `skills/tech-{name}/SKILL.md`
## Interface Contracts
```go
type OrderService interface {
CreateOrder(ctx context.Context, req CreateOrderRequest) (*Order, error)
GetOrder(ctx context.Context, id string) (*Order, error)
}
```
## Data Model
```sql
CREATE TABLE orders (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id),
status TEXT NOT NULL DEFAULT 'pending'
CHECK (status IN ('pending','processing','shipped','delivered')),
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
```
## Open Questions
- [ ] [Unresolved technical question] (@owner)
---
<!-- Below this line = L4 (deep reference) -->
## Alternatives Considered
### [Alternative Name]
- **Approach**: [Description]
- **Rejected because**: [Specific reason tied to requirements]
- **Research note**: see `skills/tech-{alternative}/SKILL.md` if a digest was produced
## Migration Plan
1. [Step] (rollback: [how to undo])
## ADR Log
| Date | Decision | Context | Consequences |
|------|----------|---------|--------------|
| YYYY-MM-DD | [What was decided] | [Why] | [Impact] |
```
## Writing Guidelines
### Decision Summary: Always Link to Research
Every non-trivial choice cites the component skill that backs it. Reviewers (and future agents) can follow the link to see *why* this version / library / pattern won, without the design doc bloating.
### Interface Contracts: Write as Code
Agents implement against interface contracts. Use real type definitions, not prose — prose requires interpretation, code is unambiguous.
### Data Model: DDL as Source of Truth
Write data models as executable DDL with CHECK constraints and indexes. Agents use these directly to create migration files.
### ASCII Diagrams
```text
──> synchronous call
══> asynchronous (event/queue)
··> optional/conditional
─X─ blocked/denied
```
## L2 Integration: Extracting Component Rules
After the design is approved, pull the **imperative** constraints (not the rationale) into path-conditional rules:
```markdown
<!-- .claude/rules/notification-service.md -->
---
paths:
- "internal/notification/**/*.go"
---
Notification service patterns (see specs/design-notifications.md and skills/tech-redis-streams/):
- Use NotificationSender interface for all delivery
- Never call FCM/APNs directly; go through sender abstraction
- Queue notifications via Redis Streams, never send in HTTP handler
```
The L2 rule links back to the component skill so that if the agent needs *why*, one hop reaches the research digest.
## Quality Checklist
```
Technical Design Quality Check:
- [ ] Links to feature spec PRD skill in frontmatter (prd field)
- [ ] component-skills list in frontmatter enumerates every tech-{x} skill produced
- [ ] TL;DR states architecture approach and key trade-off
- [ ] Decision Summary row cites a skills/tech-{x}/ digest for every non-trivial choice
- [ ] Every component has location, responsibility, interface, research link
- [ ] Interface contracts are code, not prose
- [ ] Data model is DDL with constraints
- [ ] ASCII diagram shows component relationships
- [ ] No requirements in this doc (those belong in feature spec)
- [ ] L4 separator between core design and deep reference
- [ ] Component patterns extracted to L2 rules
- [ ] Each tech-{x} skill has: version, authoritative doc URL, API we use, pitfalls
- [ ] Research claims state the version they were verified against
- [ ] File named: specs/design-{feature-name}.md
```
## Detailed Guides
- Research methodology and per-category checklists: [DEEP-RESEARCH.md](references/DEEP-RESEARCH.md)
- Component skill packaging (templates, naming, progressive disclosure): [COMPONENT-SKILLS.md](references/COMPONENT-SKILLS.md)
- Context layer mapping: [CONTEXT-LAYERS.md](references/CONTEXT-LAYERS.md)
- Complete design doc + component skill examples: [EXAMPLES.md](references/EXAMPLES.md)