arcway 0.1.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.
Files changed (274) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +711 -0
  3. package/client/env.js +55 -0
  4. package/client/fetcher.js +50 -0
  5. package/client/graphql.js +35 -0
  6. package/client/head.js +140 -0
  7. package/client/hooks/use-api.js +80 -0
  8. package/client/hooks/use-debounce.js +12 -0
  9. package/client/hooks/use-form.js +86 -0
  10. package/client/hooks/use-graphql.js +30 -0
  11. package/client/hooks/use-interval.js +12 -0
  12. package/client/hooks/use-mutation.js +27 -0
  13. package/client/hooks/use-query.js +45 -0
  14. package/client/hooks/web/use-click-outside.js +22 -0
  15. package/client/hooks/web/use-local-storage.js +42 -0
  16. package/client/index.js +62 -0
  17. package/client/page-loader.js +155 -0
  18. package/client/provider.js +53 -0
  19. package/client/query.js +13 -0
  20. package/client/router.jsx +303 -0
  21. package/client/ui/accordion.jsx +65 -0
  22. package/client/ui/accordion.stories.jsx +48 -0
  23. package/client/ui/alert-dialog.jsx +122 -0
  24. package/client/ui/alert-dialog.stories.jsx +44 -0
  25. package/client/ui/alert.jsx +52 -0
  26. package/client/ui/alert.stories.jsx +31 -0
  27. package/client/ui/app-shell.jsx +39 -0
  28. package/client/ui/app-shell.stories.jsx +51 -0
  29. package/client/ui/aspect-ratio.jsx +6 -0
  30. package/client/ui/aspect-ratio.stories.jsx +69 -0
  31. package/client/ui/avatar.jsx +78 -0
  32. package/client/ui/avatar.stories.jsx +62 -0
  33. package/client/ui/badge.jsx +34 -0
  34. package/client/ui/badge.stories.js +32 -0
  35. package/client/ui/breadcrumb.jsx +86 -0
  36. package/client/ui/breadcrumb.stories.jsx +43 -0
  37. package/client/ui/button-group.jsx +58 -0
  38. package/client/ui/button-group.stories.jsx +67 -0
  39. package/client/ui/button.jsx +46 -0
  40. package/client/ui/button.stories.js +72 -0
  41. package/client/ui/calendar.jsx +172 -0
  42. package/client/ui/card.jsx +57 -0
  43. package/client/ui/card.stories.jsx +33 -0
  44. package/client/ui/carousel.jsx +167 -0
  45. package/client/ui/chart.jsx +244 -0
  46. package/client/ui/checkbox.jsx +24 -0
  47. package/client/ui/checkbox.stories.js +33 -0
  48. package/client/ui/collapsible.jsx +12 -0
  49. package/client/ui/collapsible.stories.jsx +42 -0
  50. package/client/ui/combobox.jsx +223 -0
  51. package/client/ui/command.jsx +128 -0
  52. package/client/ui/context-menu.jsx +170 -0
  53. package/client/ui/context-menu.stories.jsx +35 -0
  54. package/client/ui/dialog.jsx +109 -0
  55. package/client/ui/dialog.stories.jsx +37 -0
  56. package/client/ui/direction.jsx +9 -0
  57. package/client/ui/drawer.jsx +87 -0
  58. package/client/ui/dropdown-menu.jsx +172 -0
  59. package/client/ui/dropdown-menu.stories.jsx +34 -0
  60. package/client/ui/empty.jsx +76 -0
  61. package/client/ui/empty.stories.jsx +64 -0
  62. package/client/ui/field.jsx +174 -0
  63. package/client/ui/field.stories.jsx +118 -0
  64. package/client/ui/form.jsx +17 -0
  65. package/client/ui/hooks/use-mobile.js +16 -0
  66. package/client/ui/hover-card.jsx +26 -0
  67. package/client/ui/hover-card.stories.jsx +28 -0
  68. package/client/ui/index.js +649 -0
  69. package/client/ui/input-group.jsx +116 -0
  70. package/client/ui/input-group.stories.jsx +65 -0
  71. package/client/ui/input-otp.jsx +62 -0
  72. package/client/ui/input.jsx +16 -0
  73. package/client/ui/input.stories.js +27 -0
  74. package/client/ui/item.jsx +155 -0
  75. package/client/ui/item.stories.jsx +118 -0
  76. package/client/ui/kbd.jsx +24 -0
  77. package/client/ui/kbd.stories.jsx +32 -0
  78. package/client/ui/label.jsx +16 -0
  79. package/client/ui/label.stories.js +25 -0
  80. package/client/ui/lib/utils.js +6 -0
  81. package/client/ui/main-content.jsx +30 -0
  82. package/client/ui/menubar.jsx +189 -0
  83. package/client/ui/menubar.stories.jsx +43 -0
  84. package/client/ui/native-select.jsx +34 -0
  85. package/client/ui/native-select.stories.jsx +67 -0
  86. package/client/ui/navigation-menu.jsx +120 -0
  87. package/client/ui/navigation-menu.stories.jsx +45 -0
  88. package/client/ui/pagination.jsx +92 -0
  89. package/client/ui/pagination.stories.jsx +52 -0
  90. package/client/ui/panel.jsx +66 -0
  91. package/client/ui/popover.jsx +54 -0
  92. package/client/ui/popover.stories.jsx +27 -0
  93. package/client/ui/progress.jsx +19 -0
  94. package/client/ui/progress.stories.js +34 -0
  95. package/client/ui/radio-group.jsx +33 -0
  96. package/client/ui/radio-group.stories.jsx +49 -0
  97. package/client/ui/resizable.jsx +33 -0
  98. package/client/ui/scroll-area.jsx +41 -0
  99. package/client/ui/scroll-area.stories.jsx +43 -0
  100. package/client/ui/select.jsx +145 -0
  101. package/client/ui/select.stories.jsx +80 -0
  102. package/client/ui/separator.jsx +18 -0
  103. package/client/ui/separator.stories.jsx +37 -0
  104. package/client/ui/sheet.jsx +95 -0
  105. package/client/ui/sheet.stories.jsx +56 -0
  106. package/client/ui/sidebar.jsx +544 -0
  107. package/client/ui/skeleton.jsx +8 -0
  108. package/client/ui/skeleton.stories.js +23 -0
  109. package/client/ui/slider.jsx +41 -0
  110. package/client/ui/slider.stories.js +31 -0
  111. package/client/ui/sonner.jsx +37 -0
  112. package/client/ui/spinner.jsx +14 -0
  113. package/client/ui/spinner.stories.js +16 -0
  114. package/client/ui/style-mira.css +1316 -0
  115. package/client/ui/switch.jsx +22 -0
  116. package/client/ui/switch.stories.js +44 -0
  117. package/client/ui/table.jsx +33 -0
  118. package/client/ui/table.stories.jsx +42 -0
  119. package/client/ui/tabs.jsx +63 -0
  120. package/client/ui/tabs.stories.jsx +45 -0
  121. package/client/ui/textarea.jsx +15 -0
  122. package/client/ui/textarea.stories.js +33 -0
  123. package/client/ui/theme.css +459 -0
  124. package/client/ui/toggle-group.jsx +62 -0
  125. package/client/ui/toggle-group.stories.jsx +68 -0
  126. package/client/ui/toggle.jsx +34 -0
  127. package/client/ui/toggle.stories.js +46 -0
  128. package/client/ui/tooltip.jsx +37 -0
  129. package/client/ui/tooltip.stories.jsx +32 -0
  130. package/client/ui/use-transition.js +35 -0
  131. package/client/ws.js +132 -0
  132. package/package.json +134 -0
  133. package/server/bin/cli.js +42 -0
  134. package/server/bin/commands/build.js +23 -0
  135. package/server/bin/commands/dev.js +57 -0
  136. package/server/bin/commands/docs.js +30 -0
  137. package/server/bin/commands/graphql-schema.js +32 -0
  138. package/server/bin/commands/lint.js +35 -0
  139. package/server/bin/commands/mcp.js +26 -0
  140. package/server/bin/commands/migrate.js +82 -0
  141. package/server/bin/commands/schema.js +41 -0
  142. package/server/bin/commands/seed.js +36 -0
  143. package/server/bin/commands/start.js +31 -0
  144. package/server/bin/commands/test.js +20 -0
  145. package/server/bin/solo.js +4 -0
  146. package/server/boot/index.js +150 -0
  147. package/server/boot.js +2 -0
  148. package/server/build.js +23 -0
  149. package/server/cache/drivers/memory.js +23 -0
  150. package/server/cache/drivers/redis.js +28 -0
  151. package/server/cache/index.js +69 -0
  152. package/server/config/loader.js +89 -0
  153. package/server/config/modules/api.js +17 -0
  154. package/server/config/modules/build.js +9 -0
  155. package/server/config/modules/cache.js +10 -0
  156. package/server/config/modules/database.js +29 -0
  157. package/server/config/modules/events.js +15 -0
  158. package/server/config/modules/files.js +15 -0
  159. package/server/config/modules/jobs.js +20 -0
  160. package/server/config/modules/logger.js +9 -0
  161. package/server/config/modules/mail.js +11 -0
  162. package/server/config/modules/mcp.js +9 -0
  163. package/server/config/modules/pages.js +20 -0
  164. package/server/config/modules/queue.js +10 -0
  165. package/server/config/modules/redis.js +9 -0
  166. package/server/config/modules/server.js +30 -0
  167. package/server/config/modules/session.js +9 -0
  168. package/server/config/modules/websocket.js +11 -0
  169. package/server/constants.js +67 -0
  170. package/server/context.js +15 -0
  171. package/server/db/index.js +87 -0
  172. package/server/db/schema/drivers/mysql.js +28 -0
  173. package/server/db/schema/drivers/pg.js +34 -0
  174. package/server/db/schema/drivers/sqlite.js +22 -0
  175. package/server/db/schema/index.js +78 -0
  176. package/server/db/seeds.js +22 -0
  177. package/server/discovery.js +67 -0
  178. package/server/docs/openapi.js +153 -0
  179. package/server/env.js +17 -0
  180. package/server/events/drivers/memory.js +45 -0
  181. package/server/events/drivers/redis.js +64 -0
  182. package/server/events/handler.js +67 -0
  183. package/server/events/index.js +35 -0
  184. package/server/events/pattern.js +5 -0
  185. package/server/files/drivers/local.js +83 -0
  186. package/server/files/drivers/s3.js +113 -0
  187. package/server/files/index.js +57 -0
  188. package/server/filewatcher/index.js +156 -0
  189. package/server/glob.js +6 -0
  190. package/server/graphql/discovery.js +70 -0
  191. package/server/graphql/handler.js +41 -0
  192. package/server/graphql/index.js +13 -0
  193. package/server/graphql/loaders.js +19 -0
  194. package/server/graphql/merge.js +48 -0
  195. package/server/graphql/subscriptions.js +43 -0
  196. package/server/health.js +34 -0
  197. package/server/helpers.js +9 -0
  198. package/server/index.js +55 -0
  199. package/server/internals.js +139 -0
  200. package/server/jobs/cron.js +10 -0
  201. package/server/jobs/drivers/knex-queue.js +207 -0
  202. package/server/jobs/drivers/lease.js +148 -0
  203. package/server/jobs/drivers/memory-queue.js +134 -0
  204. package/server/jobs/queue.js +27 -0
  205. package/server/jobs/runner.js +197 -0
  206. package/server/jobs/throughput.js +63 -0
  207. package/server/lib/vault/encrypt.js +40 -0
  208. package/server/lib/vault/ids.js +9 -0
  209. package/server/lib/vault/index.js +14 -0
  210. package/server/lib/vault/jwt.js +55 -0
  211. package/server/lib/vault/password.js +10 -0
  212. package/server/lint/boundaries.js +77 -0
  213. package/server/logger/index.js +130 -0
  214. package/server/mail/drivers/console.js +31 -0
  215. package/server/mail/drivers/smtp.js +34 -0
  216. package/server/mail/imap.js +105 -0
  217. package/server/mail/inbound-store.js +58 -0
  218. package/server/mail/inbound.js +79 -0
  219. package/server/mail/index.js +112 -0
  220. package/server/mcp/debug-api.js +137 -0
  221. package/server/mcp/helpers.js +30 -0
  222. package/server/mcp/index.js +77 -0
  223. package/server/mcp/runtime.js +7 -0
  224. package/server/mcp/server.js +19 -0
  225. package/server/mcp/tools/debugging.js +133 -0
  226. package/server/mcp/tools/introspection.js +87 -0
  227. package/server/middlewares/cors.js +30 -0
  228. package/server/middlewares/index.js +3 -0
  229. package/server/middlewares/require-session.js +15 -0
  230. package/server/module-loader.js +9 -0
  231. package/server/pages/build-client.js +187 -0
  232. package/server/pages/build-css.js +47 -0
  233. package/server/pages/build-manifest.js +55 -0
  234. package/server/pages/build-plugins.js +75 -0
  235. package/server/pages/build-server.js +115 -0
  236. package/server/pages/build.js +116 -0
  237. package/server/pages/discovery.js +120 -0
  238. package/server/pages/fonts.js +128 -0
  239. package/server/pages/handler.js +276 -0
  240. package/server/pages/hmr.js +176 -0
  241. package/server/pages/pages-router.js +78 -0
  242. package/server/pages/ssr.js +276 -0
  243. package/server/pages/static.js +92 -0
  244. package/server/pages/watcher.js +90 -0
  245. package/server/queue/drivers/knex.js +67 -0
  246. package/server/queue/drivers/redis.js +91 -0
  247. package/server/queue/index.js +61 -0
  248. package/server/rate-limit/consume.js +21 -0
  249. package/server/rate-limit/drivers/memory.js +24 -0
  250. package/server/rate-limit/drivers/redis.js +32 -0
  251. package/server/rate-limit/index.js +33 -0
  252. package/server/redis/index.js +67 -0
  253. package/server/ring-buffer.js +44 -0
  254. package/server/route.js +4 -0
  255. package/server/router/api-router.js +317 -0
  256. package/server/router/cors.js +31 -0
  257. package/server/router/middleware.js +91 -0
  258. package/server/router/routes.js +132 -0
  259. package/server/server.js +35 -0
  260. package/server/session/helpers.js +21 -0
  261. package/server/session/index.js +89 -0
  262. package/server/static/index.js +36 -0
  263. package/server/system-jobs/index.js +50 -0
  264. package/server/system-routes/index.js +84 -0
  265. package/server/testing/index.js +263 -0
  266. package/server/validation.js +41 -0
  267. package/server/watcher.js +34 -0
  268. package/server/web-server.js +231 -0
  269. package/server/ws/discovery.js +54 -0
  270. package/server/ws/index.js +14 -0
  271. package/server/ws/realtime.js +318 -0
  272. package/server/ws/registry.js +17 -0
  273. package/server/ws/server.js +152 -0
  274. package/server/ws/ws-router.js +335 -0
