create-qpq-app 0.1.19 → 0.1.21

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 (112) hide show
  1. package/lib/commonjs/steps/006_applyDomain.js +3 -8
  2. package/lib/commonjs/steps/006_applyDomain.js.map +1 -1
  3. package/lib/commonjs/steps/013_printNextSteps.js +1 -1
  4. package/lib/commonjs/steps/013_printNextSteps.js.map +1 -1
  5. package/lib/esm/steps/006_applyDomain.js +3 -8
  6. package/lib/esm/steps/006_applyDomain.js.map +1 -1
  7. package/lib/esm/steps/013_printNextSteps.js +1 -1
  8. package/lib/esm/steps/013_printNextSteps.js.map +1 -1
  9. package/package.json +2 -2
  10. package/template/README.md +6 -3
  11. package/template/apps/qpqjs/account.qpq.ts +5 -13
  12. package/template/apps/qpqjs/bootstrap.qpq.ts +29 -14
  13. package/template/apps/qpqjs/deploy.config.json +0 -1
  14. package/template/apps/qpqjs/packages/constants/src/domain.ts +3 -1
  15. package/template/apps/qpqjs/packages/service-utils/src/defineQpqjsService.ts +15 -9
  16. package/template/apps/qpqjs/services/design/views/src/components/LandingPage/components/Infrastructure.tsx +1 -2
  17. package/template/apps/qpqjs/services/shell/service/src/infrastructure.ts +1 -4
  18. package/template/apps/qpqjs/services/test/models/package.json +2 -1
  19. package/template/apps/qpqjs/services/test/models/src/EchoRequest.ts +8 -0
  20. package/template/apps/qpqjs/services/test/models/src/EchoResponse.ts +9 -0
  21. package/template/apps/qpqjs/services/test/models/src/SmokeRun.ts +11 -9
  22. package/template/apps/qpqjs/services/test/models/src/SmokeRunStarted.ts +8 -0
  23. package/template/apps/qpqjs/services/test/models/src/SmokeRunSummary.ts +10 -6
  24. package/template/apps/qpqjs/services/test/models/src/SmokeRunWithSummary.ts +9 -5
  25. package/template/apps/qpqjs/services/test/models/src/SmokeTestResult.ts +11 -9
  26. package/template/apps/qpqjs/services/test/models/src/index.ts +3 -0
  27. package/template/apps/qpqjs/services/test/service/src/echo/config/defineEcho.ts +9 -0
  28. package/template/apps/qpqjs/services/test/service/src/echo/controller/echo.ts +32 -0
  29. package/template/apps/qpqjs/services/test/service/src/echo/controller/index.ts +1 -0
  30. package/template/apps/qpqjs/services/test/service/src/infrastructure.ts +3 -0
  31. package/template/apps/qpqjs/services/test/service/src/smoke/config/defineSmoke.ts +83 -18
  32. package/template/apps/qpqjs/services/test/service/src/smoke/constants/index.ts +1 -0
  33. package/template/apps/qpqjs/services/test/service/src/smoke/constants/smokeEventDoc.ts +27 -0
  34. package/template/apps/qpqjs/services/test/service/src/smoke/constants/smokeRunQueue.ts +4 -4
  35. package/template/apps/qpqjs/services/test/service/src/smoke/controller/askRunSmokeTests.ts +5 -1
  36. package/template/apps/qpqjs/services/test/service/src/smoke/data/askRecordSmokeTestResult.ts +32 -0
  37. package/template/apps/qpqjs/services/test/service/src/smoke/data/askSetSmokeRunOutcome.ts +24 -0
  38. package/template/apps/qpqjs/services/test/service/src/smoke/data/index.ts +2 -0
  39. package/template/apps/qpqjs/services/test/service/src/smoke/eventDoc/index.ts +1 -0
  40. package/template/apps/qpqjs/services/test/service/src/smoke/eventDoc/smokeProbeDocDefinition.ts +74 -0
  41. package/template/apps/qpqjs/services/test/service/src/smoke/index.ts +1 -0
  42. package/template/apps/qpqjs/services/test/service/src/smoke/logic/smokeRun/askExecuteSmokeTest.ts +97 -0
  43. package/template/apps/qpqjs/services/test/service/src/smoke/logic/smokeRun/askStartSmokeRun.ts +18 -10
  44. package/template/apps/qpqjs/services/test/service/src/smoke/logic/smokeRun/index.ts +1 -1
  45. package/template/apps/qpqjs/services/test/service/src/smoke/models/SmokeEventDocAppendQueueEvent.ts +15 -0
  46. package/template/apps/qpqjs/services/test/service/src/smoke/models/SmokeEventDocMark.ts +8 -0
  47. package/template/apps/qpqjs/services/test/service/src/smoke/models/SmokeTestRequestedQueueEvent.ts +12 -0
  48. package/template/apps/qpqjs/services/test/service/src/smoke/models/index.ts +3 -1
  49. package/template/apps/qpqjs/services/test/service/src/smoke/queue/index.ts +2 -1
  50. package/template/apps/qpqjs/services/test/service/src/smoke/queue/onSmokeEventDocAppend.ts +64 -0
  51. package/template/apps/qpqjs/services/test/service/src/smoke/queue/onSmokeTestRequested.ts +17 -0
  52. package/template/apps/qpqjs/services/test/service/src/smoke/tests/eventDoc/askAssertContiguousEventIds.ts +18 -0
  53. package/template/apps/qpqjs/services/test/service/src/smoke/tests/eventDoc/askAssertSmokeProbeDocState.ts +32 -0
  54. package/template/apps/qpqjs/services/test/service/src/smoke/tests/eventDoc/askAwaitSmokeEventDocLog.ts +35 -0
  55. package/template/apps/qpqjs/services/test/service/src/smoke/tests/eventDoc/askRunEventDocConcurrentAppendTest.ts +82 -0
  56. package/template/apps/qpqjs/services/test/service/src/smoke/tests/eventDoc/askRunEventDocInterleavedBatchAppendTest.ts +110 -0
  57. package/template/apps/qpqjs/services/test/service/src/smoke/tests/eventDoc/index.ts +5 -0
  58. package/template/apps/qpqjs/services/test/service/src/smoke/tests/index.ts +2 -0
  59. package/template/apps/qpqjs/services/test/service/src/smoke/tests/openApi/askRunEchoRoundTripTest.ts +45 -0
  60. package/template/apps/qpqjs/services/test/service/src/smoke/tests/openApi/askRunOpenApiDocumentTest.ts +42 -0
  61. package/template/apps/qpqjs/services/test/service/src/smoke/tests/openApi/index.ts +2 -0
  62. package/template/apps/qpqjs/services/test/service/src/smoke/tests/smokeTestRegistry.ts +18 -2
  63. package/template/apps/todo/deploy.config.json +0 -1
  64. package/template/apps/todo/packages/constants/src/domain.ts +2 -1
  65. package/template/apps/todo/packages/service-utils/src/defineTodoService.ts +5 -6
  66. package/template/apps/todo/services/shell/service/src/infrastructure.ts +1 -3
  67. package/template/docusaurus/docs/actions/core/key-value-store/ask-key-value-store-upsert-with-retry.md +2 -2
  68. package/template/docusaurus/docs/actions/core/key-value-store/ask-key-value-store-upsert.md +2 -1
  69. package/template/docusaurus/docs/actions/features/event-doc/ask-event-doc-append-server-event.md +1 -1
  70. package/template/docusaurus/docs/actions/features/event-doc/ask-event-doc-create.md +1 -1
  71. package/template/docusaurus/docs/actions/features/event-doc/ask-event-doc-event-append.md +18 -12
  72. package/template/docusaurus/docs/actions/features/event-doc/ask-event-doc-event-list.md +4 -4
  73. package/template/docusaurus/docs/actions/features/event-doc/ask-event-doc-event-write.md +6 -6
  74. package/template/docusaurus/docs/actions/features/event-doc/ask-event-doc-get-by-id.md +2 -2
  75. package/template/docusaurus/docs/actions/features/event-doc/ask-event-doc-get-draft.md +2 -2
  76. package/template/docusaurus/docs/actions/webserver/dns/ask-dns-list.md +9 -8
  77. package/template/docusaurus/docs/actions/webserver/dns/ask-dns-resolve-hosts.md +42 -0
  78. package/template/docusaurus/docs/actions/webserver/open-api/_category_.json +1 -0
  79. package/template/docusaurus/docs/actions/webserver/open-api/ask-open-api-get-document.md +41 -0
  80. package/template/docusaurus/docs/config/config-aws/account-github-oidc-provider.md +31 -0
  81. package/template/docusaurus/docs/config/config-aws/domain-certificate.md +16 -32
  82. package/template/docusaurus/docs/config/config-aws/email-sender-allow-list.md +7 -14
  83. package/template/docusaurus/docs/config/config-aws/github-deploy-role.md +68 -0
  84. package/template/docusaurus/docs/config/core/user-directory.md +2 -3
  85. package/template/docusaurus/docs/config/features/admin-settings.md +2 -6
  86. package/template/docusaurus/docs/config/features/event-doc-summary.md +2 -2
  87. package/template/docusaurus/docs/config/features/open-api-routes.md +82 -0
  88. package/template/docusaurus/docs/config/features/tenanted-web-socket-queue.md +2 -3
  89. package/template/docusaurus/docs/config/features/web-socket-queue.md +1 -5
  90. package/template/docusaurus/docs/config/webserver/api.md +6 -9
  91. package/template/docusaurus/docs/config/webserver/cache.md +1 -1
  92. package/template/docusaurus/docs/config/webserver/dns.md +25 -17
  93. package/template/docusaurus/docs/config/webserver/domain-proxy.md +1 -5
  94. package/template/docusaurus/docs/config/webserver/email-sender.md +9 -15
  95. package/template/docusaurus/docs/config/webserver/route.md +1 -0
  96. package/template/docusaurus/docs/config/webserver/seo.md +1 -1
  97. package/template/docusaurus/docs/config/webserver/web-entry.md +0 -5
  98. package/template/docusaurus/docs/config/webserver/websocket.md +3 -7
  99. package/template/docusaurus/docs/domains.md +76 -0
  100. package/template/docusaurus/docs/use-cases.md +0 -6
  101. package/template/docusaurus/docusaurus.config.ts +38 -14
  102. package/template/docusaurus/sidebars.ts +5 -5
  103. package/template/docusaurus/src/components/HomepageFeatures/index.tsx +2 -2
  104. package/template/scripts/deployedSmoke.mjs +82 -23
  105. package/template/scripts/smoke/runEdgeChecks.mjs +152 -0
  106. package/template/scripts/smoke/runSmokeRun.mjs +1 -1
  107. package/template/apps/qpqjs/services/test/service/src/smoke/logic/smokeRun/askExecuteSmokeRun.ts +0 -83
  108. package/template/apps/qpqjs/services/test/service/src/smoke/models/SmokeRunRequestedQueueEvent.ts +0 -9
  109. package/template/apps/qpqjs/services/test/service/src/smoke/queue/onSmokeRunRequested.ts +0 -15
  110. package/template/docusaurus/docs/actions/webserver/open-api-spec/_category_.json +0 -1
  111. package/template/docusaurus/docs/actions/webserver/open-api-spec/ask-get-open-api-spec.md +0 -43
  112. package/template/docusaurus/docs/config/webserver/open-api.md +0 -36
