@crossdelta/platform-sdk 0.5.12 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -14,13 +14,43 @@ pf new hono-micro services/my-service -y
14
14
 
15
15
  Rules:
16
16
  - **ALWAYS** include this commands block as the FIRST thing in your output
17
- - Use exactly the path provided by the user
18
- - Do not alter naming or prepend paths
17
+ - **CRITICAL:** Services MUST be created in `services/` directory (e.g., `services/orders`, `services/notifications`)
18
+ - **If user provides just a service name** (e.g., "orders"), prepend `services/` → `services/orders`
19
+ - **If user provides full path with `services/`** (e.g., "services/orders"), use as-is
19
20
  - The `-y` flag is REQUIRED to skip prompts
20
21
 
22
+ **Path Examples:**
23
+ - ✅ User says "orders" → Generate: `pf new hono-micro services/orders -y`
24
+ - ✅ User says "payment processor" → Generate: `pf new hono-micro services/payment-processor -y`
25
+ - ✅ User says "services/orders" → Generate: `pf new hono-micro services/orders -y`
26
+ - ❌ NEVER generate: `pf new hono-micro orders -y` (missing services/ prefix)
27
+ - ❌ NEVER generate: `pf new hono-micro backend/orders -y` (wrong directory)
28
+ - ❌ NEVER generate: `pf new hono-micro apps/orders -y` (apps/ is for frontends)
29
+
30
+ ---
31
+
32
+ # 2. Post-Generation Commands
33
+
34
+ After generating a service with event **handlers** (Event Consumer or Hybrid), include a post-generation command to generate event mocks:
35
+
36
+ ```post-commands
37
+ pf event:generate services/my-service --overwrite
38
+ ```
39
+
40
+ This command:
41
+ - Scans all `src/events/*.event.ts` files in the service
42
+ - Generates mock JSON files based on the exported Zod schemas
43
+ - Creates `*.mock.json` files in `packages/contracts/src/events/`
44
+ - Enables testing with `pf event:publish <event-name>`
45
+
46
+ **When to include this command:**
47
+ - ✅ Service is an **Event Consumer** (has event handlers in `src/events/`)
48
+ - ✅ Service is **Hybrid** (has both REST + event handlers)
49
+ - ❌ Service is an **Event Publisher only** (only REST endpoints, NO event handlers)
50
+
21
51
  ---
22
52
 
23
- # 2. Dependencies Block
53
+ # 3. Dependencies Block
24
54
 
