@luckystack/server 0.6.7 → 0.7.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.
- package/CHANGELOG.md +120 -87
- package/CLAUDE.md +87 -87
- package/LICENSE +21 -21
- package/README.md +198 -198
- package/dist/{chunk-RIVFTOTI.js → chunk-X3OSC5W3.js} +1 -1
- package/dist/chunk-X3OSC5W3.js.map +1 -0
- package/dist/index.js +44 -6
- package/dist/index.js.map +1 -1
- package/dist/parseArgv.js +1 -1
- package/dist/parseArgv.js.map +1 -1
- package/docs/argv-parsing.md +256 -256
- package/docs/create-server.md +295 -295
- package/docs/http-routes.md +318 -318
- package/docs/request-pipeline.md +198 -198
- package/docs/runtime-maps.md +206 -206
- package/docs/security-defaults.md +236 -236
- package/package.json +10 -10
- package/dist/chunk-RIVFTOTI.js.map +0 -1
package/docs/create-server.md
CHANGED
|
@@ -1,295 +1,295 @@
|
|
|
1
|
-
# Create Server (`createLuckyStackServer` + `bootstrapLuckyStack`)
|
|
2
|
-
|
|
3
|
-
> Deep specs. Bron: `packages/server/src/createServer.ts`, `packages/server/src/bootstrap.ts`, `packages/server/src/verifyBootstrap.ts`, `packages/server/src/hookPayloads.ts`, `packages/server/src/types.ts`. Bijgewerkt: 2026-05-20.
|
|
4
|
-
|
|
5
|
-
## Overview
|
|
6
|
-
|
|
7
|
-
`@luckystack/server` exposes two boot entries:
|
|
8
|
-
|
|
9
|
-
- `bootstrapLuckyStack(options)` — high-level, auto-imports the project's `luckystack/<package>/*.ts` overlay folder in topological order, then delegates to `createLuckyStackServer`. This is what `create-luckystack-app` scaffolds and what most apps should call.
|
|
10
|
-
- `createLuckyStackServer(options)` — low-level factory. Skips overlay loading; the caller is responsible for getting every framework registry populated (project config, deploy config, runtime maps, OAuth providers, etc.) before it runs. Returns `{ httpServer, ioServer, listen }`.
|
|
11
|
-
|
|
12
|
-
Both honor an explicit pre-flight check via `verifyBootstrap(requirements?)`, which fails fast when a critical registration was forgotten. `createLuckyStackServer` invokes it internally so direct callers don't have to.
|
|
13
|
-
|
|
14
|
-
Boot order (effective for both entries):
|
|
15
|
-
|
|
16
|
-
1. `bootstrapLuckyStack` only: load overlay files (`core` -> `deploy` -> `login` -> `sentry` -> `presence` -> `docs-ui` -> `server`, each folder topologically followed by alphabetical `*.ts`).
|
|
17
|
-
2. If `options.loadGeneratedMaps` was supplied, register the framework-shipped runtime-maps provider before `verifyBootstrap` so the boot check sees it.
|
|
18
|
-
3. `verifyBootstrap` runs (using the per-call `requireDeployConfig` / `requireServicesConfig` / `requireOAuthProviders` flags). Throws a single descriptive `Error` if anything is missing.
|
|
19
|
-
4. Resolve `port` (`options.port` -> argv-parsed -> `SERVER_PORT` -> `80`) and `ip` (`options.ip` -> `SERVER_IP` -> `127.0.0.1`); register them with `registerBindAddress` so framework consumers see the resolved values.
|
|
20
|
-
5. In dev mode (`enableDevTools !== false` and `NODE_ENV !== 'production'`): `initConsolelog()` + dynamic-import `@luckystack/devkit` -> `initializeAll()` + `setupWatchers()`. Install `SIGINT` / `SIGTERM` handlers that force-exit.
|
|
21
|
-
6. `writeBootUuid()` writes a fresh boot UUID to Redis so `/_health` becomes truthful and the router can detect rolling restarts.
|
|
22
|
-
7. Construct `http.createServer(handleHttpRequest)` and `loadSocket(httpServer, { maxHttpBufferSize })`.
|
|
23
|
-
8. Return `{ httpServer, ioServer, listen }`. The HTTP server has NOT started listening yet; the caller invokes `listen()` to bind.
|
|
24
|
-
|
|
25
|
-
## API Reference
|
|
26
|
-
|
|
27
|
-
### `createLuckyStackServer(options?: CreateLuckyStackServerOptions): Promise<RunningLuckyStackServer>`
|
|
28
|
-
|
|
29
|
-
**Signature:**
|
|
30
|
-
|
|
31
|
-
```typescript
|
|
32
|
-
export const createLuckyStackServer = async (
|
|
33
|
-
options: CreateLuckyStackServerOptions = {}
|
|
34
|
-
): Promise<RunningLuckyStackServer>;
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
**Parameters:**
|
|
38
|
-
|
|
39
|
-
| Field | Type | Default | Purpose |
|
|
40
|
-
| --- | --- | --- | --- |
|
|
41
|
-
| `port` | `number \| string` | `getParsedPort()` -> `SERVER_PORT` -> `80` | HTTP listen port. String coerced via `parseInt(_, 10)`. |
|
|
42
|
-
| `ip` | `string` | `process.env.SERVER_IP ?? '127.0.0.1'` | Bind address. Registered with `registerBindAddress` so `checkOrigin` and other framework code see the resolved value. |
|
|
43
|
-
| `serveFile` | `StaticFileHandler` | none | Catch-all GET handler (Vite output, SPA `index.html`). Called for `/assets/*`, known static extensions, and the final SPA fallback. Without it the static fallback returns `404`. |
|
|
44
|
-
| `serveFavicon` | `FaviconHandler` | none | Handler for `/favicon.ico`. Without it the route returns `404`. |
|
|
45
|
-
| `customRoutes` | `CustomRouteHandler` | none | Inline custom-route hook. Composed after the registry handlers from `registerCustomRoute(...)`; first one to return `true` wins. |
|
|
46
|
-
| `enableDevTools` | `boolean` | `NODE_ENV !== 'production'` | Toggles `initConsolelog()` + devkit hot reload + REPL. Pass `false` to opt out. |
|
|
47
|
-
| `maxHttpBufferSize` | `number` | `5 * 1024 * 1024` | Forwarded to Socket.io. Raise for large payloads. |
|
|
48
|
-
| `requireDeployConfig` | `boolean` | `false` | When `true`, `verifyBootstrap` fails if no `DeployConfig` was registered. |
|
|
49
|
-
| `requireServicesConfig` | `boolean` | `false` | When `true`, `verifyBootstrap` fails if no `ServicesConfig` was registered (router topology). |
|
|
50
|
-
| `requireOAuthProviders` | `boolean` | `false` | When `true`, `verifyBootstrap` fails if only the default `credentials` provider is registered. |
|
|
51
|
-
| `loadGeneratedMaps` | `(preset: string) => Promise<unknown>` | none | Callback that resolves a generated runtime-maps module per preset. Triggers `registerProdRuntimeMapsProvider` internally. Required because dynamic-import resolution is module-scoped — the framework cannot resolve a relative path on the consumer's behalf. |
|
|
52
|
-
| `runtimeMapsPreset` | `string \| string[]` | argv -> `'default'` | Overrides the argv-derived preset list. |
|
|
53
|
-
|
|
54
|
-
**Returns:** `RunningLuckyStackServer`:
|
|
55
|
-
|
|
56
|
-
```typescript
|
|
57
|
-
interface RunningLuckyStackServer {
|
|
58
|
-
httpServer: http.Server;
|
|
59
|
-
ioServer: socket.io.Server;
|
|
60
|
-
listen: (callback?: () => void) => Promise<http.Server>;
|
|
61
|
-
}
|
|
62
|
-
```
|
|
63
|
-
|
|
64
|
-
`listen()` resolves when the HTTP server is bound. Logs `Server is running on http://<ip>:<port>/` when `projectConfig.logging.socketStartup` or `projectConfig.logging.devLogs` is enabled. It is safe to call once; calling twice yields a Node `ERR_SERVER_ALREADY_LISTEN` from the underlying `httpServer.listen`.
|
|
65
|
-
|
|
66
|
-
**Behavior (execution order):**
|
|
67
|
-
|
|
68
|
-
- Auto-register prod runtime-maps provider if `loadGeneratedMaps` is set.
|
|
69
|
-
- Run `verifyBootstrap` (may throw).
|
|
70
|
-
- Resolve port + ip; call `registerBindAddress({ ip, port })`.
|
|
71
|
-
- If dev-tools enabled: load console initializer + devkit (`initializeAll`, `setupWatchers`); attach SIGINT / SIGTERM force-exit handlers.
|
|
72
|
-
- `await writeBootUuid()`.
|
|
73
|
-
- Create `http.Server` whose request handler is `handleHttpRequest(req, res, options)` (see `request-pipeline.md`).
|
|
74
|
-
- `loadSocket(httpServer, { maxHttpBufferSize })` attaches the Socket.io server.
|
|
75
|
-
- Return `{ httpServer, ioServer, listen }` without listening.
|
|
76
|
-
|
|
77
|
-
**Errors / Edge cases:**
|
|
78
|
-
|
|
79
|
-
- Throws (from `verifyBootstrap`) when a required registry is missing — single multi-line `Error`.
|
|
80
|
-
- Dev-only branch dynamically imports `@luckystack/devkit`; production bundles exclude it so the import is never reached when `NODE_ENV === 'production'`.
|
|
81
|
-
- `enableDevTools: true` in production loads devkit; this is supported but unusual.
|
|
82
|
-
- SIGINT / SIGTERM handlers are installed only when dev-tools branch runs.
|
|
83
|
-
- `writeBootUuid` failures bubble — boot will abort if Redis is unreachable at start.
|
|
84
|
-
|
|
85
|
-
**Example — direct boot (skip overlay):**
|
|
86
|
-
|
|
87
|
-
```typescript
|
|
88
|
-
import 'dotenv/config';
|
|
89
|
-
import './config';
|
|
90
|
-
import './deploy.config';
|
|
91
|
-
import { initializeSentry } from '@luckystack/error-tracking';
|
|
92
|
-
import { registerPresenceHooks } from '@luckystack/presence';
|
|
93
|
-
import { createLuckyStackServer } from '@luckystack/server';
|
|
94
|
-
import { serveFile, serveFavicon } from './prod/serveFile';
|
|
95
|
-
|
|
96
|
-
initializeSentry();
|
|
97
|
-
registerPresenceHooks();
|
|
98
|
-
|
|
99
|
-
const server = await createLuckyStackServer({
|
|
100
|
-
serveFile,
|
|
101
|
-
serveFavicon,
|
|
102
|
-
loadGeneratedMaps: (preset) => import(`./prod/generatedApis.${preset}`),
|
|
103
|
-
requireDeployConfig: true,
|
|
104
|
-
});
|
|
105
|
-
await server.listen();
|
|
106
|
-
```
|
|
107
|
-
|
|
108
|
-
---
|
|
109
|
-
|
|
110
|
-
### `bootstrapLuckyStack(options?: BootstrapLuckyStackOptions): Promise<RunningLuckyStackServer>`
|
|
111
|
-
|
|
112
|
-
**Signature:**
|
|
113
|
-
|
|
114
|
-
```typescript
|
|
115
|
-
export const bootstrapLuckyStack = async (
|
|
116
|
-
options: BootstrapLuckyStackOptions = {}
|
|
117
|
-
): Promise<RunningLuckyStackServer>;
|
|
118
|
-
```
|
|
119
|
-
|
|
120
|
-
**Parameters:** extends `CreateLuckyStackServerOptions` with:
|
|
121
|
-
|
|
122
|
-
| Field | Type | Default | Purpose |
|
|
123
|
-
| --- | --- | --- | --- |
|
|
124
|
-
| `overlayRoot` | `string` | `'luckystack'` | Folder name (relative to `ROOT_DIR` from `@luckystack/core`) that holds the per-package overlay files. Absolute paths are honored as-is. |
|
|
125
|
-
| `skipOverlayLoad` | `boolean` | `false` | Skip auto-loading the overlay folder. Useful for tests or hand-built registries. |
|
|
126
|
-
|
|
127
|
-
**Returns:** identical to `createLuckyStackServer`.
|
|
128
|
-
|
|
129
|
-
**Behavior:**
|
|
130
|
-
|
|
131
|
-
- If `skipOverlayLoad !== true` and `<ROOT_DIR>/<overlayRoot>` exists, walk the canonical package order (`core`, `deploy`, `login`, `sentry`, `presence`, `docs-ui`, `server`).
|
|
132
|
-
- Inside each package folder: import `index.ts`/`index.js` first if present, then every remaining `*.ts` / `*.js` file in alphabetical order. Each file is responsible for its own side-effect registration.
|
|
133
|
-
- Then delegate to `createLuckyStackServer(options)`.
|
|
134
|
-
|
|
135
|
-
**Errors / Edge cases:**
|
|
136
|
-
|
|
137
|
-
- Missing `<overlayRoot>` folder is non-fatal — the function returns silently so projects can use the legacy single-file `config.ts` layout.
|
|
138
|
-
- Missing per-package subfolders (e.g. no `luckystack/docs-ui/`) are silently skipped.
|
|
139
|
-
- Import errors from overlay files bubble up unchanged; fix the failing module.
|
|
140
|
-
|
|
141
|
-
**Example — recommended scaffolded boot:**
|
|
142
|
-
|
|
143
|
-
```typescript
|
|
144
|
-
import '@luckystack/server/parseArgv';
|
|
145
|
-
import 'dotenv/config';
|
|
146
|
-
import './config';
|
|
147
|
-
import './deploy.config';
|
|
148
|
-
import { bootstrapLuckyStack, verifyBootstrap } from '@luckystack/server';
|
|
149
|
-
import { serveFile, serveFavicon } from './prod/serveFile';
|
|
150
|
-
|
|
151
|
-
const server = await bootstrapLuckyStack({
|
|
152
|
-
serveFile,
|
|
153
|
-
serveFavicon,
|
|
154
|
-
loadGeneratedMaps: (preset) => import(`./prod/generatedApis.${preset}`),
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
await verifyBootstrap({
|
|
158
|
-
requireDeployConfig: true,
|
|
159
|
-
requireOAuthProviders: true,
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
await server.listen();
|
|
163
|
-
```
|
|
164
|
-
|
|
165
|
-
---
|
|
166
|
-
|
|
167
|
-
### `verifyBootstrap(requirements?: BootstrapRequirements): Promise<void>`
|
|
168
|
-
|
|
169
|
-
**Signature:**
|
|
170
|
-
|
|
171
|
-
```typescript
|
|
172
|
-
export const verifyBootstrap = async (
|
|
173
|
-
requirements: BootstrapRequirements = {},
|
|
174
|
-
): Promise<void>;
|
|
175
|
-
```
|
|
176
|
-
|
|
177
|
-
**Parameters:**
|
|
178
|
-
|
|
179
|
-
| Field | Type | Default | Purpose |
|
|
180
|
-
| --- | --- | --- | --- |
|
|
181
|
-
| `requireDeployConfig` | `boolean` | `false` | Fail unless `registerDeployConfig` has been called. |
|
|
182
|
-
| `requireServicesConfig` | `boolean` | `false` | Fail unless `registerServicesConfig` has been called. |
|
|
183
|
-
| `requireOAuthProviders` | `boolean` | `false` | Fail unless an OAuth provider beyond the default `credentials` entry has been registered. |
|
|
184
|
-
|
|
185
|
-
**Returns:** `Promise<void>` — resolves silently when every required registry is in place.
|
|
186
|
-
|
|
187
|
-
**Behavior:**
|
|
188
|
-
|
|
189
|
-
- Builds a `missing[]` array. Checks in this order:
|
|
190
|
-
1. `isProjectConfigRegistered()` — always required.
|
|
191
|
-
2. `isDeployConfigRegistered()` — only when `requireDeployConfig`.
|
|
192
|
-
3. `isServicesConfigRegistered()` (lazy import) — only when `requireServicesConfig`.
|
|
193
|
-
4. `getOAuthProviders().length > 1` (lazy import from `@luckystack/login`) — only when `requireOAuthProviders`. Default registry length is `1` (`credentials`).
|
|
194
|
-
5. `isRuntimeMapsProviderRegistered()` — hard-fail in production, loud `getLogger().warn(...)` in dev/test.
|
|
195
|
-
6. `isLocalizedNormalizerRegistered()` — hard-fail in production, warn in dev/test.
|
|
196
|
-
- If `missing.length === 0`: return. Otherwise throw a single `Error` whose message lists every missing registration with a one-line remediation hint and points at `docs/ARCHITECTURE_PACKAGING.md`.
|
|
197
|
-
|
|
198
|
-
**Errors / Edge cases:**
|
|
199
|
-
|
|
200
|
-
- Throws synchronously after building the missing list. No partial-state recovery.
|
|
201
|
-
- The `RuntimeMapsProvider` warning in dev is intentional: devkit hot-reload normally registers one, so missing it during a bare boot is informational, not fatal.
|
|
202
|
-
- The `LocalizedNormalizer` warning in dev means error responses will surface the raw `errorCode` string instead of localized copy.
|
|
203
|
-
|
|
204
|
-
**Example:**
|
|
205
|
-
|
|
206
|
-
```typescript
|
|
207
|
-
import { verifyBootstrap } from '@luckystack/server';
|
|
208
|
-
|
|
209
|
-
await verifyBootstrap({
|
|
210
|
-
requireDeployConfig: true,
|
|
211
|
-
requireServicesConfig: false,
|
|
212
|
-
requireOAuthProviders: true,
|
|
213
|
-
});
|
|
214
|
-
```
|
|
215
|
-
|
|
216
|
-
## Hook payload types
|
|
217
|
-
|
|
218
|
-
Module augmentation in `hookPayloads.ts` extends `@luckystack/core`'s `HookPayloads` interface, so `dispatchHook` / `registerHook` accept these names with the correct payload typing.
|
|
219
|
-
|
|
220
|
-
| Hook name | Payload type | When it fires |
|
|
221
|
-
| --- | --- | --- |
|
|
222
|
-
| `onSocketConnect` | `OnSocketConnectPayload` (`{ socketId, token, ip }`) | Notification: every successful Socket.io connection. |
|
|
223
|
-
| `onSocketDisconnect` | `OnSocketDisconnectPayload` (`{ socketId, token, reason }`) | Notification: every Socket.io disconnect (with reason string). |
|
|
224
|
-
| `preRoomJoin` | `PreRoomJoinPayload` (`{ token, room }`) | Before the user joins a room. May stop with `HookStopSignal`. |
|
|
225
|
-
| `postRoomJoin` | `PostRoomJoinPayload` (`{ token, room, allRooms }`) | After a successful join. |
|
|
226
|
-
| `preRoomLeave` | `PreRoomLeavePayload` (`{ token, room }`) | Before the user leaves a room. May stop. |
|
|
227
|
-
| `postRoomLeave` | `PostRoomLeavePayload` (`{ token, room, allRooms }`) | After a successful leave. |
|
|
228
|
-
| `onLocationUpdate` | `OnLocationUpdatePayload` (`{ token, oldLocation?, newLocation }`) | When the presence layer reports a location change. |
|
|
229
|
-
|
|
230
|
-
Naming convention (also documented in `hookPayloads.ts`):
|
|
231
|
-
|
|
232
|
-
- `on*` — pure notifications; return values are ignored.
|
|
233
|
-
- `pre*` — may return a `HookStopSignal` (`{ errorCode: string; httpStatus?: number }`) to abort the main flow.
|
|
234
|
-
- `post*` — fire after the side-effect succeeds.
|
|
235
|
-
|
|
236
|
-
## Types
|
|
237
|
-
|
|
238
|
-
```typescript
|
|
239
|
-
export interface CreateLuckyStackServerOptions { /* see table above */ }
|
|
240
|
-
|
|
241
|
-
export interface BootstrapLuckyStackOptions extends CreateLuckyStackServerOptions {
|
|
242
|
-
overlayRoot?: string;
|
|
243
|
-
skipOverlayLoad?: boolean;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
export interface BootstrapRequirements {
|
|
247
|
-
requireDeployConfig?: boolean;
|
|
248
|
-
requireServicesConfig?: boolean;
|
|
249
|
-
requireOAuthProviders?: boolean;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
export interface RunningLuckyStackServer {
|
|
253
|
-
httpServer: http.Server;
|
|
254
|
-
ioServer: socket.io.Server;
|
|
255
|
-
listen: (callback?: () => void) => Promise<http.Server>;
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
export interface RouteContext {
|
|
259
|
-
routePath: string;
|
|
260
|
-
method: string;
|
|
261
|
-
queryString: string | undefined;
|
|
262
|
-
token: string | null;
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
export type StaticFileHandler = (req: IncomingMessage, res: ServerResponse) => unknown;
|
|
266
|
-
export type FaviconHandler = (res: ServerResponse) => unknown;
|
|
267
|
-
export type CustomRouteHandler = (
|
|
268
|
-
req: IncomingMessage,
|
|
269
|
-
res: ServerResponse,
|
|
270
|
-
ctx: RouteContext,
|
|
271
|
-
) => Promise<boolean> | boolean;
|
|
272
|
-
```
|
|
273
|
-
|
|
274
|
-
## Config keys consumed at boot
|
|
275
|
-
|
|
276
|
-
| Source | Key | Effect |
|
|
277
|
-
| --- | --- | --- |
|
|
278
|
-
| env | `SERVER_PORT` | Port fallback when neither `options.port` nor argv supplies one. |
|
|
279
|
-
| env | `SERVER_IP` | IP fallback. |
|
|
280
|
-
| env | `NODE_ENV` | Switches `enableDevTools` default, dynamic-import branch for devkit, and `verifyBootstrap` warn-vs-throw for runtime-maps / localized normalizer. |
|
|
281
|
-
| env | `SECURE` | When `'true'`, session cookies are emitted with the `Secure;` flag (see `request-pipeline.md`). |
|
|
282
|
-
| config | `projectConfig.logging.socketStartup` / `projectConfig.logging.devLogs` | Gates the "Server is running on ..." log line emitted by `listen()`. |
|
|
283
|
-
| config | `projectConfig.http.*` | Route paths + cookie + CORS + stream settings consumed at request time (not at boot). |
|
|
284
|
-
| argv | `<bundles> [port]` | Parsed by `applyServerArgv()` and surfaced via `getParsedBundles()` / `getParsedPort()`. |
|
|
285
|
-
|
|
286
|
-
## Related
|
|
287
|
-
|
|
288
|
-
- Function INDEX: `packages/server/CLAUDE.md`
|
|
289
|
-
- README (consumer quickstart): `packages/server/README.md`
|
|
290
|
-
- Argv parsing: `packages/server/docs/argv-parsing.md`
|
|
291
|
-
- Runtime maps: `packages/server/docs/runtime-maps.md`
|
|
292
|
-
- Request pipeline: `packages/server/docs/request-pipeline.md`
|
|
293
|
-
- HTTP routes: `packages/server/docs/http-routes.md`
|
|
294
|
-
- Security: `packages/server/docs/security-defaults.md`
|
|
295
|
-
- Architecture: `docs/ARCHITECTURE_PACKAGING.md`, `docs/ARCHITECTURE_SOCKET.md`, `docs/HOSTING.md`
|
|
1
|
+
# Create Server (`createLuckyStackServer` + `bootstrapLuckyStack`)
|
|
2
|
+
|
|
3
|
+
> Deep specs. Bron: `packages/server/src/createServer.ts`, `packages/server/src/bootstrap.ts`, `packages/server/src/verifyBootstrap.ts`, `packages/server/src/hookPayloads.ts`, `packages/server/src/types.ts`. Bijgewerkt: 2026-05-20.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
`@luckystack/server` exposes two boot entries:
|
|
8
|
+
|
|
9
|
+
- `bootstrapLuckyStack(options)` — high-level, auto-imports the project's `luckystack/<package>/*.ts` overlay folder in topological order, then delegates to `createLuckyStackServer`. This is what `create-luckystack-app` scaffolds and what most apps should call.
|
|
10
|
+
- `createLuckyStackServer(options)` — low-level factory. Skips overlay loading; the caller is responsible for getting every framework registry populated (project config, deploy config, runtime maps, OAuth providers, etc.) before it runs. Returns `{ httpServer, ioServer, listen }`.
|
|
11
|
+
|
|
12
|
+
Both honor an explicit pre-flight check via `verifyBootstrap(requirements?)`, which fails fast when a critical registration was forgotten. `createLuckyStackServer` invokes it internally so direct callers don't have to.
|
|
13
|
+
|
|
14
|
+
Boot order (effective for both entries):
|
|
15
|
+
|
|
16
|
+
1. `bootstrapLuckyStack` only: load overlay files (`core` -> `deploy` -> `login` -> `sentry` -> `presence` -> `docs-ui` -> `server`, each folder topologically followed by alphabetical `*.ts`).
|
|
17
|
+
2. If `options.loadGeneratedMaps` was supplied, register the framework-shipped runtime-maps provider before `verifyBootstrap` so the boot check sees it.
|
|
18
|
+
3. `verifyBootstrap` runs (using the per-call `requireDeployConfig` / `requireServicesConfig` / `requireOAuthProviders` flags). Throws a single descriptive `Error` if anything is missing.
|
|
19
|
+
4. Resolve `port` (`options.port` -> argv-parsed -> `SERVER_PORT` -> `80`) and `ip` (`options.ip` -> `SERVER_IP` -> `127.0.0.1`); register them with `registerBindAddress` so framework consumers see the resolved values.
|
|
20
|
+
5. In dev mode (`enableDevTools !== false` and `NODE_ENV !== 'production'`): `initConsolelog()` + dynamic-import `@luckystack/devkit` -> `initializeAll()` + `setupWatchers()`. Install `SIGINT` / `SIGTERM` handlers that force-exit.
|
|
21
|
+
6. `writeBootUuid()` writes a fresh boot UUID to Redis so `/_health` becomes truthful and the router can detect rolling restarts.
|
|
22
|
+
7. Construct `http.createServer(handleHttpRequest)` and `loadSocket(httpServer, { maxHttpBufferSize })`.
|
|
23
|
+
8. Return `{ httpServer, ioServer, listen }`. The HTTP server has NOT started listening yet; the caller invokes `listen()` to bind.
|
|
24
|
+
|
|
25
|
+
## API Reference
|
|
26
|
+
|
|
27
|
+
### `createLuckyStackServer(options?: CreateLuckyStackServerOptions): Promise<RunningLuckyStackServer>`
|
|
28
|
+
|
|
29
|
+
**Signature:**
|
|
30
|
+
|
|
31
|
+
```typescript
|
|
32
|
+
export const createLuckyStackServer = async (
|
|
33
|
+
options: CreateLuckyStackServerOptions = {}
|
|
34
|
+
): Promise<RunningLuckyStackServer>;
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
**Parameters:**
|
|
38
|
+
|
|
39
|
+
| Field | Type | Default | Purpose |
|
|
40
|
+
| --- | --- | --- | --- |
|
|
41
|
+
| `port` | `number \| string` | `getParsedPort()` -> `SERVER_PORT` -> `80` | HTTP listen port. String coerced via `parseInt(_, 10)`. |
|
|
42
|
+
| `ip` | `string` | `process.env.SERVER_IP ?? '127.0.0.1'` | Bind address. Registered with `registerBindAddress` so `checkOrigin` and other framework code see the resolved value. |
|
|
43
|
+
| `serveFile` | `StaticFileHandler` | none | Catch-all GET handler (Vite output, SPA `index.html`). Called for `/assets/*`, known static extensions, and the final SPA fallback. Without it the static fallback returns `404`. |
|
|
44
|
+
| `serveFavicon` | `FaviconHandler` | none | Handler for `/favicon.ico`. Without it the route returns `404`. |
|
|
45
|
+
| `customRoutes` | `CustomRouteHandler` | none | Inline custom-route hook. Composed after the registry handlers from `registerCustomRoute(...)`; first one to return `true` wins. |
|
|
46
|
+
| `enableDevTools` | `boolean` | `NODE_ENV !== 'production'` | Toggles `initConsolelog()` + devkit hot reload + REPL. Pass `false` to opt out. |
|
|
47
|
+
| `maxHttpBufferSize` | `number` | `5 * 1024 * 1024` | Forwarded to Socket.io. Raise for large payloads. |
|
|
48
|
+
| `requireDeployConfig` | `boolean` | `false` | When `true`, `verifyBootstrap` fails if no `DeployConfig` was registered. |
|
|
49
|
+
| `requireServicesConfig` | `boolean` | `false` | When `true`, `verifyBootstrap` fails if no `ServicesConfig` was registered (router topology). |
|
|
50
|
+
| `requireOAuthProviders` | `boolean` | `false` | When `true`, `verifyBootstrap` fails if only the default `credentials` provider is registered. |
|
|
51
|
+
| `loadGeneratedMaps` | `(preset: string) => Promise<unknown>` | none | Callback that resolves a generated runtime-maps module per preset. Triggers `registerProdRuntimeMapsProvider` internally. Required because dynamic-import resolution is module-scoped — the framework cannot resolve a relative path on the consumer's behalf. |
|
|
52
|
+
| `runtimeMapsPreset` | `string \| string[]` | argv -> `'default'` | Overrides the argv-derived preset list. |
|
|
53
|
+
|
|
54
|
+
**Returns:** `RunningLuckyStackServer`:
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
interface RunningLuckyStackServer {
|
|
58
|
+
httpServer: http.Server;
|
|
59
|
+
ioServer: socket.io.Server;
|
|
60
|
+
listen: (callback?: () => void) => Promise<http.Server>;
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
`listen()` resolves when the HTTP server is bound. Logs `Server is running on http://<ip>:<port>/` when `projectConfig.logging.socketStartup` or `projectConfig.logging.devLogs` is enabled. It is safe to call once; calling twice yields a Node `ERR_SERVER_ALREADY_LISTEN` from the underlying `httpServer.listen`.
|
|
65
|
+
|
|
66
|
+
**Behavior (execution order):**
|
|
67
|
+
|
|
68
|
+
- Auto-register prod runtime-maps provider if `loadGeneratedMaps` is set.
|
|
69
|
+
- Run `verifyBootstrap` (may throw).
|
|
70
|
+
- Resolve port + ip; call `registerBindAddress({ ip, port })`.
|
|
71
|
+
- If dev-tools enabled: load console initializer + devkit (`initializeAll`, `setupWatchers`); attach SIGINT / SIGTERM force-exit handlers.
|
|
72
|
+
- `await writeBootUuid()`.
|
|
73
|
+
- Create `http.Server` whose request handler is `handleHttpRequest(req, res, options)` (see `request-pipeline.md`).
|
|
74
|
+
- `loadSocket(httpServer, { maxHttpBufferSize })` attaches the Socket.io server.
|
|
75
|
+
- Return `{ httpServer, ioServer, listen }` without listening.
|
|
76
|
+
|
|
77
|
+
**Errors / Edge cases:**
|
|
78
|
+
|
|
79
|
+
- Throws (from `verifyBootstrap`) when a required registry is missing — single multi-line `Error`.
|
|
80
|
+
- Dev-only branch dynamically imports `@luckystack/devkit`; production bundles exclude it so the import is never reached when `NODE_ENV === 'production'`.
|
|
81
|
+
- `enableDevTools: true` in production loads devkit; this is supported but unusual.
|
|
82
|
+
- SIGINT / SIGTERM handlers are installed only when dev-tools branch runs.
|
|
83
|
+
- `writeBootUuid` failures bubble — boot will abort if Redis is unreachable at start.
|
|
84
|
+
|
|
85
|
+
**Example — direct boot (skip overlay):**
|
|
86
|
+
|
|
87
|
+
```typescript
|
|
88
|
+
import 'dotenv/config';
|
|
89
|
+
import './config';
|
|
90
|
+
import './deploy.config';
|
|
91
|
+
import { initializeSentry } from '@luckystack/error-tracking';
|
|
92
|
+
import { registerPresenceHooks } from '@luckystack/presence';
|
|
93
|
+
import { createLuckyStackServer } from '@luckystack/server';
|
|
94
|
+
import { serveFile, serveFavicon } from './prod/serveFile';
|
|
95
|
+
|
|
96
|
+
initializeSentry();
|
|
97
|
+
registerPresenceHooks();
|
|
98
|
+
|
|
99
|
+
const server = await createLuckyStackServer({
|
|
100
|
+
serveFile,
|
|
101
|
+
serveFavicon,
|
|
102
|
+
loadGeneratedMaps: (preset) => import(`./prod/generatedApis.${preset}`),
|
|
103
|
+
requireDeployConfig: true,
|
|
104
|
+
});
|
|
105
|
+
await server.listen();
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
### `bootstrapLuckyStack(options?: BootstrapLuckyStackOptions): Promise<RunningLuckyStackServer>`
|
|
111
|
+
|
|
112
|
+
**Signature:**
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
export const bootstrapLuckyStack = async (
|
|
116
|
+
options: BootstrapLuckyStackOptions = {}
|
|
117
|
+
): Promise<RunningLuckyStackServer>;
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
**Parameters:** extends `CreateLuckyStackServerOptions` with:
|
|
121
|
+
|
|
122
|
+
| Field | Type | Default | Purpose |
|
|
123
|
+
| --- | --- | --- | --- |
|
|
124
|
+
| `overlayRoot` | `string` | `'luckystack'` | Folder name (relative to `ROOT_DIR` from `@luckystack/core`) that holds the per-package overlay files. Absolute paths are honored as-is. |
|
|
125
|
+
| `skipOverlayLoad` | `boolean` | `false` | Skip auto-loading the overlay folder. Useful for tests or hand-built registries. |
|
|
126
|
+
|
|
127
|
+
**Returns:** identical to `createLuckyStackServer`.
|
|
128
|
+
|
|
129
|
+
**Behavior:**
|
|
130
|
+
|
|
131
|
+
- If `skipOverlayLoad !== true` and `<ROOT_DIR>/<overlayRoot>` exists, walk the canonical package order (`core`, `deploy`, `login`, `sentry`, `presence`, `docs-ui`, `server`).
|
|
132
|
+
- Inside each package folder: import `index.ts`/`index.js` first if present, then every remaining `*.ts` / `*.js` file in alphabetical order. Each file is responsible for its own side-effect registration.
|
|
133
|
+
- Then delegate to `createLuckyStackServer(options)`.
|
|
134
|
+
|
|
135
|
+
**Errors / Edge cases:**
|
|
136
|
+
|
|
137
|
+
- Missing `<overlayRoot>` folder is non-fatal — the function returns silently so projects can use the legacy single-file `config.ts` layout.
|
|
138
|
+
- Missing per-package subfolders (e.g. no `luckystack/docs-ui/`) are silently skipped.
|
|
139
|
+
- Import errors from overlay files bubble up unchanged; fix the failing module.
|
|
140
|
+
|
|
141
|
+
**Example — recommended scaffolded boot:**
|
|
142
|
+
|
|
143
|
+
```typescript
|
|
144
|
+
import '@luckystack/server/parseArgv';
|
|
145
|
+
import 'dotenv/config';
|
|
146
|
+
import './config';
|
|
147
|
+
import './deploy.config';
|
|
148
|
+
import { bootstrapLuckyStack, verifyBootstrap } from '@luckystack/server';
|
|
149
|
+
import { serveFile, serveFavicon } from './prod/serveFile';
|
|
150
|
+
|
|
151
|
+
const server = await bootstrapLuckyStack({
|
|
152
|
+
serveFile,
|
|
153
|
+
serveFavicon,
|
|
154
|
+
loadGeneratedMaps: (preset) => import(`./prod/generatedApis.${preset}`),
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
await verifyBootstrap({
|
|
158
|
+
requireDeployConfig: true,
|
|
159
|
+
requireOAuthProviders: true,
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
await server.listen();
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
### `verifyBootstrap(requirements?: BootstrapRequirements): Promise<void>`
|
|
168
|
+
|
|
169
|
+
**Signature:**
|
|
170
|
+
|
|
171
|
+
```typescript
|
|
172
|
+
export const verifyBootstrap = async (
|
|
173
|
+
requirements: BootstrapRequirements = {},
|
|
174
|
+
): Promise<void>;
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
**Parameters:**
|
|
178
|
+
|
|
179
|
+
| Field | Type | Default | Purpose |
|
|
180
|
+
| --- | --- | --- | --- |
|
|
181
|
+
| `requireDeployConfig` | `boolean` | `false` | Fail unless `registerDeployConfig` has been called. |
|
|
182
|
+
| `requireServicesConfig` | `boolean` | `false` | Fail unless `registerServicesConfig` has been called. |
|
|
183
|
+
| `requireOAuthProviders` | `boolean` | `false` | Fail unless an OAuth provider beyond the default `credentials` entry has been registered. |
|
|
184
|
+
|
|
185
|
+
**Returns:** `Promise<void>` — resolves silently when every required registry is in place.
|
|
186
|
+
|
|
187
|
+
**Behavior:**
|
|
188
|
+
|
|
189
|
+
- Builds a `missing[]` array. Checks in this order:
|
|
190
|
+
1. `isProjectConfigRegistered()` — always required.
|
|
191
|
+
2. `isDeployConfigRegistered()` — only when `requireDeployConfig`.
|
|
192
|
+
3. `isServicesConfigRegistered()` (lazy import) — only when `requireServicesConfig`.
|
|
193
|
+
4. `getOAuthProviders().length > 1` (lazy import from `@luckystack/login`) — only when `requireOAuthProviders`. Default registry length is `1` (`credentials`).
|
|
194
|
+
5. `isRuntimeMapsProviderRegistered()` — hard-fail in production, loud `getLogger().warn(...)` in dev/test.
|
|
195
|
+
6. `isLocalizedNormalizerRegistered()` — hard-fail in production, warn in dev/test.
|
|
196
|
+
- If `missing.length === 0`: return. Otherwise throw a single `Error` whose message lists every missing registration with a one-line remediation hint and points at `docs/ARCHITECTURE_PACKAGING.md`.
|
|
197
|
+
|
|
198
|
+
**Errors / Edge cases:**
|
|
199
|
+
|
|
200
|
+
- Throws synchronously after building the missing list. No partial-state recovery.
|
|
201
|
+
- The `RuntimeMapsProvider` warning in dev is intentional: devkit hot-reload normally registers one, so missing it during a bare boot is informational, not fatal.
|
|
202
|
+
- The `LocalizedNormalizer` warning in dev means error responses will surface the raw `errorCode` string instead of localized copy.
|
|
203
|
+
|
|
204
|
+
**Example:**
|
|
205
|
+
|
|
206
|
+
```typescript
|
|
207
|
+
import { verifyBootstrap } from '@luckystack/server';
|
|
208
|
+
|
|
209
|
+
await verifyBootstrap({
|
|
210
|
+
requireDeployConfig: true,
|
|
211
|
+
requireServicesConfig: false,
|
|
212
|
+
requireOAuthProviders: true,
|
|
213
|
+
});
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
## Hook payload types
|
|
217
|
+
|
|
218
|
+
Module augmentation in `hookPayloads.ts` extends `@luckystack/core`'s `HookPayloads` interface, so `dispatchHook` / `registerHook` accept these names with the correct payload typing.
|
|
219
|
+
|
|
220
|
+
| Hook name | Payload type | When it fires |
|
|
221
|
+
| --- | --- | --- |
|
|
222
|
+
| `onSocketConnect` | `OnSocketConnectPayload` (`{ socketId, token, ip }`) | Notification: every successful Socket.io connection. |
|
|
223
|
+
| `onSocketDisconnect` | `OnSocketDisconnectPayload` (`{ socketId, token, reason }`) | Notification: every Socket.io disconnect (with reason string). |
|
|
224
|
+
| `preRoomJoin` | `PreRoomJoinPayload` (`{ token, room }`) | Before the user joins a room. May stop with `HookStopSignal`. |
|
|
225
|
+
| `postRoomJoin` | `PostRoomJoinPayload` (`{ token, room, allRooms }`) | After a successful join. |
|
|
226
|
+
| `preRoomLeave` | `PreRoomLeavePayload` (`{ token, room }`) | Before the user leaves a room. May stop. |
|
|
227
|
+
| `postRoomLeave` | `PostRoomLeavePayload` (`{ token, room, allRooms }`) | After a successful leave. |
|
|
228
|
+
| `onLocationUpdate` | `OnLocationUpdatePayload` (`{ token, oldLocation?, newLocation }`) | When the presence layer reports a location change. |
|
|
229
|
+
|
|
230
|
+
Naming convention (also documented in `hookPayloads.ts`):
|
|
231
|
+
|
|
232
|
+
- `on*` — pure notifications; return values are ignored.
|
|
233
|
+
- `pre*` — may return a `HookStopSignal` (`{ errorCode: string; httpStatus?: number }`) to abort the main flow.
|
|
234
|
+
- `post*` — fire after the side-effect succeeds.
|
|
235
|
+
|
|
236
|
+
## Types
|
|
237
|
+
|
|
238
|
+
```typescript
|
|
239
|
+
export interface CreateLuckyStackServerOptions { /* see table above */ }
|
|
240
|
+
|
|
241
|
+
export interface BootstrapLuckyStackOptions extends CreateLuckyStackServerOptions {
|
|
242
|
+
overlayRoot?: string;
|
|
243
|
+
skipOverlayLoad?: boolean;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export interface BootstrapRequirements {
|
|
247
|
+
requireDeployConfig?: boolean;
|
|
248
|
+
requireServicesConfig?: boolean;
|
|
249
|
+
requireOAuthProviders?: boolean;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export interface RunningLuckyStackServer {
|
|
253
|
+
httpServer: http.Server;
|
|
254
|
+
ioServer: socket.io.Server;
|
|
255
|
+
listen: (callback?: () => void) => Promise<http.Server>;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export interface RouteContext {
|
|
259
|
+
routePath: string;
|
|
260
|
+
method: string;
|
|
261
|
+
queryString: string | undefined;
|
|
262
|
+
token: string | null;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export type StaticFileHandler = (req: IncomingMessage, res: ServerResponse) => unknown;
|
|
266
|
+
export type FaviconHandler = (res: ServerResponse) => unknown;
|
|
267
|
+
export type CustomRouteHandler = (
|
|
268
|
+
req: IncomingMessage,
|
|
269
|
+
res: ServerResponse,
|
|
270
|
+
ctx: RouteContext,
|
|
271
|
+
) => Promise<boolean> | boolean;
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
## Config keys consumed at boot
|
|
275
|
+
|
|
276
|
+
| Source | Key | Effect |
|
|
277
|
+
| --- | --- | --- |
|
|
278
|
+
| env | `SERVER_PORT` | Port fallback when neither `options.port` nor argv supplies one. |
|
|
279
|
+
| env | `SERVER_IP` | IP fallback. |
|
|
280
|
+
| env | `NODE_ENV` | Switches `enableDevTools` default, dynamic-import branch for devkit, and `verifyBootstrap` warn-vs-throw for runtime-maps / localized normalizer. |
|
|
281
|
+
| env | `SECURE` | When `'true'`, session cookies are emitted with the `Secure;` flag (see `request-pipeline.md`). |
|
|
282
|
+
| config | `projectConfig.logging.socketStartup` / `projectConfig.logging.devLogs` | Gates the "Server is running on ..." log line emitted by `listen()`. |
|
|
283
|
+
| config | `projectConfig.http.*` | Route paths + cookie + CORS + stream settings consumed at request time (not at boot). |
|
|
284
|
+
| argv | `<bundles> [port]` | Parsed by `applyServerArgv()` and surfaced via `getParsedBundles()` / `getParsedPort()`. |
|
|
285
|
+
|
|
286
|
+
## Related
|
|
287
|
+
|
|
288
|
+
- Function INDEX: `packages/server/CLAUDE.md`
|
|
289
|
+
- README (consumer quickstart): `packages/server/README.md`
|
|
290
|
+
- Argv parsing: `packages/server/docs/argv-parsing.md`
|
|
291
|
+
- Runtime maps: `packages/server/docs/runtime-maps.md`
|
|
292
|
+
- Request pipeline: `packages/server/docs/request-pipeline.md`
|
|
293
|
+
- HTTP routes: `packages/server/docs/http-routes.md`
|
|
294
|
+
- Security: `packages/server/docs/security-defaults.md`
|
|
295
|
+
- Architecture: `docs/ARCHITECTURE_PACKAGING.md`, `docs/ARCHITECTURE_SOCKET.md`, `docs/HOSTING.md`
|