@shivaedev/platform 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (204) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/README.md +371 -112
  3. package/dist/errors/rejectedField.d.ts +7 -0
  4. package/dist/errors/rejectedField.d.ts.map +1 -0
  5. package/dist/errors/{rejected-field.js → rejectedField.js} +4 -2
  6. package/dist/errors/rejectedField.js.map +1 -0
  7. package/dist/errors/taxonomy.d.ts +10 -0
  8. package/dist/errors/taxonomy.d.ts.map +1 -1
  9. package/dist/errors/taxonomy.js +4 -0
  10. package/dist/errors/taxonomy.js.map +1 -1
  11. package/dist/rpc/identity.d.ts.map +1 -1
  12. package/dist/rpc/identity.js.map +1 -1
  13. package/dist/rpc/middleware.d.ts +1 -1
  14. package/dist/rpc/middleware.d.ts.map +1 -1
  15. package/dist/rpc/middleware.js +1 -1
  16. package/dist/rpc/middleware.js.map +1 -1
  17. package/dist/server/appHost.d.ts +61 -0
  18. package/dist/server/appHost.d.ts.map +1 -0
  19. package/dist/server/appHost.js +14 -0
  20. package/dist/server/appHost.js.map +1 -0
  21. package/dist/server/appHostHttp.d.ts +11 -0
  22. package/dist/server/appHostHttp.d.ts.map +1 -0
  23. package/dist/server/appHostHttp.js +51 -0
  24. package/dist/server/appHostHttp.js.map +1 -0
  25. package/dist/{better-auth → server/better-auth}/adapter.d.ts +1 -1
  26. package/dist/server/better-auth/adapter.d.ts.map +1 -0
  27. package/dist/{better-auth → server/better-auth}/adapter.js +26 -14
  28. package/dist/server/better-auth/adapter.js.map +1 -0
  29. package/dist/{better-auth → server/better-auth}/relation.d.ts +15 -15
  30. package/dist/server/better-auth/relation.d.ts.map +1 -0
  31. package/dist/{better-auth → server/better-auth}/relation.js +2 -2
  32. package/dist/server/better-auth/relation.js.map +1 -0
  33. package/dist/server/better-auth/rowAdapter.d.ts +8 -0
  34. package/dist/server/better-auth/rowAdapter.d.ts.map +1 -0
  35. package/dist/server/better-auth/rowAdapter.js +52 -0
  36. package/dist/server/better-auth/rowAdapter.js.map +1 -0
  37. package/dist/{better-auth → server/better-auth}/where.d.ts +1 -1
  38. package/dist/server/better-auth/where.d.ts.map +1 -0
  39. package/dist/{better-auth → server/better-auth}/where.js +22 -18
  40. package/dist/server/better-auth/where.js.map +1 -0
  41. package/dist/server/boundedRpcRequest.d.ts +3 -0
  42. package/dist/server/boundedRpcRequest.d.ts.map +1 -0
  43. package/dist/server/boundedRpcRequest.js +41 -0
  44. package/dist/server/boundedRpcRequest.js.map +1 -0
  45. package/dist/server/databaseLayer.d.ts +6 -0
  46. package/dist/server/databaseLayer.d.ts.map +1 -0
  47. package/dist/server/databaseLayer.js +29 -0
  48. package/dist/server/databaseLayer.js.map +1 -0
  49. package/dist/server/hostOptions.d.ts +28 -0
  50. package/dist/server/hostOptions.d.ts.map +1 -0
  51. package/dist/server/hostOptions.js +2 -0
  52. package/dist/server/hostOptions.js.map +1 -0
  53. package/dist/{node-http/subscription-signal.d.ts → server/node-http/subscriptionSignal.d.ts} +2 -2
  54. package/dist/server/node-http/subscriptionSignal.d.ts.map +1 -0
  55. package/dist/{node-http/subscription-signal.js → server/node-http/subscriptionSignal.js} +20 -13
  56. package/dist/server/node-http/subscriptionSignal.js.map +1 -0
  57. package/dist/server/rpc-server/adapters/betterAuthSessions.d.ts +4 -0
  58. package/dist/server/rpc-server/adapters/betterAuthSessions.d.ts.map +1 -0
  59. package/dist/server/rpc-server/adapters/betterAuthSessions.js +14 -0
  60. package/dist/server/rpc-server/adapters/betterAuthSessions.js.map +1 -0
  61. package/dist/server/rpc-server/boundary.d.ts +9 -0
  62. package/dist/server/rpc-server/boundary.d.ts.map +1 -0
  63. package/dist/server/rpc-server/boundary.js +18 -0
  64. package/dist/server/rpc-server/boundary.js.map +1 -0
  65. package/dist/server/rpc-server/local.d.ts +7 -0
  66. package/dist/server/rpc-server/local.d.ts.map +1 -0
  67. package/dist/server/rpc-server/local.js +70 -0
  68. package/dist/server/rpc-server/local.js.map +1 -0
  69. package/dist/{rpc-server → server/rpc-server}/origin.d.ts +3 -3
  70. package/dist/server/rpc-server/origin.d.ts.map +1 -0
  71. package/dist/server/rpc-server/origin.js +16 -0
  72. package/dist/server/rpc-server/origin.js.map +1 -0
  73. package/dist/server/rpc-server/redact.d.ts +3 -0
  74. package/dist/server/rpc-server/redact.d.ts.map +1 -0
  75. package/dist/{rpc-server → server/rpc-server}/redact.js +9 -7
  76. package/dist/server/rpc-server/redact.js.map +1 -0
  77. package/dist/server/rpc-server/redactCause.d.ts +6 -0
  78. package/dist/server/rpc-server/redactCause.d.ts.map +1 -0
  79. package/dist/server/rpc-server/redactCause.js +36 -0
  80. package/dist/server/rpc-server/redactCause.js.map +1 -0
  81. package/dist/server/rpc-server/sensitive.d.ts +5 -0
  82. package/dist/server/rpc-server/sensitive.d.ts.map +1 -0
  83. package/dist/{rpc-server → server/rpc-server}/sensitive.js +21 -17
  84. package/dist/server/rpc-server/sensitive.js.map +1 -0
  85. package/dist/server/rpc-server/session.d.ts +21 -0
  86. package/dist/server/rpc-server/session.d.ts.map +1 -0
  87. package/dist/server/rpc-server/session.js +37 -0
  88. package/dist/server/rpc-server/session.js.map +1 -0
  89. package/dist/{rpc-server → server/rpc-server}/tracing.d.ts +1 -1
  90. package/dist/server/rpc-server/tracing.d.ts.map +1 -0
  91. package/dist/server/rpc-server/tracing.js +42 -0
  92. package/dist/server/rpc-server/tracing.js.map +1 -0
  93. package/dist/server/rpc-server/transportHeaders.d.ts +4 -0
  94. package/dist/server/rpc-server/transportHeaders.d.ts.map +1 -0
  95. package/dist/server/rpc-server/transportHeaders.js +6 -0
  96. package/dist/server/rpc-server/transportHeaders.js.map +1 -0
  97. package/dist/server/rpc-server/trustedProxy.d.ts +6 -0
  98. package/dist/server/rpc-server/trustedProxy.d.ts.map +1 -0
  99. package/dist/server/rpc-server/trustedProxy.js +35 -0
  100. package/dist/server/rpc-server/trustedProxy.js.map +1 -0
  101. package/dist/server/runtime/makePlatform.d.ts +4 -0
  102. package/dist/server/runtime/makePlatform.d.ts.map +1 -0
  103. package/dist/{runtime/make.js → server/runtime/makePlatform.js} +11 -9
  104. package/dist/server/runtime/makePlatform.js.map +1 -0
  105. package/dist/server/runtime/types.d.ts +19 -0
  106. package/dist/server/runtime/types.d.ts.map +1 -0
  107. package/dist/{runtime → server/runtime}/types.js.map +1 -1
  108. package/dist/server/testing/makePlatformIt.d.ts +8 -0
  109. package/dist/server/testing/makePlatformIt.d.ts.map +1 -0
  110. package/dist/{testing/vitest.js → server/testing/makePlatformIt.js} +7 -3
  111. package/dist/server/testing/makePlatformIt.js.map +1 -0
  112. package/dist/server/testing/types.d.ts +28 -0
  113. package/dist/server/testing/types.d.ts.map +1 -0
  114. package/dist/{testing → server/testing}/types.js.map +1 -1
  115. package/docs/north-star.md +36 -0
  116. package/docs/roadmap.md +30 -0
  117. package/package.json +333 -32
  118. package/src/errors/{rejected-field.ts → rejectedField.ts} +3 -6
  119. package/src/errors/taxonomy.ts +2 -8
  120. package/src/rpc/identity.ts +0 -4
  121. package/src/rpc/middleware.ts +19 -10
  122. package/src/server/appHost.ts +130 -0
  123. package/src/server/appHostHttp.ts +101 -0
  124. package/src/{better-auth → server/better-auth}/adapter.ts +26 -30
  125. package/src/{better-auth → server/better-auth}/relation.ts +17 -19
  126. package/src/{better-auth/row-adapter.ts → server/better-auth/rowAdapter.ts} +19 -21
  127. package/src/{better-auth → server/better-auth}/where.ts +22 -32
  128. package/src/server/boundedRpcRequest.ts +47 -0
  129. package/src/server/databaseLayer.ts +36 -0
  130. package/src/server/hostOptions.ts +25 -0
  131. package/src/{node-http/subscription-signal.ts → server/node-http/subscriptionSignal.ts} +19 -22
  132. package/src/{rpc-server/adapters/better-auth-sessions.ts → server/rpc-server/adapters/betterAuthSessions.ts} +10 -11
  133. package/src/server/rpc-server/boundary.ts +26 -0
  134. package/src/server/rpc-server/local.ts +72 -0
  135. package/src/{rpc-server → server/rpc-server}/origin.ts +11 -14
  136. package/src/{rpc-server → server/rpc-server}/redact.ts +9 -11
  137. package/src/server/rpc-server/redactCause.ts +40 -0
  138. package/src/{rpc-server → server/rpc-server}/sensitive.ts +17 -22
  139. package/src/{rpc-server → server/rpc-server}/session.ts +23 -24
  140. package/src/{rpc-server → server/rpc-server}/tracing.ts +16 -19
  141. package/src/{rpc-server/transport.ts → server/rpc-server/transportHeaders.ts} +4 -4
  142. package/src/server/rpc-server/trustedProxy.ts +36 -0
  143. package/src/{runtime/make.ts → server/runtime/makePlatform.ts} +19 -27
  144. package/src/server/runtime/types.ts +24 -0
  145. package/src/{testing/vitest.ts → server/testing/makePlatformIt.ts} +27 -23
  146. package/src/server/testing/types.ts +38 -0
  147. package/dist/better-auth/adapter.d.ts.map +0 -1
  148. package/dist/better-auth/adapter.js.map +0 -1
  149. package/dist/better-auth/relation.d.ts.map +0 -1
  150. package/dist/better-auth/relation.js.map +0 -1
  151. package/dist/better-auth/row-adapter.d.ts +0 -8
  152. package/dist/better-auth/row-adapter.d.ts.map +0 -1
  153. package/dist/better-auth/row-adapter.js +0 -46
  154. package/dist/better-auth/row-adapter.js.map +0 -1
  155. package/dist/better-auth/where.d.ts.map +0 -1
  156. package/dist/better-auth/where.js.map +0 -1
  157. package/dist/errors/rejected-field.d.ts +0 -7
  158. package/dist/errors/rejected-field.d.ts.map +0 -1
  159. package/dist/errors/rejected-field.js.map +0 -1
  160. package/dist/node-http/subscription-signal.d.ts.map +0 -1
  161. package/dist/node-http/subscription-signal.js.map +0 -1
  162. package/dist/rpc-server/adapters/better-auth-sessions.d.ts +0 -4
  163. package/dist/rpc-server/adapters/better-auth-sessions.d.ts.map +0 -1
  164. package/dist/rpc-server/adapters/better-auth-sessions.js +0 -11
  165. package/dist/rpc-server/adapters/better-auth-sessions.js.map +0 -1
  166. package/dist/rpc-server/origin.d.ts.map +0 -1
  167. package/dist/rpc-server/origin.js +0 -12
  168. package/dist/rpc-server/origin.js.map +0 -1
  169. package/dist/rpc-server/redact-cause.d.ts +0 -6
  170. package/dist/rpc-server/redact-cause.d.ts.map +0 -1
  171. package/dist/rpc-server/redact-cause.js +0 -28
  172. package/dist/rpc-server/redact-cause.js.map +0 -1
  173. package/dist/rpc-server/redact.d.ts +0 -3
  174. package/dist/rpc-server/redact.d.ts.map +0 -1
  175. package/dist/rpc-server/redact.js.map +0 -1
  176. package/dist/rpc-server/sensitive.d.ts +0 -5
  177. package/dist/rpc-server/sensitive.d.ts.map +0 -1
  178. package/dist/rpc-server/sensitive.js.map +0 -1
  179. package/dist/rpc-server/session.d.ts +0 -21
  180. package/dist/rpc-server/session.d.ts.map +0 -1
  181. package/dist/rpc-server/session.js +0 -31
  182. package/dist/rpc-server/session.js.map +0 -1
  183. package/dist/rpc-server/tracing.d.ts.map +0 -1
  184. package/dist/rpc-server/tracing.js +0 -34
  185. package/dist/rpc-server/tracing.js.map +0 -1
  186. package/dist/rpc-server/transport.d.ts +0 -4
  187. package/dist/rpc-server/transport.d.ts.map +0 -1
  188. package/dist/rpc-server/transport.js +0 -4
  189. package/dist/rpc-server/transport.js.map +0 -1
  190. package/dist/runtime/make.d.ts +0 -4
  191. package/dist/runtime/make.d.ts.map +0 -1
  192. package/dist/runtime/make.js.map +0 -1
  193. package/dist/runtime/types.d.ts +0 -19
  194. package/dist/runtime/types.d.ts.map +0 -1
  195. package/dist/testing/types.d.ts +0 -28
  196. package/dist/testing/types.d.ts.map +0 -1
  197. package/dist/testing/vitest.d.ts +0 -8
  198. package/dist/testing/vitest.d.ts.map +0 -1
  199. package/dist/testing/vitest.js.map +0 -1
  200. package/src/rpc-server/redact-cause.ts +0 -41
  201. package/src/runtime/types.ts +0 -24
  202. package/src/testing/types.ts +0 -43
  203. /package/dist/{runtime → server/runtime}/types.js +0 -0
  204. /package/dist/{testing → server/testing}/types.js +0 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,51 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.7.0 - 2026-10-13