25
55
  ```dependencies
26
56
  zod
@@ -32,26 +62,43 @@ Rules:
32
62
  - No versions.
33
63
  - Only packages not included in the scaffold.
34
64
  - **CRITICAL:** Only use packages that actually exist on npm. Verify package names are correct (e.g., `@pusher/push-notifications-server` not `pusher-beams`). When unsure, use well-known, established packages.
65
+ - **IMPORTANT:** Workspace packages (like `{{scope}}/contracts`) are automatically detected and installed with `workspace:*` protocol. Just list the package name without version.
66
+
67
+ ### Core Package Names (ALWAYS USE THESE EXACT NAMES)
68
+
69
+ **CRITICAL:** The following packages are part of the monorepo and MUST be imported with these exact names:
70
+
71
+ - `@crossdelta/cloudevents` - CloudEvents/NATS utilities (`handleEvent`, `consumeJetStreamEvents`, `publish`)
72
+ - `@crossdelta/telemetry` - OpenTelemetry setup
73
+ - `@crossdelta/infrastructure` - Infrastructure builders (for infra/ files only)
74
+ - `{{scope}}/contracts` - Shared event contracts (when using Advanced Mode)
75
+
76
+ **DO NOT use invented package names like:**
77
+ - ❌ `shared-events`
78
+ - ❌ `@orderboss/cloudevents`
79
+ - ❌ `@platform/events`
80
+ - ❌ Any other variations
35
81
 
36
82
  ---
37
83
 
38
- # 3. Required Source Files
84
+ # 4. Required Source Files
39
85
 
40
86
  Generated services must include:
41
87
 
42
88
  - `src/index.ts`
43
- - `src/handlers/*.event.ts`
89
+ - `src/events/*.event.ts` (event handlers - NOT `src/handlers/`)
44
90
  - `src/use-cases/*.use-case.ts`
45
91
  - `src/use-cases/*.test.ts`
46
92
  - `README.md`
47
93
 
48
94
  Rules:
49
95
  - Paths must be relative to the service root.
96
+ - Event handlers MUST go in `src/events/`, NOT `src/handlers/`.
50
97
  - Do NOT generate controller or module folders unless requested.
51
98
 
52
99
  ---
53
100
 
54
- # 4. Code Style
101
+ # 5. Code Style
55
102
 
56
103
  - Biome-compatible formatting.
57
104
  - Strict TS.
@@ -62,11 +109,89 @@ Rules:
62
109
 
63
110
  ---
64
111
 
65
- # 5. Service Structure
112
+ # 6. Service Structure
113
+
114
+ ## 🔑 CRITICAL: Event Publishers vs Event Consumers
115
+
116
+ **Read the service description carefully to determine the service type:**
117
+
118
+ ### 📤 Event Publisher (REST API that publishes events)
119
+
120
+ **Keywords in description:** "publishes", "creates", "manages", "REST API", "HTTP endpoints"
121
+
122
+ **Example:** "Order management service that handles order creation. Publishes order.created events."
123
+
124
+ **Structure:**
125
+ - ✅ REST endpoints (POST, GET, PUT, DELETE)
126
+ - ✅ Use `publish()` from `@crossdelta/cloudevents`
127
+ - ✅ **RECOMMENDED:** Create contracts in `packages/contracts` for published events
128
+ - ❌ NO event handlers in `src/events/`
129
+ - ❌ NO `consumeJetStreamEvents()`
130
+
131
+ **Event Publishing Strategy:**
132
+
133
+ Publishers can use **contracts** (recommended) or **string literals**:
134
+
135
+ ```ts
136
+ // Option 1: With contract (RECOMMENDED - type-safe)
137
+ import { OrderCreatedContract } from '@scope/contracts'
138
+ await publish(OrderCreatedContract, orderData)
139
+
140
+ // Option 2: String literal (OK for prototyping)
141
+ await publish('order.created', orderData, {
142
+ source: 'my-service',
143
+ subject: orderId,
144
+ })
145
+ ```
146
+
147
+ **When to create contracts for published events:**
148
+ - ✅ **Default/Recommended** - Create contract in `packages/contracts` for type-safety
149
+ - ✅ Event will be consumed by other services (now or future)
150
+ - ✅ Event schema should be documented and versioned
151
+ - ❌ Only skip if rapid prototyping or service-internal events
152
+
153
+ **Example index.ts with contracts:**
154
+ ```ts
155
+ import '@crossdelta/telemetry'
156
+
157
+ import { publish } from '@crossdelta/cloudevents'
158
+ import { OrderCreatedContract } from '@scope/contracts'
159
+ import { Hono } from 'hono'
160
+ import { CreateOrderSchema } from './types/orders'
161
+ import { createOrder } from './use-cases/create-order.use-case'
162
+
163
+ const port = Number(process.env.PORT || process.env.MY_SERVICE_PORT) || 4001
164
+ const app = new Hono()
165
+ const orders = new Map()
166
+
167
+ app.get('/health', (c) => c.json({ status: 'ok' }))
168
+
169
+ app.post('/orders', async (c) => {
170
+ const data = CreateOrderSchema.parse(await c.req.json())
171
+ const order = await createOrder(data, orders)
172
+
173
+ // Publish with contract (type-safe)
174
+ await publish(OrderCreatedContract, order)
175
+
176
+ return c.json({ success: true, order }, 201)
177
+ })
178
+
179
+ Bun.serve({ port, fetch: app.fetch })
180
+ ```
181
+
182
+ ### 📥 Event Consumer (Reacts to events from other services)
183
+
184
+ **Keywords in description:** "consumes", "listens to", "reacts to", "handles events", "processes events"
66
185
 
67
- ### index.ts
68
- **IMPORTANT:** Telemetry MUST be the first import!
186
+ **Example:** "Notification service that sends emails when orders are created. Consumes order.created events."
69
187
 
188
+ **Structure:**
189
+ - ✅ Event handlers in `src/events/*.event.ts`
190
+ - ✅ Use `consumeJetStreamEvents()` in index.ts
191
+ - ✅ Use `handleEvent()` for each event type
192
+ - ❌ Usually NO REST endpoints (except /health)
193
+
194
+ **Example index.ts:**
70
195
  ```ts
71
196
  import '@crossdelta/telemetry'
72
197
 
@@ -81,28 +206,154 @@ app.get('/health', (c) => c.json({ status: 'ok' }))
81
206
  // Start NATS consumer - handlers are auto-discovered
82
207
  consumeJetStreamEvents({
83
208
  stream: 'ORDERS',
84
- subjects: ['orders.>'],
85
209
  consumer: 'my-service',
86
- discover: './src/handlers/**/*.event.ts',
210
+ discover: './src/events/**/*.event.ts',
87
211
  })
88
212
 
89
213
  Bun.serve({ port, fetch: app.fetch })