@@ -95,11 +95,10 @@ Each handler is a `QpqFunctionRuntime` — a reference to a story entry point, u
95
95
  ```typescript
96
96
  export type AuthDirectoryDnsRecord = {
97
97
  subdomain: string;
98
- rootDomain: string;
99
98
  };
100
99
  ```
101
100
 
102
- Attaches a Cognito custom domain (`subdomain.rootDomain`) to the pool and creates the matching Route 53 alias record. Because Cognito custom domains run on CloudFront, the ACM certificate is looked up in `us-east-1`.
101
+ Attaches a Cognito custom domain to the pool and creates the matching Route 53 alias record. A pool has one custom domain, so it lives on the **primary** root from [defineDns](../webserver/dns.md) (the `{ subdomain }` target under the app's domain resolver). Because Cognito custom domains run on CloudFront, the ACM certificate is looked up in `us-east-1`.
103
102
 
104
103
  ## Custom auth runtime
105
104
 
@@ -167,7 +166,7 @@ export default [
167
166
  // Required MFA served from a custom domain
168
167
  defineUserDirectory('staff', {
169
168
  mfa: { mode: UserDirectoryMfaMode.required },
170
- dnsRecord: { subdomain: 'auth', rootDomain: 'example.com' },
169
+ dnsRecord: { subdomain: 'auth' },
171
170
  }),
172
171
 
173
172
  // Authenticate against a directory owned by another service
@@ -20,7 +20,7 @@ export default [
20
20
  ...defineAdminUserDirectory({ owner: { module: 'log' } }),
21
21
 
22
22
  // The admin dashboard backend, owned by the 'log' service
23
- ...defineAdminSettings('log', 'example.com', {
23
+ ...defineAdminSettings('log', {
24
24
  logRetentionDays: 90,
25
25
  services: ['api', 'workers', 'web'],
26
26
  }),
@@ -32,7 +32,6 @@ export default [
32
32
  ```typescript
33
33
  function defineAdminSettings(
34
34
  logServiceName: string,
35
- rootDomain: string,
36
35
  advancedSettings?: QPQConfigAdvancedLogSettings,
37
36
  ): QPQConfig;
38
37
  ```
@@ -43,9 +42,6 @@ function defineAdminSettings(
43
42
 
44
43
  The name of the service that **owns** the admin resources. Owner-stamped settings (the log storage drives, the admin WebSocket event bus/queues, the key-value stores, the auth and log routes, the alarm pipeline, and the [session event doc](./admin-session-event-doc.md)) only flatten into deployable resources when the deploying service matches this name; elsewhere they resolve as foreign references. Owner resolution runs at IAM-scoping time.
45
44
 
46
- ### `rootDomain` — `string` (required)
47
-
48
- The root domain the admin service is hosted on. Used to stand up the admin WebSocket endpoint (`defineWebSocketQueue`) under the `qpqadmin` subdomain of this domain.
49
45
 
50
46
  ### `advancedSettings` — `QPQConfigAdvancedLogSettings` (optional)
51
47
 
@@ -90,7 +86,7 @@ export default [
90
86
  ...defineAdminUserDirectory({ owner: { module: 'log' } }),
91
87
 
92
88
  // Retain logs 30 days, move to deep cold storage after 7.
93
- ...defineAdminSettings('log', 'example.com', {
89
+ ...defineAdminSettings('log', {
94
90
  logRetentionDays: 30,
95
91
  coldStorageAfterDays: 7,
96
92
  services: ['api', 'workers'],
@@ -8,8 +8,8 @@ description: Declare the stores for an event-sourced document collection — its
8
8
  Declares the **stores** that back an event-document collection, without any routes. It returns a `QPQConfig` (an array of config settings) that expands to five underlying core stores:
9
9
 
10
10
  1. A **summary key-value store** (partition key `type`, sort key `id`) — the queryable record for each document, derived by folding the identity/lifecycle events of its log. Rows are the [`EventDocSummary`](../../actions/features/event-doc/ask-event-doc-get-by-id.md) shape and carry the version history. A secondary index on `(type, updatedAt)` supports the recently-updated ordering [askEventDocList](../../actions/features/event-doc/ask-event-doc-list.md) returns. It is a pure projection: nothing on the append path writes it directly. The events store below declares an [`onStream`](../core/key-value-store.md#change-data-capture-onstream) handler that rebuilds a document's summary row from its log whenever an event is appended, so the summary is eventually (not immediately) consistent with the log.
11
- 2. An **append-only events store** (`<storeName>EventLog`, partition key `pk`, string sort key `sk`) — the live ordered log every document is folded from, keyed on a sortable event id (UUIDv7). It has **no** secondary index on purpose: the local dev-server query processor can't target one, so all event reads go through the main table.
12
- 3. A **snapshots store** (`<storeName>SS`, partition key `pk`, sort key `sk`) — per-view folded states at points along the log (`pk = docId#view`, `sk` the same sortable event id the log is ordered by). Populated only for the document types you enable via `options.snapshotFunctions`; a type with no entry there simply is not snapshotted. For a type with a registered functions object, the `onStream` handler runs ONE incremental fold per delivery — resuming from the newest usable snapshot and folding only the gap since it — and persists both the snapshot set and the summary row from that same fold (the fold's `summary` view IS the queryable record). The whole-log summary re-derivation remains only as the fallback: types with no registered functions, Remove stream records (a transfer rewrote the log, so snapshots can't seed), and folds that decline.
11
+ 2. An **append-only events store** (`<storeName>EventLog`, partition key `pk`, numeric sort key `sk`) — the live ordered log every document is folded from, keyed on the event's contiguous position (`INIT_STATE` is `0`, every append is head + 1, claimed by a conditional write with retry under contention). Log order IS commit order. It has **no** secondary index on purpose: the local dev-server query processor can't target one, so all event reads go through the main table.
12
+ 3. A **snapshots store** (`<storeName>SS`, partition key `pk`, sort key `sk`) — per-view folded states at points along the log (`pk = docId#view`, `sk` the same contiguous event id the log is ordered by). Populated only for the document types you enable via `options.snapshotFunctions`; a type with no entry there simply is not snapshotted. For a type with a registered functions object, the `onStream` handler runs ONE incremental fold per delivery — resuming from the newest usable snapshot and folding only the gap since it — and persists both the snapshot set and the summary row from that same fold (the fold's `summary` view IS the queryable record). The whole-log summary re-derivation remains only as the fallback: types with no registered functions, Remove stream records (a transfer rewrote the log, so snapshots can't seed), and folds that decline.
13
13
  4. A **legacy events store** (`<storeName>Events`, partition key `pk`, numeric sort key `sk`) — the pre-sortable-id log, kept declared but unread/unwritten at runtime so its data stays reachable until it is migrated into the events store above.
14
14
  5. A **storage drive** (`<storeName>edocs`, lower-cased) — the collection's blob bucket, holding each document's immutable uploaded assets (and later its derived runtime artifacts) under per-document prefixes.
15
15
 
@@ -0,0 +1,82 @@
1
+ ---
2
+ title: defineOpenApiRoutes
3
+ description: Serve a generated OpenAPI document and an interactive reference page for the service's routes.
4
+ ---
5
+
6
+ # defineOpenApiRoutes
7
+
8
+ Mounts two routes: an interactive API reference page and the OpenAPI 3.1 document it reads. Nothing is written by hand. The document is generated from the service's route config, so every `defineRoute` shows up in it, and routes that declare a schema show their request and response shapes too.
9
+
10
+ - **On AWS:** two ordinary routes on the API Lambda. The document is built on request from the config already bundled with the service, so there is no extra infrastructure and nothing to keep in sync.
11
+
12
+ ```typescript
13
+ import { defineOpenApiRoutes } from 'quidproquo-features';
14
+
15
+ export default [
16
+ // GET /v1/docs (reference page) and GET /v1/docs/openapi.json (document)
17
+ defineOpenApiRoutes({ info: { title: 'Widgets API', version: '1.2.0' } }),
18
+ ];
19
+ ```
20
+
21
+ The reference page loads [Scalar](https://scalar.com) from a CDN and points it at the document next to it. Browsers need to reach `cdn.jsdelivr.net` for the page to render; the document itself has no such dependency.
22
+
23
+ ## Signature
24
+
25
+ ```typescript
26
+ function defineOpenApiRoutes(options?: OpenApiRoutesOptions): QPQConfig;
27
+ ```
28
+
29
+ ## Parameters
30
+
31
+ ### `options` — `OpenApiRoutesOptions` (optional)
32
+
33
+ | Property | Type | Default | Description |
34
+ | --- | --- | --- | --- |
35
+ | `basePath` | `string` | `'/docs'` | Where the reference page is served. The document is always at `${basePath}/openapi.json`. |
36
+ | `version` | `number` | `1` | Version prefix, as with [defineVersionedRoute](./versioned-route.md). |
37
+ | `info` | `OpenApiDocumentOptions` | – | `title`, `version` and `description` for the document's info block. The title defaults to the application and module name. |
38
+ | `routeAuthSettings` | `RouteAuthSettings` | – | Gate the docs behind a user directory or api key. Public when omitted. |
39
+
40
+ ## Describing a route
41
+
42
+ Any route can carry a `schema` in its options: `bodyJsonSchema`, `queryJsonSchema` and `responseJsonSchema` as plain JSON Schema (from zod, `z.toJSONSchema(MySchema)`), plus `summary`, `description` and `tags`. Set `hidden: true` to keep a route out of the document.
43
+
44
+ Routes written with `dynamicRoute` (or a `createRouteDefinition` family) take zod schemas instead and get validation for free. The body and query are parsed before the handler runs, a mismatch becomes a 422, and the parsed values arrive typed as the handler's third argument.
45
+
46
+ ```typescript
47
+ import { z } from 'zod/v4';
48
+ import { dynamicRoute } from 'quidproquo-features';
49
+
50
+ export const create = dynamicRoute(
51
+ ['POST', '/widgets'],
52
+ function* (event, params, { body }) {
53
+ // body is { name: string }
54
+ return qpqWebServerUtils.toJsonEventResponse({ id: 'w1', name: body.name });
55
+ },
56
+ {
57
+ schema: {
58
+ summary: 'Create a widget',
59
+ tags: ['widgets'],
60
+ body: z.object({ name: z.string().min(1) }),
61
+ response: z.object({ id: z.string(), name: z.string() }),
62
+ },
63
+ knownErrors: { [WidgetErrorTypeEnum.Duplicate]: 409 },
64
+ },
65
+ );
66
+ ```
67
+
68
+ ## What the document contains
69
+
70
+ - One operation per route, keyed by path and method, with a stable `operationId` derived from both.
71
+ - Path parameters read from the `{param}` segments of the path.
72
+ - One query parameter per property of the query schema.
73
+ - A JSON request body when a body schema is declared, and a typed 200 response when a response schema is.
74
+ - A 422 response on routes that validate input, and a 401 on routes with auth settings.
75
+ - Security requirements from the route's merged auth settings: bearer JWT for a user directory, `x-api-key` for api keys. Only the schemes in use are published.
76
+ - A server entry per non-deprecated [defineApi](../webserver/api.md) when the service has a domain.
77
+
78
+ ## Related
79
+
80
+ - [askOpenApiGetDocument](../../actions/webserver/open-api/ask-open-api-get-document.md) — the action the document route yields; use it to serve the document somewhere else.
81
+ - [defineRoute](../webserver/route.md) — the `schema` option on plain routes.
82
+ - [defineVersionedRoute](./versioned-route.md) — the version prefix these routes sit under.
@@ -19,7 +19,7 @@ export default [
19
19
  routeAuthSettings: { userDirectoryName: 'users' },
20
20
  }),
21
21
 
22
- defineTenantedWebSocketQueue('my-event-bus', 'api', 'example.com', {
22
+ defineTenantedWebSocketQueue('my-event-bus', 'api', {
23
23
  userDirectoryName: 'users',
24
24
  }),
25
25
  ];
@@ -31,7 +31,6 @@ export default [
31
31
  function defineTenantedWebSocketQueue(
32
32
  eventBusName: string,
33
33
  apiName: string,
34
- rootDomain: string,
35
34
  advancedSettings?: QPQConfigAdvancedTenantedWebsocketQueueSettings,
36
35
  ): QPQConfig;
37
36
  ```
@@ -40,7 +39,7 @@ function defineTenantedWebSocketQueue(
40
39
 
41
40
  ## Parameters
42
41
 
43
- Same as [defineWebSocketQueue](./web-socket-queue.md#parameters): `eventBusName`, `apiName`, `rootDomain`, and `advancedSettings` (without `connectionScopeResolver`, which this always sets to `TENANT_CONNECTION_SCOPE_RESOLVER_FN`).
42
+ Same as [defineWebSocketQueue](./web-socket-queue.md#parameters): `eventBusName`, `apiName`, and `advancedSettings` (without `connectionScopeResolver`, which this always sets to `TENANT_CONNECTION_SCOPE_RESOLVER_FN`).
44
43
 
45
44
  ## Returns
46
45
 
@@ -16,7 +16,7 @@ Defines a **WebSocket queue**: a batteries-included messaging layer on top of a
16
16
  import { defineWebSocketQueue } from 'quidproquo-features';
17
17
 
18
18
  export default [
19
- defineWebSocketQueue('my-event-bus', 'api', 'example.com', {
19
+ defineWebSocketQueue('my-event-bus', 'api', {
20
20
  userDirectoryName: 'users',
21
21
  }),
22
22
  ];
@@ -40,7 +40,6 @@ Because the underlying WebSocket API is created with `apiName` equal to the `api
40
40
  function defineWebSocketQueue(
41
41
  eventBusName: string,
42
42
  apiName: string,
43
- rootDomain: string,
44
43
  advancedSettings?: QPQConfigAdvancedWebsocketQueueSettings,
45
44
  ): QPQConfig;
46
45
  ```
@@ -57,9 +56,6 @@ Name of the [event bus](../core/event-bus.md) the queue is associated with (stor
57
56
 
58
57
  Logical name of the WebSocket API. Drives the resource names of the deployed API, the connection key-value store (`qpq-wsq-<apiName>`), and the globals — and is the value you pass as `websocketApiName` to send actions. Also used as the API subdomain.
59
58
 
60
- ### `rootDomain` — `string` (required)
61
-
62
- The registered domain the WebSocket API is served under (passed straight through to the underlying [defineWebsocket](../webserver/websocket.md)).
63
59
 
64
60
  ### `advancedSettings` — `QPQConfigAdvancedWebsocketQueueSettings` (optional)
65
61
 
@@ -7,13 +7,13 @@ description: Define an HTTP API — the public web endpoint (a custom subdomain
7
7
 
8
8
  Defines an **API**: the public HTTP endpoint for a service. An API declares the subdomain and root domain the service is reachable on; every [route](./route.md) you declare in the same service is served under it. A service typically declares exactly one API.
9
9
 
10
- - **On AWS:** deploys a **regional API Gateway REST API** (`LambdaRestApi`, proxy mode, binary media types `*/*`) backed by a single Lambda that handles every route (`ApiQpqWebserverApiConstruct` in `quidproquo-deploy-awscdk`). The API is attached to a custom domain (`apiSubdomain.rootDomain`) via a base-path mapping keyed on the module name, so multiple modules can share one domain. Access logs (JSON, one-year retention) and CloudWatch metrics are enabled; a 5XX alarm is wired by default, plus 401/403 rate alarms when error notifications are configured. When WAF protection is enabled for the deploy, the shared regional Web ACL is associated with the stage.
10
+ - **On AWS:** deploys a **regional API Gateway REST API** (`LambdaRestApi`, proxy mode, binary media types `*/*`) backed by a single Lambda that handles every route (`ApiQpqWebserverApiConstruct` in `quidproquo-deploy-awscdk`). The API is attached to a custom domain on every root declared with [defineDns](./dns.md) (the `{ subdomain: apiName }` target under the app's [domain resolver](../../domains.md)) via a base-path mapping keyed on the module name, so multiple modules can share one domain. Access logs (JSON, one-year retention) and CloudWatch metrics are enabled; a 5XX alarm is wired by default, plus 401/403 rate alarms when error notifications are configured. When WAF protection is enabled for the deploy, the shared regional Web ACL is associated with the stage.
11
11
 
12
12
  ```typescript
13
13
  import { defineApi } from 'quidproquo-webserver';
14
14
 
15
15
  export default [
16
- defineApi('api', 'example.com'),
16
+ defineApi('api'),
17
17
  ];
18
18
  ```
19
19
 
@@ -24,7 +24,6 @@ This serves the service's routes at `https://api.<module-base>.example.com/<modu
24
24
  ```typescript
25
25
  function defineApi(
26
26
  apiName: string,
27
- rootDomain: string,
28
27
  options?: QPQConfigAdvancedApiSettings,
29
28
  ): ApiQPQWebServerConfigSetting;
30
29
  ```
@@ -33,11 +32,8 @@ function defineApi(
33
32
 
34
33
  ### `apiName` — `string` (required)
35
34
 
36
- The API's name and its `uniqueKey` within the config. Unless overridden by `options.subDomain`, it is also the **subdomain** the API is served on (so `defineApi('api', 'example.com')` serves on `api.<...>.example.com`).
35
+ The API's name and its `uniqueKey` within the config. Unless overridden by `options.subDomain`, it is also the **subdomain** the API is served on: the host is the `{ subdomain: apiName }` target resolved on each root (`api.development.example.com` with the default shape).
37
36
 
38
- ### `rootDomain` — `string` (required)
39
-
40
- The root domain the API is hosted under, e.g. `'example.com'`. The deploy derives the full hostname from this plus the environment/feature and the API subdomain, so the same config deploys to multiple environments without collisions.
41
37
 
42
38
  ### `options` — `QPQConfigAdvancedApiSettings` (optional)
43
39
 
@@ -56,7 +52,7 @@ import { defineApi, defineRoute } from 'quidproquo-webserver';
56
52
 
57
53
  export default [
58
54
  // Serve on a custom subdomain, capped at 50 concurrent requests
59
- defineApi('public', 'example.com', {
55
+ defineApi('public', {
60
56
  subDomain: 'api',
61
57
  maxConcurrentExecutions: 50,
62
58
  }),
@@ -71,5 +67,6 @@ export default [
71
67
  - [defineRoute](./route.md) — declares the individual method+path endpoints served under the API.
72
68
  - [defineDefaultRouteOptions](./default-route-options.md) — service-wide defaults (CORS, auth) merged into every route.
73
69
  - [defineServiceFunction](./service-function.md) — RPC-style callable functions, deployed as their own Lambdas rather than under the API.
74
- - [defineDomainCertificate](../config-aws/domain-certificate.md) — the ACM certificate for the API's domain (resolved with the same `rootDomain` prefixing).
70
+ - [defineDns](./dns.md) — the roots the API's domain is created on.
71
+ - [defineDomainCertificate](../config-aws/domain-certificate.md) — the ACM certificate covering the API's host on every root.
75
72
  - [defineWafProtection](../config-aws/waf-protection.md) — opt the API Gateway stage into the app's WAF web ACL.
@@ -81,7 +81,7 @@ export default [
81
81
  buildPath: './web/dist',
82
82
  cacheSettingsName: 'default',
83
83
  ignoreCache: ['index.html'],
84
- domain: { onRootDomain: true, rootDomain: 'example.com' },
84
+ domain: { onRootDomain: true },
85
85
  }),
86
86
  ];
87
87
  ```
@@ -1,19 +1,21 @@
1
1
  ---
2
2
  title: defineDns
3
- description: Declare the base DNS domain a service lives under, so every domain, certificate, and DNS record it deploys hangs off one root.
3
+ description: Declare the root domains a service is served on; every hostname the service deploys is derived from them.
4
4
  ---
5
5
 
6
6
  # defineDns
7
7
 
8
- Declares the **base DNS domain** for a service. Everything else in quidproquo-webserver that needs a hostname API custom domains, web entries, [domain proxies](./domain-proxy.md), and [subdomain redirects](./subdomain-redirect.md) derives its fully-qualified name from this single `dnsBase`. A service has one DNS base.
8
+ Declares the **root domains** for a service, primary first. Everything else that needs a hostname (API custom domains, web entries, websockets, [domain proxies](./domain-proxy.md), [subdomain redirects](./subdomain-redirect.md), email identities, CORS and CSP origins) is derived from this list through the app's [domain resolver](../../domains.md). A service has one `defineDns`; a second one is a synth error.
9
9
 
10
- - **On AWS:** `defineDns` does **not** create a Route53 hosted zone by itself. The hosted zone for `dnsBase` (after environment/feature prefixing) must already exist in Route53 — deploy constructs look it up with `HostedZone.fromLookup`, then add the `A` records they need into that existing zone. Think of `defineDns` as declaring "this is my root domain"; the zone that owns it is expected to be there already.
10
+ - **On AWS:** `defineDns` does **not** create a Route53 hosted zone. Deploy constructs look zones up with `HostedZone.fromLookup` and add records into them. A host's zone is its root's site root when it sits under it (`api.development.example.com` in `development.example.com`), otherwise the root itself (`development-api.example.com` in `example.com`); see [Domains](../../domains.md).
11
11
 
12
12
  ```typescript
13
13
  import { defineDns } from 'quidproquo-webserver';
14
14
 
15
15
  export default [
16
- defineDns('example.com'),
16
+ defineDns(['example.com', 'example.org'], {
17
+ resolver: { basePath: __dirname, relativePath: 'domainResolver', functionName: 'domainResolver' },
18
+ }),
17
19
  ];
18
20
  ```
19
21
 
@@ -21,22 +23,22 @@ export default [
21
23
 
22
24
  ```typescript
23
25
  function defineDns(
24
- dnsBase: string,
26
+ rootDomains: string | string[],
27
+ options?: { resolver?: QpqPureFunction },
25
28
  ): DnsQPQWebServerConfigSetting;
26
29
  ```
27
30
 
28
31
  ## Parameters
29
32
 
30
- ### `dnsBase` — `string` (required)
33
+ ### `rootDomains` — `string | string[]` (required)
34
+
35
+ The root domains the service is served on. The first is the **primary**: anything that must bake exactly one absolute URL (email links, module-federation remotes, the Cognito custom domain) uses it. Every other resource is created on every root, so the app is fully live on all of them at once.
31
36
 
32
- The root domain the service is served from, e.g. `'example.com'`. This value becomes the config's `uniqueKey` and is the base that all of the service's hostnames are built on. quidproquo prefixes it per environment and feature before use, so a single config deploys cleanly to multiple environments:
37
+ Hostnames are not built here. They come from the app's [domain resolver](../../domains.md), which by default produces `[subdomain.][service.][feature.][environment.]root` with no environment label in production.
33
38
 
34
- - In `production` the base stays `example.com`.
35
- - In another environment it becomes `<environment>.example.com` (e.g. `development.example.com`).
36
- - With a feature branch it becomes `<feature>.<environment>.example.com`.
37
- - A service's own resources then live under `<service>.<base>` (e.g. an API at `api.<service>.development.example.com`).
39
+ ### `options.resolver` `QpqPureFunction` (optional)
38
40
 
39
- The list of declared bases is what [askDnsList](../../actions/webserver/dns/ask-dns-list.md) returns at runtime.
41
+ A pointer (`{ basePath, relativePath, functionName }`) to the app's `DomainResolver` export. The deploy tooling `require`s it at synth and build time, and the runtime loads it through the dynamic module loader (the pointer is a bundled src entry like a route), so every site resolves hosts the same way: `resolveHosts(qpqConfig, target, resolver)`. Omit it for the default shape.
40
42
 
41
43
  ## Examples
42
44
 
@@ -44,14 +46,20 @@ The list of declared bases is what [askDnsList](../../actions/webserver/dns/ask-
44
46
  import { defineDns } from 'quidproquo-webserver';
45
47
 
46
48
  export default [
47
- // Serve this service under example.com (and its env/feature-prefixed variants)
49
+ // One root
48
50
  defineDns('example.com'),
51
+
52
+ // Two roots, example.com primary: every api, web entry and websocket is served on both,
53
+ // shaped by the app's own resolver
54
+ defineDns(['example.com', 'example.org'], {
55
+ resolver: { basePath: __dirname, relativePath: 'domainResolver', functionName: 'domainResolver' },
56
+ }),
49
57
  ];
50
58
  ```
51
59
 
52
60
  ## Related
53
61
 
54
- - [askDnsList](../../actions/webserver/dns/ask-dns-list.md) — returns the `dnsBase` values declared with `defineDns`.
55
- - [defineCertificate](./certificate.md) — TLS certificate for a domain rooted at this base.
56
- - [defineDomainProxy](./domain-proxy.md) — front a domain under this base with CloudFront.
57
- - [defineSubdomainRedirect](./subdomain-redirect.md) — redirect a subdomain under this base to another URL.
62
+ - [Domains](../../domains.md) — how roots, the resolver, zones and certificates fit together.
63
+ - [askDnsList](../../actions/webserver/dns/ask-dns-list.md) — returns the root list at runtime.
64
+ - [askDnsResolveHosts](../../actions/webserver/dns/ask-dns-resolve-hosts.md) — a target's host on every root at runtime.
65
+ - [defineDomainCertificate](../config-aws/domain-certificate.md) — the per-region certificate covering every root.
@@ -16,7 +16,6 @@ export default [
16
16
  defineDomainProxy('marketing-proxy', {
17
17
  httpProxyDomain: 'origin.internal.example.com',
18
18
  domain: {
19
- rootDomain: 'example.com',
20
19
  onRootDomain: true,
21
20
  },
22
21
  domainProxyViewerProtocolPolicy: DomainProxyViewerProtocolPolicy.REDIRECT_TO_HTTPS,
@@ -53,7 +52,6 @@ Unique name for this proxy (the config's `uniqueKey`). Used to derive the distri
53
52
 
54
53
  ```typescript
55
54
  export interface DomainProxyDomainOptions {
56
- rootDomain: string;
57
55
  subDomainNames?: string[];
58
56
  onRootDomain: boolean;
59
57
  }
@@ -61,7 +59,6 @@ export interface DomainProxyDomainOptions {
61
59
 
62
60
  | Property | Type | Description |
63
61
  | --- | --- | --- |
64
- | `rootDomain` | `string` | The root domain (matches the service's [`dnsBase`](./dns.md)); the apex is resolved from it with the usual environment/feature prefixing, and the hosted zone is looked up in Route53. |
65
62
  | `subDomainNames` | `string[]` | Subdomains to serve, each expanded to `<subDomain>.<apex>`. An `A` alias record is created for each. |
66
63
  | `onRootDomain` | `boolean` | When `true` and no subdomains are given, the distribution also answers on the apex domain itself. |
67
64
 
@@ -91,7 +88,6 @@ export default [
91
88
  defineDomainProxy('api-proxy', {
92
89
  httpProxyDomain: 'backend.example.net',
93
90
  domain: {
94
- rootDomain: 'example.com',
95
91
  subDomainNames: ['api', 'api-v2'],
96
92
  onRootDomain: false,
97
93
  },
@@ -104,6 +100,6 @@ export default [
104
100
 
105
101
  ## Related
106
102
 
107
- - [defineDns](./dns.md) — declares the `rootDomain` this proxy attaches to.
103
+ - [defineDns](./dns.md) — declares the roots this proxy is served on.
108
104
  - [defineCertificate](./certificate.md) — related certificate configuration (the proxy uses the central certificate).
109
105
  - [defineSubdomainRedirect](./subdomain-redirect.md) — redirect a subdomain instead of proxying it.
@@ -1,43 +1,37 @@
1
1
  ---
2
2
  title: defineEmailSender
3
- description: Declare a domain a service is allowed to send email from.
3
+ description: Declare that a service sends email from the app's root domains.
4
4
  ---
5
5
 
6
6
  # defineEmailSender
7
7
 
8
- Declares a domain the service can send email from with [askEmailSendEmail](../../actions/webserver/email/ask-email-send-email.md). The domain must live under a base declared with [defineDns](./dns.md) deploy resolves it the same way (environment/feature prefixed) before creating the sending identity.
8
+ Declares that the service sends email with [askEmailSendEmail](../../actions/webserver/email/ask-email-send-email.md) from the app's root domains. The sending identity on each root is that root's site root under the app's [domain resolver](../../domains.md) (`development.example.com` with the default shape).
9
9
 
10
- - **On AWS:** creates an SES `EmailIdentity` for the resolved domain in its Route53 hosted zone (DKIM records land there automatically), and scopes the service role's `ses:SendEmail` / `ses:SendRawEmail` grant to that identity's exact ARN. `ses:SendRawEmail` is needed for emails with attachments, which are sent as raw MIME. No grant or identity is created for services that declare no `defineEmailSender`.
10
+ - **On AWS:** creates an SES `EmailIdentity` per root in its Route53 hosted zone (DKIM records land there automatically), and scopes the service role's `ses:SendEmail` / `ses:SendRawEmail` grant to those identities' exact ARNs. `ses:SendRawEmail` is needed for emails with attachments, which are sent as raw MIME. No grant or identity is created for services that declare no `defineEmailSender`.
11
11
 
12
12
  ```typescript
13
13
  import { defineDns, defineEmailSender } from 'quidproquo-webserver';
14
14
 
15
15
  export default [
16
16
  defineDns('example.com'),
17
- defineEmailSender('example.com'),
17
+ defineEmailSender(),
18
18
  ];
19
19
  ```
20
20
 
21
21
  ## Signature
22
22
 
23
23
  ```typescript
24
- function defineEmailSender(
25
- rootDomain: string,
26
- ): EmailSenderQPQWebServerConfigSetting;
24
+ function defineEmailSender(): EmailSenderQPQWebServerConfigSetting;
27
25
  ```
28
26
 
29
- ## Parameters
30
-
31
- ### `rootDomain` — `string` (required)
32
-
33
- The domain to send from, resolved the same way as [defineDns](./dns.md)'s `dnsBase` (environment/feature prefixed). This value is also the config's `uniqueKey`, so a service declares one sender per root domain.
27
+ Takes no arguments; a service declares it once.
34
28
 
35
29
  ## Returns
36
30
 
37
- An `EmailSenderQPQWebServerConfigSetting` config entry. Deploy reads every declared entry with `qpqWebServerUtils.getEmailSenderSettings` to create the SES identities and IAM grants.
31
+ An `EmailSenderQPQWebServerConfigSetting` config entry. Deploy reads it with `qpqWebServerUtils.getEmailSenderSettings` to create the SES identities and IAM grants.
38
32
 
39
33
  ## Related
40
34
 
41
- - [askEmailSendEmail](../../actions/webserver/email/ask-email-send-email.md) — send email from a domain declared here.
42
- - [defineDns](./dns.md) — the base domain this sender's `rootDomain` resolves against.
35
+ - [askEmailSendEmail](../../actions/webserver/email/ask-email-send-email.md) — send email from the identities declared here.
36
+ - [defineDns](./dns.md) — the roots the identities are created on.
43
37
  - [defineEmailSenderAllowList](../config-aws/email-sender-allow-list.md) — recipient addresses allowed while the SES account is in sandbox mode.
@@ -48,6 +48,7 @@ A reference to the story that handles the request. Usually written as a relative
48
48
  | --- | --- | --- | --- |
49
49
  | `allowedOrigins` | `(string \| ServiceAllowedOrigin)[]` | – | Browser origins allowed to call this route (CORS). Either literal origin strings or `ServiceAllowedOrigin` descriptors resolved to another qpq service's domain. Merged with any [default route options](./default-route-options.md). |
50
50
  | `routeAuthSettings` | `RouteAuthSettings` | – | How the route is authenticated/authorized. See below. |
51
+ | `schema` | `RouteSchema` | – | `bodyJsonSchema`, `queryJsonSchema` and `responseJsonSchema` as JSON Schema, plus `summary`, `description`, `tags` and `hidden`. Feeds the generated OpenAPI document; see [defineOpenApiRoutes](../features/open-api-routes.md). Routes written with `dynamicRoute` fill this in from zod schemas for you. |
51
52
 
52
53
  #### `RouteAuthSettings`
53
54
 
@@ -53,7 +53,7 @@ import { defineSeo, defineWebEntry } from 'quidproquo-webserver';
53
53
  export default [
54
54
  defineWebEntry('website', {
55
55
  buildPath: './web/dist',
56
- domain: { onRootDomain: true, rootDomain: 'example.com' },
56
+ domain: { onRootDomain: true },
57
57
  }),
58
58
 
59
59
  // Render product pages server-side for crawlers & link previews
@@ -17,7 +17,6 @@ export default [
17
17
  buildPath: './web/dist',
18
18
  domain: {
19
19
  onRootDomain: true,
20
- rootDomain: 'example.com',
21
20
  },
22
21
  }),
23
22
  ];
@@ -60,13 +59,11 @@ The web entry's name and `uniqueKey`. This is the name you pass to [askWebEntryI
60
59
  export interface WebDomainOptions {
61
60
  subDomainName?: string;
62
61
  onRootDomain: boolean;
63
- rootDomain: string;
64
62
  }
65
63
  ```
66
64
 
67
65
  | Property | Type | Description |
68
66
  | --- | --- | --- |
69
- | `rootDomain` | `string` | The apex/root domain to host under, e.g. `'example.com'`. The ACM certificate and Route 53 hosted zone are resolved from this. |
70
67
  | `onRootDomain` | `boolean` | Serve on the apex domain itself (`example.com`). |
71
68
  | `subDomainName` | `string` (optional) | Serve on a subdomain instead of / in addition to the apex, e.g. `'app'` → `app.example.com`. |
72
69
 
@@ -112,7 +109,6 @@ export default [
112
109
  ignoreCache: ['index.html', 'remoteEntry.js', 'mf-manifest.json'],
113
110
  domain: {
114
111
  onRootDomain: true,
115
- rootDomain: 'example.com',
116
112
  },
117
113
  }),
118
114
  ];
@@ -126,7 +122,6 @@ const options: QPQConfigAdvancedWebEntrySettings = {
126
122
  domain: {
127
123
  subDomainName: 'views',
128
124
  onRootDomain: false,
129
- rootDomain: 'example.com',
130
125
  },
131
126
  storageDrive: {
132
127
  sourceStorageDrive: 'views',
@@ -7,13 +7,13 @@ description: Define a WebSocket API with connect, disconnect, and message route
7
7
 
8
8
  Defines a **WebSocket API**: a persistent, bidirectional connection endpoint served on its own subdomain. You supply story entry points that run when a client connects, disconnects, or sends a message, and the runtime wires them to the platform's WebSocket transport. Stories push messages back to connected clients with [askWebsocketSendMessage](../../actions/webserver/websocket/ask-websocket-send-message.md).
9
9
 
10
- - **On AWS:** deploys an **API Gateway v2 WebSocket API** (`QpqApiWebserverWebsocketConstruct` in `quidproquo-deploy-awscdk`) fronted by a single Lambda proxy integration. Three routes — `$connect`, `$disconnect`, and `$default` — all target that Lambda, which dispatches to the matching handler in `eventProcessors`. The API is mapped to a custom subdomain (`apiSubdomain.rootDomain`) and served from the `prod` stage. Routes use `authorizationType: NONE`; do your own auth inside the `onConnect`/`onMessage` handlers. When `deprecated` is set, nothing is deployed.
10
+ - **On AWS:** deploys an **API Gateway v2 WebSocket API** (`QpqApiWebserverWebsocketConstruct` in `quidproquo-deploy-awscdk`) fronted by a single Lambda proxy integration. Three routes — `$connect`, `$disconnect`, and `$default` — all target that Lambda, which dispatches to the matching handler in `eventProcessors`. The API is mapped to a custom domain on every root from [defineDns](./dns.md) (the `{ subdomain: apiSubdomain, service }` target, or `{ subdomain: apiSubdomain }` when `onRootDomain` is set) and served from the `prod` stage. Routes use `authorizationType: NONE`; do your own auth inside the `onConnect`/`onMessage` handlers. When `deprecated` is set, nothing is deployed.
11
11
 
12
12
  ```typescript
13
13
  import { defineWebsocket } from 'quidproquo-webserver';
14
14
 
15
15
  export default [
16
- defineWebsocket('ws', 'example.com', {
16
+ defineWebsocket('ws', {
17
17
  onConnect: '/entry/ws/onConnect::onConnect',
18
18
  onDisconnect: '/entry/ws/onDisconnect::onDisconnect',
19
19
  onMessage: '/entry/ws/onMessage::onMessage',
@@ -54,7 +54,6 @@ export type WebsocketEvent<T extends string | Blob | ArrayBuffer = string> = {
54
54
  ```typescript
55
55
  function defineWebsocket(
56
56
  apiSubdomain: string,
57
- rootDomain: string,
58
57
  eventProcessors: QpqWebSocketEventProcessors,
59
58
  options?: QPQConfigAdvancedWebSocketSettings,
60
59
  ): WebSocketQPQWebServerConfigSetting;
@@ -64,11 +63,8 @@ function defineWebsocket(
64
63
 
65
64
  ### `apiSubdomain` — `string` (required)
66
65
 
67
- The subdomain the WebSocket API is served on. Combined with `rootDomain` it forms the connection host (`apiSubdomain.rootDomain`). It alone is the setting's `uniqueKey`.
66
+ The subdomain the WebSocket API is served on; with the default shape the host is `apiSubdomain.<service>.<base>` (or `apiSubdomain.<base>` when `onRootDomain` is set). It alone is the setting's `uniqueKey`.
68
67
 
69
- ### `rootDomain` — `string` (required)
70
-
71
- The registered domain the API lives under. On AWS a custom-domain mapping is created for `apiSubdomain.rootDomain` (or, when `onRootDomain` is set, directly under the base/service domain).
72
68
 
73
69
  ### `eventProcessors` — `QpqWebSocketEventProcessors` (required)
74
70
 
@@ -0,0 +1,76 @@
1
+ ---
2
+ title: Domains
3
+ description: How root domains, the app's hostname shape, hosted zones and certificates fit together.
4
+ ---
5
+
6
+ # Domains
7
+
8
+ An app is served on one or more **root domains** and every hostname it needs is derived from them. Three things decide the final names:
9
+
10
+ 1. **The root list**: [defineDns](./config/webserver/dns.md), primary first, declared by the bootstrap fragment and by every service (normally from one shared constant).
11
+ 2. **The domain resolver**: the app's hostname shape, a pure `DomainResolver` export that `defineDns` points at with a `QpqPureFunction` (`{ basePath, relativePath, functionName }`). Omit the pointer for the default shape.
12
+ 3. **Hosted zones** (AWS only): derived from the resolver's output, nothing to declare. A host's records go in its root's site root when it sits under it (`api.development.example.com` in the delegated `development.example.com` zone), otherwise in the root itself (`development-api.example.com` in `example.com`). Whichever applies must already exist in Route53.
13
+
14
+ ## Targets
15
+
16
+ Every place that needs a host asks for a **target**, `{ subdomain?, service? }`, and gets one host per root:
17
+
18
+ | Setting | Target |
19
+ | --- | --- |
20
+ | `defineApi('api')` | `{ subdomain: 'api' }` |
21
+ | `defineWebEntry('site', { domain: { onRootDomain: true } })` | `{}` (the site root) |
22
+ | `defineWebEntry('views', { domain: { subDomainName: 'views', onRootDomain: true } })` | `{ subdomain: 'views' }` |
23
+ | `defineWebsocket('ws', ...)` on service `chat` | `{ subdomain: 'ws', service: 'chat' }` |
24
+ | CORS or CSP entry `{ api: 'admin', service: 'admin' }` | `{ subdomain: 'admin', service: 'admin' }` |
25
+
26
+ ## The resolver
27
+
28
+ ```typescript
29
+ // packages/domain/src/domainResolver.ts
30
+ import { DomainResolver } from 'quidproquo';
31
+
32
+ // dev-api.example.com, dev-ws-chat.example.com, example.com in production
33
+ export const domainResolver: DomainResolver = {
34
+ resolveHost: ({ rootDomain, environment, feature, service, subdomain }) => {
35
+ const env = environment === 'production' ? undefined : environment;
36
+ const label = [feature, env, subdomain, service].filter(Boolean).join('-');
37
+ return label ? `${label}.${rootDomain}` : rootDomain;
38
+ },
39
+ };
40
+
41
+ // packages/domain/src/index.ts: the pointer every config passes to defineDns
42
+ export const DOMAIN_RESOLVER = { basePath: __dirname, relativePath: 'domainResolver', functionName: 'domainResolver' };
43
+ ```
44
+
45
+ ```typescript
46
+ defineDns(APP_DOMAINS, { resolver: DOMAIN_RESOLVER });
47
+ ```
48
+
49
+ The resolver receives the root, environment, feature and target and returns a host that must be the root or end with `.<root>`. It can branch per root. It must be pure: no actions, no I/O, deterministic. The CDK app and the rspack builds `require` the pointer under ts-node; the lambda runtime bundles it as a src entry and loads it through the dynamic module loader once per processor (CORS, OpenAPI servers, the CloudFront origin-request domain, [askDnsResolveHosts](./actions/webserver/dns/ask-dns-resolve-hosts.md)). Nothing is precomputed and config is never rewritten: every site calls `resolveHosts(qpqConfig, target, resolver)`.
50
+
51
+ The default shape, with no pointer, is `[subdomain.][service.][feature.][environment.]root` with no environment label in production.
52
+
53
+ ## The browser
54
+
55
+ Nothing here reaches the browser. How a page finds its api is the app's own concern, as before.
56
+
57
+ ## Certificates
58
+
59
+ [defineDomainCertificate](./config/config-aws/domain-certificate.md) issues one certificate per region covering its targets on every root. CloudFront serves all roots from one distribution with the `us-east-1` cert; API Gateway custom domains use the deploy-region cert.
60
+
61
+ ## Orphaned certificates
62
+
63
+ A certificate's names are immutable, so any change to the name set (a root added or removed, a target added, a resolver change) issues a new certificate and retains the old one until every distribution and api domain has redeployed. Retained certificates never expire away on their own. They are tagged `application` / `environment` (and `feature`), so list them per app and delete the ones nothing uses:
64
+
65
+ ```bash
66
+ aws acm list-certificates --region us-east-1 \
67
+ --query 'CertificateSummaryList[?InUseBy==`[]`].[CertificateArn,DomainName,Status]' --output table
68
+ ```
69
+
70
+ ## Adding a root
71
+
72
+ 1. Create the Route53 zone for the new root in the deploy account.
73
+ 2. Append it to the app's domain constant, which every `defineDns` reads.
74
+ 3. Deploy the domain phase (new certificates, api domains), then each service.
75
+
76
+ The first root keeps the existing CloudFormation logical ids; extra roots get suffixed ones, so adding is purely additive and removing is purely deletes. Reordering the list rebuilds domain resources (aliases, records, api domain names), so do it in a quiet window.