4
+
5
+ ### Added
6
+
7
+ - `appHost` accepts `rpcPath` to choose the NDJSON RPC route; it still defaults to `/rpc`.
8
+ - `appHost` accepts `policy: "loopback"` for local RPC without a session provider; `AppLocalHostOptions` describes this form. The transport still requires a loopback peer and Host, matching or absent Origin, and NDJSON content type; proxy trust does not apply to this policy.
9
+ - `appHost` can omit `migrations` to start without a PostgreSQL pool or database configuration. `AppHttpHostOptions` describes this form while `AppHostOptions` retains its required migrations. Applications can provide an `appDatabase` layer through `services`; existing callers that supply migrations keep the migrated database composition.
10
+ - `appDatabase` and the migrated `appHost` accept `config.maxConnections` to bound the PostgreSQL pool.
11
+ - `appHost` forwards `config.disablePreemptiveShutdown` to the native Node host so a scoped daemon can interrupt its RPC streams during shutdown. The native default remains unchanged when omitted.
12
+ - `appHost` accepts an optional native HTTP router layer through `routes`, so application endpoints such as Better Auth share the RPC and asset host.
13
+ - `appHost.assets` also accepts native static-server options, including `spa: true` for browser route deep links. Existing directory strings keep their behavior.
14
+ - `AppHostOptions.config.trustedProxy` / `TRUSTED_PROXY` and `localRpcGuard(effect, trustedProxy?)` accept comma-separated IPv4 and IPv6 addresses and CIDR ranges, including IPv4-mapped IPv6 peers. Invalid proxy addresses or ranges fail `appHost` startup with a typed `ConfigError` before migrations, services or HTTP start.
15
+ - `appHost` accepts `config.maxBodySize` and the `MAX_BODY_SIZE` environment variable to limit RPC request bodies in bytes; explicit config takes precedence.
16
+
17
+ ### Changed
18
+
19
+ - `appHost` accepts native router layers with typed request error annotations in `routes`, so applications can keep their route failures typed through the HTTP boundary.
20
+ - RPC request bodies default to a 1 MiB limit. Oversized NDJSON requests return a 4xx before any RPC handler runs.
21
+
22
+ ### Breaking
23
+
24
+ - Requires stable Effect 4.0.4 in place of 4.0.0-rc.112. Effect 4.0.4 moves the former `effect/unstable/*` modules to `effect/*` and renames constructors such as `Config.string` to `Config.String`. Install `@effect/platform-node`, `@effect/sql-pg` and `@effect/vitest` 4.0.4 alongside it; `@effect/vitest` 4.0.4 requires Vitest 5.
25
+
26
+ ## 0.6.1 - 2026-10-12
27
+
28
+ ### Added
29
+
30
+ - `appHost`, `appDatabase`, `AppChanges`, `AppHostOptions` and `AppDatabaseOptions` in `appHost.ts`: one scoped local HTTP/NDJSON RPC host with transport guard, session middleware, static assets, migrated PostgreSQL and commit-bound live hints; the database Layer also serves workers. The host requires the optional `@effect/platform-node` and `@effect/sql-pg` peers.
31
+ - `AppHostOptions.config.host` / `HOST` and `config.trustedProxy` / `TRUSTED_PROXY` allow a non-loopback listener and same-origin HTTP/NDJSON RPC through an exact trusted proxy peer. `localRpcGuard(effect, trustedProxy?)` accepts the same proxy address; loopback defaults and typed RPC refusals are preserved.
32
+ - `rpc-server/local.ts` exports `localRpcGuard(effect)` to reject local HTTP RPC requests before the handler runs unless their remote address and Host are loopback, their Origin is absent or matches the HTTP Host, and their content type matches the native RPC serialization. It needs no session provider. The module also exports `loopbackOnly(handler)` and `sameOrigin(request)` for local HTTP routes.
33
+ - Export `InternalServerError` and `ServiceUnavailable` Schema errors from `errors/taxonomy.ts` for RPC contracts without authentication middleware.
34
+ - Export `rpcBoundary(schema, { unexpected, message? })` and `RpcBoundaryOptions` from `rpc-server/boundary.ts`. Declared rejections keep their identity; unexpected failures and defects become one declared rejection with a redacted public message and the original server-only cause. Success and interruption remain intact.
35
+
36
+ ### Changed
37
+
38
+ - Pin the optional `better-auth` peer dependency to 1.6.33, the current 1.6
39
+ release, up from 1.6.30.
40
+ - Keep the existing core defining-module import paths through explicit exports while grouping their peer-free implementation under `src/core/` and enforcing its optional-peer boundary from the package manifest.
41
+ - Prefix testing harness type parameters with `T`; their constraints, defaults and inferred types stay the same.
42
+ - Document the package purpose, request boundaries, integrations and API with agent guidance, a north star and a roadmap; publish `docs/` alongside the README.
43
+ - Build package JavaScript, declarations and source maps with TypeScript 7.
44
+
45
+ ### Fixed
46
+
47
+ - Declare `@shivaedev/types` as a dependency of the shipped testing entry point instead of a development-only dependency.
48
+
3
49
  ## 0.6.0 - 2026-10-04