90
214
  ```
91
215
 
216
+ **IMPORTANT:** Do NOT include `subjects` parameter in `consumeJetStreamEvents` - it's not supported.
217
+
218
+ ### � HTTP CloudEvent Receiver (Receives CloudEvents via HTTP POST)
219
+
220
+ **Keywords in description:** "webhook", "receives CloudEvents via HTTP", "HTTP push endpoint", "Pub/Sub Push"
221
+
222
+ **Example:** "Webhook service that receives payment events from Stripe as CloudEvents via HTTP POST."
223
+
224
+ **Structure:**
225
+ - ✅ Event handlers in `src/events/*.event.ts`
226
+ - ✅ Use `cloudEvents()` middleware in index.ts
227
+ - ✅ Use `handleEvent()` for each event type
228
+ - ❌ NO `consumeJetStreamEvents()` - events come via HTTP, not NATS
229
+
230
+ **Example index.ts:**
231
+ ```ts
232
+ import '@crossdelta/telemetry'
233
+
234
+ import { cloudEvents } from '@crossdelta/cloudevents'
235
+ import { Hono } from 'hono'
236
+
237
+ const port = Number(process.env.PORT || process.env.MY_SERVICE_PORT) || 4003
238
+ const app = new Hono()
239
+
240
+ app.get('/health', (c) => c.json({ status: 'ok' }))
241
+
242
+ // HTTP CloudEvent receiver - handlers are auto-discovered
243
+ app.use('/webhooks/*', cloudEvents({
244
+ discover: './src/events/**/*.event.ts',
245
+ log: true
246
+ }))
247
+
248
+ Bun.serve({ port, fetch: app.fetch })
249
+ ```
250
+
251
+ **When to use:**
252
+ - ✅ Receiving webhooks (Stripe, GitHub, etc.)
253
+ - ✅ Google Cloud Pub/Sub Push endpoints
254
+ - ✅ CloudEvent-to-CloudEvent bridges
255
+ - ❌ NOT for regular REST APIs (use Event Publisher pattern instead)
256
+
257
+ ### �🔄 Hybrid Service (Both Publisher and Consumer)
258
+
259
+ Some services both consume events AND expose REST endpoints:
260
+
261
+ **Example:** "Payment service that processes payments via REST API and listens for order.created events."
262
+
263
+ **Structure:**
264
+ - ✅ REST endpoints that publish events
265
+ - ✅ Event handlers that consume events
266
+ - ✅ Use both `publish()` and `consumeJetStreamEvents()`
267
+
268
+ ---
269
+
270
+ ### index.ts Rules
271
+
92
272
  Contains only:
93
273
  - Telemetry import (MUST be first)
94
274
  - Server setup (Hono)
95
275
  - Health endpoint
96
- - Event consumer registration (if consuming events)
276
+ - REST endpoints (for Publishers/Hybrid)
277
+ - Event consumer registration (for Consumers/Hybrid)
97
278
  - Port configuration from env vars
98
279
 
280
+ **Import schemas and types from `src/types/` directory:**
281
+ ```ts
282
+ import { CreateOrderSchema, UpdateOrderSchema } from './types/orders'
283
+
284
+ app.post('/orders', async (c) => {
285
+ const data = CreateOrderSchema.parse(await c.req.json())
286
+ const order = await createOrder(data, orders)
287
+ await publish('order.created', order, { source: 'my-service' })
288
+ return c.json({ success: true, order }, 201)
289
+ })
290
+ ```
291
+
292
+ ---
293
+
294
+ # 6.5. Types & Schemas Structure
295
+
296
+ **CRITICAL:** All Zod schemas and types must live in `src/types/` directory.
297
+
298
+ ```
299
+ src/
300
+ ├── index.ts # Adapters (REST endpoints)
301
+ ├── types/ # Schemas & Types (shared)
302
+ │ ├── orders.ts # Order-related schemas & types
303
+ │ ├── payments.ts # Payment-related schemas & types
304
+ │ └── index.ts
305
+ ├── events/ # Event handlers (if Consumer)
306
+ │ └── *.event.ts
307
+ └── use-cases/ # Business logic
308
+ └── *.use-case.ts
309
+ ```
310
+
311
+ **Types file structure:**
312
+ ```ts
313
+ // src/types/orders.ts
314
+ import { z } from 'zod'
315
+
316
+ // Define Zod schemas
317
+ export const CreateOrderSchema = z.object({
318
+ customerId: z.string().min(1),
319
+ items: z.array(z.object({
320
+ productId: z.string().min(1),
321
+ quantity: z.number().int().min(1),
322
+ price: z.number().min(0),
323
+ })).min(1),
324
+ total: z.number().min(0),
325
+ })
326
+
327
+ export const UpdateOrderSchema = z.object({
328
+ items: z.array(z.object({
329
+ productId: z.string().min(1),
330
+ quantity: z.number().int().min(1),
331
+ price: z.number().min(0),
332
+ })).optional(),
333
+ total: z.number().min(0).optional(),
334
+ status: z.enum(['created', 'updated', 'completed']).optional(),
335
+ })
336
+
337
+ // Export inferred types
338
+ export type CreateOrderInput = z.infer<typeof CreateOrderSchema>
339
+ export type UpdateOrderInput = z.infer<typeof UpdateOrderSchema>
340
+ ```
341
+
342
+ **Why `src/types/`?**
343
+ - ✅ Single source of truth (Zod schema → TypeScript types)
344
+ - ✅ No circular dependencies (types → use-cases, types → adapters)
345
+ - ✅ No duplication between schemas and types
346
+ - ✅ Shared between adapters (REST, Events) and use-cases
347
+
99
348
  ---
100
349
 
101
- # 6. Use-Case Rules (Lean Hexagonal Architecture)
350
+ # 7. Use-Case Rules (Lean Hexagonal Architecture)
102
351
 
103
352
  Services follow a **Lean Hexagonal Architecture** pattern:
104
- - **Handlers** = Adapters (receive events, delegate to use-cases)
105
- - **Use-Cases** = Application Core (business logic, orchestration)
353
+ - **Types & Schemas** (in `src/types/`) = Zod schemas and inferred types (shared by all layers)
354
+ - **Event Handlers** (in `src/events/`) = Adapters (receive events, validate, delegate to use-cases)
355
+ - **REST Endpoints** (in `src/index.ts`) = Adapters (receive HTTP requests, validate, delegate to use-cases)
356
+ - **Use-Cases** (in `src/use-cases/`) = Application Core (business logic, NO validation, NO schemas)
106
357
  - No explicit ports/interfaces unless needed
107
358
 
108
359
  **IMPORTANT:** A single service description may result in **multiple use-cases**. Break down complex functionality into focused, single-responsibility use-cases.
@@ -114,66 +365,400 @@ Example: "Payment processing service" might generate:
114
365
  - `refund-payment.use-case.ts` - Handle refund requests
115
366
 
116
367
  Use-Case Rules:
117
- - All domain logic lives in `use-cases/`
368
+ - All domain logic lives in `src/use-cases/`
118
369
  - One use-case = one responsibility
119
370
  - Pure functions preferred
120
- - No framework imports
121
- - Use inferred schema types from handlers
371
+ - No framework imports (no Hono, no Zod, no @crossdelta/cloudevents)
372
+ - **CRITICAL: Import types from `src/types/`** - do NOT redefine types in use-cases
373
+ - **CRITICAL: NO Zod schemas in use-cases!** Schemas live in `src/types/`
374
+ - **CRITICAL: NO manual validation (if/throw) for input data!** Trust that adapters validated the data
375
+ - **CRITICAL: NO globalThis hacks or in-memory state!** Pass state as parameters (Dependency Injection)
376
+ - **DO NOT export domain entity types from use-cases** - define those in `src/types/` if needed
377
+ - **DO NOT create repository interfaces** - keep it lean, inject concrete dependencies
378
+ - For complex persistence needs, add `// TODO: Replace with proper database persistence`
379
+ - Return plain data (no Response objects, no validation)
122
380
  - Compose use-cases when needed (call one from another)
