Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
},
{
"name": "trogonstack-nats",
"description": "NATS messaging skills for designing subject hierarchies with naming conventions and wildcard patterns",
"description": "NATS channel design across Core NATS and JetStream, including AsyncAPI fragments, NACK-managed streams, optional consumers, tenancy, authorization, and migrations",
"source": "./plugins/trogonstack-nats",
"category": "development"
},
Expand Down
2 changes: 1 addition & 1 deletion .cursor-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
{
"name": "trogonstack-nats",
"source": "plugins/trogonstack-nats",
"description": "NATS messaging skills for designing subject hierarchies with naming conventions and wildcard patterns"
"description": "NATS channel design across Core NATS and JetStream, including AsyncAPI fragments, NACK-managed streams, optional consumers, tenancy, authorization, and migrations"
},
{
"name": "trogonstack-datadog",
Expand Down
6 changes: 6 additions & 0 deletions .github/workflows/validate-plugins.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,9 @@ jobs:
run: npm install --no-save ajv@8 ajv-formats@3
- name: Validate plugin registrations
run: node scripts/validate-plugins.mjs
- name: Validate tagged NATS subject examples
run: |
node plugins/trogonstack-nats/skills/nats-design-subject/scripts/validate-subjects.mjs \
--root plugins/trogonstack-nats/skills/nats-design-subject
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
node plugins/trogonstack-nats/skills/nats-design-subject/scripts/validate-subjects.mjs \
--file plugins/trogonstack-nats/skills/nats-design-subject/references/subject-fixtures.yaml
2 changes: 1 addition & 1 deletion plugins/trogonstack-nats/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "trogonstack-nats",
"description": "NATS messaging skills for designing subject hierarchies with naming conventions and wildcard patterns",
"description": "NATS channel design across Core NATS and JetStream, including AsyncAPI fragments, NACK-managed streams, optional consumers, tenancy, authorization, and migrations",
"version": "0.2.0",
"author": {
"name": "TrogonStack",
Expand Down
2 changes: 1 addition & 1 deletion plugins/trogonstack-nats/.cursor-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "trogonstack-nats",
"displayName": "TrogonStack NATS",
"description": "NATS messaging skills for designing subject hierarchies with naming conventions and wildcard patterns",
"description": "NATS channel design across Core NATS and JetStream, including AsyncAPI fragments, NACK-managed streams, optional consumers, tenancy, authorization, and migrations",
"version": "0.2.0",
"author": {
"name": "TrogonStack"
Expand Down
2 changes: 1 addition & 1 deletion plugins/trogonstack-nats/plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
"name": "trogonstack-nats",
"version": "0.2.0",
"description": "NATS messaging skills for designing subject hierarchies with naming conventions and wildcard patterns",
"description": "NATS channel design across Core NATS and JetStream, including AsyncAPI fragments, NACK-managed streams, optional consumers, tenancy, authorization, and migrations",
"author": {
"name": "TrogonStack",
"url": "https://github.com/TrogonStack"
Expand Down
395 changes: 147 additions & 248 deletions plugins/trogonstack-nats/skills/nats-design-subject/SKILL.md

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,12 @@ orders.created.us-west.order-789
- Kills hierarchical filtering
- Subscriber must use wildcard: `orders.*.created.us-west.>` (matches thousands)
- Cannot efficiently say "all orders created in US West"
- High memory usage for wildcard subscriptions
- Makes the intended subscriber filters harder to express

**Detection**:
```nats
# If you're writing subscribers like this, you have the problem:
subscribers: orders.*.created.> # Wildcard in middle = high-cardinality left
subscribers: orders.*.created.> # Wildcard in middle = high-cardinality identifiers early in the subject
```

**Fix**:
Expand Down Expand Up @@ -57,7 +57,7 @@ Phase 3 (week 5):

**Real Example (E-Commerce)**:
```
BEFORE (❌ high-cardinality left):
BEFORE (❌ high-cardinality identifiers early in the subject):
orders.12345678.created (customer ID on left)
orders.12345678.shipped (must filter by customer first)
orders.12345679.created (10 million customers = 10 million prefixes)
Expand Down Expand Up @@ -124,7 +124,7 @@ admins.us-west.updated.admin-456 (L2=region)

Frustration: Subscriber needs:
users.*.updated.> # Some have profile in L2
users.>.updated.> # Others don't
users.updated.> # Others don't

CONSISTENT ORDERING (✓):
users.profile.updated.us-west.user-123 (always: domain.context.action.region.id)
Expand All @@ -133,7 +133,7 @@ users.admin.updated.us-west.user-456 (always same order)
Subscriber:
users.profile.updated.us-west.>
users.admin.updated.us-west.>
users.>.updated.us-west.> # All same ordering!
users.*.updated.us-west.> # All same ordering!
```

---
Expand All @@ -145,7 +145,7 @@ users.>.updated.us-west.> # All same ordering!
✗ BAD (9 segments):
orders.created.v1.us-west.us-west-2a.customer-123.order-456.line-item-789.warehouse-101

✓ GOOD (4-5 segments):
✓ GOOD (bounded subject with details in the message):
orders.created.us-west.order-456
(other details in message headers/payload)
```
Expand Down Expand Up @@ -173,7 +173,7 @@ echo "orders.created.v1.us-west.customer.order-456.line-item.warehouse" | tr '.'
**Fix**:
1. Move low-cardinality data to message headers/properties
2. Use JetStream consumer filters for fine-grained routing
3. Keep subjects to 4-5 segments max
3. Keep subjects within the project segment budget. Move details that do not need subject filtering into headers or payloads.

**Better Approach Using JetStream**:
```
Expand Down Expand Up @@ -388,10 +388,12 @@ grep '[A-Z]' subjects.txt
grep '_' subjects.txt
```

**Standard**:
- All lowercase
- Use hyphens, never underscores
- Never use CamelCase or snake_case in segments
**Project convention example**:
- Use lowercase segments
- Use hyphens when separating words
- Keep one casing and separator policy across the hierarchy

NATS permits underscores and case-sensitive names. Enforce a stricter style only when the project has chosen it.

**Real Example**:
```
Expand Down Expand Up @@ -429,7 +431,7 @@ orders.created.eu-central.order-456

// Subscribers documented:
Dashboard: orders.> (all regional order events)
Region monitor: orders.>.us-west.> (all orders in region)
Region monitor: orders.*.us-west.> (all orders in region)
Created events only: orders.created.> (all creations, all regions)
```

Expand All @@ -451,8 +453,8 @@ For each domain, document:
2. Subscriber patterns:
- All events: domain.>
- Domain-specific: domain.action.>
- Scope-specific: domain.>.scope.>
- Specific resource: domain.>.scope.resource-id
- Scope-specific: domain.*.scope.>
- Specific resource: domain.*.scope.resource-id

3. Validate each pattern works and is efficient
```
Expand All @@ -474,7 +476,7 @@ Planned subscribers:
- All telemetry: devices.telemetry.>
- Regional: devices.telemetry.us-west.>
- Specific device: devices.telemetry.us-west.sensor-456.>
- Specific metric: devices.telemetry.>.>.temperature
- Specific metric: devices.telemetry.*.*.temperature
```

---
Expand Down Expand Up @@ -608,7 +610,7 @@ Using `{tenant}.>` as the main isolation boundary in a system that needs strict
✓ Action/event type is always L2 unless a fixed namespace token is required
✓ No IDs or UUIDs before action/scope
✓ Consistent casing (lowercase) and separators (hyphens)
✓ Max 5-6 segments per subject
✓ Subject depth follows the project convention and stays within NATS limits
✓ Subscriber patterns documented
✓ No two wildcard subscriptions can match the same subject
✓ IDs at consistent positions across all subject types
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
# NATS Channel Contracts

A NATS subject is a routing key. Core NATS and JetStream determine how the message is delivered and whether it is stored. Keep those decisions in one channel contract.

JetStream does not require a separate application subject namespace. A stream captures ordinary subjects. `$JS` subjects are the JetStream control plane and must remain separate from application channels.

Use [AsyncAPI 3.0](https://www.asyncapi.com/docs/reference/specification/v3.0.0) with the small [channel fragments](gateway-asyncapi-fragments.yaml) to describe application behavior. Its `send` and `receive` operations belong to the application that owns the document. Use [NACK JetStream resources](nack-jetstream.yaml) for Kubernetes-owned streams and optional consumers. Do not put a downstream consumer into a loosely coupled producer contract. A reader can publish its own receive-side contract and own a JetStream consumer separately.

The NATS-specific examples below use list-valued `subjects`, `request_subjects`, and `reply_subjects` fields, even when one value is present. AsyncAPI keeps its singular `address` field, and JetStream examples keep exact server fields such as `filter_subject`.

| Concern | Core NATS | JetStream |
| --- | --- | --- |
| Delivery | Live publish-subscribe or request-reply | Stream-backed delivery through consumers |
| Disconnect | A disconnected subscriber misses the message | A durable consumer can resume or replay |
| Publisher result | Publish has no storage acknowledgement | JetStream publish can return a stream acknowledgement |
| State | No stream or consumer state | Stream and consumer state must be provisioned and observed |
| Use | RPC and best-effort live signals | Durable events and work that require replay or retry |

## Channel kinds

### RPC

Use Core NATS request-reply when the caller needs one response now:

```yaml
id: gateway.rpc.v1.health
kind: rpc
request_subjects:
- gateway.rpc.v1.health
reply_subjects:
- _INBOX.<generated-by-client>
transport: core
storage: none
```

The client creates a unique inbox and sends it as the reply subject. The responder uses that reply subject. A timeout and retry policy are part of the contract.

### Live signal

Use Core NATS publish-subscribe for best-effort notifications:

```yaml
id: gateway.signal.v1.configuration_changed
kind: live-signal
subjects:
- gateway.signal.v1.configuration_changed
transport: core
storage: none
```

A disconnected subscriber misses the message. Do not use this kind for an event that must be replayed.

### Durable event

Use JetStream for a fact that must survive a disconnect or be replayed:

```yaml
id: gateway.ingress.raw.github.v1
kind: durable-event
subjects:
- gateway.ingress.raw.github.v1.<event>
transport: jetstream
stream: GITHUB_PRIMARY
retention: 7d
ack: explicit
```

Core subscribers may observe the same published subject for low-latency reactions. Durable consumers remain the correctness path.

The publisher must choose its acknowledgement requirement explicitly. A Core publish to a subject captured by a stream does not tell the publisher that storage completed. A JetStream publish acknowledgement is the appropriate contract when the publisher needs that proof.

Use the same shape for a second ingress source when it has a separate ownership or retention policy:

```yaml
id: gateway.ingress.slack.v1
kind: durable-event
subjects:
- gateway.ingress.raw.slack.v1.<event>
transport: jetstream
stream: SLACK_PRIMARY
retention: 7d
ack: explicit
```

### Durable work

Use a JetStream work-queue stream and add a durable consumer only when worker instances need shared, retryable work:

```yaml
id: gateway.work.v1.installation_sync
kind: durable-work
subjects:
- gateway.work.v1.installation_sync
transport: jetstream
stream: GATEWAY_WORK
retention: workqueue
consumer: installation-sync
ack: explicit
```

Set redelivery, backoff, maximum attempts, and dead-letter behavior as part of the work contract.

The `consumer` field belongs to the worker's receive-side contract. Omit it from a publisher-only event contract.

## Gateway example

```text
gateway.ingress.raw.github.v1.pull_request
gateway.ingress.raw.slack.v1.event_callback
gateway.events.v1.conversation.message
gateway.rpc.v1.health
gateway.work.v1.installation_sync
```

The GitHub and Slack ingress streams can remain separate when their retention, permissions, or consumers differ. A stream may exist before a reader is deployed. The important rule is that request subjects do not sit under the ingress event wildcard.

The `nats-subjects` tag marks the examples that the bundled validator reads. The `yaml` language tag keeps the fixture readable as a small data document. Narrative examples use ordinary fences and are not treated as machine-readable contracts.

```yaml nats-subjects
- kind: publish
subjects:
- gateway.ingress.raw.github.v1.pull_request
- kind: subscribe
subjects:
- gateway.ingress.raw.github.v1.>
- kind: stream
subjects:
- gateway.ingress.raw.github.v1.>
# This validates an optional reader filter. It does not provision a consumer.
- kind: consumer
subjects:
- gateway.ingress.raw.github.v1.pull_request
- kind: publish
subjects:
- gateway.ingress.raw.slack.v1.event_callback
- kind: subscribe
subjects:
- gateway.ingress.raw.slack.v1.>
- kind: publish
subjects:
- gateway.rpc.v1.health
- kind: deliver
subjects:
- _INBOX.generated
```

## Ownership

Declare one owner for each stream and consumer. In Kubernetes, NACK owns the JetStream resources:

```yaml
owner: gitops
provisioner: nack
publisher: trogon-gateway
```

The application can validate that a required stream exists. It should not silently create a second definition that competes with NACK. Apply mTLS connection settings to NACK through its Helm values and Kubernetes Secrets, not through application startup code.

## Management readback

Management should expose the same descriptor with desired and observed state. Consumer fields are optional and appear only when a reader owns one:

```text
channel: gateway.ingress.raw.github.v1
subjects:
- gateway.ingress.raw.github.v1.>
transport: jetstream
stream: GITHUB_PRIMARY
stream_state: ready
consumers: optional, owned by readers
backlog: 0
owner: gitops
```

Core channels omit stream fields and instead expose responder, queue group, timeout, and authorization status.
Loading
Loading