@eventmodelers/node-kit 0.0.10 → 0.0.12

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.
Files changed (45) hide show
  1. package/package.json +1 -1
  2. package/templates/.claude/skills/build-automation/SKILL.md +260 -0
  3. package/templates/.claude/skills/build-state-change/SKILL.md +329 -0
  4. package/templates/.claude/skills/build-state-view/SKILL.md +384 -0
  5. package/templates/.claude/skills/learn-eventmodelers-api/SKILL.md +609 -0
  6. package/templates/.claude/skills/load-slice/SKILL.md +69 -14
  7. package/templates/realtime-agent/src/index.js +12 -17
  8. package/templates/root/.env.example +22 -0
  9. package/templates/root/Claude.md +58 -0
  10. package/templates/root/agent.sh +15 -0
  11. package/templates/root/backend-prompt.md +139 -0
  12. package/templates/root/flyway.conf +17 -0
  13. package/templates/root/package.json +52 -0
  14. package/templates/root/ralph.sh +47 -26
  15. package/templates/root/server.ts +213 -0
  16. package/templates/root/setup-env.sh +55 -0
  17. package/templates/root/src/common/assertions.ts +6 -0
  18. package/templates/root/src/common/db.ts +32 -0
  19. package/templates/root/src/common/loadPostgresEventstore.ts +39 -0
  20. package/templates/root/src/common/parseEndpoint.ts +51 -0
  21. package/templates/root/src/common/processorDlq.ts +28 -0
  22. package/templates/root/src/common/realtimeBroadcast.ts +19 -0
  23. package/templates/root/src/common/replay.ts +16 -0
  24. package/templates/root/src/common/routes.ts +19 -0
  25. package/templates/root/src/common/testHelpers.ts +54 -0
  26. package/templates/root/src/slices/example/routes.ts +134 -0
  27. package/templates/root/src/supabase/LoginHandler.ts +36 -0
  28. package/templates/root/src/supabase/ProtectedPageProps.ts +21 -0
  29. package/templates/root/src/supabase/README.md +171 -0
  30. package/templates/root/src/supabase/api.ts +56 -0
  31. package/templates/root/src/supabase/component.ts +12 -0
  32. package/templates/root/src/supabase/requireOrgaAdmin.ts +32 -0
  33. package/templates/root/src/supabase/requireUser.ts +72 -0
  34. package/templates/root/src/supabase/serverProps.ts +25 -0
  35. package/templates/root/src/supabase/staticProps.ts +10 -0
  36. package/templates/root/src/swagger.ts +34 -0
  37. package/templates/root/src/util/assertions.ts +6 -0
  38. package/templates/root/src/util/hash.ts +9 -0
  39. package/templates/root/src/util/sanitize.ts +23 -0
  40. package/templates/root/supabase/config.toml +295 -0
  41. package/templates/root/supabase/migrations/V1__schema.sql.example +12 -0
  42. package/templates/root/supabase/seed.sql +1 -0
  43. package/templates/root/tsconfig.json +32 -0
  44. package/templates/root/vercel.json +8 -0
  45. package/templates/root/model.md +0 -1
