@eventmodelers/cli 0.0.1

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 (220) hide show
  1. package/README.md +175 -0
  2. package/cli.js +676 -0
  3. package/package.json +35 -0
  4. package/shared/build-kit/code-export.mjs +560 -0
  5. package/shared/build-kit/lib/ollama-agent.js +147 -0
  6. package/shared/build-kit/package.json +11 -0
  7. package/shared/build-kit/ralph-ollama.js +39 -0
  8. package/shared/build-kit/realtime-agent.js +18 -0
  9. package/stacks/axon/templates/.claude/skills/build-automation/SKILL.md +407 -0
  10. package/stacks/axon/templates/.claude/skills/build-automation/references/feature-flag-patterns.md +114 -0
  11. package/stacks/axon/templates/.claude/skills/build-automation-workflow/SKILL.md +496 -0
  12. package/stacks/axon/templates/.claude/skills/build-automation-workflow/references/axon-workflow-api.md +279 -0
  13. package/stacks/axon/templates/.claude/skills/build-automation-workflow/references/feature-flag-patterns.md +114 -0
  14. package/stacks/axon/templates/.claude/skills/build-state-change/SKILL.md +396 -0
  15. package/stacks/axon/templates/.claude/skills/build-state-change/references/axon-test-fixture-patterns.md +162 -0
  16. package/stacks/axon/templates/.claude/skills/build-state-change/references/feature-flag-patterns.md +56 -0
  17. package/stacks/axon/templates/.claude/skills/build-state-change/references/rest-api-patterns.md +115 -0
  18. package/stacks/axon/templates/.claude/skills/build-state-view/SKILL.md +282 -0
  19. package/stacks/axon/templates/.claude/skills/connect/SKILL.md +178 -0
  20. package/stacks/axon/templates/.claude/skills/learn-eventmodelers-api/SKILL.md +611 -0
  21. package/stacks/axon/templates/.claude/skills/load-slice/SKILL.md +141 -0
  22. package/stacks/axon/templates/.claude/skills/update-slice-status/SKILL.md +105 -0
  23. package/stacks/axon/templates/build-kit/lib/AGENT.md +47 -0
  24. package/stacks/axon/templates/build-kit/lib/agent.sh +20 -0
  25. package/stacks/axon/templates/build-kit/lib/backend-prompt.md +158 -0
  26. package/stacks/axon/templates/build-kit/lib/prompt.md +126 -0
  27. package/stacks/axon/templates/build-kit/lib/ralph.js +369 -0
  28. package/stacks/axon/templates/build-kit/ralph-claude.js +44 -0
  29. package/stacks/axon/templates/build-kit/ralph.sh +98 -0
  30. package/stacks/axon/templates/root/.env.example +5 -0
  31. package/stacks/axon/templates/root/CLAUDE.md +60 -0
  32. package/stacks/axon/templates/root/README.md +44 -0
  33. package/stacks/axon/templates/root/docker-compose.yml +64 -0
  34. package/stacks/axon/templates/root/mvnw +259 -0
  35. package/stacks/axon/templates/root/mvnw.cmd +149 -0
  36. package/stacks/axon/templates/root/pom.xml +125 -0
  37. package/stacks/axon/templates/root/src/main/java/io/axoniq/quickstart/QuickstartApplication.java +128 -0
  38. package/stacks/axon/templates/root/src/main/java/io/axoniq/quickstart/config/WebConfig.java +155 -0
  39. package/stacks/axon/templates/root/src/main/resources/application.properties +2 -0
  40. package/stacks/axon/templates/root/src/main/resources/static/bg-body.png +0 -0
  41. package/stacks/axon/templates/root/src/main/resources/static/bg-header.png +0 -0
  42. package/stacks/axon/templates/root/src/main/resources/static/index.html +596 -0
  43. package/stacks/cratis-csharp/templates/.claude/skills/_shared/cratis-conventions.md +251 -0
  44. package/stacks/cratis-csharp/templates/.claude/skills/build-automation/SKILL.md +99 -0
  45. package/stacks/cratis-csharp/templates/.claude/skills/build-automation/references/patterns.md +115 -0
  46. package/stacks/cratis-csharp/templates/.claude/skills/build-state-change/SKILL.md +163 -0
  47. package/stacks/cratis-csharp/templates/.claude/skills/build-state-change/references/patterns.md +234 -0
  48. package/stacks/cratis-csharp/templates/.claude/skills/build-state-view/SKILL.md +120 -0
  49. package/stacks/cratis-csharp/templates/.claude/skills/build-state-view/references/patterns.md +166 -0
  50. package/stacks/cratis-csharp/templates/.claude/skills/connect/SKILL.md +169 -0
  51. package/stacks/cratis-csharp/templates/.claude/skills/learn-eventmodelers-api/SKILL.md +609 -0
  52. package/stacks/cratis-csharp/templates/.claude/skills/load-slice/SKILL.md +141 -0
  53. package/stacks/cratis-csharp/templates/.claude/skills/update-slice-status/SKILL.md +105 -0
  54. package/stacks/cratis-csharp/templates/build-kit/lib/AGENT.md +58 -0
  55. package/stacks/cratis-csharp/templates/build-kit/lib/agent.sh +20 -0
  56. package/stacks/cratis-csharp/templates/build-kit/lib/backend-prompt.md +129 -0
  57. package/stacks/cratis-csharp/templates/build-kit/lib/prompt.md +124 -0
  58. package/stacks/cratis-csharp/templates/build-kit/lib/ralph.js +302 -0
  59. package/stacks/cratis-csharp/templates/build-kit/ralph-claude.js +37 -0
  60. package/stacks/cratis-csharp/templates/build-kit/ralph.sh +98 -0
  61. package/stacks/cratis-csharp/templates/root/.frontend/index.css +29 -0
  62. package/stacks/cratis-csharp/templates/root/.frontend/index.html +17 -0
  63. package/stacks/cratis-csharp/templates/root/.frontend/main.tsx +18 -0
  64. package/stacks/cratis-csharp/templates/root/.frontend/tsconfig.json +42 -0
  65. package/stacks/cratis-csharp/templates/root/.frontend/tsconfig.node.json +11 -0
  66. package/stacks/cratis-csharp/templates/root/.frontend/vite.config.ts +56 -0
  67. package/stacks/cratis-csharp/templates/root/App.tsx +23 -0
  68. package/stacks/cratis-csharp/templates/root/CLAUDE.md +62 -0
  69. package/stacks/cratis-csharp/templates/root/CratisApp.csproj +25 -0
  70. package/stacks/cratis-csharp/templates/root/CratisApp.sln +18 -0
  71. package/stacks/cratis-csharp/templates/root/GlobalUsings.cs +3 -0
  72. package/stacks/cratis-csharp/templates/root/Home.tsx +102 -0
  73. package/stacks/cratis-csharp/templates/root/Program.cs +26 -0
  74. package/stacks/cratis-csharp/templates/root/README.md +192 -0
  75. package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/Listing/AllListings.ts +47 -0
  76. package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/Listing/Listing.cs +11 -0
  77. package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/Listing/Listing.ts +12 -0
  78. package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/Listing/ListingDataTable.tsx +17 -0
  79. package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/Listing/index.ts +1 -0
  80. package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/Registration/Register.ts +51 -0
  81. package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/Registration/RegisterDialog.tsx +18 -0
  82. package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/Registration/Registration.cs +27 -0
  83. package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/Registration/index.ts +1 -0
  84. package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/SomeFeature.tsx +22 -0
  85. package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/SomeName.cs +3 -0
  86. package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/index.ts +1 -0
  87. package/stacks/cratis-csharp/templates/root/appsettings.Development.json +9 -0
  88. package/stacks/cratis-csharp/templates/root/appsettings.json +26 -0
  89. package/stacks/cratis-csharp/templates/root/docker-compose.yml +23 -0
  90. package/stacks/cratis-csharp/templates/root/package.json +33 -0
  91. package/stacks/cratis-csharp/templates/root/tsconfig.json +3 -0
  92. package/stacks/modeling-kit/templates/.claude/settings.local.json +13 -0
  93. package/stacks/modeling-kit/templates/.claude/skills/analyze-existing-model/SKILL.md +215 -0
  94. package/stacks/modeling-kit/templates/.claude/skills/attributes/SKILL.md +176 -0
  95. package/stacks/modeling-kit/templates/.claude/skills/connect/SKILL.md +182 -0
  96. package/stacks/modeling-kit/templates/.claude/skills/discover-storyboard/SKILL.md +383 -0
  97. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-applying-conways-law/SKILL.md +465 -0
  98. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-brainstorming-events/SKILL.md +587 -0
  99. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-brainstorming-events/references/facilitating-event-modeling-workshops.md +626 -0
  100. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-checking-completeness/SKILL.md +542 -0
  101. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-designing-event-models/SKILL.md +497 -0
  102. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-elaborating-scenarios/SKILL.md +783 -0
  103. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-identifying-inputs/SKILL.md +604 -0
  104. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-identifying-outputs/SKILL.md +699 -0
  105. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-integrating-legacy-systems/SKILL.md +674 -0
  106. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-optimizing-stream-design/SKILL.md +231 -0
  107. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-optimizing-stream-design/references/domain-patterns.md +150 -0
  108. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-optimizing-stream-design/references/patterns.md +329 -0
  109. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-optimizing-stream-design/references/snapshotting.md +204 -0
  110. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-orchestrating-event-modeling/SKILL.md +352 -0
  111. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-orchestrating-event-modeling/references/project-planning-with-event-modeling.md +341 -0
  112. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-plotting-events/SKILL.md +153 -0
  113. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-slicing-event-models/SKILL.md +136 -0
  114. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-slicing-event-models/references/examples.md +78 -0
  115. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-slicing-event-models/references/patterns.md +85 -0
  116. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-storyboarding-events/SKILL.md +655 -0
  117. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-translating-external-events/SKILL.md +479 -0
  118. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-validating-event-models/SKILL.md +234 -0
  119. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-validating-event-models-checklist/SKILL.md +353 -0
  120. package/stacks/modeling-kit/templates/.claude/skills/examples/SKILL.md +200 -0
  121. package/stacks/modeling-kit/templates/.claude/skills/handle-comment/SKILL.md +94 -0
  122. package/stacks/modeling-kit/templates/.claude/skills/learn-eventmodelers-api/SKILL.md +441 -0
  123. package/stacks/modeling-kit/templates/.claude/skills/place-element/SKILL.md +290 -0
  124. package/stacks/modeling-kit/templates/.claude/skills/storyboard/SKILL.md +212 -0
  125. package/stacks/modeling-kit/templates/.claude/skills/storyboard-screen/SKILL.md +123 -0
  126. package/stacks/modeling-kit/templates/.claude/skills/timeline/SKILL.md +353 -0
  127. package/stacks/modeling-kit/templates/.claude/skills/update-slice-status/SKILL.md +110 -0
  128. package/stacks/modeling-kit/templates/.claude/skills/wdyt/SKILL.md +134 -0
  129. package/stacks/modeling-kit/templates/kit/lib/agent.sh +20 -0
  130. package/stacks/modeling-kit/templates/kit/lib/ollama-agent.js +147 -0
  131. package/stacks/modeling-kit/templates/kit/lib/ralph.js +286 -0
  132. package/stacks/modeling-kit/templates/kit/package.json +12 -0
  133. package/stacks/modeling-kit/templates/kit/ralph-claude.js +50 -0
  134. package/stacks/modeling-kit/templates/kit/ralph-ollama.js +38 -0
  135. package/stacks/modeling-kit/templates/kit/ralph.sh +61 -0
  136. package/stacks/modeling-kit/templates/kit/realtime-agent.js +18 -0
  137. package/stacks/modeling-kit/templates/root/CLAUDE.md +56 -0
  138. package/stacks/node/templates/.claude/skills/build-automation/SKILL.md +266 -0
  139. package/stacks/node/templates/.claude/skills/build-state-change/SKILL.md +344 -0
  140. package/stacks/node/templates/.claude/skills/build-state-view/SKILL.md +412 -0
  141. package/stacks/node/templates/.claude/skills/connect/SKILL.md +178 -0
  142. package/stacks/node/templates/.claude/skills/learn-eventmodelers-api/SKILL.md +628 -0
  143. package/stacks/node/templates/.claude/skills/load-slice/SKILL.md +143 -0
  144. package/stacks/node/templates/.claude/skills/update-slice-status/SKILL.md +110 -0
  145. package/stacks/node/templates/build-kit/README.md +86 -0
  146. package/stacks/node/templates/build-kit/lib/AGENT.md +47 -0
  147. package/stacks/node/templates/build-kit/lib/agent.sh +20 -0
  148. package/stacks/node/templates/build-kit/lib/backend-prompt.md +152 -0
  149. package/stacks/node/templates/build-kit/lib/prompt.md +122 -0
  150. package/stacks/node/templates/build-kit/lib/ralph.js +369 -0
  151. package/stacks/node/templates/build-kit/ralph-claude.js +44 -0
  152. package/stacks/node/templates/build-kit/ralph.sh +98 -0
  153. package/stacks/node/templates/root/.env.example +15 -0
  154. package/stacks/node/templates/root/CLAUDE.md +60 -0
  155. package/stacks/node/templates/root/docker-compose.yml +15 -0
  156. package/stacks/node/templates/root/flyway.conf +17 -0
  157. package/stacks/node/templates/root/migrations/V1__schema.sql.example +12 -0
  158. package/stacks/node/templates/root/package.json +50 -0
  159. package/stacks/node/templates/root/server.ts +130 -0
  160. package/stacks/node/templates/root/setup-env.sh +53 -0
  161. package/stacks/node/templates/root/src/common/assertions.ts +6 -0
  162. package/stacks/node/templates/root/src/common/db.ts +32 -0
  163. package/stacks/node/templates/root/src/common/loadPostgresEventstore.ts +23 -0
  164. package/stacks/node/templates/root/src/common/parseEndpoint.ts +51 -0
  165. package/stacks/node/templates/root/src/common/processorDlq.ts +28 -0
  166. package/stacks/node/templates/root/src/common/realtimeBroadcast.ts +12 -0
  167. package/stacks/node/templates/root/src/common/replay.ts +16 -0
  168. package/stacks/node/templates/root/src/common/routes.ts +19 -0
  169. package/stacks/node/templates/root/src/common/testHelpers.ts +44 -0
  170. package/stacks/node/templates/root/src/swagger.ts +34 -0
  171. package/stacks/node/templates/root/src/util/assertions.ts +6 -0
  172. package/stacks/node/templates/root/src/util/hash.ts +9 -0
  173. package/stacks/node/templates/root/src/util/sanitize.ts +23 -0
  174. package/stacks/node/templates/root/tsconfig.json +32 -0
  175. package/stacks/node/templates/root/vercel.json +8 -0
  176. package/stacks/supabase/templates/.claude/skills/build-automation/SKILL.md +266 -0
  177. package/stacks/supabase/templates/.claude/skills/build-state-change/SKILL.md +344 -0
  178. package/stacks/supabase/templates/.claude/skills/build-state-view/SKILL.md +390 -0
  179. package/stacks/supabase/templates/.claude/skills/build-webhook/SKILL.md +303 -0
  180. package/stacks/supabase/templates/.claude/skills/connect/SKILL.md +178 -0
  181. package/stacks/supabase/templates/.claude/skills/learn-eventmodelers-api/SKILL.md +628 -0
  182. package/stacks/supabase/templates/.claude/skills/load-slice/SKILL.md +143 -0
  183. package/stacks/supabase/templates/.claude/skills/update-slice-status/SKILL.md +110 -0
  184. package/stacks/supabase/templates/build-kit/README.md +86 -0
  185. package/stacks/supabase/templates/build-kit/lib/AGENT.md +47 -0
  186. package/stacks/supabase/templates/build-kit/lib/agent.sh +20 -0
  187. package/stacks/supabase/templates/build-kit/lib/backend-prompt.md +152 -0
  188. package/stacks/supabase/templates/build-kit/lib/prompt.md +123 -0
  189. package/stacks/supabase/templates/build-kit/lib/ralph.js +369 -0
  190. package/stacks/supabase/templates/build-kit/ralph-claude.js +44 -0
  191. package/stacks/supabase/templates/build-kit/ralph.sh +98 -0
  192. package/stacks/supabase/templates/root/.env.example +22 -0
  193. package/stacks/supabase/templates/root/CLAUDE.md +61 -0
  194. package/stacks/supabase/templates/root/flyway.conf +17 -0
  195. package/stacks/supabase/templates/root/package.json +52 -0
  196. package/stacks/supabase/templates/root/server.ts +177 -0
  197. package/stacks/supabase/templates/root/setup-env.sh +53 -0
  198. package/stacks/supabase/templates/root/src/common/assertions.ts +6 -0
  199. package/stacks/supabase/templates/root/src/common/db.ts +32 -0
  200. package/stacks/supabase/templates/root/src/common/loadPostgresEventstore.ts +23 -0
  201. package/stacks/supabase/templates/root/src/common/parseEndpoint.ts +51 -0
  202. package/stacks/supabase/templates/root/src/common/processorDlq.ts +28 -0
  203. package/stacks/supabase/templates/root/src/common/realtimeBroadcast.ts +19 -0
  204. package/stacks/supabase/templates/root/src/common/replay.ts +16 -0
  205. package/stacks/supabase/templates/root/src/common/routes.ts +23 -0
  206. package/stacks/supabase/templates/root/src/common/testHelpers.ts +54 -0
  207. package/stacks/supabase/templates/root/src/supabase/README.md +141 -0
  208. package/stacks/supabase/templates/root/src/supabase/api.ts +56 -0
  209. package/stacks/supabase/templates/root/src/supabase/requireOrgaAdmin.ts +32 -0
  210. package/stacks/supabase/templates/root/src/supabase/requireSysUser.ts +40 -0
  211. package/stacks/supabase/templates/root/src/supabase/requireUser.ts +72 -0
  212. package/stacks/supabase/templates/root/src/swagger.ts +34 -0
  213. package/stacks/supabase/templates/root/src/util/assertions.ts +6 -0
  214. package/stacks/supabase/templates/root/src/util/hash.ts +9 -0
  215. package/stacks/supabase/templates/root/src/util/sanitize.ts +23 -0
  216. package/stacks/supabase/templates/root/supabase/config.toml +295 -0
  217. package/stacks/supabase/templates/root/supabase/migrations/V1__schema.sql.example +12 -0
  218. package/stacks/supabase/templates/root/supabase/seed.sql +1 -0
  219. package/stacks/supabase/templates/root/tsconfig.json +32 -0
  220. package/stacks/supabase/templates/root/vercel.json +8 -0