package/README.md ADDED
@@ -0,0 +1,711 @@
1
+ # Arcway
2
+
3
+ A convention-based TypeScript framework for building modular monoliths with strict domain boundaries.
4
+
5
+ Arcway uses file-system conventions to discover routes, services, jobs, events, and middleware — no manual wiring required. Each domain is isolated with its own database scope, event emitter, queue, cache, and file storage.
6
+
7
+ ## Quick Start
8
+
9
+ ```bash
10
+ npm install arcway
11
+ ```
12
+
13
+ Create a project:
14
+
15
+ ```
16
+ my-app/
17
+ ├── arcway.config.ts
18
+ ├── domains/
19
+ │ └── users/
20
+ │ ├── config.ts
21
+ │ └── api/
22
+ │ └── index.ts
23
+ └── db/
24
+ └── migrations/
25
+ ```
26
+
27
+ **arcway.config.ts**
28
+
29
+ ```typescript
30
+ import type { FrameworkConfig } from 'arcway';
31
+
32
+ export default {
33
+ database: {
34
+ client: 'sqlite',
35
+ connection: './dev.db',
36
+ },
37
+ } satisfies FrameworkConfig;
38
+ ```
39
+
40
+ **domains/users/config.ts**
41
+
42
+ ```typescript
43
+ import type { DomainConfig } from 'arcway';
44
+
45
+ export default {
46
+ name: 'users',
47
+ tables: ['users'],
48
+ } satisfies DomainConfig;
49
+ ```
50
+
51
+ **domains/users/api/index.ts**
52
+
53
+ ```typescript
54
+ import type { RouteMethodConfig } from 'arcway';
55
+
56
+ export const GET: RouteMethodConfig = {
57
+ handler: async (ctx) => {
58
+ const users = await ctx.db('users').select('*');
59
+ return { data: users };
60
+ },
61
+ };
62
+
63
+ export const POST: RouteMethodConfig = {
64
+ handler: async (ctx) => {
65
+ const [id] = await ctx.db('users').insert(ctx.body);
66
+ return { status: 201, data: { id } };
67
+ },
68
+ };
69
+ ```
70
+
71
+ Start the dev server:
72
+
73
+ ```bash
74
+ npx arcway dev
75
+ ```
76
+
77
+ This boots the full stack: database, migrations, route discovery, event bus, job runner, and HTTP server.
78
+
79
+ ## CLI Commands
80
+
81
+ | Command | Description |
82
+ | ------------------------------- | ------------------------------------------------------------------ |
83
+ | `arcway dev` | Start development server (console logging, CORS enabled) |
84
+ | `arcway start` | Start production server (JSON logging, health check at `/health`) |
85
+ | `arcway build [outDir]` | Compile TypeScript to production bundle (default: `dist`) |
86
+ | `arcway seed` | Run database seed files from `db/seeds/` |
87
+ | `arcway docs [outFile]` | Generate OpenAPI spec from route schemas (default: `openapi.json`) |
88
+ | `arcway test [--watch] [pattern]` | Run domain tests (`domains/*/tests/**/*.test.ts`) |
89
+ | `arcway lint` | Check for domain boundary violations |
90
+
91
+ ## Project Structure
92
+
93
+ ```
94
+ project-root/
95
+ ├── arcway.config.ts # Framework configuration
96
+ ├── domains/
97
+ │ ├── users/
98
+ │ │ ├── config.ts # Domain name, owned tables, events, hooks
99
+ │ │ ├── services/index.ts # Exported service functions
100
+ │ │ ├── api/ # HTTP route handlers
101
+ │ │ │ ├── index.ts # GET /users, POST /users
102
+ │ │ │ ├── [id].ts # GET /users/:id, PUT /users/:id
103
+ │ │ │ ├── [id]/projects.ts # GET /users/:id/projects
104
+ │ │ │ ├── admin/settings.ts # GET /users/admin/settings
105
+ │ │ │ ├── _middleware.ts # Middleware for all /users/* routes
106
+ │ │ │ └── admin/
107
+ │ │ │ └── _middleware.ts # Middleware for /users/admin/* only
108
+ │ │ ├── jobs/ # Background job definitions
109
+ │ │ │ └── send-welcome.ts
110
+ │ │ ├── listeners/ # Event subscribers
111
+ │ │ │ └── on-signup.ts
112
+ │ │ └── tests/ # Domain unit tests
113
+ │ │ └── users.test.ts
114
+ │ └── billing/
115
+ │ ├── config.ts
116
+ │ ├── services/index.ts
117
+ │ └── listeners/
118
+ │ └── on-user-created.ts
119
+ └── db/
120
+ ├── migrations/ # Knex migration files
121
+ │ └── 001_create_tables.js
122
+ └── seeds/ # Database seed files
123
+ └── 001_users.ts
124
+ ```
125
+
126
+ Files starting with `_` (except `_middleware.ts`) are excluded from route/job/listener discovery.
127
+
128
+ ## Configuration
129
+
130
+ ### Framework Config
131
+
132
+ ```typescript
133
+ import type { FrameworkConfig } from 'arcway';
134
+
135
+ export default {
136
+ server: {
137
+ port: 3000,
138
+ shutdownTimeoutMs: 10_000,
139
+ maxBodySize: 1_048_576, // 1 MB
140
+ },
141
+ api: {
142
+ cors: {
143
+ // Or true/false
144
+ origin: ['https://app.com'],
145
+ methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
146
+ allowedHeaders: ['Content-Type', 'Authorization'],
147
+ exposedHeaders: ['X-Request-Id'],
148
+ credentials: true,
149
+ maxAge: 86400,
150
+ },
151
+ },
152
+ database: {
153
+ client: 'postgres',
154
+ connection: 'postgres://user:pass@localhost/mydb',
155
+ },
156
+ queue: {
157
+ driver: 'redis', // 'knex' (default) or 'redis'
158
+ lockCooldownMs: 300_000,
159
+ redis: { url: 'redis://localhost:6379' },
160
+ },
161
+ cache: {
162
+ driver: 'redis',
163
+ defaultTtlMs: 60_000,
164
+ redis: { url: 'redis://localhost:6379' },
165
+ },
166
+ events: {
167
+ driver: 'redis', // 'memory' (default) or 'redis'
168
+ redis: { url: 'redis://localhost:6379' },
169
+ },
170
+ files: {
171
+ driver: 's3', // 'local' (default) or 's3'
172
+ s3: {
173
+ bucket: 'my-bucket',
174
+ region: 'us-east-1',
175
+ },
176
+ },
177
+ } satisfies FrameworkConfig;
178
+ ```
179
+
180
+ ### CORS
181
+
182
+ CORS behavior by default:
183
+
184
+ - **Development** (`arcway dev`): Permissive — all origins allowed.
185
+ - **Production** (`arcway start`): Disabled — no CORS headers unless configured.
186
+
187
+ Configure with `api.cors`:
188
+
189
+ - `true` — enable permissive CORS in any mode.
190
+ - `false` — disable CORS entirely.
191
+ - `CorsConfig` object — use specific settings.
192
+
193
+ ### Domain Config
194
+
195
+ ```typescript
196
+ import type { DomainConfig } from 'arcway';
197
+ import { z } from 'zod';
198
+
199
+ export default {
200
+ name: 'users',
201
+ tables: ['users', 'sessions'],
202
+ events: [
203
+ { name: 'users/created', schema: z.object({ userId: z.number() }) },
204
+ { name: 'users/deleted' },
205
+ ],
206
+ hooks: {
207
+ onInit: async (ctx) => {
208
+ /* Before server starts */
209
+ },
210
+ onReady: async (ctx) => {
211
+ /* After server is listening */
212
+ },
213
+ onShutdown: async (ctx) => {
214
+ /* Graceful shutdown */
215
+ },
216
+ },
217
+ } satisfies DomainConfig;
218
+ ```
219
+
220
+ ## Routes
221
+
222
+ Route files live in `domains/<name>/api/` and map to URL patterns by file path:
223
+
224
+ | File | URL Pattern |
225
+ | ----------------------- | -------------------------- |
226
+ | `api/index.ts` | `/<domain>` |
227
+ | `api/[id].ts` | `/<domain>/:id` |
228
+ | `api/[id]/projects.ts` | `/<domain>/:id/projects` |
229
+ | `api/admin/settings.ts` | `/<domain>/admin/settings` |
230
+
231
+ Export named constants for each HTTP method:
232
+
233
+ ```typescript
234
+ import { z } from 'zod';
235
+ import type { RouteMethodConfig } from 'arcway';
236
+
237
+ export const GET: RouteMethodConfig = {
238
+ schema: {
239
+ query: z.object({ page: z.coerce.number().default(1) }),
240
+ },
241
+ meta: {
242
+ summary: 'List users',
243
+ tags: ['users'],
244
+ },
245
+ handler: async (ctx) => {
246
+ const users = await ctx
247
+ .db('users')
248
+ .select('*')
249
+ .limit(20)
250
+ .offset((ctx.query.page - 1) * 20);
251
+ return { data: users };
252
+ },
253
+ };
254
+
255
+ export const POST: RouteMethodConfig = {
256
+ schema: {
257
+ body: z.object({
258
+ name: z.string().min(1),
259
+ email: z.string().email(),
260
+ }),
261
+ },
262
+ handler: async (ctx) => {
263
+ const { name, email } = ctx.body as { name: string; email: string };
264
+ const [id] = await ctx.db('users').insert({ name, email });
265
+ await ctx.events.emit('users/created', { userId: id });
266
+ return { status: 201, data: { id } };
267
+ },
268
+ };
269
+ ```
270
+
271
+ ### Route Context
272
+
273
+ Every handler receives a `RouteContext`:
274
+
275
+ ```typescript
276
+ interface RouteContext {
277
+ domain: string; // Domain name
278
+ db: Knex; // Scoped database connection
279
+ services: ServicesProxy; // Cross-domain service calls
280
+ events: DomainEvents; // Event emission
281
+ queue: DomainQueue; // Persistent queue
282
+ cache: DomainCache; // Key-value cache
283
+ files: DomainFiles; // File storage
284
+ params: Record<string, string>; // URL parameters (:id)
285
+ query: Record<string, string>; // Query string (?key=value)
286
+ body: unknown; // Parsed request body
287
+ headers: Record<string, string>; // Request headers
288
+ }
289
+ ```
290
+
291
+ ### Route Response
292
+
293
+ ```typescript
294
+ interface RouteResponse {
295
+ status?: number; // Default: 200 (or 400 if error)
296
+ data?: unknown; // Wrapped in { data: ... }
297
+ error?: {
298
+ code: string;
299
+ message: string;
300
+ details?: unknown;
301
+ };
302
+ headers?: Record<string, string>;
303
+ }
304
+ ```
305
+
306
+ ## Middleware
307
+
308
+ Place `_middleware.ts` files in `api/` directories. Middleware applies to all routes at that level and below.
309
+
310
+ ```typescript
311
+ // domains/users/api/_middleware.ts — applies to all /users/* routes
312
+ import type { MiddlewareFn } from 'arcway';
313
+
314
+ const logger: MiddlewareFn = async (ctx, next) => {
315
+ const start = Date.now();
316
+ const response = await next();
317
+ console.log(`${ctx.headers['method']} took ${Date.now() - start}ms`);
318
+ return response;
319
+ };
320
+
321
+ export default logger;
322
+ ```
323
+
324
+ ```typescript
325
+ // domains/users/api/admin/_middleware.ts — applies to /users/admin/* only
326
+ import type { MiddlewareFn } from 'arcway';
327
+
328
+ const auth: MiddlewareFn = async (ctx, next) => {
329
+ if (ctx.headers['authorization'] !== 'Bearer valid-token') {
330
+ return {
331
+ status: 401,
332
+ error: { code: 'UNAUTHORIZED', message: 'Invalid token' },
333
+ };
334
+ }
335
+ return next();
336
+ };
337
+
338
+ export default auth;
339
+ ```
340
+
341
+ Middleware can also export an array of functions:
342
+
343
+ ```typescript
344
+ export default [loggerMiddleware, authMiddleware];
345
+ ```
346
+
347
+ Middleware chains execute outermost-first. A request to `/users/admin/settings` runs:
348
+
349
+ 1. `/users/api/_middleware.ts` (root)
350
+ 2. `/users/api/admin/_middleware.ts` (admin)
351
+ 3. Route handler
352
+
353
+ Return without calling `next()` to short-circuit (e.g., return 401).
354
+
355
+ ## Services
356
+
357
+ Services enable cross-domain function calls with automatic context injection.
358
+
359
+ ```typescript
360
+ // domains/users/services/index.ts
361
+ import type { DomainContext } from 'arcway';
362
+
363
+ export default {
364
+ getById: async (ctx: DomainContext, id: string) => {
365
+ return ctx.db('users').where({ id }).first();
366
+ },
367
+ list: async (ctx: DomainContext) => {
368
+ return ctx.db('users').select('*');
369
+ },
370
+ };
371
+ ```
372
+
373
+ Call from another domain — `ctx` is injected automatically:
374
+
375
+ ```typescript
376
+ // In a billing route handler
377
+ const user = await ctx.services.users.getById(userId);
378
+ ```
379
+
380
+ ## Events
381
+
382
+ Domains declare events they can emit in `config.ts`:
383
+
384
+ ```typescript
385
+ events: [
386
+ { name: 'users/created', schema: z.object({ userId: z.number() }) },
387
+ ],
388
+ ```
389
+
390
+ Emit from any handler, service, or job:
391
+
392
+ ```typescript
393
+ const results = await ctx.events.emit('users/created', { userId: 42 });
394
+ ```
395
+
396
+ Always `await` the emit call — immediate handlers run in-process during emit and their results are returned as an array of `EventResult`.
397
+
398
+ ### Listeners
399
+
400
+ Listeners define how they handle events via two handler functions:
401
+
402
+ - **`handler`** — dispatched via the event bus (eventually consistent, fire-and-forget)
403
+ - **`handlerImmediate`** — runs in-process during `emit()`, before bus dispatch
404
+
405
+ At least one must be defined. Both can be defined on the same listener.
406
+
407
+ ```typescript
408
+ // domains/billing/listeners/on-user-created.ts (async — via bus)
409
+ import type { ListenerDefinition } from 'arcway';
410
+
411
+ export default {
412
+ event: 'users/created',
413
+ handler: async (ctx, payload) => {
414
+ const { userId } = payload as { userId: number };
415
+ await ctx.db('billing_accounts').insert({ user_id: userId, balance: 0 });
416
+ },
417
+ } satisfies ListenerDefinition;
418
+ ```
419
+
420
+ ```typescript
421
+ // domains/orgs/listeners/on-user-created.ts (immediate — in-process)
422
+ import type { ListenerDefinition } from 'arcway';
423
+
424
+ export default {
425
+ event: 'users/created',
426
+ handlerImmediate: async (ctx, payload) => {
427
+ const { userId } = payload as { userId: number };
428
+ const [org] = await ctx.db('orgs').insert({ owner_id: userId, name: 'My Org' });
429
+ if (!org) {
430
+ return { error: { code: 'ORG_CREATE_FAILED', message: 'Failed to create org' } };
431
+ }
432
+ },
433
+ } satisfies ListenerDefinition;
434
+ ```
435
+
436
+ Event patterns support wildcards: `'users/*'` matches all events from the users domain.
437
+
438
+ ## Jobs
439
+
440
+ Background jobs support one-off queuing and cron scheduling.
441
+
442
+ ```typescript
443
+ // domains/billing/jobs/generate-invoice.ts
444
+ import { z } from 'zod';
445
+ import type { JobDefinition } from 'arcway';
446
+
447
+ export default {
448
+ name: 'generate-invoice',
449
+ schema: z.object({ userId: z.number(), month: z.string() }),
450
+ retries: 3,
451
+ schedule: '0 0 1 * *', // First of each month
452
+ handler: async (ctx, payload) => {
453
+ const { userId, month } = payload as { userId: number; month: string };
454
+ // Generate invoice...
455
+ },
456
+ } satisfies JobDefinition;
457
+ ```
458
+
459
+ Queue a job from any handler:
460
+
461
+ ```typescript
462
+ await ctx.queue.push('generate-invoice', { userId: 42, month: '2025-01' });
463
+ ```
464
+
465
+ Failed jobs retry with exponential backoff (1s, 2s, 4s, 8s...).
466
+
467
+ ## Queue
468
+
469
+ Domain-scoped persistent queue for background processing:
470
+
471
+ ```typescript
472
+ // Push work
473
+ await ctx.queue.push('email-send', { to: 'user@example.com', body: '...' });
474
+
475
+ // Pop and process (typically in a job handler)
476
+ const items = await ctx.queue.pop('email-send', 10);
477
+ for (const item of items) {
478
+ await sendEmail(item.data);
479
+ await ctx.queue.remove([item.id]);
480
+ }
481
+ ```
482
+
483
+ Drivers: `knex` (default, database-backed) or `redis`.
484
+
485
+ ## Cache
486
+
487
+ Domain-scoped key-value cache with TTL support:
488
+
489
+ ```typescript
490
+ // Set with TTL
491
+ await ctx.cache.set('user:42', userData, 60_000);
492
+
493
+ // Get
494
+ const cached = await ctx.cache.get('user:42');
495
+
496
+ // Cache-aside pattern
497
+ const user = await ctx.cache.wrap(
498
+ 'user:42',
499
+ async () => {
500
+ return ctx.db('users').where({ id: 42 }).first();
501
+ },
502
+ 60_000,
503
+ );
504
+
505
+ // Delete
506
+ await ctx.cache.delete('user:42');
507
+ ```
508
+
509
+ Drivers: `knex` (default) or `redis`.
510
+
511
+ ## File Storage
512
+
513
+ Domain-scoped file operations:
514
+
515
+ ```typescript
516
+ // Write
517
+ await ctx.files.write('avatars/user-42.png', imageBuffer);
518
+
519
+ // Read
520
+ const data = await ctx.files.read('avatars/user-42.png');
521
+
522
+ // List
523
+ const files = await ctx.files.list('avatars/');
524
+
525
+ // Check existence
526
+ const exists = await ctx.files.exists('avatars/user-42.png');
527
+
528
+ // Delete
529
+ await ctx.files.delete('avatars/user-42.png');
530
+ ```
531
+
532
+ Drivers: `local` (default, filesystem) or `s3`.
533
+
534
+ ## Rate Limiting
535
+
536
+ Rate limiting is available as a middleware factory with pluggable backends:
537
+
538
+ ```typescript
539
+ // domains/api-gateway/api/_middleware.ts
540
+ import { createRateLimitMiddleware, MemoryRateLimitStore } from 'arcway';
541
+
542
+ const store = new MemoryRateLimitStore();
543
+
544
+ export default createRateLimitMiddleware(
545
+ {
546
+ max: 100, // 100 requests
547
+ windowMs: 60_000, // per minute
548
+ // keyFn: (ctx) => ctx.headers['x-api-key'] ?? 'anonymous',
549
+ // message: 'Rate limit exceeded',
550
+ },
551
+ store,
552
+ );
553
+ ```
554
+
555
+ The middleware uses a sliding window algorithm. By default, it keys on the client IP from `X-Forwarded-For` or `X-Real-IP` headers.
556
+
557
+ Response headers are added automatically:
558
+
559
+ - `X-RateLimit-Limit` — configured maximum
560
+ - `X-RateLimit-Remaining` — remaining requests in window
561
+ - `X-RateLimit-Reset` — Unix timestamp when window resets
562
+ - `Retry-After` — seconds until retry (on 429 responses only)
563
+
564
+ Implement `RateLimitStore` for custom backends (Redis, etc.):
565
+
566
+ ```typescript
567
+ interface RateLimitStore {
568
+ check(key: string, max: number, windowMs: number): Promise<RateLimitResult>;
569
+ destroy(): void;
570
+ }
571
+ ```
572
+
573
+ ## Database Access Control
574
+
575
+ Domains can only write to tables listed in their `config.ts`. Attempting to write to another domain's table throws `DomainAccessViolation`:
576
+
577
+ ```typescript
578
+ // In users domain (tables: ['users', 'sessions'])
579
+ await ctx.db('users').insert({ name: 'Alice' }); // OK
580
+ await ctx.db('billing').insert({ amount: 100 }); // Throws DomainAccessViolation
581
+ ```
582
+
583
+ Read access is unrestricted. Cross-domain writes go through services.
584
+
585
+ ## Domain Boundary Linting
586
+
587
+ `arcway lint` statically checks for illegal imports between domains:
588
+
589
+ ```bash
590
+ $ arcway lint
591
+ Checking domain boundaries...
592
+ Domains: users, billing, projects
593
+ Files scanned: 24
594
+
595
+ 1 violation(s) found:
596
+
597
+ domains/billing/services/index.ts:3
598
+ Domain 'billing' imports directly from domain 'users'
599
+ Import: ../../users/services/index.ts
600
+ ```
601
+
602
+ Domains must communicate through `ctx.services`, `ctx.events`, or `ctx.queue` — never by importing each other's files directly.
603
+
604
+ ## Testing
605
+
606
+ Arcway provides test utilities that create isolated domain contexts with in-memory SQLite:
607
+
608
+ ```typescript
609
+ import { describe, it, expect, beforeAll, afterAll } from 'vitest';
610
+ import { createTestContext, type TestContext } from 'arcway';
611
+
612
+ describe('users service', () => {
613
+ let t: TestContext;
614
+
615
+ beforeAll(async () => {
616
+ t = await createTestContext({
617
+ domain: 'users',
618
+ tables: ['users'],
619
+ migrationsDir: 'db/migrations',
620
+ });
621
+ });
622
+
623
+ afterAll(() => t.cleanup());
624
+
625
+ it('creates a user', async () => {
626
+ const [id] = await t.ctx.db('users').insert({ name: 'Alice' });
627
+ const user = await t.ctx.db('users').where({ id }).first();
628
+ expect(user.name).toBe('Alice');
629
+ });
630
+ });
631
+ ```
632
+
633
+ The test context stubs all infrastructure (events, queue, cache, files) so tests run fast and in isolation.
634
+
635
+ ## Seeds
636
+
637
+ Seed files live in `db/seeds/` and run in alphabetical order:
638
+
639
+ ```typescript
640
+ // db/seeds/001_users.ts
641
+ import type { Knex } from 'knex';
642
+
643
+ export default async function seed(db: Knex): Promise<void> {
644
+ await db('users')
645
+ .insert([
646
+ { id: 1, name: 'Alice', email: 'alice@test.com' },
647
+ { id: 2, name: 'Bob', email: 'bob@test.com' },
648
+ ])
649
+ .onConflict('id')
650
+ .merge();
651
+ }
652
+ ```
653
+
654
+ Run with `arcway seed`. Migrations execute first to ensure the schema is current.
655
+
656
+ ## OpenAPI Generation
657
+
658
+ `arcway docs` generates an OpenAPI 3.0 spec from route schemas:
659
+
660
+ ```bash
661
+ arcway docs openapi.json
662
+ ```
663
+
664
+ Routes with `meta` and `schema` fields produce documented endpoints:
665
+
666
+ ```typescript
667
+ export const GET: RouteMethodConfig = {
668
+ schema: {
669
+ params: z.object({ id: z.string() }),
670
+ query: z.object({ fields: z.string().optional() }),
671
+ },
672
+ meta: {
673
+ summary: 'Get user by ID',
674
+ description: 'Returns a single user record.',
675
+ tags: ['users'],
676
+ },
677
+ handler: async (ctx) => {
678
+ /* ... */
679
+ },
680
+ };
681
+ ```
682
+
683
+ ## Production Build
684
+
685
+ ```bash
686
+ arcway build # Compile to dist/
687
+ cd dist && arcway start
688
+ ```
689
+
690
+ The build step compiles TypeScript with esbuild, copies non-TS files (migrations, JSON), and preserves the directory structure. The production server uses native `import()` — no tsx required at runtime.
691
+
692
+ ## Boot Sequence
693
+
694
+ When `arcway dev` or `arcway start` runs, the framework:
695
+
696
+ 1. Loads `arcway.config.ts`
697
+ 2. Discovers all domains in `domains/`
698
+ 3. Connects to the database and runs migrations
699
+ 4. Initializes queue, cache, and file drivers
700
+ 5. Creates scoped domain contexts
701
+ 6. Discovers services and wires cross-domain proxies
702
+ 7. Creates event bus and registers listeners
703
+ 8. Discovers and registers jobs
704
+ 9. Runs `onInit` hooks
705
+ 10. Discovers routes and middleware
706
+ 11. Resolves CORS configuration
707
+ 12. Creates and starts HTTP server
708
+ 13. Runs `onReady` hooks
709
+ 14. Starts job runner (cron scheduler)
710
+
711
+ Graceful shutdown reverses the process: stops the job runner, runs `onShutdown` hooks, drains connections, and closes the server.