@@ -0,0 +1,609 @@
1
+ ---
2
+ name: learn-eventmodelers-api
3
+ description: Teaches an agent everything about the eventmodelers platform API — all endpoints, their purpose, request payloads, response shapes, authentication, and element types.
4
+ ---
5
+
6
+ # Eventmodelers Platform API Reference
7
+
8
+ You now have complete knowledge of the eventmodelers platform API. Use this reference whenever you need to call, implement, or reason about any endpoint.
9
+
10
+ ---
11
+
12
+ ## Architecture Overview
13
+
14
+ - **Framework**: Express.js + `@event-driven-io/emmett` (event sourcing)
15
+ - **Adapter**: `@event-driven-io/emmett-expressjs`
16
+ - **Database**: PostgreSQL via Knex
17
+ - **Storage / Auth**: Supabase
18
+ - **Route discovery**: Dynamic glob (`**/routes{,-*}.js`) loaded from `dist/src/slices`
19
+ - **Base URL** (local): `http://localhost:3000`
20
+
21
+ ---
22
+
23
+ ## Authentication & Headers
24
+
25
+ | Header | Required | Purpose |
26
+ |---|---|---|
27
+ | `Authorization` | Some routes | Supabase JWT bearer token |
28
+ | `x-user-id` | Node operations | User identifier |
29
+ | `x-causation-id` | Optional | Event causation tracing |
30
+ | `x-correlation-id` | Optional | Correlation tracing |
31
+
32
+ - CORS allowed origins: `localhost:3000`, `localhost:3001`, `https://app.eventmodelers.de`
33
+
34
+ ---
35
+
36
+ ## Element Types
37
+
38
+ ```typescript
39
+ MODEL_CONTEXT // Context/domain modeling container
40
+ CHAPTER // Timeline/sequence container
41
+ ACTOR // System participant (swimlane label)
42
+ AUTOMATION // Automated action
43
+ API // External service
44
+ SCREEN // UI screen
45
+ COMMAND // State-changing operation
46
+ EVENT // Domain event
47
+ SPEC_ERROR // Error scenario
48
+ TABLE // Data table
49
+ READMODEL // Query result / materialized view
50
+ SCENARIO // GWT scenario
51
+ LANE // Timeline row
52
+ SLICE_BORDER // Slice boundary marker
53
+ ```
54
+
55
+ ---
56
+
57
+ ## Standard HTTP Status Codes
58
+
59
+ | Code | Meaning |
60
+ |---|---|
61
+ | 200 | OK with data |
62
+ | 201 | Created |
63
+ | 204 | No content |
64
+ | 400 | Validation error / bad input |
65
+ | 401 | Authentication required |
66
+ | 404 | Resource not found |
67
+ | 409 | Conflict (e.g. duplicate) |
68
+ | 500 | Server error |
69
+
70
+ ---
71
+
72
+ ## 1. Boards
73
+
74
+ **File**: `src/slices/change/api-boards/routes.ts`
75
+
76
+ ### POST `/api/org/:orgId/boards/:boardId/events`
77
+ Persist board/timeline row events as an array of mixed event types.
78
+
79
+ **Request body**: Array of node, comment, edge, or board events
80
+ **Response**: `200` — processed results array
81
+
82
+ ---
83
+
84
+ ### GET `/api/boards`
85
+ List all boards.
86
+
87
+ **Response**: `200` — `Board[]`
88
+
89
+ ---
90
+
91
+ ### DELETE `/api/org/:orgId/boards/:boardId`
92
+ Delete a board.
93
+
94
+ **Response**: `204`
95
+
96
+ ---
97
+
98
+ ### GET `/api/org/:orgId/boards/:boardId/events/search`
99
+ Search events by node name.
100
+
101
+ **Query params**: `name` (string)
102
+ **Response**: `200` — matching event array
103
+
104
+ ---
105
+
106
+ ### GET `/api/org/:orgId/boards/:boardId/events`
107
+ Get all board events in sequence.
108
+
109
+ **Response**: `200` — event array
110
+
111
+ ---
112
+
113
+ ### GET `/api/org/:orgId/boards/:boardId/nodes/:nodeId/comments`
114
+ Get all comments for a node.
115
+
116
+ **Response**: `200` — comment array
117
+
118
+ ---
119
+
120
+ ### POST `/api/org/:orgId/boards/:boardId/bucket`
121
+ Create a Supabase storage bucket for the board.
122
+
123
+ **Response**: `200` — `{ ok: boolean, bucket: string, alreadyExisted: boolean }`
124
+
125
+ ---
126
+
127
+ ## 2. Chapters & Timelines
128
+
129
+ **File**: `src/slices/change/api-chapters/routes.ts`
130
+
131
+ ### POST `/api/org/:orgId/boards/:boardId/chapters`
132
+ Create a chapter node.
133
+
134
+ **Request body**: `{ position?: { x: number, y: number } }`
135
+ **Response**: `200` — chapter data
136
+
137
+ ---
138
+
139
+ ### POST `/api/org/:orgId/boards/:boardId/timelines/:timelineId/columns`
140
+ Add a column to a timeline.
141
+
142
+ **Request body**: `{ index?: number }` (integer index, optional)
143
+ **Response**: `200` — `{ columnId: string, index: number, totalColumns: number }`
144
+
145
+ ---
146
+
147
+ ### DELETE `/api/org/:orgId/boards/:boardId/timelines/:timelineId/columns/:columnId`
148
+ Delete a column from a timeline. Removes the column and all its cells. Cannot delete the last column.
149
+
150
+ **Response**:
151
+ - `200` — `{ columnId: string, totalColumns: number }`
152
+ - `400` — validation error (e.g. last column)
153
+ - `404` — timeline or column not found
154
+
155
+ ---
156
+
157
+ ### POST `/api/org/:orgId/boards/:boardId/timelines/:timelineId/lanes`
158
+ Add a lane (row) to a timeline.
159
+
160
+ **Request body**:
161
+ ```typescript
162
+ {
163
+ type: 'actor' | 'interaction' | 'swimlane' | 'spec' | 'feedback'
164
+ label?: string
165
+ index?: number
166
+ height?: number
167
+ }
168
+ ```
169
+ **Response**: `200` — lane data
170
+
171
+ ---
172
+
173
+ ### POST `/api/org/:orgId/boards/:boardId/timelines/:timelineId/cells/:cellId/drop`
174
+ Drop a node into a timeline cell. Validates placement rules.
175
+
176
+ **Request body**: `{ nodeId: string, nodeType: ElementType }`
177
+
178
+ **Placement rules**:
179
+ - `swimlane` lane → accepts `EVENT`
180
+ - `interaction` lane → accepts `COMMAND`, `READMODEL`
181
+ - `actor` lane → accepts `SCREEN`, `AUTOMATION`
182
+ - `feedback` lane → accepts markdown
183
+ - `spec` lane → accepts `SPEC_NODE`
184
+
185
+ **Response**:
186
+ - `200` — drop result
187
+ - `400` — placement violation
188
+ - `404` — cell or node not found
189
+
190
+ ---
191
+
192
+ ## 3. Nodes
193
+
194
+ **File**: `src/slices/change/api-nodes/routes.ts`
195
+
196
+ All node endpoints require header: `x-user-id`
197
+
198
+ ### POST `/api/org/:orgId/boards/:boardId/nodes/events`
199
+ Submit node change events.
200
+
201
+ **Request body**: `NodeChangeEvent[]`
202
+
203
+ ```typescript
204
+ interface NodeChangeEvent {
205
+ id: string // uuid
206
+ eventType: 'node:created' | 'node:changed' | 'node:deleted'
207
+ nodeId: string
208
+ boardId: string
209
+ timestamp: number // unix ms
210
+ userId?: string
211
+ hash?: string // content hash
212
+ changedAttributes?: string[] // dot-paths e.g. 'meta.title'
213
+ node?: {
214
+ id: string
215
+ data: {
216
+ backgroundColor?: string
217
+ title?: string
218
+ type?: string
219
+ url?: string
220
+ // ...other node data fields
221
+ }
222
+ }
223
+ meta?: {
224
+ type: ElementType
225
+ title?: string
226
+ description?: string
227
+ fields?: Record<string, unknown>
228
+ // ...
229
+ }
230
+ edges?: Array<{
231
+ id: string
232
+ source: string
233
+ target: string
234
+ sourceHandle?: string
235
+ targetHandle?: string
236
+ }>
237
+ chapterId?: string // for cell placement
238
+ cellName?: string // spreadsheet-style e.g. "B2"
239
+ }
240
+ ```
241
+
242
+ **Response**: `200` — `{ hashes: { [eventId: string]: string } }`
243
+
244
+ ---
245
+
246
+ ### GET `/api/org/:orgId/boards/:boardId/nodes`
247
+ List all nodes on a board.
248
+
249
+ **Query params**: `type?: ElementType`
250
+ **Response**: `200` — node record array
251
+
252
+ ---
253
+
254
+ ### GET `/api/org/:orgId/boards/:boardId/nodes/:nodeId`
255
+ Get a single node.
256
+
257
+ **Response**: `200` — node record OR `404`
258
+
259
+ ---
260
+
261
+ ## 4. Images
262
+
263
+ **File**: `src/slices/change/api-images/routes.ts`
264
+
265
+ ### POST `/api/org/:orgId/boards/:boardId/images/:imageId`
266
+ Update a board image.
267
+
268
+ **Request**: `multipart/form-data` — field `file` (binary)
269
+ **Response**: `204`
270
+
271
+ ---
272
+
273
+ ### POST `/api/org/:orgId/boards/:boardId/imagesnapshots/:imageId`
274
+ Update an image snapshot.
275
+
276
+ **Request**: `multipart/form-data` — field `file` (binary)
277
+ **Response**: `204`
278
+
279
+ ---
280
+
281
+ ### POST `/api/org/:orgId/boards/:boardId/image-nodes/:nodeId`
282
+ Create an image node.
283
+
284
+ **Request**: `multipart/form-data` — fields: `file`, `chapterId`, `cellName`
285
+ **Response**: `204`
286
+
287
+ ---
288
+
289
+ ### POST `/api/org/:orgId/boards/:boardId/images/:imageId/sketch`
290
+ Render a sketch description to WebP and upload.
291
+
292
+ **Request body**:
293
+ ```typescript
294
+ {
295
+ elements: object[] // sketch element descriptors
296
+ semanticDescription?: string // human-readable description stored in metadata
297
+ }
298
+ ```
299
+ **Response**: `204`
300
+
301
+ ---
302
+
303
+ ### POST `/api/org/:orgId/boards/:boardId/image-nodes/:nodeId/sketch`
304
+ Create a SCREEN node from a sketch description.
305
+
306
+ **Request body**:
307
+ ```typescript
308
+ {
309
+ chapterId: string
310
+ cellName: string
311
+ description: { elements: object[] }
312
+ semanticDescription?: string
313
+ }
314
+ ```
315
+ **Response**: `204` OR `400` (validation error)
316
+
317
+ ---
318
+
319
+ ## 5. Slices
320
+
321
+ **File**: `src/slices/change/api-slices/routes.ts`
322
+
323
+ ### POST `/api/org/:orgId/boards/:boardId/timelines/:timelineId/slices`
324
+ Create a complete slice (1 column + 3 nodes automatically placed).
325
+
326
+ **Request body**:
327
+ ```typescript
328
+ {
329
+ type: 'state-change' | 'state-view' | 'automation'
330
+ index?: number
331
+ nodes?: {
332
+ actor?: Partial<NodeData>
333
+ interaction?: Partial<NodeData>
334
+ swimlane?: Partial<NodeData>
335
+ }
336
+ }
337
+ ```
338
+
339
+ **Slice node mapping**:
340
+ - `state-change` → SCREEN (actor) + COMMAND (interaction) + EVENT (swimlane)
341
+ - `state-view` → SCREEN (actor) + READMODEL (interaction) + EVENT (swimlane)
342
+ - `automation` → AUTOMATION (actor) + COMMAND (interaction) + EVENT (swimlane)
343
+
344
+ **Response**: `200` — slice data
345
+
346
+ ---
347
+
348
+ ## 6. Specifications (GWT Scenarios)
349
+
350
+ **File**: `src/slices/change/api-specs/routes.ts`
351
+
352
+ ### POST `/api/org/:orgId/boards/:boardId/contexts/:contextName/slices/:sliceName/scenarios`
353
+ Append a Given-When-Then scenario to a spec node.
354
+
355
+ **Request body**:
356
+ ```typescript
357
+ {
358
+ id: string
359
+ title: string
360
+ vertical?: boolean
361
+ examples?: unknown[]
362
+ given: string[] // nodeIds — must be EVENTs from same timeline
363
+ when: string[] // nodeIds — at most one COMMAND; empty if then has READMODEL
364
+ then: string[] // nodeIds — EVENTs only OR exactly one READMODEL (not mixed)
365
+ }
366
+ ```
367
+
368
+ **Validation rules**:
369
+ - `given`: only EVENTs from same timeline
370
+ - `when`: max one COMMAND; must be empty when `then` contains a READMODEL
371
+ - `then`: all EVENTs OR exactly one READMODEL — never mixed
372
+ - All referenced nodes must belong to the same chapter/timeline
373
+
374
+ **Response**:
375
+ - `201` — `{ scenario, scenarios, specNodeId, isNewNode: boolean }`
376
+ - `400` — validation error
377
+ - `404` — context or slice not found
378
+ - `409` — duplicate scenario title
379
+
380
+ ---
381
+
382
+ ### GET `/api/org/:orgId/boards/:boardId/contexts/:contextName/spec-info`
383
+ Get valid elements for a context (by name lookup).
384
+
385
+ **Response**: `200` — `{ chapterId: string, elements: ElementRecord[] }`
386
+
387
+ ---
388
+
389
+ ### GET `/api/org/:orgId/boards/:boardId/contexts/:contextName/slices/:sliceName/spec-info`
390
+ Get valid elements for a specific slice.
391
+
392
+ **Response**: `200` — `{ chapterId: string, elements: ElementRecord[] }`
393
+
394
+ ---
395
+
396
+ ## 7. Config Import
397
+
398
+ **File**: `src/slices/change/config-import/routes.ts`
399
+
400
+ ### POST `/api/org/:orgId/boards/:boardId/import-config`
401
+ Import an EventModelingJson config to populate a board.
402
+
403
+ **Request**: `multipart/form-data` with field `file` OR `application/json` body:
404
+ ```typescript
405
+ { slices: SliceDefinition[] }
406
+ ```
407
+
408
+ **Response**: `200` — transformed canvas with nodes and edges
409
+
410
+ ---
411
+
412
+ ## 8. Slice Data
413
+
414
+ **File**: `src/slices/slicedata/routes.ts`
415
+
416
+ ### GET ` `
417
+ Build structured slice data from board state.
418
+
419
+ **Query params** (one required): `contextId` OR `contextName`; optional: `sliceId`
420
+ **Response**: `200` — slice data matching event modeling schema
421
+
422
+ ---
423
+
424
+ ### GET `/api/org/:orgId/boards/:boardId/slicedata/slices`
425
+ List all slices on a board.
426
+
427
+ **Response**: `200` — `{ slices: Array<{ id: string, title: string, status: string }> }`
428
+
429
+ ---
430
+
431
+ ## 9. Extensions
432
+
433
+ **File**: `src/slices/extensions/routes.ts`
434
+
435
+ ### GET `/api/org/:orgId/boards/:boardId/extensions`
436
+ List extension configs for a board.
437
+
438
+ **Response**: `200` — extension record array
439
+
440
+ ---
441
+
442
+ ### PUT `/api/org/:orgId/boards/:boardId/extensions/:type`
443
+ Enable or disable an extension.
444
+
445
+ **Request body**: `{ enabled: boolean, config?: object }`
446
+ **Response**: `200` — updated extension config
447
+
448
+ ---
449
+
450
+ ## 10. Snapshots
451
+
452
+ **File**: `src/slices/Snapshots/routes.ts`
453
+
454
+ All snapshot endpoints require Supabase JWT authentication.
455
+
456
+ **Constraints**: max 3 snapshots per user, max 30-day retention, max 50 MB file size.
457
+
458
+ ### GET `/api/snapshots`
459
+ List current user's snapshots.
460
+
461
+ **Response**: `200` — `Array<{ id, name, payload_id, expiry, shared }>`
462
+
463
+ ---
464
+
465
+ ### POST `/api/snapshots`
466
+ Create a snapshot.
467
+
468
+ **Request**: `multipart/form-data` — fields: `payloadFile` (binary), `name` (string), `retention?` (days, max 30)
469
+ **Response**: `201` — `{ ok: true, id: string }`
470
+
471
+ ---
472
+
473
+ ### GET `/api/snapshots/:id`
474
+ Load a snapshot's payload.
475
+
476
+ **Response**: `200` — snapshot payload JSON
477
+
478
+ ---
479
+
480
+ ### PATCH `/api/snapshots/:id/share`
481
+ Share a snapshot (makes it publicly accessible).
482
+
483
+ **Response**: `200` — `{ ok: true }`
484
+
485
+ ---
486
+
487
+ ### DELETE `/api/snapshots/:id`
488
+ Delete a snapshot.
489
+
490
+ **Response**: `200` — `{ ok: true }`
491
+
492
+ ---
493
+
494
+ ## 11. User Management — Commands (Event Sourced)
495
+
496
+ All commands respond with:
497
+ ```typescript
498
+ {
499
+ ok: true
500
+ next_expected_stream_version: number
501
+ last_event_global_position: number
502
+ }
503
+ ```
504
+
505
+ Optional headers on all: `correlation_id`, `causation_id`
506
+
507
+ ### POST `/api/creategroup`
508
+ **Body**: `{ groupId: string, name: string }`
509
+ **Event emitted**: `GroupCreated`
510
+
511
+ ---
512
+
513
+ ### POST `/api/inviteuser`
514
+ **Body**: `{ groupId: string, email: string, invitationId: string }`
515
+ **Event emitted**: `UserInvited`
516
+
517
+ ---
518
+
519
+ ### POST `/api/acceptinvite`
520
+ **Body**: `{ userId: string, groupId: string, invitationId: string }`
521
+ **Event emitted**: `InvitationAccepted`
522
+
523
+ ---
524
+
525
+ ### POST `/api/assignrole`
526
+ **Body**: `{ userId: string, groupId: string, role: string }`
527
+ **Event emitted**: `RoleAssigned`
528
+
529
+ ---
530
+
531
+ ## 12. User Management — Read Models (Projections)
532
+
533
+ All require authentication. Optional query param `_id` to filter by ID.
534
+
535
+ ### GET `/api/query/group-details-lookup`
536
+ Group details. Filter: `?_id=groupId`
537
+
538
+ ### GET `/api/query/open-invites`
539
+ Pending invitations. Filter: `?_id=invitationId`
540
+
541
+ ### GET `/api/query/user-group-assignments`
542
+ User-to-group mappings. Filter: `?_id=groupId`
543
+
544
+ ### GET `/api/query/users-to-assign-to-groups`
545
+ Users available for group assignment. Filter: `?_id=userId`
546
+
547
+ ---
548
+
549
+ ## 13. Utility
550
+
551
+ ### GET `/api/user`
552
+ Get current authenticated user info.
553
+
554
+ **Response**: `{ user_id: string, email: string, metadata: object }`
555
+
556
+ ### GET `/api-docs`
557
+ Swagger UI (interactive API explorer)
558
+
559
+ ### GET `/swagger.json`
560
+ OpenAPI specification (JSON)
561
+
562
+ ---
563
+
564
+ ## Domain Events
565
+
566
+ ### Snapshot Events (`src/events/SnapshotsEvents.ts`)
567
+
568
+ ```typescript
569
+ SnapshotStored // { name, id, payloadId, expiry }
570
+ SnapshotDeleted // { id }
571
+ SnapshotCleanedUp // { id }
572
+ PublishedSnapshotDeleted // { id }
573
+ SnapshotShared // { id }
574
+ SnapshotPublished // { id, payloadId, bucket, path }
575
+ ```
576
+
577
+ ### User Management Events (`src/events/UserManagementEvents.ts`)
578
+
579
+ ```typescript
580
+ GroupCreated // { groupId, owner, name }
581
+ UserAssignedToGroup // { groupId, userId }
582
+ UserInvited // { groupId, invitationId, email }
583
+ InvitationAccepted // { invitationId, groupId, userId }
584
+ RoleAssigned // { groupId, userId, role }
585
+ ```
586
+
587
+ All events support optional metadata: `user_id`, `correlation_id`, `causation_id`
588
+
589
+ ---
590
+
591
+ ## Key Source Files
592
+
593
+ | File | Purpose |
594
+ |---|---|
595
+ | `src/slices/change/types.ts` | `ElementType`, `NodeChangeEvent`, `EdgeEvent` |
596
+ | `src/slices/change/api-boards/routes.ts` | Board CRUD + event persistence |
597
+ | `src/slices/change/api-chapters/routes.ts` | Chapters, columns, lanes, cell drops |
598
+ | `src/slices/change/api-nodes/routes.ts` | Node event sourcing |
599
+ | `src/slices/change/api-images/routes.ts` | Image upload + sketch rendering |
600
+ | `src/slices/change/api-slices/routes.ts` | Slice creation |
601
+ | `src/slices/change/api-specs/routes.ts` | GWT scenario management |
602
+ | `src/slices/change/config-import/routes.ts` | Config import |
603
+ | `src/slices/slicedata/routes.ts` | Slice data read models |
604
+ | `src/slices/extensions/routes.ts` | Extension management |
605
+ | `src/slices/Snapshots/routes.ts` | Snapshot CRUD |
606
+ | `src/slices/usermanagement/*/routes*.ts` | User management commands + projections |
607
+ | `src/events/SnapshotsEvents.ts` | Snapshot domain events |
608
+ | `src/events/UserManagementEvents.ts` | User management domain events |
609
+ | `backend/src/server.ts` | Route wiring, CORS, `/api/user` |