@luckystack/server 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.
@@ -0,0 +1,256 @@
1
+ # Argv Parsing (`parseServerArgv` + `applyServerArgv`)
2
+
3
+ > Deep specs. Bron: `packages/server/src/argv.ts`, `packages/server/src/parseArgv.ts`. Bijgewerkt: 2026-05-20.
4
+
5
+ ## Overview
6
+
7
+ `@luckystack/server` accepts two positional CLI arguments on boot:
8
+
9
+ ```
10
+ npm run server -- <bundle[,bundle...]> [port]
11
+ ```
12
+
13
+ - Arg 0 — preset list. Comma-separated; duplicates collapsed; runtime maps from each preset are shallow-merged at boot.
14
+ - Arg 1 — listen port. Numeric. Optional.
15
+
16
+ Argv replaces the legacy `LUCKYSTACK_BUNDLE` + `SERVER_PORT` environment toggles with one shape consumed by `createProdRuntimeMapsProvider` (preset) and `createLuckyStackServer` (port).
17
+
18
+ The module exposes:
19
+
20
+ - A pure parser: `parseServerArgv(argv)`.
21
+ - A side-effect runner that reads `process.argv.slice(2)` once and caches: `applyServerArgv()`.
22
+ - Read accessors: `getParsedBundles()`, `getParsedPort()`.
23
+ - A side-effect-only entrypoint `@luckystack/server/parseArgv` that simply imports `applyServerArgv` and runs it.
24
+
25
+ The side-effect entry MUST be the FIRST import in the consumer's `server.ts` because the parsed port is written back to `process.env.SERVER_PORT`, and downstream modules read that variable at top-level evaluation time:
26
+
27
+ - `@luckystack/core` env Zod schema
28
+ - `@luckystack/core` `bindAddress.ts` fallback
29
+ - The consumer's `config.ts` `backendUrl` constant
30
+ - `@luckystack/login` `oauthProviders.ts` callback URL builder
31
+
32
+ Importing anything that pulls one of those four before `parseArgv` runs will lock in the wrong port.
33
+
34
+ ## API Reference
35
+
36
+ ### `parseServerArgv(argv: string[]): ParsedServerArgv`
37
+
38
+ **Signature:**
39
+
40
+ ```typescript
41
+ export interface ParsedServerArgv {
42
+ bundles: string[];
43
+ port: number | null;
44
+ }
45
+
46
+ export const parseServerArgv = (argv: string[]): ParsedServerArgv;
47
+ ```
48
+
49
+ **Parameters:**
50
+
51
+ | Field | Type | Purpose |
52
+ | --- | --- | --- |
53
+ | `argv` | `string[]` | Positional args (typically `process.argv.slice(2)`). |
54
+
55
+ **Returns:** `{ bundles, port }`:
56
+
57
+ - `bundles: string[]` — deduplicated, trimmed, non-empty entries from arg 0. Empty array when arg 0 is missing or empty.
58
+ - `port: number | null` — `parseInt(argv[1], 10)` when arg 1 is supplied; `null` otherwise.
59
+
60
+ **Behavior:**
61
+
62
+ - Reject more than 2 positional arguments by throwing `Error('[luckystack:argv] unexpected positional argument(s): "<rest>". Usage: npm run server -- <bundle[,bundle...]> [port]')`.
63
+ - For arg 0: split on `,`, `trim()` each piece, drop falsy, collapse via `Array.from(new Set(...))`.
64
+ - For arg 1: must match `/^\d+$/`. Otherwise throws `Error('[luckystack:argv] port argument must be numeric, got: "<value>". Usage: npm run server -- <bundle[,bundle...]> [port]')`.
65
+
66
+ **Errors / Edge cases:**
67
+
68
+ - Whitespace in arg 0 (`"billing, vehicles"`) is supported — trimmed.
69
+ - A trailing comma (`"billing,"`) is silently dropped.
70
+ - A leading `0` in the port string is accepted (`/^\d+$/`) and parsed normally.
71
+ - An empty arg 0 (`""`) yields `bundles: []`; the downstream resolver falls back to `['default']`.
72
+
73
+ **Example:**
74
+
75
+ ```typescript
76
+ parseServerArgv(['billing,vehicles', '4001']);
77
+ // => { bundles: ['billing', 'vehicles'], port: 4001 }
78
+
79
+ parseServerArgv([]);
80
+ // => { bundles: [], port: null }
81
+
82
+ parseServerArgv(['billing', '4001', 'oops']);
83
+ // => throws (too many positionals)
84
+
85
+ parseServerArgv(['billing', 'PORT']);
86
+ // => throws (non-numeric port)
87
+ ```
88
+
89
+ ---
90
+
91
+ ### `applyServerArgv(): void`
92
+
93
+ **Signature:**
94
+
95
+ ```typescript
96
+ export const applyServerArgv = (): void;
97
+ ```
98
+
99
+ **Parameters:** none. Reads `process.argv.slice(2)`.
100
+
101
+ **Returns:** `void`.
102
+
103
+ **Behavior:**
104
+
105
+ - Idempotent. Subsequent calls return immediately via the module-level `hasRun` latch.
106
+ - First call:
107
+ 1. `parseServerArgv(process.argv.slice(2))` (throws on malformed input).
108
+ 2. Caches `bundles` + `port` in module state.
109
+ 3. When `port !== null`, writes `process.env.SERVER_PORT = String(port)`. This is the writeback that lets the four downstream env-readers (listed in Overview) see the resolved port without per-call refactoring.
110
+
111
+ **Errors / Edge cases:**
112
+
113
+ - Throwing during this call aborts boot before any other module has a chance to read `SERVER_PORT`.
114
+ - Calling `applyServerArgv` after another module has already read `SERVER_PORT` is too late — that consumer has already captured the old value.
115
+
116
+ **Example:**
117
+
118
+ ```typescript
119
+ // server.ts — first line
120
+ import '@luckystack/server/parseArgv';
121
+ // rest of bootstrap...
122
+ ```
123
+
124
+ Or call it explicitly when you control the boot timing:
125
+
126
+ ```typescript
127
+ import { applyServerArgv } from '@luckystack/server';
128
+ applyServerArgv();
129
+ ```
130
+
131
+ ---
132
+
133
+ ### `getParsedBundles(): string[]`
134
+
135
+ **Signature:**
136
+
137
+ ```typescript
138
+ export const getParsedBundles = (): string[];
139
+ ```
140
+
141
+ **Returns:** the cached `bundles` array. Empty until `applyServerArgv()` has run.
142
+
143
+ **Behavior:**
144
+
145
+ - Read-only; never throws.
146
+ - Used by `createProdRuntimeMapsProvider` to resolve which preset(s) to load when neither `options.preset` nor a literal string is supplied.
147
+
148
+ **Example:**
149
+
150
+ ```typescript
151
+ import { applyServerArgv, getParsedBundles } from '@luckystack/server';
152
+
153
+ applyServerArgv();
154
+ console.log(getParsedBundles()); // e.g. ['billing', 'vehicles']
155
+ ```
156
+
157
+ ---
158
+
159
+ ### `getParsedPort(): number | null`
160
+
161
+ **Signature:**
162
+
163
+ ```typescript
164
+ export const getParsedPort = (): number | null;
165
+ ```
166
+
167
+ **Returns:** the cached `port`. `null` until `applyServerArgv()` has run with a numeric arg 1.
168
+
169
+ **Behavior:**
170
+
171
+ - Read-only; never throws.
172
+ - Consumed by `createLuckyStackServer` as one of the port-resolution fallbacks:
173
+ 1. `options.port`
174
+ 2. `getParsedPort()`
175
+ 3. `process.env.SERVER_PORT`
176
+ 4. `80`
177
+
178
+ **Example:**
179
+
180
+ ```typescript
181
+ import { getParsedPort } from '@luckystack/server';
182
+
183
+ const port = getParsedPort();
184
+ if (port !== null) {
185
+ console.log(`argv supplied port ${port}`);
186
+ }
187
+ ```
188
+
189
+ ---
190
+
191
+ ### Side-effect entrypoint: `@luckystack/server/parseArgv`
192
+
193
+ **Module body (verbatim):**
194
+
195
+ ```typescript
196
+ import { applyServerArgv } from './argv';
197
+
198
+ applyServerArgv();
199
+ ```
200
+
201
+ **Usage:** import as the FIRST line of your `server.ts`:
202
+
203
+ ```typescript
204
+ import '@luckystack/server/parseArgv';
205
+ ```
206
+
207
+ Anything that depends on `process.env.SERVER_PORT` MUST be imported below this line. Common pitfalls:
208
+
209
+ - Importing `'./config'` (which evaluates `backendUrl` at top level) before `parseArgv`.
210
+ - Importing `@luckystack/core` modules that pull the env Zod schema before `parseArgv`.
211
+
212
+ Both will freeze the wrong port into module-level constants.
213
+
214
+ ## Resolution order summary
215
+
216
+ | Consumer | Source of port |
217
+ | --- | --- |
218
+ | `createLuckyStackServer` | `options.port` -> `getParsedPort()` -> `SERVER_PORT` -> `80` |
219
+ | `createLuckyStackServer` IP | `options.ip` -> `SERVER_IP` -> `127.0.0.1` |
220
+ | `createProdRuntimeMapsProvider` | `options.preset` (string -> single-entry array; non-empty array -> dedup) -> `getParsedBundles()` -> `['default']` |
221
+
222
+ ## CLI examples
223
+
224
+ ```bash
225
+ # Default preset, port 80
226
+ npm run server
227
+
228
+ # Single bundle, port 80
229
+ npm run server -- billing
230
+
231
+ # Two bundles merged, port 4001
232
+ npm run server -- billing,vehicles 4001
233
+
234
+ # Whitespace in bundle list is fine
235
+ npm run server -- "billing , vehicles" 4001
236
+
237
+ # Invalid port — boot aborts with descriptive error
238
+ npm run server -- billing PORT
239
+ # Error: [luckystack:argv] port argument must be numeric, got: "PORT".
240
+
241
+ # Extra positional — boot aborts
242
+ npm run server -- billing 4001 oops
243
+ # Error: [luckystack:argv] unexpected positional argument(s): "oops".
244
+ ```
245
+
246
+ ## Interaction with runtime maps
247
+
248
+ `createProdRuntimeMapsProvider({ loadGenerated, preset? })` calls `getParsedBundles()` when `preset` is omitted or empty. Each resolved preset is dynamically imported via the consumer-supplied `loadGenerated` callback, then shallow-merged into one runtime view. Key collisions across presets throw at boot. See `runtime-maps.md` for the merge semantics.
249
+
250
+ ## Related
251
+
252
+ - Function INDEX: `packages/server/CLAUDE.md`
253
+ - Runtime maps: `packages/server/docs/runtime-maps.md`
254
+ - Create server: `packages/server/docs/create-server.md`
255
+ - Architecture: `docs/ARCHITECTURE_PACKAGING.md` (preset bundles, multi-service builds)
256
+ - README: `packages/server/README.md`
@@ -0,0 +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`