123
381
 
382
+ **Example - CORRECT use-case (Lean Hexagonal):**
383
+ ```ts
384
+ // ✅ Import types from src/types/, concrete dependency injection
385
+ import type { CreateOrderInput } from '../types/orders'
386
+
387
+ // Inject concrete dependencies (Map, Set, etc.) - keep it lean
388
+ export const createOrder = async (
389
+ input: CreateOrderInput,
390
+ orderStore: Map<string, any> // Concrete dependency, but injected
391
+ ) => {
392
+ const orderId = `order-${Date.now()}`
393
+
394
+ const order = {
395
+ orderId,
396
+ customerId: input.customerId,
397
+ items: input.items,
398
+ total: input.total,
399
+ status: 'created',
400
+ createdAt: new Date().toISOString(),
401
+ }
402
+
403
+ orderStore.set(orderId, order)
404
+ return order
405
+ }
406
+ ```
407
+
408
+ **Example - CORRECT use-case with extended input:**
409
+ ```ts
410
+ // ✅ Extend imported type for additional parameters
411
+ import type { UpdateOrderInput } from '../types/orders'
412
+
413
+ export const updateOrder = async (
414
+ input: UpdateOrderInput & { orderId: string }, // Extend with orderId
415
+ orderStore: Map<string, any>
416
+ ) => {
417
+ const order = orderStore.get(input.orderId)
418
+ if (!order) {
419
+ throw new Error('Order not found') // ✅ Business logic validation OK
420
+ }
421
+
422
+ const updatedOrder = {
423
+ ...order,
424
+ ...input,
425
+ updatedAt: new Date().toISOString(),
426
+ }
427
+
428
+ orderStore.set(input.orderId, updatedOrder)
429
+ return updatedOrder
430
+ }
431
+ ```
432
+
433
+ **Example - CORRECT use-case with DB TODO:**
434
+ ```ts
435
+ import type { CreateOrderInput } from '../types/orders'
436
+
437
+ export const createOrder = async (
438
+ input: CreateOrderInput,
439
+ orderStore: Map<string, any> // TODO: Replace with proper database persistence
440
+ ) => {
441
+ const orderId = `order-${Date.now()}`
442
+ const order = { orderId, ...input, status: 'created', createdAt: new Date().toISOString() }
443
+ orderStore.set(orderId, order)
444
+ return order
445
+ }
446
+ ```
447
+
448
+ **Example - WRONG use-case:**
449
+ ```ts
450
+ // ❌ globalThis hack for state
451
+ const orders = globalThis.__ORDERS__ || (globalThis.__ORDERS__ = {}) // ❌ NO!
452
+
453
+ // ❌ Domain entity types exported from use-case
454
+ export type Order = { ... } // ❌ NO! Define in adapter
455
+
456
+ // ❌ Repository interface (too complex for simple services)
457
+ export interface OrderRepository { // ❌ NO! Keep it lean
458
+ save: (order: any) => Promise<void>
459
+ }
460
+
461
+ // ❌ Manual validation in use-case
462
+ export const createOrder = async (input: CreateOrderInput) => {
463
+ if (!input.customerId) throw new Error('Missing customerId') // ❌ NO!
464
+ **Example - WRONG use-case:**
465
+ ```ts
466
+ // ❌ Defining types in use-case instead of importing from src/types/
467
+ export type CreateOrderInput = { ... } // ❌ NO! Import from src/types/
468
+
469
+ // ❌ Zod schema in use-case
470
+ import { z } from 'zod'
471
+ const CreateOrderSchema = z.object({ ... }) // ❌ NO! Define in src/types/
472
+
473
+ // ❌ Manual input validation
474
+ export const createOrder = async (input: CreateOrderInput) => {
475
+ if (!input.customerId) throw new Error('Missing customerId') // ❌ NO!
476
+ // Trust adapters validated the data!
477
+ }
478
+ ```
479
+
480
+ Types and schemas belong in **`src/types/`**, NOT in use-cases!
481
+
124
482
  ---
125
483
 
126
- # 7. Event Handler Rules
484
+ # 8. Event Handler Rules (For Event Consumers Only!)
127
485
 
128
- Handlers MUST:
129
- - Live under `src/handlers/*.event.ts`
130
- - Use Zod for validation (NO `type` field in schema!)
131
- - Export inferred types for use in use-cases
132
- - Delegate ALL logic to use-cases
133
- - Contain NO business logic
486
+ **CRITICAL:** Event handlers are ONLY for services that **consume/react to** events from other services!
134
487
 
135
- **CRITICAL:** Schema validates ONLY the event data payload (without `type` field). Event type is declared in the options object.
488
+ **Do NOT create event handlers if:**
489
+ - ❌ Service description says "publishes events" or "creates/manages X"
490
+ - ❌ Service is a REST API that publishes events
491
+ - ❌ Service is the source/origin of the events
136
492
 
137
- Example:
493
+ **DO create event handlers if:**
494
+ - ✅ Service description says "consumes", "listens to", "reacts to", "handles events"
495
+ - ✅ Service processes events published by other services
496
+
497
+ **CRITICAL: Event handlers MUST live in `src/events/` directory, NOT `src/handlers/`!**
498
+
499
+ ## Event Handling Modes
500
+
501
+ The platform supports **Advanced Mode** (contracts) with automatic creation for missing contracts.
502
+
503
+ ### 🎯 DECISION RULE (CRITICAL - READ FIRST!)
504
+
505
+ **Always use Advanced Mode (contracts) - Missing contracts are auto-created!**
506
+
507
+ When generating an Event Consumer service:
508
+
509
+ 1. **Use contracts** from `{{scope}}/contracts` package
510
+ 2. **Import and use** the contract in the event handler (`import { OrderCreatedContract } from '{{scope}}/contracts'`)
511
+ 3. **If contract doesn't exist**, `pf event:generate` will create it automatically:
512
+ - Extracts event name from handler
513
+ - Creates contract in `packages/contracts/src/events/`
514
+ - Updates `packages/contracts/src/index.ts`
515
+ - Generates mock JSON file
516
+
517
+ **Example Flow:**
518
+ ```
519
+ User prompt: "Event Consumer that listens to order.created events"
520
+ → AI creates: src/events/order-created.event.ts (imports OrderCreatedContract from @scope/contracts)
521
+ → pf event:generate runs:
522
+ ✓ Detects missing contract
523
+ ✓ Creates packages/contracts/src/events/order-created.ts (contract)
524
+ ✓ Creates packages/contracts/src/events/order-created.mock.json
525
+ ```
526
+
527
+ **Why This Approach?**
528
+ - ✅ AI generates contracts reference (type-safe from start)
529
+ - ✅ Automatic contract creation when missing
530
+ - ✅ End result: Strong type-safety via Schema Registry Pattern
531
+ - ✅ No manual contract creation needed
532
+
533
+ **IMPORTANT:** Always include `pf event:generate` in post-commands block for Event Consumers!
534
+
535
+ ### 🟢 ADVANCED MODE (Default - Recommended)
536
+
537
+ **Use Advanced Mode for Event Consumers** - contracts will be auto-created if missing:
538
+
539
+ 1. **Add dependency** `{{scope}}/contracts` in dependencies block
540
+ 2. **Import contract** in event handler
541
+ 3. **Use with `handleEvent`** for type-safe event handling
542
+
543
+ **Example Event Handler:**
138
544
  ```ts
139
545
  import { handleEvent } from '@crossdelta/cloudevents'
140
- import { z } from 'zod'
546
+ import { OrderCreatedContract, type OrderCreatedData } from '@scope/contracts'
141
547
  import { processOrder } from '../use-cases/process-order.use-case'
142
548
 
143
- const OrderCreatedSchema = z.object({
144
- orderId: z.string(),
145
- customerId: z.string(),
146
- total: z.number(),
549
+ export default handleEvent(OrderCreatedContract,
550
+ async (data: OrderCreatedData) => {
551
+ await processOrder(data)
552
+ },
553
+ )
554
+ ```
555
+
556
+ **Why Contracts as Default?**
557
+ - ✅ Strong type-safety between Publisher and Consumer
558
+ - ✅ Single source of truth for event schemas
559
+ - ✅ Schema evolution and versioning
560
+ - ✅ Automatic mock generation
561
+ - ✅ Better documentation
562
+
563
+ **IMPORTANT:** The `packages/contracts` package is your Schema Registry. Always use contracts from there for Event Consumers.
564
+
565
+ ### � ADVANCED MODE (Shared Contracts - RECOMMENDED DEFAULT)
566
+
567
+ Use **Advanced Mode** for all production services:
568
+
569
+ **Steps for Advanced Mode:**
570
+
571
+ 1. **Check if contract exists** in `packages/contracts/src/events/`
572
+ 2. **If exists:** Import and use the contract
573
+ 3. **If NOT exists:** Create contract first in `packages/contracts`, then use it
574
+
575
+ **Advanced Mode Example:**
576
+
577
+ ```ts
578
+ // packages/contracts/src/events/order-created.ts (create if not exists)
579
+ import { createContract } from '@crossdelta/cloudevents'
580
+ import { z } from 'zod'
581
+
582
+ export const OrderCreatedContract = createContract({
583
+ type: 'order.created',
584
+ schema: z.object({
585
+ orderId: z.string(),
586
+ customerId: z.string(),
587
+ items: z.array(z.object({
588
+ productId: z.string(),
589
+ quantity: z.number(),
590
+ price: z.number(),
591
+ })),
592
+ total: z.number(),
593
+ status: z.enum(['created']),
594
+ createdAt: z.string(),
595
+ }),
147
596
  })
148
597
 
149
- // Export type for use in use-cases
150
- export type OrderCreatedEvent = z.infer<typeof OrderCreatedSchema>
598
+ export type OrderCreatedData = z.infer<typeof OrderCreatedContract.schema>
599
+ ```
600
+
601
+ ```ts
602
+ // services/my-service/src/events/order-created.event.ts
603
+ import { handleEvent } from '@crossdelta/cloudevents'
604
+ import { OrderCreatedContract, type OrderCreatedData } from '@scope/contracts'
605
+ import { processOrder } from '../use-cases/process-order.use-case'
151
606
 
152
607
  export default handleEvent(
153
- {
154
- schema: OrderCreatedSchema,
155
- type: 'orders.created', // Event type here, NOT in schema
608
+ OrderCreatedContract, // Type-safe contract
609
+ async (data: OrderCreatedData) => {
610
+ await processOrder(data)
156
611
  },
157
- async (data) => {
612
+ )
613
+ ```
614
+
615
+ ```ts
616
+ // services/my-service/src/use-cases/process-order.use-case.ts
617
+ import type { OrderCreatedData } from '@scope/contracts'
618
+
619
+ export const processOrder = async (data: OrderCreatedData) => {
620
+ // Business logic with fully typed data
621
+ }
622
+ ```
623
+
624
+ **Dependencies:**
625
+ Add `{{scope}}/contracts` to your service's `package.json`:
626
+ ```json
627
+ {
628
+ "dependencies": {
629
+ "@scope/contracts": "workspace:*"
630
+ }
631
+ }
632
+ ```
633
+
634
+ ### � DEPRECATED: Basic Mode
635
+
636
+ **Basic Mode is deprecated and no longer recommended.**
637
+
638
+ The platform automatically creates missing contracts when you run `pf event:generate`.
639
+
640
+ **Previous Basic Mode approach:**
641
+ - ❌ Local schemas in `src/types/events.ts`
642
+ - ❌ String literals for event types
643
+ - ❌ No type-safety between services
644
+
645
+ **Current approach (Advanced Mode only):**
646
+ - ✅ Always use contracts from `{{scope}}/contracts`
647
+ - ✅ Contracts auto-created if missing
648
+ - ✅ Type-safety across all services
649
+ - ✅ Single source of truth
650
+
651
+ **Migration:** If you have Basic Mode handlers, run `pf event:generate` to automatically migrate them to Advanced Mode.
652
+
653
+ export const processOrder = async (data: OrderCreatedData) => {
654
+ // Use data.orderId, data.customerId, etc.
655
+ }
656
+ ```
657
+
658
+ {{AVAILABLE_CONTRACTS}}
659
+
660
+ **DO NOT:**
661
+ - ❌ Redefine schemas that exist in contracts
662
+ - ❌ Create inline schemas for existing event types
663
+ - ❌ Duplicate event type definitions
664
+
665
+ **Advanced Mode Example:**
666
+ ```ts
667
+ import { handleEvent } from '@crossdelta/cloudevents'
668
+ import { OrderCreatedContract, type OrderCreatedData } from '{{scope}}/contracts'
669
+ import { processOrder } from '../use-cases/process-order.use-case'
670
+
671
+ export default handleEvent(
672
+ OrderCreatedContract, // Import from contracts package
673
+ async (data: OrderCreatedData) => {
158
674
  await processOrder(data)
159
675
  },
160
676
  )
161
677
  ```
162
678
 
679
+ **If you need only a subset of fields**, extract them in the handler:
680
+ ```ts
681
+ export default handleEvent(
682
+ OrderCreatedContract,
683
+ async (data: OrderCreatedData) => {
684
+ // Extract only what you need
685
+ const { orderId, items } = data
686
+ await updateInventory({ orderId, items })
687
+ },
688
+ )
689
+ ```
690
+
691
+ **Contract Definition** (in `packages/contracts/src/schemas/order-created.schema.ts`):
692
+ ```ts
693
+ import { createContract } from '@crossdelta/cloudevents'
694
+ import { z } from 'zod'
695
+
696
+ export const OrderCreatedSchema = z.object({
697
+ orderId: z.string(),
698
+ customerId: z.string(),
699
+ total: z.number(),
700
+ })
701
+
702
+ export type OrderCreatedData = z.infer<typeof OrderCreatedSchema>
703
+
704
+ export const OrderCreatedContract = createContract({
705
+ type: 'orders.created',
706
+ schema: OrderCreatedSchema,
707
+ })
708
+ ```
709
+
710
+ ## General Event Handler Requirements
711
+
712
+ - Live under `src/events/*.event.ts` (NOT `src/handlers/`)
713
+ - Use Zod for validation (NO `type` field in schema!)
714
+ - Delegate ALL logic to use-cases
715
+ - Contain NO business logic
716
+ - **ALWAYS import from `@crossdelta/cloudevents`** (NOT `shared-events` or any other package!)
717
+
718
+ **CRITICAL:** Schema validates ONLY the event data payload (without `type` field). Event type is declared in the options object or contract.
719
+
720
+ **CRITICAL:** ALWAYS use the exact import: `import { handleEvent } from '@crossdelta/cloudevents'`
721
+
163
722
  **Naming conventions:**
164
723
  - Schema constants: PascalCase with `Schema` suffix (e.g., `OrderCreatedSchema`)
165
- - Exported types: PascalCase with `Event` suffix (e.g., `OrderCreatedEvent`)
724
+ - Exported types: PascalCase with `Data` suffix (e.g., `OrderCreatedData`)
725
+ - Contract constants: PascalCase with `Contract` suffix (e.g., `OrderCreatedContract`)
726
+
727
+ **Contract Structure (Advanced Mode - Default):**
728
+ ```ts
729
+ // packages/contracts/src/events/order-created.ts
730
+ import { createContract } from '@crossdelta/cloudevents'
731
+ import { z } from 'zod'
732
+
733
+ export const OrderCreatedSchema = z.object({
734
+ orderId: z.string(),
735
+ customerId: z.string(),
736
+ total: z.number(),
737
+ })
738
+
739
+ export const OrderCreatedContract = createContract({
740
+ type: 'order.created',
741
+ schema: OrderCreatedSchema,
742
+ })
743
+
744
+ export type OrderCreatedData = z.infer<typeof OrderCreatedContract.schema>
745
+ ```
746
+
747
+ **Decision Rule:**
748
+ - **Always use Advanced Mode** - contracts from `{{scope}}/contracts`
749
+ - **Contracts auto-created** - `pf event:generate` creates missing contracts
750
+ - Meaning → `packages/contracts`
751
+ - Behavior → `services/*/src`
166
752
 
167
753
  Forbidden in handlers:
168
754
  - `type: z.literal('...')` in schema (redundant and causes errors)
169
755
  - DB access
170
- - External API calls
171
756
  - Multi-step logic
172
757
  - Env var parsing
173
758
 
174
759
  ---
175
760
 
176
- # 8. Testing Rules
761
+ # 9. Testing Rules
177
762
 
178
763
  **CRITICAL:**
179
764
  - Test ONLY use-cases (NOT event handlers)
@@ -223,30 +808,134 @@ describe('Send Notification Use Case', () => {
223
808
 
224
809
  ---
225
810
 
226
- # 9. README Requirements
811
+ # 10. README Requirements
227
812
 
228
813
  MUST include:
229
814
  - Description
815
+ - Features list
230
816
  - Environment variable table
231
- - Published/consumed events
817
+ - Published/consumed events (with schemas)
232
818
  - API endpoints
819
+ - **Testing section with event mock examples**
233
820
  - Dev commands
234
821
 
822
+ ### README Structure
823
+
824
+ ```markdown
825
+ # Service Name
826
+
827
+ Brief description of what this service does.
828
+
829
+ ## Features
830
+
831
+ - Feature 1
832
+ - Feature 2
833
+
834
+ ## Environment Variables
835
+
836
+ | Variable | Description | Required | Default |
837
+ |----------|-------------|----------|---------|
838
+ | `SERVICE_PORT` | Port the service runs on | No | 4001 |
839
+ | `NATS_URL` | NATS server URL | Yes | - |
840
+
841
+ ## Events
842
+
843
+ ### Publishes
844
+ - `orderboss.service.event` - Description
845
+ ```json
846
+ {
847
+ "field": "value"
848
+ }
849
+ ```
850
+
851
+ ### Consumes
852
+ - `orderboss.other.event` - Description
853
+ ```json
854
+ {
855
+ "field": "value"
856
+ }
857
+ ```
858
+
859
+ ## API Endpoints
860
+
861
+ ### `GET /health`
862
+ Health check endpoint.
863
+
864
+ **Response:**
865
+ \`\`\`json
866
+ { "status": "ok" }
867
+ \`\`\`
868
+
869
+ ## Testing
870
+
871
+ ### Event Mocks
872
+
873
+ This service includes generated mock files for all event handlers in `src/events/*.mock.json`. These mocks can be used for testing and development.
874
+
875
+ **List all available event mocks:**
876
+ \`\`\`bash
877
+ pf event:list
878
+ \`\`\`
879
+
880
+ **Publish a test event using a mock:**
881
+ \`\`\`bash
882
+ pf event:publish order-created
883
+ \`\`\`
884
+
885
+ **Regenerate event mocks:**
886
+ \`\`\`bash
887
+ pf event:generate . --overwrite
888
+ \`\`\`
889
+
890
+ ### Manual Testing
891
+
892
+ To test event handlers manually:
893
+
894
+ 1. Start the service:
895
+ \`\`\`bash
896
+ bun dev
897
+ \`\`\`
898
+
899
+ 2. In another terminal, publish a test event:
900
+ \`\`\`bash
901
+ pf event:publish order-created
902
+ \`\`\`
903
+
904
+ 3. Check the service logs for the handler output
905
+
906
+ ### Unit Tests
907
+
908
+ Run use-case tests:
909
+ \`\`\`bash
910
+ bun test
911
+ \`\`\`
912
+
913
+ ## Development
914
+
915
+ \`\`\`bash
916
+ bun dev # Start in development mode
917
+ bun test # Run tests
918
+ bun lint # Check code quality
919
+ \`\`\`
920
+ ```
921
+
235
922
  ---
236
923
 
237
- # 10. Absolute Rules
924
+ # 11. Absolute Rules
238
925
 
239
926
  DO NOT:
240
927
  - Generate full rewrites
241
928
  - Add new architecture concepts
242
929
  - Use raw NATS clients
243
- - Put logic in handlers or index.ts
930
+ - Put logic in event handlers or index.ts
244
931
  - Insert semicolons
245
932
  - Add unused folders/files
933
+ - **Use `src/handlers/` directory (use `src/events/` instead!)**
246
934
 
247
935
  DO:
248
936
  - Produce minimal, clean code
249
937
  - Follow structure and conventions strictly
250
938
  - Keep output compact and correct
939
+ - **Always place event handlers in `src/events/` directory**
251
940
 
252
941
  ---