@@ -0,0 +1,177 @@
1
+ import {join} from 'path';
2
+ import {getApplication, startAPI, WebApiSetup} from '@event-driven-io/emmett-expressjs';
3
+ import {glob} from "glob";
4
+ import express, {Application, Request, Response} from 'express';
5
+ import {jsonBigIntReplacer} from './src/util/sanitize';
6
+ import {requireUser} from "./src/supabase/requireUser";
7
+ import {getKnexInstance, closeDb} from "./src/common/db";
8
+ import swaggerUi from 'swagger-ui-express'
9
+ import {specs} from './src/swagger';
10
+ import cors from 'cors';
11
+ import {findEventstore} from "./src/common/loadPostgresEventstore";
12
+ import {PostgresEventStore} from "@event-driven-io/emmett-postgresql";
13
+
14
+ async function startServer() {
15
+
16
+ const eventStore = await findEventstore()
17
+ const slicesBase = join(__dirname, 'dist/src/slices');
18
+ const routesPattern = join(slicesBase, '**/routes{,-*}.js');
19
+
20
+ const routeFiles = await glob(routesPattern, {nodir: true});
21
+ console.log('Found route files:', routeFiles);
22
+
23
+ const processorPattern = join(slicesBase, '**/processor{,-*}.js');
24
+ const processorFiles = await glob(processorPattern, {nodir: true});
25
+ console.log('Found processor files:', processorFiles);
26
+
27
+ // Common routes (e.g. projection replay) are privileged/operational and are
28
+ // only mounted when explicitly enabled via env.
29
+ const commonRoutesEnabled = process.env.COMMON_ROUTES_ENABLED === 'true';
30
+ const commonPattern = join(__dirname, 'src/common/routes{,-*}.@(ts|js)');
31
+ const commonRouteFiles = commonRoutesEnabled
32
+ ? await glob(commonPattern, {nodir: true})
33
+ : [];
34
+ console.log(commonRoutesEnabled
35
+ ? `Found common route files: ${commonRouteFiles}`
36
+ : 'Common routes disabled (set COMMON_ROUTES_ENABLED=true to enable)');
37
+
38
+
39
+ const rootApp: Application = express();
40
+ rootApp.set('json replacer', jsonBigIntReplacer);
41
+
42
+ const corsOrigins = process.env.CORS_ORIGINS?.split(',').map(o => o.trim()) ?? ['http://localhost:3000', 'http://localhost:3001'];
43
+ rootApp.use(cors({
44
+ origin: corsOrigins,
45
+ credentials: true,
46
+ methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
47
+ allowedHeaders: ['Content-Type', 'Content-Encoding', 'accept-encoding', 'Authorization','x-user-id','x-causation-id','x-correlation-id']
48
+ }));
49
+
50
+ const webApis: WebApiSetup[] = [];
51
+
52
+ for (const file of routeFiles.concat(commonRouteFiles)) {
53
+ const webApiModule: { api: () => WebApiSetup } = await import(file);
54
+ if (typeof webApiModule.api == 'function') {
55
+ var module = webApiModule.api()
56
+ webApis.push(module);
57
+ } else {
58
+ console.error(`Expected api function to be defined in ${file}`);
59
+ }
60
+ }
61
+
62
+ const startedProcessors: Array<{ stop: () => Promise<void> }> = [];
63
+
64
+ for (const processorFile of processorFiles) {
65
+ const processor: { processor: { start: (eventStore: PostgresEventStore) => Promise<void>; stop: () => Promise<void> } } = await import(processorFile);
66
+ if (typeof processor.processor.start == "function") {
67
+ console.log(`starting processor ${processorFile}`)
68
+ processor.processor.start(eventStore).catch(err => console.error(`Processor ${processorFile} failed:`, err));
69
+ startedProcessors.push(processor.processor);
70
+ }
71
+ }
72
+
73
+ const shutdown = async (signal: string) => {
74
+ console.log(`${signal} received, shutting down processors...`);
75
+ await Promise.allSettled(startedProcessors.map(p => p.stop()));
76
+ await eventStore.close();
77
+ await closeDb();
78
+ console.log('shutdown complete');
79
+ process.exit(0);
80
+ };
81
+
82
+ process.on('SIGINT', () => shutdown('SIGINT'));
83
+ process.on('SIGTERM', () => shutdown('SIGTERM'));
84
+
85
+ // Get the main application from emmett
86
+ const childApp: Application = getApplication({
87
+ apis: webApis,
88
+ disableJsonMiddleware: false,
89
+ enableDefaultExpressEtag: true,
90
+ });
91
+ childApp.set('json replacer', jsonBigIntReplacer);
92
+
93
+ // Protected user info endpoint - requires JWT token in Authorization header
94
+ childApp.get('/api/user', async (req: Request, res: Response) => {
95
+ console.log('API user route hit'); // Debug log
96
+ try {
97
+ const result = await requireUser(req, res, false)
98
+ if (result.error) {
99
+ // Response already sent by requireUser if sendUnauthorized=true
100
+ if (!res.headersSent) {
101
+ res.status(401).json({error: result.error})
102
+ }
103
+ } else {
104
+ res.status(200).json({
105
+ user_id: result.user.id,
106
+ email: result.user.email,
107
+ metadata: result.user.user_metadata
108
+ })
109
+ }
110
+ } catch (error) {
111
+ console.error('Error in /api/user:', error);
112
+ if (!res.headersSent) {
113
+ res.status(500).json({error: 'Internal server error'});
114
+ }
115
+ }
116
+ });
117
+
118
+ // Swagger UI endpoints
119
+ childApp.use('/api-docs', swaggerUi.serve);
120
+ childApp.get('/api-docs', swaggerUi.setup(specs, {
121
+ swaggerOptions: {
122
+ urls: [
123
+ {
124
+ url: '/swagger.json',
125
+ name: 'JSON',
126
+ },
127
+ ],
128
+ },
129
+ }));
130
+
131
+ // OpenAPI spec endpoint
132
+ childApp.get('/swagger.json', (req: Request, res: Response) => {
133
+ res.setHeader('Content-Type', 'application/json');
134
+ res.send(specs);
135
+ });
136
+
137
+ const port = parseInt(process.env.PORT || '3000', 10);
138
+ console.log(`> Ready on port ${port}`);
139
+
140
+ rootApp.use((req: Request, _res: Response, next) => {
141
+ console.log(`[${req.method}] ${req.path}`);
142
+ next();
143
+ });
144
+
145
+ rootApp.use(express.json());
146
+
147
+ // Closed-by-default auth gate: every route requires a valid Supabase JWT
148
+ // except the explicitly public paths below. This guarantees new slice
149
+ // routes are protected even if a handler forgets to call requireUser.
150
+ const PUBLIC_PATHS = ['/api-docs', '/swagger.json', '/health'];
151
+ const isPublicPath = (p: string): boolean =>
152
+ PUBLIC_PATHS.some(pub => p === pub || p.startsWith(pub + '/'));
153
+
154
+ rootApp.use(async (req: Request, res: Response, next) => {
155
+ if (req.method === 'OPTIONS') return next(); // CORS preflight
156
+ if (isPublicPath(req.path)) return next(); // docs / health
157
+ const {error} = await requireUser(req, res); // sends 401 on failure
158
+ if (error) return; // response already sent
159
+ next();
160
+ });
161
+
162
+ rootApp.use(childApp)
163
+ // Start the main application
164
+ startAPI(rootApp, {port: port});
165
+
166
+ process.on('unhandledRejection', (reason, promise) => {
167
+ console.error('⛔ Unhandled Rejection:', reason);
168
+ if (reason instanceof Error && reason.stack) {
169
+ console.error('Stack trace:\n', reason.stack);
170
+ }
171
+ });
172
+ }
173
+
174
+ startServer().catch(error => {
175
+ console.error('Failed to start server:', error);
176
+ process.exit(1);
177
+ });
@@ -0,0 +1,53 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ prompt() {
5
+ local var_name="$1"
6
+ local prompt_text="$2"
7
+ local value
8
+ read -rp "$prompt_text: " value
9
+ if [[ -z "$value" ]]; then
10
+ echo "Error: $var_name cannot be empty." >&2
11
+ exit 1
12
+ fi
13
+ echo "$value"
14
+ }
15
+
16
+ prompt_secret() {
17
+ local var_name="$1"
18
+ local prompt_text="$2"
19
+ local value
20
+ read -rsp "$prompt_text: " value
21
+ echo "" >&2
22
+ if [[ -z "$value" ]]; then
23
+ echo "Error: $var_name cannot be empty." >&2
24
+ exit 1
25
+ fi
26
+ echo "$value"
27
+ }
28
+
29
+ echo "=== Supabase .env setup ==="
30
+ echo ""
31
+
32
+ PROJECT_ID=$(prompt SUPABASE_PROJECT_ID "Supabase Project ID")
33
+ DB_PASSWORD=$(prompt_secret SUPABASE_DB_PASSWORD "Database Password")
34
+ PUBLISHABLE_KEY=$(prompt_secret SUPABASE_PUBLISHABLE_KEY "Supabase Publishable Key")
35
+ SECRET_KEY=$(prompt_secret SUPABASE_SECRET_KEY "Supabase Secret Key")
36
+ BACKEND_URL=$(prompt BACKEND_URL "Backend URL (e.g. https://api.eventmodelers.ai)")
37
+
38
+ cat > .env <<EOF
39
+ SUPABASE_URL=https://${PROJECT_ID}.supabase.co
40
+ SUPABASE_PUBLISHABLE_KEY=${PUBLISHABLE_KEY}
41
+ SUPABASE_DB_URL=postgresql://postgres.${PROJECT_ID}:${DB_PASSWORD}@aws-1-eu-central-1.pooler.supabase.com:5432/postgres?prepareThreshold=0
42
+ SUPABASE_SECRET_KEY=${SECRET_KEY}
43
+
44
+ BACKEND_URL=${BACKEND_URL}
45
+
46
+ # Flyway configuration
47
+ FLYWAY_URL=jdbc:postgresql://aws-1-eu-west-1.pooler.supabase.com:6543/postgres?user=postgres.${PROJECT_ID}&password=${DB_PASSWORD}
48
+ FLYWAY_USER=postgres.${PROJECT_ID}
49
+ FLYWAY_PASSWORD=${DB_PASSWORD}
50
+ EOF
51
+
52
+ echo ""
53
+ echo ".env created successfully."
@@ -0,0 +1,6 @@
1
+ export function assertNotEmpty<T>(value: T): NonNullable<T> {
2
+ if (value === null || value === undefined) {
3
+ throw new Error("Expected non-empty value");
4
+ }
5
+ return value!!;
6
+ }
@@ -0,0 +1,32 @@
1
+ import knex, {Knex} from "knex";
2
+ import pg from "pg";
3
+
4
+ export const postgresUrl = process.env.SUPABASE_DB_URL ?? "missing-url"
5
+
6
+ let knexInstance: Knex | null = null;
7
+ let sharedPool: pg.Pool | null = null;
8
+
9
+ export const getKnexInstance = (): Knex => {
10
+ if (!knexInstance) {
11
+ knexInstance = knex({
12
+ client: 'pg',
13
+ connection: postgresUrl,
14
+ pool: { min: 0, max: 5 },
15
+ });
16
+ }
17
+ return knexInstance;
18
+ };
19
+
20
+ export const getSharedPool = (): pg.Pool => {
21
+ if (!sharedPool) {
22
+ sharedPool = new pg.Pool({ connectionString: postgresUrl, max: 5 });
23
+ }
24
+ return sharedPool;
25
+ };
26
+
27
+ export const closeDb = async (): Promise<void> => {
28
+ await knexInstance?.destroy();
29
+ await sharedPool?.end();
30
+ knexInstance = null;
31
+ sharedPool = null;
32
+ };
@@ -0,0 +1,23 @@
1
+ import {getPostgreSQLEventStore} from "@event-driven-io/emmett-postgresql";
2
+ import {projections} from "@event-driven-io/emmett";
3
+ import {postgresUrl, getSharedPool} from "./db";
4
+
5
+ let eventStoreInstance: ReturnType<typeof getPostgreSQLEventStore> | null = null;
6
+
7
+ export const findEventstore = async () => {
8
+ if (!eventStoreInstance) {
9
+ eventStoreInstance = getPostgreSQLEventStore(postgresUrl, {
10
+ schema: {
11
+ autoMigration: "CreateOrUpdate"
12
+ },
13
+ connectionOptions: {
14
+ pooled: true,
15
+ pool: getSharedPool(),
16
+ },
17
+ projections: projections.inline([
18
+ ]),
19
+ });
20
+ await eventStoreInstance.schema.migrate();
21
+ }
22
+ return eventStoreInstance;
23
+ };
@@ -0,0 +1,51 @@
1
+ /*
2
+ * Copyright (c) 2025 Nebulit GmbH
3
+ * Licensed under the MIT License.
4
+ */
5
+
6
+ const serviceURI = "http://localhost:3000"
7
+
8
+ export function parseEndpoint(endpoint: string, data?: any) {
9
+ var parsedEndpoint = endpoint?.startsWith("/") ? endpoint.substring(1) : endpoint
10
+ return serviceURI + "/" + lowercaseFirstCharacter(parsedEndpoint).replace(/{(\w+)}/g, (match, param) => {
11
+ return param && data && data[param] !== undefined ? data[param] : match;
12
+ })
13
+ }
14
+
15
+
16
+ export function parseQueryEndpoint(
17
+ endpoint: string,
18
+ queries?: Record<string, string>
19
+ ) {
20
+ const parsedEndpoint = endpoint.startsWith("/")
21
+ ? endpoint.substring(1)
22
+ : endpoint;
23
+
24
+ const basePath =
25
+ serviceURI + "/api/query/" + parsedEndpoint;
26
+
27
+ const queryString = queries
28
+ ? "?" + new URLSearchParams(filterEmptyEntries(queries)).toString()
29
+ : "";
30
+
31
+ return basePath + queryString;
32
+ }
33
+
34
+ function filterEmptyEntries(queries?: Record<string, string>): Record<string, string> {
35
+ if (!queries) return {};
36
+ return Object.fromEntries(
37
+ Object.entries(queries).filter(([key, value]) => value !== "")
38
+ );
39
+ }
40
+
41
+
42
+ function lowercaseFirstCharacter(inputString: string) {
43
+ // Check if the string is not empty
44
+ if (inputString?.length > 0) {
45
+ // Capitalize the first character and concatenate the rest of the string
46
+ return inputString.charAt(0).toLowerCase() + inputString.substring(1);
47
+ } else {
48
+ // Return an empty string if the input is empty
49
+ return "";
50
+ }
51
+ }
@@ -0,0 +1,28 @@
1
+ import {getKnexInstance} from './db';
2
+ import type {AnyRecordedMessageMetadata, RecordedMessage} from '@event-driven-io/emmett';
3
+
4
+ export const storeDlqMessage = async (
5
+ processorId: string,
6
+ message: RecordedMessage<any, AnyRecordedMessageMetadata>,
7
+ error: unknown,
8
+ ): Promise<void> => {
9
+
10
+ try {
11
+ console.log(`Processing DLQ ${JSON.stringify({ type: message.type, data: message.data, metadata: message.metadata } , (key, value) =>
12
+ typeof value === 'bigint' ? value.toString() : value
13
+ )}`)
14
+ await getKnexInstance()('processor_dlq').insert({
15
+ processor_id: processorId,
16
+ stream_id: message.metadata.streamName,
17
+ event: JSON.parse(
18
+ JSON.stringify({ type: message.type, data: message.data, metadata: message.metadata } , (key, value) =>
19
+ typeof value === 'bigint' ? value.toString() : value
20
+ )
21
+ ),
22
+ error: error instanceof Error ? error.message : String(error),
23
+ });
24
+ } catch (dlqError) {
25
+ console.error('Failed to write to processor_dlq:', dlqError);
26
+ }
27
+ };
28
+
@@ -0,0 +1,19 @@
1
+ const url = `${process.env.SUPABASE_URL}/realtime/v1/api/broadcast`;
2
+ const key = process.env.SUPABASE_SECRET_KEY!;
3
+
4
+ export async function broadcastRealtime(topic: string, event: string, payload: unknown, privateChannel = true): Promise<void> {
5
+ const res = await fetch(url, {
6
+ method: 'POST',
7
+ headers: {
8
+ 'Content-Type': 'application/json',
9
+ 'Authorization': `Bearer ${key}`,
10
+ 'apikey': key,
11
+ },
12
+ body: JSON.stringify({
13
+ messages: [{ topic, event, payload, private: privateChannel }],
14
+ }),
15
+ });
16
+ if (!res.ok) {
17
+ throw new Error(`realtime broadcast failed: ${res.status} ${await res.text()}`);
18
+ }
19
+ }
@@ -0,0 +1,16 @@
1
+ import {PostgreSQLProjectionDefinition, rebuildPostgreSQLProjections} from "@event-driven-io/emmett-postgresql";
2
+ import {postgresUrl} from "./db";
3
+ import {glob} from "glob";
4
+ import path from "path";
5
+
6
+ const slicesRoot = path.resolve(__dirname, '../slices');
7
+
8
+ export const replayProjection = async (projectionName: string): Promise<void> => {
9
+ const [filePath] = await glob(`**/${projectionName}.{ts|js}`, {cwd: slicesRoot, absolute: true});
10
+ if (!filePath) throw new Error(`Projection not found: ${projectionName}`);
11
+
12
+ const projectionImport = await import(filePath);
13
+ const projection: PostgreSQLProjectionDefinition = projectionImport[projectionName];
14
+
15
+ return rebuildPostgreSQLProjections({projection, connectionString: postgresUrl}).start();
16
+ }
@@ -0,0 +1,23 @@
1
+ import {Request, Response, Router} from 'express';
2
+ import {WebApiSetup} from "@event-driven-io/emmett-expressjs";
3
+ import {assertNotEmpty} from "../util/assertions";
4
+ import {replayProjection} from "./replay";
5
+ import {requireSysUser} from "../supabase/requireSysUser";
6
+
7
+
8
+ export const api =
9
+ (
10
+ // external dependencies
11
+ ): WebApiSetup =>
12
+ (router: Router): void => {
13
+
14
+ router.post('/api/replay/:projection', async (req: Request, res: Response) => {
15
+ // Replay is a privileged operational action — require a sys user.
16
+ const {error} = await requireSysUser(req, res); // sends 401/403 on failure
17
+ if (error) return;
18
+ const projection = assertNotEmpty(req.params.projection)
19
+ await replayProjection(projection)
20
+ res.status(200).json({"projection":projection})
21
+ });
22
+ };
23
+
@@ -0,0 +1,54 @@
1
+ import {execSync} from 'child_process';
2
+ import {readFileSync, writeFileSync, unlinkSync} from 'fs';
3
+ import {tmpdir} from 'os';
4
+ import {join} from 'path';
5
+ import knex from 'knex';
6
+
7
+ export async function runFlywayMigrations(connectionString: string): Promise<void> {
8
+ const stubsPath = join(process.cwd(), 'supabase', 'migrations', '_V0__supabase_stubs.sql');
9
+ const stubsSql = readFileSync(stubsPath, 'utf8');
10
+ const db = knex({client: 'pg', connection: connectionString});
11
+ try {
12
+ await db.raw(stubsSql);
13
+ } finally {
14
+ await db.destroy();
15
+ }
16
+
17
+ const url = new URL(connectionString);
18
+ const jdbcUrl = `jdbc:postgresql://${url.hostname}:${url.port || 5432}${url.pathname}`;
19
+ const user = url.username;
20
+ const password = url.password;
21
+
22
+ const tempConfigPath = join(tmpdir(), `flyway-test-${Date.now()}.conf`);
23
+ const migrationsPath = join(process.cwd(), 'supabase', 'migrations');
24
+
25
+ const config = `
26
+ flyway.url=${jdbcUrl}
27
+ flyway.user=${user}
28
+ flyway.password=${password}
29
+ flyway.locations=filesystem:${migrationsPath}
30
+ flyway.schemas=public
31
+ flyway.placeholderReplacement=false
32
+ flyway.validateOnMigrate=true
33
+ flyway.cleanDisabled=false
34
+ `;
35
+
36
+ try {
37
+ writeFileSync(tempConfigPath, config, 'utf8');
38
+ execSync(`flyway -configFiles=${tempConfigPath} migrate`, {
39
+ stdio: 'pipe',
40
+ encoding: 'utf8'
41
+ });
42
+ } catch (error: any) {
43
+ console.error('Flyway migration failed:', error.message);
44
+ if (error.stdout) console.error('STDOUT:', error.stdout);
45
+ if (error.stderr) console.error('STDERR:', error.stderr);
46
+ throw new Error(`Flyway migration failed: ${error.message}`);
47
+ } finally {
48
+ try {
49
+ unlinkSync(tempConfigPath);
50
+ } catch {
51
+ // ignore
52
+ }
53
+ }
54
+ }
@@ -0,0 +1,141 @@
1
+ # Supabase JWT Authentication for Backend API
2
+
3
+ This backend API uses Supabase JWT tokens for authentication. Clients must include a valid JWT token in the
4
+ `Authorization` header.
5
+
6
+ ## Quick Start
7
+
8
+ 1. **Set up environment variables** in `.env`:
9
+ ```env
10
+ SUPABASE_URL=http://127.0.0.1:54321
11
+ SUPABASE_PUBLISHABLE_KEY=your-anon-key-here
12
+ ```
13
+
14
+ 2. **Start the server**: `npm run dev`
15
+
16
+ 3. **Get a test JWT token**: sign in a user via the Supabase client SDK or `supabase auth` CLI and use the returned `access_token` in your API requests.
17
+
18
+ ## How It Works
19
+
20
+ 1. **Client obtains JWT token** from Supabase (via your frontend app)
21
+ 2. **Client sends requests** with `Authorization: Bearer <jwt-token>` header
22
+ 3. **Backend verifies JWT** using Supabase and extracts user info
23
+ 4. **Protected routes** return user data or 401 Unauthorized
24
+
25
+ ## Usage Examples
26
+
27
+ ### Option 1: Using `requireUser` function
28
+
29
+ ```typescript
30
+ import {requireUser} from './src/supabase/requireUser';
31
+ import {Request, Response} from 'express';
32
+
33
+ app.get('/api/protected', async (req: Request, res: Response) => {
34
+ const result = await requireUser(req, res, false);
35
+
36
+ if (result.error) {
37
+ return res.status(401).json({error: result.error});
38
+ }
39
+
40
+ const user = result.user;
41
+ res.json({
42
+ message: 'Protected data',
43
+ userId: user.id,
44
+ email: user.email
45
+ });
46
+ });
47
+ ```
48
+
49
+ ### Testing with curl
50
+
51
+ ```bash
52
+ # Get JWT token from your Supabase client first
53
+ TOKEN="your-jwt-token-here"
54
+
55
+ # Test protected endpoint
56
+ curl -H "Authorization: Bearer $TOKEN" http://localhost:3000/api/user
57
+
58
+ # Expected response:
59
+ # {
60
+ # "userId": "...",
61
+ # "email": "user@example.com",
62
+ # "metadata": { ... }
63
+ # }
64
+ ```
65
+
66
+ ### Testing with JavaScript fetch
67
+
68
+ ```javascript
69
+ const token = supabase.auth.session()?.access_token;
70
+
71
+ fetch('http://localhost:3000/api/user', {
72
+ headers: {
73
+ 'Authorization': `Bearer ${token}`
74
+ }
75
+ })
76
+ .then(res => res.json())
77
+ .then(data => console.log(data));
78
+ ```
79
+
80
+ ## API Endpoints
81
+
82
+ ### `GET /api/user`
83
+
84
+ Returns current authenticated user information.
85
+
86
+ **Headers:**
87
+
88
+ - `Authorization: Bearer <jwt-token>` (required)
89
+
90
+ **Success Response (200):**
91
+
92
+ ```json
93
+ {
94
+ "userId": "uuid",
95
+ "email": "user@example.com",
96
+ "metadata": { ... }
97
+ }
98
+ ```
99
+
100
+ **Error Responses:**
101
+
102
+ - `401 Unauthorized`: Missing or invalid token
103
+ - `500 Internal Server Error`: Server error
104
+
105
+ ## Files
106
+
107
+ - **`api.ts`**: Supabase client creation
108
+ - **`requireUser.ts`**: JWT verification function
109
+ - **`requireSysUser.ts`**: Requires an authenticated system ("sys") user
110
+ - **`requireOrgaAdmin.ts`**: Requires the caller to be an org admin
111
+ - **`README.md`**: This documentation
112
+
113
+ ## Architecture
114
+
115
+ ```
116
+ Client Request
117
+ |
118
+ v
119
+ Authorization: Bearer <JWT>
120
+ |
121
+ v
122
+ Express Route
123
+ |
124
+ v
125
+ requireUser() / requireSysUser() / requireOrgaAdmin()
126
+ |
127
+ v
128
+ Supabase JWT Verification
129
+ |
130
+ +---> Valid: Continue with user data
131
+ |
132
+ +---> Invalid: Return 401 Unauthorized
133
+ ```
134
+
135
+ ## Security Notes
136
+
137
+ - JWT tokens are verified with Supabase on every request
138
+ - No session storage on the backend (stateless)
139
+ - Tokens expire based on Supabase configuration
140
+ - Always use HTTPS in production
141
+ - Store the anon key securely (use environment variables)
@@ -0,0 +1,56 @@
1
+ import {createClient as createSupabaseClient} from '@supabase/supabase-js'
2
+ import {Request} from 'express'
3
+
4
+ let _serviceClient: ReturnType<typeof createSupabaseClient> | null = null;
5
+
6
+ export const createServiceClient = () => {
7
+ if (!_serviceClient) {
8
+ _serviceClient = createSupabaseClient(
9
+ process.env.SUPABASE_URL!,
10
+ process.env.SUPABASE_SECRET_KEY!
11
+ );
12
+ }
13
+ return _serviceClient;
14
+ }
15
+
16
+ /**
17
+ * Creates a Supabase client for verifying JWT tokens
18
+ * Used in backend API endpoints
19
+ */
20
+ export default function createClient() {
21
+ return createSupabaseClient(
22
+ process.env.SUPABASE_URL!,
23
+ process.env.SUPABASE_PUBLISHABLE_KEY!,
24
+ {
25
+ auth: {
26
+ persistSession: false,
27
+ autoRefreshToken: false,
28
+ detectSessionInUrl: false,
29
+ },
30
+ }
31
+ )
32
+ }
33
+
34
+ /**
35
+ * Creates an authenticated Supabase client with the user's JWT token
36
+ * This ensures Row Level Security (RLS) policies are applied with the user's context
37
+ */
38
+ export async function createAuthenticatedClient(req: Request) {
39
+ const token = req.headers.authorization?.replace('Bearer ', '') || '';
40
+ return createSupabaseClient(
41
+ process.env.SUPABASE_URL!,
42
+ process.env.SUPABASE_PUBLISHABLE_KEY!,
43
+ {
44
+ auth: {
45
+ persistSession: false,
46
+ autoRefreshToken: false,
47
+ detectSessionInUrl: false,
48
+ },
49
+ global: {
50
+ headers: {
51
+ Authorization: `Bearer ${token}`,
52
+ },
53
+ },
54
+ }
55
+ );
56
+ }