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
59 changes: 20 additions & 39 deletions plugins/trogonstack-nats/skills/nats-design-subject/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,17 @@ allowed-tools:

# Design NATS Channels

Design a NATS channel contract that stays understandable as the system grows. A subject is the routing key. Core NATS and JetStream are delivery and storage choices that must remain explicit in the contract.
Design a NATS channel contract that stays understandable as the system grows. A subject is the routing key, not a declaration of transport or storage. Record delivery, acknowledgement, and ownership separately.

JetStream is not a second application subject tree. Streams bind to ordinary application subjects. The `$JS` subjects are the control plane and need separate permissions and management ownership.

## Start with the channel kind
## Start with the message behavior

Choose the semantic kind before choosing the subject shape:
Choose what the publisher and reader expect before choosing a subject shape. These kinds describe behavior; they are not required subject tokens:

| Kind | Transport | Contract |
| Kind | Typical transport | Contract |
| --- | --- | --- |
| RPC | Core NATS request-reply | A caller waits for one response or a timeout. |
| Request/reply | Core NATS | A caller waits for a business response or a timeout. |
| Live signal | Core NATS publish-subscribe | Delivery is best effort while subscribers are connected. |
| Durable event | JetStream | The event can be replayed and consumers acknowledge progress. |
| Durable work | JetStream | A worker pool shares a durable consumer and retries unacknowledged work. |
Expand All @@ -34,7 +34,7 @@ If a stream captures a subject, a Core publish can be stored without giving the
Skip questions that the user already answered. Ask for the missing contract decisions:

- Is this a new design or a migration?
- What channel kinds are needed: RPC, live signal, durable event, or durable work?
- What behavior is needed: request/reply, live signal, durable event, or durable work?
- Is tenancy an NATS account boundary, a shared-account subject prefix, or both?
- Which principals publish, subscribe, request, respond, or manage JetStream resources?
- What must survive a disconnect, and what can be lost?
Expand All @@ -60,35 +60,31 @@ kind: durable-event
subjects:
- gateway.ingress.raw.github.v1.<event>
transport: jetstream
stream: GATEWAY_GITHUB_PRIMARY
stream: GITHUB_PRIMARY
owner: gitops
retention: 7d
ack: explicit
```

For an RPC channel:
For a request/reply channel:

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

The channel record is the source for the subjects, stream filters, consumer filters, permissions, and management status. Do not make an application silently create production streams that are also declared by GitOps or an operator.
The request subject has no `.rpc.` token. Such a token is an optional local naming convention, not a NATS requirement. The channel contract states the behavior. Combine it with provisioned resources, such as NACK manifests, and observed state to show topology; do not infer transport from spelling. An application must not create a stream that GitOps or an operator already owns.

### Choose the subject shape

Use a stable, low-cardinality prefix followed by the fields subscribers need to filter:

```text
<domain>.<event-or-action>.<bounded-scope>.<identifier>
```
Choose tokens that identify the message and support the intended subscriptions and permissions. A stable, low-cardinality prefix helps with bounded filters. Use a kind token such as `rpc` or `event` only when it serves that design; no kind token or universal token order is required.

Examples:

Expand Down Expand Up @@ -126,6 +122,8 @@ For every subject template, document the intended filters:

For a request, document the request subject and reply-to behavior. NATS request-reply uses a generated inbox. Responders publish to the request's reply subject; they do not invent a shared reply subject for each correlation ID.

Compare request subjects with stream filters. A request that expects a business response should not be captured by an unrelated JetStream stream. The [NACK example](references/nack-jetstream.yaml) captures ingress events and work, not `gateway.health.v1.check`.

For each principal, list publish and subscribe permissions separately. A broad `>` permission also reaches the JetStream control plane, so scope permissions to the channel and the required `$JS` or `_INBOX` subjects.

### Design JetStream storage and consumers
Expand Down Expand Up @@ -158,7 +156,7 @@ logical channel
-> retention, backlog, and last error
```

For the Trogon gateway, GitOps applies the NACK resources while the gateway publishes and validates the ingress subjects. The application must not create or update competing stream definitions. The ownership must be visible in the channel contract.
In the gateway example, GitOps applies the NACK resources while the gateway publishes ingress events. The request/reply channel has no NACK resource. Show that separation in the channel contract and management view.

### Plan migrations

Expand All @@ -175,27 +173,7 @@ Make consumers idempotent with an application key when a migration can deliver a

## Output

Write a design that includes:

```markdown
# NATS Channel Architecture: [System]

## Account and tenancy boundary

## Channel contracts

## Subject templates and valid wildcard paths

## Core NATS request-reply and live signals

## JetStream streams and consumers

## Permissions and reserved subjects

## Provisioning owner and management readback

## Migration and validation plan
```
Present a topology a reader can trace from application behavior to the deployed resources. Use the sections that fit the system. Include the relevant account boundary, channel contracts, subject filters, permissions, provisioning owner, and desired versus observed state. Include a migration plan when changing an existing hierarchy.

## Validation