4
50
 
5
51
  ### Changed
package/README.md CHANGED
@@ -1,161 +1,420 @@
1
1
  # @shivaedev/platform
2
2
 
3
- Opinionated shared setup for ShivaeDev Effect applications. This package favors
4
- one consistent application shape over supporting every possible combination of
5
- database, transport, and test framework.
3
+ Shared errors, request identity and server boundaries for Effect applications. A feature can say who called it and why it refused, while the application supplies its session policy once. The package also connects Effect services to Promise callers, Node subscriptions and optional Better Auth, Prisma and tRPC integrations.
6
4
 
7
- Generic Prisma and tRPC integrations remain available separately from
8
- `@shivaedev/effect-prisma` and `@shivaedev/effect-trpc`.
5
+ ## Why you want this
9
6
 
10
- `effect` is the only required peer. The modules under `runtime/`, `node-http/`,
11
- `errors/`, `rpc/` and `rpc-server/` need nothing else. Install the optional peers
12
- for the modules that use them:
7
+ Every feature needs the same boundary questions answered: which user made this request, is the session missing or its provider unavailable, and which failures can the caller act on? Writing that glue repeatedly makes it harder to see the feature's own decision:
13
8
 
14
- - `better-auth/`: `better-auth`, `@shivaedev/effect-prisma` and
15
- `@prisma-next/sql-orm-client`
16
- - `testing/`: `@shivaedev/effect-prisma`, `@shivaedev/effect-trpc`,
17
- `@trpc/server`, and `@effect/vitest`
9
+ ```ts
10
+ const Handlers = Account.toLayer({
11
+ ReadOwn: ({ userId }) =>
12
+ Effect.gen(function* () {
13
+ const identity = yield* Identity;
14
+ if (identity.id !== userId) {
15
+ return yield* new Forbidden({ message: "Not your account" });
16
+ }
17
+ return identity.id;
18
+ }),
19
+ });
20
+ ```
21
+
22
+ `Account` is a native RPC group with authenticated middleware. Its handler reads the identity for this invocation and decides whether that user may read the account. Platform supplies the common error classes and request services; feature code keeps the resource-ownership check. Concurrent requests receive their own identities, and a provider outage reaches the caller as `AuthUnavailable` instead of pretending the caller signed out.
23
+
24
+ ## Using it
25
+
26
+ ### How to think about it
27
+
28
+ There are three parts to a native RPC boundary:
29
+
30
+ 1. **The shared contract** names Schema errors and middleware tags. The browser imports these declarations too, so `errors/` and `rpc/` contain browser-safe data and services.
31
+ 2. **The server composition** implements the tags with Layers from `rpc-server/`. A session provider resolves a session; an Origin policy decides whether its request may reach that provider; tracing supplies a request id and diagnostic context.
32
+ 3. **The feature handler** reads `Identity`, `OptionalIdentity` or `RequestId` and runs application code. Authentication supplies a user id. Authorization still decides whether that user may perform this operation.
33
+
34
+ An **Origin policy** is a function of the transport request's origin, headers and RPC name. A **session provider** returns `Option<Session>` or fails with `AuthUnavailable`; a session carries `user.id`. These are application inputs, not a second authentication framework.
35
+
36
+ The optional modules solve different boundaries. `runtime/` carries Effect services into Promise callers, `node-http/` combines subscription cancellation sources, `better-auth/adapter.ts` connects Better Auth storage to Effect Prisma, and `testing/vitest.ts` composes the Prisma and tRPC test harnesses. Choose the modules that match the application rather than adopting every integration.
37
+
38
+ ### Host the application
39
+
40
+ `appHost` is the server entry point for an Effect app. For a PostgreSQL app, supply the native RPC group, handler and service Layers, migration loader, session policy and static asset directory. The host completes migrations before acquiring application services, starts Node HTTP with NDJSON RPC at `/rpc`, and provides SQL, Reactivity and `AppChanges` to those Layers.
41
+
42
+ ```ts
43
+ import { NodeRuntime } from "@effect/platform-node";
44
+ import { Layer } from "effect";
45
+ import { appHost } from "@shivaedev/platform/appHost.ts";
46
+ import { Api, Handlers, Services, migrations, sessions } from "./app.ts";
47
+
48
+ const Server = appHost({
49
+ group: Api,
50
+ handlers: Handlers,
51
+ services: Services,
52
+ migrations: { loader: migrations },
53
+ sessions,
54
+ assets: "./public",
55
+ });
56
+
57
+ NodeRuntime.runMain(Layer.launch(Server));
58
+ ```
59
+
60
+ `app.ts` exports the application's contracts, native Layers, numbered `Migrator.Loader` and `SessionPolicy`. The host needs the optional peers `@effect/platform-node` and `@effect/sql-pg` at the workspace's Effect version; install both for this entry point. The `errors/`, `node-http/`, `rpc/`, `rpc-server/` and `runtime/` entries still work without them.
61
+
62
+ `assets` accepts a directory string or native `HttpStaticServer.layer` options. For a browser app whose routes must also work on direct navigation or reload, use `{ root: "dist", spa: true }` or merge `HttpStaticServer.layer({ root: "dist", spa: true })` from `effect/http` alongside the handler Layer. Native route Layers can register additional endpoints in that same composition; `routes` also accepts a native `Layer<never, never, HttpRouter>` for endpoints on the same listener. The [starter](https://github.com/ShivaeDev/platform/tree/main/examples/starter) uses it for static assets and Better Auth at `/api/auth/*`, with provider-owned cookies and the host's RPC session middleware.
63
+
64
+ The listener uses `HOST` or `127.0.0.1`, uses `PORT` or 3000, and reads `DATABASE_URL`. Set overrides in the one `config` object: `host`, `port` (0 selects an ephemeral port), `trustedProxy`, `maxBodySize`, `databaseUrl` (an Effect `Redacted<string>`), `applicationName`, and optional `changes`. Migration options accept `table` and `lockTimeout` (default `effect_sql_migrations` and five seconds). Native Layer failures remain in the build error channel; missing application dependencies remain in the requirement type.
65
+
66
+ RPC request bodies are limited to 1 MiB by default. Set `MAX_BODY_SIZE` to a byte count or override it with `config.maxBodySize`, a nonnegative safe integer. An oversized NDJSON body returns a 4xx before any RPC handler runs; real HTTP tests cover fixed-length and chunked requests, byte counting, and configuration precedence. This limit applies to the host's RPC route.
67
+
68
+ RPC uses the existing `localRpcGuard`: it requires a loopback peer and Host, same-origin or absent Origin, and the NDJSON content type. Session lookup uses transport headers. With a session policy, contracts declare `Authenticated`, `MaybeAuthenticated` and `RequestTracing` as needed; the host supplies their implementations. Declared errors retain their Schema types. Applications still own resource authorization and use `rpcBoundary` when they need to map unexpected failures.
69
+
70
+ For local tools, use `policy: "loopback"` with contracts that do not use session middleware to serve RPC without a session provider. The guard requires a loopback peer and Host, absent or same-origin Origin, and NDJSON content type. This policy does not use `TRUSTED_PROXY` or `config.trustedProxy`. Set `rpcPath` to choose another native RPC route; `/rpc` remains the default.
18
71
 
19
- ## Runtime
72
+ Omit `migrations` to host an app without a database. This form creates no SQL pool and reads no `DATABASE_URL`; `AppHttpHostOptions` describes it. `AppHostOptions` retains its required migrations for existing typed callers. A service that needs SQL still requires a SQL Layer. Provide `appDatabase` to the application’s service Layer when it owns database composition, as Fleet's daemon does with its migration loader and namespace ledger. Real HTTP tests cover the custom route, session-free loopback guard and database-free startup; Fleet's PostgreSQL daemon tests cover the adopted composition.
20
73
 
21
- Build one managed runtime for the application and share it with integrations:
74
+ Set `config.maxConnections` on `appDatabase` or a migrated `appHost` to bound the pool. A daemon with long-lived RPC streams can use `config.disablePreemptiveShutdown: true` to let scope closure interrupt those streams before closing the listener; omitting it keeps Node's native graceful-shutdown default. Fleet's daemon tests cover one bounded pool and scoped provider and listener cleanup, including follower reconnection after shutdown.
75
+
76
+ For a deployment behind a proxy, set `HOST=0.0.0.0` and `TRUSTED_PROXY` to a comma-separated list of proxy peer IPs or CIDR ranges (IPv4 or IPv6), or use `config.host` and `config.trustedProxy`. Only matching peers may supply `X-Forwarded-Host` and `X-Forwarded-Proto` (`http` or `https`) for the guard's same-origin check; the proxy must overwrite both headers and preserve Origin. A single authority is required, and comma-separated forwarding chains are refused. IPv4-mapped IPv6 peers match their IPv4 address. Malformed addresses or ranges fail `appHost` startup with a typed `ConfigError`. NDJSON and the application's session Origin policy still apply. Real forwarding-proxy tests cover trusted and untrusted peers, configuration overrides and typed refusals; they do not establish any hosting provider's proxy configuration. See the [deployment quickstart](https://github.com/ShivaeDev/platform/blob/main/docs/framework/quickstart.md#deploy).
77
+
78
+ A subscription handler reads `AppChanges` and returns `changes.stream(keys)` after its authorization check. Writes use `transact` and `invalidateOnCommit` from effect-sql: local streams receive committed invalidations and rollback emits none. For delivery between processes, set `config.changes: { channel: "app_changes" }`; the host binds PostgreSQL LISTEN/NOTIFY to the same SQL pool and resyncs streams when its listener recovers. The native heartbeat and retry options may be supplied in `changes`. A worker can provide `appDatabase({ migrations, config })` to its service Layer to share this database and delivery composition without starting HTTP.
79
+
80
+ The [composed PostgreSQL fixture](https://github.com/ShivaeDev/platform/blob/main/docs/framework/composed-example.md) uses this host for its HTTP servers and `appDatabase` for its worker. It proves commit visibility, rollback silence, typed refusals and delivery between server processes. Platform's host tests also cover static files, environment configuration, guard rejection before session lookup, startup ordering, migration failure and scoped shutdown.
81
+
82
+ ### 1. Once per application: contracts and session policy
83
+
84
+ Declare the RPC group in a shared module:
22
85
 
23
86
  ```ts
24
- import { makePlatformRuntime } from "@shivaedev/platform/runtime/make.ts"
87
+ import { Effect, Schema } from "effect";
88
+ import { Rpc, RpcGroup } from "effect/rpc";
89
+ import { Forbidden } from "@shivaedev/platform/errors/taxonomy.ts";
90
+ import { Identity } from "@shivaedev/platform/rpc/identity.ts";
91
+ import { Authenticated, RequestTracing } from "@shivaedev/platform/rpc/middleware.ts";
92
+
93
+ export const Account = RpcGroup.make(
94
+ Rpc.make("ReadOwn", {
95
+ error: Forbidden,
96
+ payload: { userId: Schema.String },
97
+ success: Schema.String,
98
+ }),
99
+ ).middleware(Authenticated).middleware(RequestTracing);
100
+
101
+ export const Handlers = Account.toLayer({
102
+ ReadOwn: ({ userId }) =>
103
+ Effect.gen(function* () {
104
+ const identity = yield* Identity;
105
+ if (identity.id !== userId) {
106
+ return yield* new Forbidden({ message: "Not your account" });
107
+ }
108
+ return identity.id;
109
+ }),
110
+ });
111
+ ```
112
+
113
+ The middleware declaration makes `Identity` available to guarded handlers and adds `Unauthorized`, `AuthUnavailable` and `Forbidden` to the client's failure type. An unguarded handler that requires `Identity` cannot become a service-free handler Layer. Keep the group declaration in the shared module and the handler implementation on the server when organizing application files.
25
114
 
26
- export const runtime = makePlatformRuntime(ApplicationLive, {
27
- developmentCacheKey: "application",
28
- })
115
+ Supply a provider and an explicit Origin policy as the host's `sessions` input. For a Better Auth provider, adapt its existing session lookup:
116
+
117
+ ```ts
118
+ import { betterAuthSessions } from "@shivaedev/platform/rpc-server/adapters/better-auth-sessions.ts";
119
+ import { trustedOrigins } from "@shivaedev/platform/rpc-server/origin.ts";
120
+ import { auth } from "./auth.ts";
121
+
122
+ export const sessions = {
123
+ origin: trustedOrigins({ allow: ["http://localhost:3000"], missing: "allow" }),
124
+ provider: betterAuthSessions((headers) => auth.api.getSession({ headers })),
125
+ };
29
126
  ```
30
127
 
31
- The optional cache key keeps one runtime across development module reloads.
32
- The runtime also carries transaction- and request-scoped service overrides
33
- across promise boundaries.
128
+ `trustedOrigins({ allow, missing })` accepts an exact allowlist and supports `missing: "reject"` or `"allow"`; a custom `OriginPolicy` can decide from the RPC name or transport headers. A denied policy fails with `Forbidden` before provider lookup. The host's HTTP guard also checks Origin against the local request's Host.
34
129
 
35
- ## Node HTTP
130
+ The application's `auth` owns its storage and session configuration. A null lookup becomes a missing session. A thrown or rejected lookup is logged in redacted form and fails with the generic `AuthUnavailable` message. It remains a failure even on an RPC with optional authentication.
36
131
 
37
- Long-lived responses under Bun's `node:http` compatibility layer may not abort
38
- their Web request signal when the underlying socket disappears. Combine the
39
- available Web, procedure, request, and socket signals at the subscription
40
- boundary:
132
+ ### Transport headers and request ids
133
+
134
+ Session resolution reads `HttpServerRequest`: the HTTP POST or WebSocket upgrade request. Credentials and Origin supplied inside an RPC message cannot replace those transport headers. An application-owned middleware can use `transportHeaders(rpcHeaders)` for the same source of headers.
135
+
136
+ Tracing uses the RPC headers for correlation. `requestTracingLayer()` accepts `x-request-id`; `{ header: "x-correlation-id" }` changes the name. Missing, malformed or oversized values receive generated 32-character lowercase hexadecimal ids. A request id reaches the `RequestId` service, log annotations as `requestId`, and the server span as `request.id`; the RPC method reaches both as `rpc.method`.
137
+
138
+ Caller-chosen request ids are correlation data. Keep authorization tied to the authenticated identity.
139
+
140
+ ### 2. Once per feature: identity and typed refusals
141
+
142
+ Use `Authenticated` when the operation needs `Identity`, and `MaybeAuthenticated` with `maybeAuthenticatedLayer(policy)` when it reads `OptionalIdentity`:
41
143
 
42
144
  ```ts
43
- import { nodeSubscriptionSignal } from "@shivaedev/platform/node-http/subscription-signal.ts"
44
-
45
- const subscription = nodeSubscriptionSignal({
46
- request,
47
- nodeRequest,
48
- signals: [procedureSignal],
49
- })
50
-
51
- try {
52
- await consume(subscription.signal)
53
- } finally {
54
- subscription.dispose()
55
- }
145
+ import { Effect, Option, Schema } from "effect";
146
+ import { Rpc, RpcGroup } from "effect/rpc";
147
+ import { OptionalIdentity } from "@shivaedev/platform/rpc/identity.ts";
148
+ import { MaybeAuthenticated } from "@shivaedev/platform/rpc/middleware.ts";
149
+
150
+ export const Greetings = RpcGroup.make(
151
+ Rpc.make("Greeting", { success: Schema.String }),
152
+ ).middleware(MaybeAuthenticated);
153
+
154
+ export const GreetingHandlers = Greetings.toLayer({
155
+ Greeting: () =>
156
+ Effect.map(OptionalIdentity, (identity) =>
157
+ Option.match(identity, {
158
+ onNone: () => "hello anonymous",
159
+ onSome: ({ id }) => `hello ${id}`,
160
+ }),
161
+ ),
162
+ });
56
163
  ```
57
164
 
58
- The node request may be supplied directly or carried by an srvx Web request.
59
- Applications still own subscription limits, event buses, and transport policy.
165
+ A missing session produces `Option.none()` here. The same policy still rejects forbidden origins and distinguishes provider failure from an anonymous caller. Signed-in authenticated requests annotate the user's id on logs as `userId` and spans as `user.id`.
166
+
167
+ For an HTTP RPC server that serves only the local machine, wrap its HTTP effect with `localRpcGuard` from `@shivaedev/platform/rpc-server/local.ts`. Provide the native `HttpServerRequest` and `RpcSerialization` services around the returned effect, using the same serialization as the RPC server. No session provider is required.
168
+
169
+ By default, the guard returns HTTP 403 before running the effect unless the transport reports a loopback peer address, the Host is a loopback host, any Origin matches the HTTP Host, and the content type matches the serialization. An absent peer address is rejected. Real Node HTTP tests cover accepted local requests and rejected Origin, Host and content-type requests, including a DNS-rebinding Host on a loopback connection. The optional second argument, `localRpcGuard(effect, trustedProxy)`, enables the same address and range forwarding policy as `appHost`.
60
170
 
61
- ## Better Auth
171
+ The same module exports `loopbackOnly(handler)` for local page routes and `sameOrigin(request)` for an HTTP Origin check. `loopbackOnly` checks Host and any known peer address; it permits an absent peer address so embedded Fetch page handlers remain usable. It does not check Origin or content type.
62
172
 
63
- `effectPrismaAdapter` stores Better Auth data through the application's Effect
64
- Prisma database and runtime, including Better Auth transactions:
173
+ Declare expected refusals in the RPC's error schema:
65
174
 
66
175
  ```ts
67
- import { effectPrismaAdapter } from "@shivaedev/platform/better-auth/adapter.ts"
68
- import { betterAuth } from "better-auth"
176
+ import { Schema } from "effect";
177
+ import { Rpc } from "effect/rpc";
178
+ import { BadRequest, Conflict } from "@shivaedev/platform/errors/taxonomy.ts";
179
+
180
+ export const Rename = Rpc.make("Rename", {
181
+ error: Schema.Union([BadRequest, Conflict]),
182
+ payload: { name: Schema.String },
183
+ success: Schema.String,
184
+ });
185
+ ```
186
+
187
+ A handler fails with `new Conflict({ field: "name", message: "Name is taken" })`. Native RPC JSON decodes the rejection back to the error instance with its field. Taxonomy errors can be declared on an RPC with no authentication middleware. `rejectedField` reads either that instance or tagged JSON:
69
188
 
70
- const auth = betterAuth({
71
- database: effectPrismaAdapter(Database, runtime),
72
- })
189
+ ```ts
190
+ import { rejectedField } from "@shivaedev/platform/errors/rejected-field.ts";
191
+ import { Conflict } from "@shivaedev/platform/errors/taxonomy.ts";
192
+
193
+ const field = rejectedField(
194
+ new Conflict({ field: "name", message: "Name is taken" }),
195
+ );
73
196
  ```
74
197
 
75
- The adapter owns database translation only. Providers, plugins, cookies,
76
- session policy, and authorization remain application configuration. Native
77
- experimental Better Auth joins are not supported; the standard adapter join
78
- fallback remains available.
198
+ `field` is `Option.some({ field: "name", message: "Name is taken" })`. Any value with string `_tag`, `field` and `message` qualifies, including an application's own tagged error. An untagged object, a missing field or a non-string field yields `Option.none()`.
79
199
 
80
- ## Errors and native RPC request context
200
+ For tRPC, use these classes with the rejection schema and formatter from [effect-trpc](https://github.com/ShivaeDev/platform/tree/main/packages/effect-trpc). The composed HTTP boundary carries rejection data and maps statuses, including `TooManyRequests` as 429 and `AuthUnavailable` as 503.
81
201
 
82
- `@shivaedev/platform/errors/taxonomy.ts` exports browser-safe `Schema.TaggedError` classes:
83
- `NotFound`, `Unauthorized`, `Forbidden`, `BadRequest` and `Conflict` (both with an
84
- optional `field`), `PreconditionFailed`, `TooManyRequests` and `AuthUnavailable`.
85
- Use them as native RPC error schemas or effect-contract rejections.
86
- `rejectedField(error)` from `@shivaedev/platform/errors/rejected-field.ts` extracts `{ field, message }` from any field rejection so a
87
- form can show it.
202
+ ### Handling unexpected failures
88
203
 
89
- `@shivaedev/platform/rpc/middleware.ts` declares the `RequestTracing`, `Authenticated` and
90
- `MaybeAuthenticated` middleware, and `@shivaedev/platform/rpc/identity.ts` declares the
91
- `RequestId`, `Identity` and `OptionalIdentity` services they provide.
92
- These modules are browser-safe, so shared `RpcGroup` declarations can use them. Implement
93
- the middleware on the server with the modules under `@shivaedev/platform/rpc-server/`:
204
+ At the handler edge, `rpcBoundary(schema, options)` keeps declared rejections and maps other failures and defects into one declared fallback. It leaves successful values and interruption intact:
94
205
 
95
206
  ```ts
96
- import { Authenticated, RequestTracing } from "@shivaedev/platform/rpc/middleware.ts"
97
- import { authenticatedLayer } from "@shivaedev/platform/rpc-server/session.ts"
98
- import { betterAuthSessions } from "@shivaedev/platform/rpc-server/adapters/better-auth-sessions.ts"
99
- import { requestTracingLayer } from "@shivaedev/platform/rpc-server/tracing.ts"
100
- import { trustedOrigins } from "@shivaedev/platform/rpc-server/origin.ts"
101
-
102
- const Api = RpcGroup.make(/* ... */).middleware(Authenticated).middleware(RequestTracing)
103
-
104
- const Middleware = Layer.mergeAll(
105
- requestTracingLayer(),
106
- authenticatedLayer({
107
- provider: betterAuthSessions((headers) => auth.api.getSession({ headers })),
108
- origin: trustedOrigins({ allow: ["https://app.example"], missing: "reject" }),
207
+ import { Schema } from "effect";
208
+ import { Conflict, InternalServerError } from "@shivaedev/platform/errors/taxonomy.ts";
209
+ import { rpcBoundary } from "@shivaedev/platform/rpc-server/boundary.ts";
210
+
211
+ const Rejection = Schema.Union([Conflict, InternalServerError]);
212
+ const expose = rpcBoundary(Rejection, {
213
+ unexpected: (message) => new InternalServerError({ message }),
214
+ });
215
+
216
+ export const save = storageSave.pipe(expose);
217
+ ```
218
+
219
+ `storageSave` is the application's Effect. `unexpected` constructs a member of the declared schema from the public message. The default message is `Internal server error`; `message` supplies application text, with credential-shaped strings redacted. The original Effect cause is kept on the server rejection's non-enumerable `cause`, so an RPC server span can trace it. Keep `cause` out of the wire schema, as the taxonomy classes do. `ServiceUnavailable` describes a declared availability failure without implying a session-provider outage.
220
+
221
+ The native HTTP RPC fixture in `src/server/rpc-server/unauthenticatedRejections.spec.ts` proves decoded taxonomy types, redacted fallback messages and the original server trace cause without session middleware. Work Board's `src/server/rpc/rejectionTypes.spec.ts` proves its adoption through real loopback HTTP.
222
+
223
+ ### Request diagnostics
224
+
225
+ Tracing logs declared failures with `RPC failure` and their tag, and defects with `RPC defect`. The diagnostic payload masks nested credential-shaped keys and Effect `Redacted` values. Credential-shaped strings, such as bearer tokens, URL passwords and sensitive `key=value` text, are masked too. Strings retain at most 2,048 characters before a truncation marker; collections retain at most 50 entries with a marker for the remainder, and binary values become size summaries.
226
+
227
+ Defects are also redacted in the cause that reaches the client, server span and error reporters. An `Error` keeps its error shape with redacted fields and cause. Extend the sensitive-key rule explicitly:
228
+
229
+ ```ts
230
+ import { isSensitiveKey } from "@shivaedev/platform/rpc-server/sensitive.ts";
231
+ import { requestTracingLayer } from "@shivaedev/platform/rpc-server/tracing.ts";
232
+
233
+ export const Tracing = requestTracingLayer({
234
+ sensitive: (key) => isSensitiveKey(key) || key === "email",
235
+ });
236
+ ```
237
+
238
+ The example adds `email` to the default policy. For reporting outside RPC, `redactingErrorReporter(reporter)` wraps an Effect error reporter and passes it a redacted cause while preserving reporting hints such as severity and redacted attributes.
239
+
240
+ ### Promise callers: one application runtime
241
+
242
+ Build the application Layer once. `runWithServices` supplies an invocation's overrides to subsequent calls on the same runtime, across Promise awaits:
243
+
244
+ ```ts
245
+ import { Context, Layer } from "effect";
246
+ import { makePlatformRuntime } from "@shivaedev/platform/runtime/make.ts";
247
+
248
+ class Greeting extends Context.Service<Greeting, string>()("app/Greeting") {}
249
+
250
+ const runtime = makePlatformRuntime(Layer.succeed(Greeting, "application"));
251
+ const base = await runtime.runPromise(Greeting);
252
+ const request = await runtime.runWithServices(
253
+ Context.make(Greeting, "request"),
254
+ async () => {
255
+ await Promise.resolve();
256
+ return runtime.runPromise(Greeting);
257
+ },
258
+ );
259
+ await runtime.dispose();
260
+ ```
261
+
262
+ `base` is `"application"` and `request` is `"request"`. The application's resource Layer is acquired once across calls. An explicit `developmentCacheKey` shares a runtime between calls with that key during development. Each key belongs to one application runtime definition.
263
+
264
+ A nested `runWithServices` call replaces the outer invocation's overrides for
265
+ its callback. Merge the contexts explicitly when the nested call must retain
266
+ those overrides.
267
+
268
+ ### Node subscriptions: combine cancellation and clean up
269
+
270
+ A long-lived response can receive cancellation from a Web request, a procedure and its underlying Node connection:
271
+
272
+ ```ts
273
+ import type { IncomingMessage } from "node:http";
274
+ import { nodeSubscriptionSignal } from "@shivaedev/platform/node-http/subscription-signal.ts";
275
+
276
+ export async function consumeSubscription(
277
+ request: Request,
278
+ nodeRequest: IncomingMessage,
279
+ procedureSignal: AbortSignal,
280
+ consume: (signal: AbortSignal) => Promise<void>,
281
+ ) {
282
+ const subscription = nodeSubscriptionSignal({
283
+ nodeRequest,
284
+ request,
285
+ signals: [procedureSignal],
286
+ });
287
+ try {
288
+ await consume(subscription.signal);
289
+ } finally {
290
+ subscription.dispose();
291
+ }
292
+ }
293
+ ```
294
+
295
+ The signal aborts on an additional signal, socket close, incomplete request close or an already destroyed connection. Normal completion of a request does not abort a continuing response. Without an explicit `nodeRequest`, the helper can read one from an srvx Web request's `runtime.node.req`. `dispose()` removes Node listeners and can be called repeatedly.
296
+
297
+ ### Optional Better Auth storage on Effect Prisma
298
+
299
+ When Better Auth's rows belong to an Effect Prisma database, give its adapter the same database service and runtime:
300
+
301
+ ```ts
302
+ import { betterAuth } from "better-auth";
303
+ import { effectPrismaAdapter } from "@shivaedev/platform/better-auth/adapter.ts";
304
+ import { makePlatformRuntime } from "@shivaedev/platform/runtime/make.ts";
305
+ import { Database, DatabaseLive } from "./database.ts";
306
+
307
+ export const runtime = makePlatformRuntime(DatabaseLive);
308
+ export const auth = betterAuth({
309
+ database: effectPrismaAdapter(Database, runtime, {
310
+ modelName: (model) =>
311
+ `Auth${model.length === 0 ? model : `${model[0]?.toUpperCase()}${model.slice(1)}`}`,
109
312
  }),
110
- )
313
+ });
111
314
  ```
112
315
 
113
- Each RPC gets its own request id and resolves its own session. The Origin check
114
- and session lookup read the HTTP request's own headers, never header pairs a
115
- client puts inside an RPC message. An outage of the
116
- session provider fails with `AuthUnavailable`, never as a signed-out user.
117
- Failures log once with a redacted payload. The Origin policy is a function the
118
- application supplies; see the
119
- [request context guide](../../docs/framework/request-context.md) for the
120
- trade-offs between browsers and native clients.
316
+ The application's database module owns its generated contract and Layer. This mapping selects `AuthUser` for Better Auth's `user` model. The adapter translates create, read, update, count and delete operations through Effect Prisma. It carries a Better Auth transaction's services through its Promise callback; a failed callback rolls the transaction back.
317
+
318
+ An empty `update` filter returns null. Empty filters on `updateMany` and `deleteMany` affect all rows, so callers must choose those operations deliberately.
121
319
 
122
- ## Testing
320
+ ### In tests: one Prisma/tRPC composition
123
321
 
124
- Configure the application database, tRPC caller, and test Layer once:
322
+ Set up the application harness once in `test-support/`:
125
323
 
126
324
  ```ts
127
- import { makePlatformIt } from "@shivaedev/platform/testing/vitest.ts"
325
+ import { Effect } from "effect";
326
+ import { makePlatformIt } from "@shivaedev/platform/testing/vitest.ts";
327
+ import { adapter, Database, router, TestLive } from "./application.ts";
128
328
 
129
329
  export const it = makePlatformIt(Database)({
130
- adapter: effectTrpc,
131
- createCaller: (options = defaultActor) => appRouter.createCaller(options),
132
- layer: TestLive,
133
- extend: ({ db, trpc }) =>
330
+ adapter,
331
+ createCaller: (options = { actor: "default" }) => router.createCaller(options),
332
+ extend: ({ db }) =>
134
333
  Effect.succeed({
135
- actors,
136
- factories: makeFactories(db),
137
- fixtures: makeFixtures({ db, trpc }),
334
+ userExists: (id: string) => db.AuthUser.where({ id }).exists(),
138
335
  }),
139
- })
336
+ layer: TestLive,
337
+ });
140
338
  ```
141
339
 
142
- Every `effectApp` test runs inside a real transaction. tRPC procedures, direct
143
- database calls, factories, and fixtures share that transaction, which is rolled
144
- back after both successful and failed tests:
340
+ `application.ts` supplies the application's Effect tRPC adapter, generated database service, router and test Layer. The example router has `createUser` and `findUser` procedures, and its caller context has an `actor` string. `extend` adds application helpers alongside `db`, `trpc` and `promise`; types are inferred from the database, caller and extension.
341
+
342
+ Write each feature test against the caller and database:
145
343
 
146
344
  ```ts
147
- it.effectApp("creates a movie", function* ({ trpc, db, factories, promise }) {
148
- const input = factories.movie()
149
- const movie = yield* trpc.movie.create(input)
150
- const stored = yield* db.Movie.where({ id: movie.id }).first()
345
+ import { expect } from "@effect/vitest";
346
+ import { Option } from "effect";
347
+ import { it } from "./test-support/it.ts";
348
+
349
+ it.effectApp("stores the user's email", function* ({ db, trpc, userExists }) {
350
+ const id = crypto.randomUUID();
351
+ const input = { email: `${id}@example.test`, id, name: "Ada" };
352
+ const created = yield* trpc.createUser(input);
353
+ const stored = Option.getOrThrow(yield* db.AuthUser.where({ id }).first());
354
+
355
+ expect(stored.email).toBe(input.email);
356
+ expect(yield* userExists(created.id)).toBe(true);
357
+ });
358
+ ```
151
359
 
152
- expect(stored?.title).toBe(input.title)
153
- })
360
+ The direct database, tRPC caller and extension share the test's PostgreSQL transaction. Successful and expected-failing tests roll their writes back. `trpc({ actor: "admin" })` selects another caller context within the test. `promise(() => auth.api.getSession({ headers }))` runs a Promise boundary with the test's services; the Better Auth adapter tests use that helper to join the rollback transaction.
361
+
362
+ ### API
363
+
364
+ Import the module that defines the name; there is no package-root entry. Paths below follow `@shivaedev/platform/`.
365
+
366
+ | Module | Names and purpose |
367
+ | --- | --- |
368
+ | `appHost.ts` | `appHost({ group, handlers, services, migrations?, sessions?, policy?, rpcPath?, assets?, routes?, config? })`, `appDatabase({ migrations, config? })`, `AppChanges`, `AppHostOptions`, `AppHttpHostOptions`, `AppLocalHostOptions`, `AppDatabaseOptions`: scoped HTTP and PostgreSQL composition with loopback defaults and optional proxy trust. |
369
+ | `errors/taxonomy.ts` | `NotFound`, `Unauthorized`, `Forbidden`, `BadRequest`, `Conflict`, `PreconditionFailed`, `TooManyRequests`, `AuthUnavailable`, `InternalServerError`, `ServiceUnavailable`: Schema tagged error classes with a `message`; `BadRequest` and `Conflict` also accept an optional `field`. |
370
+ | `errors/rejected-field.ts` | `rejectedField(unknown): Option<RejectedField>`; `RejectedField` is `{ field: string, message: string }`. |
371
+ | `rpc/identity.ts` | `RequestId`: string service. `Identity`: `IdentityValue`, `{ id: string }`. `OptionalIdentity`: `Option<IdentityValue>`. |
372
+ | `rpc/middleware.ts` | `RequestTracing`, `Authenticated`, `MaybeAuthenticated`: native RPC middleware declarations. |
373
+ | `rpc-server/boundary.ts` | `rpcBoundary(schema, { unexpected, message? })`; `RpcBoundaryOptions<Error>`: declared failures pass through, unexpected failures become one declared rejection. |
374
+ | `rpc-server/session.ts` | `authenticatedLayer(policy)`, `maybeAuthenticatedLayer(policy)`, `resolveSession(policy, headers, rpc)`; `SessionShape`, `SessionProvider<Session>`, `SessionPolicy<Session>`. |
375
+ | `rpc-server/adapters/better-auth-sessions.ts` | `betterAuthSessions(getSession)`; `GetSession<Session>` is a Promise lookup returning `Session \| null`. |
376
+ | `rpc-server/origin.ts` | `trustedOrigins({ allow, missing })`, `originRequest(headers, rpc)`; `OriginPolicy`, `OriginRequest`, `TrustedOriginsOptions`. |
377
+ | `rpc-server/local.ts` | `localRpcGuard(effect)`: HTTP RPC guard using native request and serialization services; `loopbackOnly(handler)`, `sameOrigin(request)`. |
378
+ | `rpc-server/transport.ts` | `transportHeaders(rpcHeaders)`: Effect returning the headers used for a transport-level decision. |
379
+ | `rpc-server/tracing.ts` | `requestTracingLayer({ header?, sensitive? })`; `RequestTracingOptions`. |
380
+ | `rpc-server/redact.ts` | `redact(value, sensitive?)`: unknown diagnostic value. |
381
+ | `rpc-server/redact-cause.ts` | `redactCause(cause, sensitive?)`, `redactDefect(defect, sensitive?)`, `redactingErrorReporter(reporter, sensitive?)`. |
382
+ | `rpc-server/sensitive.ts` | `isSensitiveKey`, `redactText`, `REDACTED`; `SensitiveKey` is `(key: string) => boolean`. |
383
+ | `runtime/make.ts` | `makePlatformRuntime(layer, { developmentCacheKey? })`. |
384
+ | `runtime/types.ts` | `PlatformRuntime<Services, BuildError>`, `PlatformRuntimeOptions`, `PlatformRuntimeServices<Runtime>`, `PlatformRuntimeBuildError<Runtime>`. |
385
+ | `node-http/subscription-signal.ts` | `nodeSubscriptionSignal({ nodeRequest?, request?, signals? })` returns `{ signal, dispose }`; `NodeSubscriptionSignalOptions`, `NodeSubscriptionSignal`. |
386
+ | `better-auth/adapter.ts` | `effectPrismaAdapter(Database, runtime, { debugLogs?, modelName?, usePlural? })`; `EffectPrismaAdapterOptions`. |
387
+ | `testing/vitest.ts` | `makePlatformIt(Database)({ adapter, createCaller, layer, extend? })`. |
388
+ | `testing/types.ts` | `MakePlatformItOptions`, `PlatformHarness`, `PlatformIt`, `PlatformTest`, `PlatformTester`. |
389
+
390
+ `PlatformRuntime` exposes `runPromise(effect, { signal? })`, `runPromiseExit(effect, { signal? })`, `runWithServices(context, evaluate)`, `contextEffect`, `currentServices()` and `dispose()`. The harness exposes native Vitest methods plus `effectApp(name, body, options?)`; its tester also has `skip`, `skipIf`, `runIf`, `only`, `each` and `fails`.
391
+
392
+ The exports also include the adapter's translation modules: `makeRowAdapter` and `RelationQuery` in `better-auth/row-adapter.ts`, `refineRelation` and `DynamicRelation` in `better-auth/relation.ts`, and `whereExpression`, `CleanedWhere`, `DynamicField` and `Expression` in `better-auth/where.ts`. Application storage setup uses `effectPrismaAdapter`.
393
+
394
+ ### Install, setup and limits
395
+
396
+ ```sh
397
+ pnpm add @shivaedev/platform effect@4.0.4
154
398
  ```
155
399
 
156
- `promise(() => ...)` runs a promise-based application boundary, such as Better
157
- Auth or an HTTP handler, with the same transaction-scoped Effect services.
400
+ Effect is the only required peer. The peer-free implementation lives in the shared `src/errors/` and `src/rpc/` folders and the `src/server/node-http/`, `src/server/rpc-server/` and `src/server/runtime/` folders; explicit exports retain the defining-module import paths listed above. The package-local `quality.fences.config.ts` prevents core from reaching optional peers listed in the manifest. The core errors, RPC declarations, server middleware, runtime and Node HTTP modules can be imported without the optional peers, including the Promise-based `betterAuthSessions` function. The runtime uses Node's `AsyncLocalStorage`; browser consumers import the shared `errors/` and `rpc/` modules.
401
+
402
+ Install the optional peers for the selected integration:
403
+
404
+ | Integration | Additional peers |
405
+ | --- | --- |
406
+ | `appHost.ts` | `@effect/platform-node`, `@effect/sql-pg` |
407
+ | `better-auth/adapter.ts` | `better-auth`, `@shivaedev/effect-prisma`, `@prisma-next/sql-orm-client` |
408
+ | `testing/vitest.ts` | `@effect/vitest`, `@shivaedev/effect-prisma`, `@shivaedev/effect-trpc`, `@trpc/server` |
409
+
410
+ - The package declares Node 24 or newer and uses the Effect version in its peer manifest.
411
+ - Keep the auth provider's configuration, cookie settings and resource authorization in the application. The session adapter takes only a lookup function and returns a session result; it does not write provider refresh cookies into RPC responses.
412
+ - Without an `HttpServerRequest`, `transportHeaders` falls back to the supplied RPC headers. An application using another transport must decide what headers it can trust.
413
+ - Add tracing last on the native group, as in the example, so it wraps the authentication middleware too. Middleware outside that tracing boundary is responsible for its own diagnostics.
414
+ - Redaction recognizes common shapes. Keep secrets out of messages and extend the key predicate for application-specific sensitive fields.
415
+ - A development cache key is ignored when `NODE_ENV` is `production`. The application owns runtime disposal.
416
+ - The Better Auth storage adapter's default model mapping capitalizes the model name. Supply `modelName` for a generated schema with another naming convention. Experimental native Better Auth joins are rejected.
417
+ - Package session tests use real Better Auth with ephemeral SQLite and injected HTTP; the WebSocket test uses an in-memory socket. Node signal tests simulate close events. These checks do not establish deployed browser, mobile or Bun behavior.
418
+ - The storage and harness tests require real PostgreSQL and skip without `PLATFORM_EFFECT_PRISMA_TEST_DATABASE_URL`. A skipped test supplies no transaction evidence.
158
419
 
159
- Calling `trpc(options)` creates a caller for another application actor without
160
- rebuilding the worker-scoped test Layer. `effectApp` also supports `skip`,
161
- `skipIf`, `runIf`, `only`, `each`, and `fails`.
420
+ For the purpose and trade-offs, read [the north star](https://github.com/ShivaeDev/platform/blob/main/packages/platform/docs/north-star.md). Implementation status and maintainer decisions live in [the package roadmap](https://github.com/ShivaeDev/platform/blob/main/packages/platform/docs/roadmap.md). The [framework request-context guide](https://github.com/ShivaeDev/platform/blob/main/docs/framework/request-context.md) explains the browser and native-client Origin choices.
@@ -0,0 +1,7 @@
1
+ import { Option } from "effect";
2
+ export interface RejectedField {
3
+ readonly field: string;
4
+ readonly message: string;
5
+ }
6
+ export declare function rejectedField(error: unknown): Option.Option<RejectedField>;
7
+ //# sourceMappingURL=rejectedField.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rejectedField.d.ts","sourceRoot":"","sources":["../../src/errors/rejectedField.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAU,MAAM,QAAQ,CAAC;AACxC,MAAM,WAAW,aAAa;IAC7B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CACzB;AAGD,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAAC,MAAM,CAAC,aAAa,CAAC,CAE1E"}
@@ -1,5 +1,7 @@
1
1
  import { Option, Schema } from "effect";
2
2
  const Rejected = Schema.Struct({ _tag: Schema.String, field: Schema.String, message: Schema.String });
3
3
  const decode = Schema.decodeUnknownOption(Rejected);
4
- export const rejectedField = (error) => Option.map(decode(error), ({ field, message }) => ({ field, message }));
5
- //# sourceMappingURL=rejected-field.js.map
4
+ export function rejectedField(error) {
5
+ return Option.map(decode(error), ({ field, message }) => ({ field, message }));
6
+ }
7
+ //# sourceMappingURL=rejectedField.js.map