Expand All @@ -205,6 +183,7 @@ Before presenting the design:
- Validate every wildcard expression.
- Verify that `>` appears only as the final token.
- Verify that request replies use a generated inbox or an explicitly documented alternative.
- Compare request subjects with stream filters. Document any intentional capture and its response behavior.
- Check stream and consumer filter overlap.
- Check permissions against the actual subject and JetStream control-plane requirements.
- Mark machine-readable Markdown fixtures with the `nats-subjects` info tag. For example, use ` ```yaml nats-subjects` and put a `kind` and `subjects` list in each YAML item. Keep the list shape even when it has one value. Ordinary examples stay untagged.
Expand All @@ -214,6 +193,8 @@ Before presenting the design:
node plugins/trogonstack-nats/skills/nats-design-subject/scripts/validate-subjects.mjs --root plugins/trogonstack-nats/skills/nats-design-subject
```

The bundled validator checks subject syntax and tagged examples. It does not verify stream overlap, permissions, responder availability, or deployed topology.

## References

- [Channel contracts and Core versus JetStream](references/channel-contract.md)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# 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.
A NATS subject is a routing key. Its tokens need not encode transport, storage, or whether a response is expected. Describe those behaviors in the channel contract, then show how the deployed resources implement them.

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.

Expand All @@ -18,22 +18,22 @@ The NATS-specific examples below use list-valued `subjects`, `request_subjects`,

## Channel kinds

### RPC
### Request/reply

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

```yaml
id: gateway.rpc.v1.health
kind: rpc
id: gateway.health.v1.check
kind: request-reply
request_subjects:
- gateway.rpc.v1.health
- gateway.health.v1.check
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.
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. The `kind` field expresses request/reply behavior; `.rpc.` is not required in the subject. Add a kind token only when it helps a chosen subscription or permission boundary.

### Live signal

Expand Down Expand Up @@ -108,11 +108,11 @@ The `consumer` field belongs to the worker's receive-side contract. Omit it from
gateway.ingress.raw.github.v1.pull_request
gateway.ingress.raw.slack.v1.event_callback
gateway.events.v1.conversation.message
gateway.rpc.v1.health
gateway.health.v1.check
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 GitHub and Slack ingress streams can remain separate when their retention, permissions, or consumers differ. A stream may exist before a reader is deployed. Their [NACK filters](nack-jetstream.yaml) capture ingress events and work, not the health request. That boundary comes from the declared filters and contract, not from a required `.rpc.` prefix. Compare request subjects with stream filters before treating them as separate paths.

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.

Expand All @@ -138,7 +138,7 @@ The `nats-subjects` tag marks the examples that the bundled validator reads. The
- gateway.ingress.raw.slack.v1.>
- kind: publish
subjects:
- gateway.rpc.v1.health
- gateway.health.v1.check
- kind: deliver
subjects:
- _INBOX.generated
Expand All @@ -158,7 +158,7 @@ The application can validate that a required stream exists. It should not silent

## 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:
Build management readback from the declared channel behavior, NACK resources, and observed state. Consumer fields appear only when a reader owns one:

```text
channel: gateway.ingress.raw.github.v1
Expand All @@ -172,4 +172,18 @@ backlog: 0
owner: gitops
```

Core channels omit stream fields and instead expose responder, queue group, timeout, and authorization status.
The request/reply channel has no stream or consumer entry. Its readback can show the responder, timeout, and permission path instead:

```yaml
channel: gateway.health.v1.check
kind: request-reply
transport: core
request_subjects:
- gateway.health.v1.check
reply_subjects:
- _INBOX.<generated-by-client>
responder: gateway
storage: none
```

Observed responder readiness and authorization status belong beside this declared shape. Neither is proved by the subject name.
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ channels:
slackWebhook:
$ref: '#/components/messages/slackWebhook'
healthRequest:
address: gateway.rpc.v1.health
address: gateway.health.v1.check
messages:
healthRequest:
$ref: '#/components/messages/healthRequest'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,24 +126,23 @@ acme-corp.orders.created.order-123

**Use when**: Synchronous microservices, command-response, RPC-like communication.

Choose a concrete subject that identifies the requested operation. The caller supplies the reply-to address:

```
Request Subject: {service}.request.{request-type}
Request Subject: orders.get-order
Reply-To: _INBOX.<generated-by-client>

Examples:
orders.request.get-order
orders.request.calculate-total
payments.request.authorize
```

The client creates a unique `_INBOX.<generated>` reply subject and sends it as the request reply-to.
```

`orders.calculate-total` and `payments.authorize` could use the same pattern. The request/reply behavior comes from the contract and reply-to address, not a required `request` or `rpc` token. Use a kind token when it helps the intended filters or permissions.

**Implementation**:
```
Requester publishes to: orders.request.get-order
Requester publishes to: orders.get-order
With reply-to: _INBOX.abc123

Responder subscribes to: orders.request.>
Responder subscribes to: orders.get-order
Reads reply-to header
Publishes response to: _INBOX.abc123
```
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
- gateway.ingress.raw.slack.v1.>
- kind: publish
subjects:
- gateway.rpc.v1.health
- gateway.health.v1.check
- kind: deliver
subjects:
- _INBOX.generated
Loading