@shivaedev/platform 0.6.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (177) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +361 -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.map +1 -1
  14. package/dist/rpc/middleware.js.map +1 -1
  15. package/dist/server/appHost.d.ts +56 -0
  16. package/dist/server/appHost.d.ts.map +1 -0
  17. package/dist/server/appHost.js +55 -0
  18. package/dist/server/appHost.js.map +1 -0
  19. package/dist/{better-auth → server/better-auth}/adapter.d.ts +1 -1
  20. package/dist/server/better-auth/adapter.d.ts.map +1 -0
  21. package/dist/{better-auth → server/better-auth}/adapter.js +26 -14
  22. package/dist/server/better-auth/adapter.js.map +1 -0
  23. package/dist/{better-auth → server/better-auth}/relation.d.ts +15 -15
  24. package/dist/server/better-auth/relation.d.ts.map +1 -0
  25. package/dist/{better-auth → server/better-auth}/relation.js +2 -2
  26. package/dist/server/better-auth/relation.js.map +1 -0
  27. package/dist/server/better-auth/rowAdapter.d.ts +8 -0
  28. package/dist/server/better-auth/rowAdapter.d.ts.map +1 -0
  29. package/dist/server/better-auth/rowAdapter.js +52 -0
  30. package/dist/server/better-auth/rowAdapter.js.map +1 -0
  31. package/dist/{better-auth → server/better-auth}/where.d.ts +1 -1
  32. package/dist/server/better-auth/where.d.ts.map +1 -0
  33. package/dist/{better-auth → server/better-auth}/where.js +22 -18
  34. package/dist/server/better-auth/where.js.map +1 -0
  35. package/dist/{node-http/subscription-signal.d.ts → server/node-http/subscriptionSignal.d.ts} +2 -2
  36. package/dist/server/node-http/subscriptionSignal.d.ts.map +1 -0
  37. package/dist/{node-http/subscription-signal.js → server/node-http/subscriptionSignal.js} +20 -13
  38. package/dist/server/node-http/subscriptionSignal.js.map +1 -0
  39. package/dist/server/rpc-server/adapters/betterAuthSessions.d.ts +4 -0
  40. package/dist/server/rpc-server/adapters/betterAuthSessions.d.ts.map +1 -0
  41. package/dist/server/rpc-server/adapters/betterAuthSessions.js +14 -0
  42. package/dist/server/rpc-server/adapters/betterAuthSessions.js.map +1 -0
  43. package/dist/server/rpc-server/boundary.d.ts +9 -0
  44. package/dist/server/rpc-server/boundary.d.ts.map +1 -0
  45. package/dist/server/rpc-server/boundary.js +18 -0
  46. package/dist/server/rpc-server/boundary.js.map +1 -0
  47. package/dist/server/rpc-server/local.d.ts +7 -0
  48. package/dist/server/rpc-server/local.d.ts.map +1 -0
  49. package/dist/server/rpc-server/local.js +66 -0
  50. package/dist/server/rpc-server/local.js.map +1 -0
  51. package/dist/{rpc-server → server/rpc-server}/origin.d.ts +2 -2
  52. package/dist/server/rpc-server/origin.d.ts.map +1 -0
  53. package/dist/server/rpc-server/origin.js +16 -0
  54. package/dist/server/rpc-server/origin.js.map +1 -0
  55. package/dist/server/rpc-server/redact.d.ts +3 -0
  56. package/dist/server/rpc-server/redact.d.ts.map +1 -0
  57. package/dist/{rpc-server → server/rpc-server}/redact.js +9 -7
  58. package/dist/server/rpc-server/redact.js.map +1 -0
  59. package/dist/server/rpc-server/redactCause.d.ts +6 -0
  60. package/dist/server/rpc-server/redactCause.d.ts.map +1 -0
  61. package/dist/server/rpc-server/redactCause.js +36 -0
  62. package/dist/server/rpc-server/redactCause.js.map +1 -0
  63. package/dist/server/rpc-server/sensitive.d.ts +5 -0
  64. package/dist/server/rpc-server/sensitive.d.ts.map +1 -0
  65. package/dist/{rpc-server → server/rpc-server}/sensitive.js +21 -17
  66. package/dist/server/rpc-server/sensitive.js.map +1 -0
  67. package/dist/server/rpc-server/session.d.ts +21 -0
  68. package/dist/server/rpc-server/session.d.ts.map +1 -0
  69. package/dist/server/rpc-server/session.js +37 -0
  70. package/dist/server/rpc-server/session.js.map +1 -0
  71. package/dist/{rpc-server → server/rpc-server}/tracing.d.ts +1 -1
  72. package/dist/server/rpc-server/tracing.d.ts.map +1 -0
  73. package/dist/server/rpc-server/tracing.js +42 -0
  74. package/dist/server/rpc-server/tracing.js.map +1 -0
  75. package/dist/server/rpc-server/transportHeaders.d.ts +4 -0
  76. package/dist/server/rpc-server/transportHeaders.d.ts.map +1 -0
  77. package/dist/server/rpc-server/transportHeaders.js +6 -0
  78. package/dist/server/rpc-server/transportHeaders.js.map +1 -0
  79. package/dist/server/runtime/makePlatform.d.ts +4 -0
  80. package/dist/server/runtime/makePlatform.d.ts.map +1 -0
  81. package/dist/{runtime/make.js → server/runtime/makePlatform.js} +10 -8
  82. package/dist/server/runtime/makePlatform.js.map +1 -0
  83. package/dist/server/runtime/types.d.ts +19 -0
  84. package/dist/server/runtime/types.d.ts.map +1 -0
  85. package/dist/{runtime → server/runtime}/types.js.map +1 -1
  86. package/dist/server/testing/makePlatformIt.d.ts +8 -0
  87. package/dist/server/testing/makePlatformIt.d.ts.map +1 -0
  88. package/dist/{testing/vitest.js → server/testing/makePlatformIt.js} +7 -3
  89. package/dist/server/testing/makePlatformIt.js.map +1 -0
  90. package/dist/server/testing/types.d.ts +28 -0
  91. package/dist/server/testing/types.d.ts.map +1 -0
  92. package/dist/{testing → server/testing}/types.js.map +1 -1
  93. package/docs/north-star.md +36 -0
  94. package/docs/roadmap.md +30 -0
  95. package/package.json +317 -16
  96. package/src/errors/{rejected-field.ts → rejectedField.ts} +3 -6
  97. package/src/errors/taxonomy.ts +2 -8
  98. package/src/rpc/identity.ts +0 -4
  99. package/src/rpc/middleware.ts +18 -9
  100. package/src/server/appHost.ts +134 -0
  101. package/src/{better-auth → server/better-auth}/adapter.ts +26 -30
  102. package/src/{better-auth → server/better-auth}/relation.ts +17 -19
  103. package/src/{better-auth/row-adapter.ts → server/better-auth/rowAdapter.ts} +19 -21
  104. package/src/{better-auth → server/better-auth}/where.ts +22 -32
  105. package/src/{node-http/subscription-signal.ts → server/node-http/subscriptionSignal.ts} +19 -22
  106. package/src/{rpc-server/adapters/better-auth-sessions.ts → server/rpc-server/adapters/betterAuthSessions.ts} +9 -10
  107. package/src/server/rpc-server/boundary.ts +26 -0
  108. package/src/server/rpc-server/local.ts +70 -0
  109. package/src/{rpc-server → server/rpc-server}/origin.ts +10 -13
  110. package/src/{rpc-server → server/rpc-server}/redact.ts +9 -11
  111. package/src/server/rpc-server/redactCause.ts +40 -0
  112. package/src/{rpc-server → server/rpc-server}/sensitive.ts +17 -22
  113. package/src/{rpc-server → server/rpc-server}/session.ts +22 -23
  114. package/src/{rpc-server → server/rpc-server}/tracing.ts +15 -18
  115. package/src/{rpc-server/transport.ts → server/rpc-server/transportHeaders.ts} +3 -3
  116. package/src/{runtime/make.ts → server/runtime/makePlatform.ts} +18 -26
  117. package/src/server/runtime/types.ts +24 -0
  118. package/src/{testing/vitest.ts → server/testing/makePlatformIt.ts} +27 -23
  119. package/src/server/testing/types.ts +38 -0
  120. package/dist/better-auth/adapter.d.ts.map +0 -1
  121. package/dist/better-auth/adapter.js.map +0 -1
  122. package/dist/better-auth/relation.d.ts.map +0 -1
  123. package/dist/better-auth/relation.js.map +0 -1
  124. package/dist/better-auth/row-adapter.d.ts +0 -8
  125. package/dist/better-auth/row-adapter.d.ts.map +0 -1
  126. package/dist/better-auth/row-adapter.js +0 -46
  127. package/dist/better-auth/row-adapter.js.map +0 -1
  128. package/dist/better-auth/where.d.ts.map +0 -1
  129. package/dist/better-auth/where.js.map +0 -1
  130. package/dist/errors/rejected-field.d.ts +0 -7
  131. package/dist/errors/rejected-field.d.ts.map +0 -1
  132. package/dist/errors/rejected-field.js.map +0 -1
  133. package/dist/node-http/subscription-signal.d.ts.map +0 -1
  134. package/dist/node-http/subscription-signal.js.map +0 -1
  135. package/dist/rpc-server/adapters/better-auth-sessions.d.ts +0 -4
  136. package/dist/rpc-server/adapters/better-auth-sessions.d.ts.map +0 -1
  137. package/dist/rpc-server/adapters/better-auth-sessions.js +0 -11
  138. package/dist/rpc-server/adapters/better-auth-sessions.js.map +0 -1
  139. package/dist/rpc-server/origin.d.ts.map +0 -1
  140. package/dist/rpc-server/origin.js +0 -12
  141. package/dist/rpc-server/origin.js.map +0 -1
  142. package/dist/rpc-server/redact-cause.d.ts +0 -6
  143. package/dist/rpc-server/redact-cause.d.ts.map +0 -1
  144. package/dist/rpc-server/redact-cause.js +0 -28
  145. package/dist/rpc-server/redact-cause.js.map +0 -1
  146. package/dist/rpc-server/redact.d.ts +0 -3
  147. package/dist/rpc-server/redact.d.ts.map +0 -1
  148. package/dist/rpc-server/redact.js.map +0 -1
  149. package/dist/rpc-server/sensitive.d.ts +0 -5
  150. package/dist/rpc-server/sensitive.d.ts.map +0 -1
  151. package/dist/rpc-server/sensitive.js.map +0 -1
  152. package/dist/rpc-server/session.d.ts +0 -21
  153. package/dist/rpc-server/session.d.ts.map +0 -1
  154. package/dist/rpc-server/session.js +0 -31
  155. package/dist/rpc-server/session.js.map +0 -1
  156. package/dist/rpc-server/tracing.d.ts.map +0 -1
  157. package/dist/rpc-server/tracing.js +0 -34
  158. package/dist/rpc-server/tracing.js.map +0 -1
  159. package/dist/rpc-server/transport.d.ts +0 -4
  160. package/dist/rpc-server/transport.d.ts.map +0 -1
  161. package/dist/rpc-server/transport.js +0 -4
  162. package/dist/rpc-server/transport.js.map +0 -1
  163. package/dist/runtime/make.d.ts +0 -4
  164. package/dist/runtime/make.d.ts.map +0 -1
  165. package/dist/runtime/make.js.map +0 -1
  166. package/dist/runtime/types.d.ts +0 -19
  167. package/dist/runtime/types.d.ts.map +0 -1
  168. package/dist/testing/types.d.ts +0 -28
  169. package/dist/testing/types.d.ts.map +0 -1
  170. package/dist/testing/vitest.d.ts +0 -8
  171. package/dist/testing/vitest.d.ts.map +0 -1
  172. package/dist/testing/vitest.js.map +0 -1
  173. package/src/rpc-server/redact-cause.ts +0 -41
  174. package/src/runtime/types.ts +0 -24
  175. package/src/testing/types.ts +0 -43
  176. /package/dist/{runtime → server/runtime}/types.js +0 -0
  177. /package/dist/{testing → server/testing}/types.js +0 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,28 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.6.1 - 2026-10-12
4
+
5
+ ### Added
6
+
7
+ - `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.
8
+ - `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.
9
+ - `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.
10
+ - Export `InternalServerError` and `ServiceUnavailable` Schema errors from `errors/taxonomy.ts` for RPC contracts without authentication middleware.
11
+ - 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.
12
+
13
+ ### Changed
14
+
15
+ - Pin the optional `better-auth` peer dependency to 1.6.33, the current 1.6
16
+ release, up from 1.6.30.
17
+ - 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.
18
+ - Prefix testing harness type parameters with `T`; their constraints, defaults and inferred types stay the same.
19
+ - Document the package purpose, request boundaries, integrations and API with agent guidance, a north star and a roadmap; publish `docs/` alongside the README.
20
+ - Build package JavaScript, declarations and source maps with TypeScript 7.
21
+
22
+ ### Fixed
23
+
24
+ - Declare `@shivaedev/types` as a dependency of the shipped testing entry point instead of a development-only dependency.
25
+
3
26
  ## 0.6.0 - 2026-10-04
4
27
 
5
28
  ### Changed
package/README.md CHANGED
@@ -1,161 +1,410 @@
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.
18
37
 
19
- ## Runtime
38
+ ### Host the application
20
39
 
21
- Build one managed runtime for the application and share it with integrations:
40
+ `appHost` is the server entry point 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.
22
41
 
23
42
  ```ts
24
- import { makePlatformRuntime } from "@shivaedev/platform/runtime/make.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
+ 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`, `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.
63
+
64
+ 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. 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.
65
+
66
+ For a deployment behind a proxy, set `HOST=0.0.0.0` and `TRUSTED_PROXY` to the proxy's exact peer IP, or use `config.host` and `config.trustedProxy`. Only that peer 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. 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).
25
67
 
26
- export const runtime = makePlatformRuntime(ApplicationLive, {
27
- developmentCacheKey: "application",
28
- })
68
+ 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.
69
+
70
+ 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.
71
+
72
+ ### 1. Once per application: contracts and session policy
73
+
74
+ Declare the RPC group in a shared module:
75
+
76
+ ```ts
77
+ import { Effect, Schema } from "effect";
78
+ import { Rpc, RpcGroup } from "effect/unstable/rpc";
79
+ import { Forbidden } from "@shivaedev/platform/errors/taxonomy.ts";
80
+ import { Identity } from "@shivaedev/platform/rpc/identity.ts";
81
+ import { Authenticated, RequestTracing } from "@shivaedev/platform/rpc/middleware.ts";
82
+
83
+ export const Account = RpcGroup.make(
84
+ Rpc.make("ReadOwn", {
85
+ error: Forbidden,
86
+ payload: { userId: Schema.String },
87
+ success: Schema.String,
88
+ }),
89
+ ).middleware(Authenticated).middleware(RequestTracing);
90
+
91
+ export const Handlers = Account.toLayer({
92
+ ReadOwn: ({ userId }) =>
93
+ Effect.gen(function* () {
94
+ const identity = yield* Identity;
95
+ if (identity.id !== userId) {
96
+ return yield* new Forbidden({ message: "Not your account" });
97
+ }
98
+ return identity.id;
99
+ }),
100
+ });
101
+ ```
102
+
103
+ 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.
104
+
105
+ Supply a provider and an explicit Origin policy as the host's `sessions` input. For a Better Auth provider, adapt its existing session lookup:
106
+
107
+ ```ts
108
+ import { betterAuthSessions } from "@shivaedev/platform/rpc-server/adapters/better-auth-sessions.ts";
109
+ import { trustedOrigins } from "@shivaedev/platform/rpc-server/origin.ts";
110
+ import { auth } from "./auth.ts";
111
+
112
+ export const sessions = {
113
+ origin: trustedOrigins({ allow: ["http://localhost:3000"], missing: "allow" }),
114
+ provider: betterAuthSessions((headers) => auth.api.getSession({ headers })),
115
+ };
29
116
  ```
30
117
 
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.
118
+ `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.
119
+
120
+ 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.
121
+
122
+ ### Transport headers and request ids
123
+
124
+ 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.
34
125
 
35
- ## Node HTTP
126
+ 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`.
36
127
 
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:
128
+ Caller-chosen request ids are correlation data. Keep authorization tied to the authenticated identity.
129
+
130
+ ### 2. Once per feature: identity and typed refusals
131
+
132
+ Use `Authenticated` when the operation needs `Identity`, and `MaybeAuthenticated` with `maybeAuthenticatedLayer(policy)` when it reads `OptionalIdentity`:
41
133
 
42
134
  ```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
- }
135
+ import { Effect, Option, Schema } from "effect";
136
+ import { Rpc, RpcGroup } from "effect/unstable/rpc";
137
+ import { OptionalIdentity } from "@shivaedev/platform/rpc/identity.ts";
138
+ import { MaybeAuthenticated } from "@shivaedev/platform/rpc/middleware.ts";
139
+
140
+ export const Greetings = RpcGroup.make(
141
+ Rpc.make("Greeting", { success: Schema.String }),
142
+ ).middleware(MaybeAuthenticated);
143
+
144
+ export const GreetingHandlers = Greetings.toLayer({
145
+ Greeting: () =>
146
+ Effect.map(OptionalIdentity, (identity) =>
147
+ Option.match(identity, {
148
+ onNone: () => "hello anonymous",
149
+ onSome: ({ id }) => `hello ${id}`,
150
+ }),
151
+ ),
152
+ });
153
+ ```
154
+
155
+ 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`.
156
+
157
+ 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.
158
+
159
+ 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 exact-peer forwarding policy as `appHost`.
160
+
161
+ 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.
162
+
163
+ Declare expected refusals in the RPC's error schema:
164
+
165
+ ```ts
166
+ import { Schema } from "effect";
167
+ import { Rpc } from "effect/unstable/rpc";
168
+ import { BadRequest, Conflict } from "@shivaedev/platform/errors/taxonomy.ts";
169
+
170
+ export const Rename = Rpc.make("Rename", {
171
+ error: Schema.Union([BadRequest, Conflict]),
172
+ payload: { name: Schema.String },
173
+ success: Schema.String,
174
+ });
175
+ ```
176
+
177
+ 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:
178
+
179
+ ```ts
180
+ import { rejectedField } from "@shivaedev/platform/errors/rejected-field.ts";
181
+ import { Conflict } from "@shivaedev/platform/errors/taxonomy.ts";
182
+
183
+ const field = rejectedField(
184
+ new Conflict({ field: "name", message: "Name is taken" }),
185
+ );
56
186
  ```
57
187
 
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.
188
+ `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()`.
189
+
190
+ 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.
60
191
 
61
- ## Better Auth
192
+ ### Handling unexpected failures
62
193
 
63
- `effectPrismaAdapter` stores Better Auth data through the application's Effect
64
- Prisma database and runtime, including Better Auth transactions:
194
+ 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:
65
195
 
66
196
  ```ts
67
- import { effectPrismaAdapter } from "@shivaedev/platform/better-auth/adapter.ts"
68
- import { betterAuth } from "better-auth"
197
+ import { Schema } from "effect";
198
+ import { Conflict, InternalServerError } from "@shivaedev/platform/errors/taxonomy.ts";
199
+ import { rpcBoundary } from "@shivaedev/platform/rpc-server/boundary.ts";
69
200
 
70
- const auth = betterAuth({
71
- database: effectPrismaAdapter(Database, runtime),
72
- })
201
+ const Rejection = Schema.Union([Conflict, InternalServerError]);
202
+ const expose = rpcBoundary(Rejection, {
203
+ unexpected: (message) => new InternalServerError({ message }),
204
+ });
205
+
206
+ export const save = storageSave.pipe(expose);
73
207
  ```
74
208
 
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.
209
+ `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.
210
+
211
+ 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.
79
212
 
80
- ## Errors and native RPC request context
213
+ ### Request diagnostics
81
214
 
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.
215
+ 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.
88
216
 
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/`:
217
+ 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:
94
218
 
95
219
  ```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" }),
220
+ import { isSensitiveKey } from "@shivaedev/platform/rpc-server/sensitive.ts";
221
+ import { requestTracingLayer } from "@shivaedev/platform/rpc-server/tracing.ts";
222
+
223
+ export const Tracing = requestTracingLayer({
224
+ sensitive: (key) => isSensitiveKey(key) || key === "email",
225
+ });
226
+ ```
227
+
228
+ 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.
229
+
230
+ ### Promise callers: one application runtime
231
+
232
+ Build the application Layer once. `runWithServices` supplies an invocation's overrides to subsequent calls on the same runtime, across Promise awaits:
233
+
234
+ ```ts
235
+ import { Context, Layer } from "effect";
236
+ import { makePlatformRuntime } from "@shivaedev/platform/runtime/make.ts";
237
+
238
+ class Greeting extends Context.Service<Greeting, string>()("app/Greeting") {}
239
+
240
+ const runtime = makePlatformRuntime(Layer.succeed(Greeting, "application"));
241
+ const base = await runtime.runPromise(Greeting);
242
+ const request = await runtime.runWithServices(
243
+ Context.make(Greeting, "request"),
244
+ async () => {
245
+ await Promise.resolve();
246
+ return runtime.runPromise(Greeting);
247
+ },
248
+ );
249
+ await runtime.dispose();
250
+ ```
251
+
252
+ `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.
253
+
254
+ A nested `runWithServices` call replaces the outer invocation's overrides for
255
+ its callback. Merge the contexts explicitly when the nested call must retain
256
+ those overrides.
257
+
258
+ ### Node subscriptions: combine cancellation and clean up
259
+
260
+ A long-lived response can receive cancellation from a Web request, a procedure and its underlying Node connection:
261
+
262
+ ```ts
263
+ import type { IncomingMessage } from "node:http";
264
+ import { nodeSubscriptionSignal } from "@shivaedev/platform/node-http/subscription-signal.ts";
265
+
266
+ export async function consumeSubscription(
267
+ request: Request,
268
+ nodeRequest: IncomingMessage,
269
+ procedureSignal: AbortSignal,
270
+ consume: (signal: AbortSignal) => Promise<void>,
271
+ ) {
272
+ const subscription = nodeSubscriptionSignal({
273
+ nodeRequest,
274
+ request,
275
+ signals: [procedureSignal],
276
+ });
277
+ try {
278
+ await consume(subscription.signal);
279
+ } finally {
280
+ subscription.dispose();
281
+ }
282
+ }
283
+ ```
284
+
285
+ 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.
286
+
287
+ ### Optional Better Auth storage on Effect Prisma
288
+
289
+ When Better Auth's rows belong to an Effect Prisma database, give its adapter the same database service and runtime:
290
+
291
+ ```ts
292
+ import { betterAuth } from "better-auth";
293
+ import { effectPrismaAdapter } from "@shivaedev/platform/better-auth/adapter.ts";
294
+ import { makePlatformRuntime } from "@shivaedev/platform/runtime/make.ts";
295
+ import { Database, DatabaseLive } from "./database.ts";
296
+
297
+ export const runtime = makePlatformRuntime(DatabaseLive);
298
+ export const auth = betterAuth({
299
+ database: effectPrismaAdapter(Database, runtime, {
300
+ modelName: (model) =>
301
+ `Auth${model.length === 0 ? model : `${model[0]?.toUpperCase()}${model.slice(1)}`}`,
109
302
  }),
110
- )
303
+ });
111
304
  ```
112
305
 
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.
306
+ 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.
307
+
308
+ An empty `update` filter returns null. Empty filters on `updateMany` and `deleteMany` affect all rows, so callers must choose those operations deliberately.
121
309
 
122
- ## Testing
310
+ ### In tests: one Prisma/tRPC composition
123
311
 
124
- Configure the application database, tRPC caller, and test Layer once:
312
+ Set up the application harness once in `test-support/`:
125
313
 
126
314
  ```ts
127
- import { makePlatformIt } from "@shivaedev/platform/testing/vitest.ts"
315
+ import { Effect } from "effect";
316
+ import { makePlatformIt } from "@shivaedev/platform/testing/vitest.ts";
317
+ import { adapter, Database, router, TestLive } from "./application.ts";
128
318
 
129
319
  export const it = makePlatformIt(Database)({
130
- adapter: effectTrpc,
131
- createCaller: (options = defaultActor) => appRouter.createCaller(options),
132
- layer: TestLive,
133
- extend: ({ db, trpc }) =>
320
+ adapter,
321
+ createCaller: (options = { actor: "default" }) => router.createCaller(options),
322
+ extend: ({ db }) =>
134
323
  Effect.succeed({
135
- actors,
136
- factories: makeFactories(db),
137
- fixtures: makeFixtures({ db, trpc }),
324
+ userExists: (id: string) => db.AuthUser.where({ id }).exists(),
138
325
  }),
139
- })
326
+ layer: TestLive,
327
+ });
140
328
  ```
141
329
 
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:
330
+ `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.
331
+
332
+ Write each feature test against the caller and database:
145
333
 
146
334
  ```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()
335
+ import { expect } from "@effect/vitest";
336
+ import { Option } from "effect";
337
+ import { it } from "./test-support/it.ts";
338
+
339
+ it.effectApp("stores the user's email", function* ({ db, trpc, userExists }) {
340
+ const id = crypto.randomUUID();
341
+ const input = { email: `${id}@example.test`, id, name: "Ada" };
342
+ const created = yield* trpc.createUser(input);
343
+ const stored = Option.getOrThrow(yield* db.AuthUser.where({ id }).first());
344
+
345
+ expect(stored.email).toBe(input.email);
346
+ expect(yield* userExists(created.id)).toBe(true);
347
+ });
348
+ ```
151
349
 
152
- expect(stored?.title).toBe(input.title)
153
- })
350
+ 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.
351
+
352
+ ### API
353
+
354
+ Import the module that defines the name; there is no package-root entry. Paths below follow `@shivaedev/platform/`.
355
+
356
+ | Module | Names and purpose |
357
+ | --- | --- |
358
+ | `appHost.ts` | `appHost({ group, handlers, services, migrations, sessions, assets?, config? })`, `appDatabase({ migrations, config? })`, `AppChanges`, `AppHostOptions`, `AppDatabaseOptions`: scoped HTTP and PostgreSQL composition with loopback defaults and optional proxy trust. |
359
+ | `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`. |
360
+ | `errors/rejected-field.ts` | `rejectedField(unknown): Option<RejectedField>`; `RejectedField` is `{ field: string, message: string }`. |
361
+ | `rpc/identity.ts` | `RequestId`: string service. `Identity`: `IdentityValue`, `{ id: string }`. `OptionalIdentity`: `Option<IdentityValue>`. |
362
+ | `rpc/middleware.ts` | `RequestTracing`, `Authenticated`, `MaybeAuthenticated`: native RPC middleware declarations. |
363
+ | `rpc-server/boundary.ts` | `rpcBoundary(schema, { unexpected, message? })`; `RpcBoundaryOptions<Error>`: declared failures pass through, unexpected failures become one declared rejection. |
364
+ | `rpc-server/session.ts` | `authenticatedLayer(policy)`, `maybeAuthenticatedLayer(policy)`, `resolveSession(policy, headers, rpc)`; `SessionShape`, `SessionProvider<Session>`, `SessionPolicy<Session>`. |
365
+ | `rpc-server/adapters/better-auth-sessions.ts` | `betterAuthSessions(getSession)`; `GetSession<Session>` is a Promise lookup returning `Session \| null`. |
366
+ | `rpc-server/origin.ts` | `trustedOrigins({ allow, missing })`, `originRequest(headers, rpc)`; `OriginPolicy`, `OriginRequest`, `TrustedOriginsOptions`. |
367
+ | `rpc-server/local.ts` | `localRpcGuard(effect)`: HTTP RPC guard using native request and serialization services; `loopbackOnly(handler)`, `sameOrigin(request)`. |
368
+ | `rpc-server/transport.ts` | `transportHeaders(rpcHeaders)`: Effect returning the headers used for a transport-level decision. |
369
+ | `rpc-server/tracing.ts` | `requestTracingLayer({ header?, sensitive? })`; `RequestTracingOptions`. |
370
+ | `rpc-server/redact.ts` | `redact(value, sensitive?)`: unknown diagnostic value. |
371
+ | `rpc-server/redact-cause.ts` | `redactCause(cause, sensitive?)`, `redactDefect(defect, sensitive?)`, `redactingErrorReporter(reporter, sensitive?)`. |
372
+ | `rpc-server/sensitive.ts` | `isSensitiveKey`, `redactText`, `REDACTED`; `SensitiveKey` is `(key: string) => boolean`. |
373
+ | `runtime/make.ts` | `makePlatformRuntime(layer, { developmentCacheKey? })`. |
374
+ | `runtime/types.ts` | `PlatformRuntime<Services, BuildError>`, `PlatformRuntimeOptions`, `PlatformRuntimeServices<Runtime>`, `PlatformRuntimeBuildError<Runtime>`. |
375
+ | `node-http/subscription-signal.ts` | `nodeSubscriptionSignal({ nodeRequest?, request?, signals? })` returns `{ signal, dispose }`; `NodeSubscriptionSignalOptions`, `NodeSubscriptionSignal`. |
376
+ | `better-auth/adapter.ts` | `effectPrismaAdapter(Database, runtime, { debugLogs?, modelName?, usePlural? })`; `EffectPrismaAdapterOptions`. |
377
+ | `testing/vitest.ts` | `makePlatformIt(Database)({ adapter, createCaller, layer, extend? })`. |
378
+ | `testing/types.ts` | `MakePlatformItOptions`, `PlatformHarness`, `PlatformIt`, `PlatformTest`, `PlatformTester`. |
379
+
380
+ `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`.
381
+
382
+ 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`.
383
+
384
+ ### Install, setup and limits
385
+
386
+ ```sh
387
+ pnpm add @shivaedev/platform effect@4.0.0-rc.112
154
388
  ```
155
389
 
156
- `promise(() => ...)` runs a promise-based application boundary, such as Better
157
- Auth or an HTTP handler, with the same transaction-scoped Effect services.
390
+ 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.
391
+
392
+ Install the optional peers for the selected integration:
393
+
394
+ | Integration | Additional peers |
395
+ | --- | --- |
396
+ | `appHost.ts` | `@effect/platform-node`, `@effect/sql-pg` |
397
+ | `better-auth/adapter.ts` | `better-auth`, `@shivaedev/effect-prisma`, `@prisma-next/sql-orm-client` |
398
+ | `testing/vitest.ts` | `@effect/vitest`, `@shivaedev/effect-prisma`, `@shivaedev/effect-trpc`, `@trpc/server` |
399
+
400
+ - The package declares Node 24 or newer and uses the Effect version in its peer manifest.
401
+ - 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.
402
+ - Without an `HttpServerRequest`, `transportHeaders` falls back to the supplied RPC headers. An application using another transport must decide what headers it can trust.
403
+ - 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.
404
+ - Redaction recognizes common shapes. Keep secrets out of messages and extend the key predicate for application-specific sensitive fields.
405
+ - A development cache key is ignored when `NODE_ENV` is `production`. The application owns runtime disposal.
406
+ - 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.
407
+ - 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.
408
+ - 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
409
 
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`.
410
+ 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
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rejectedField.js","sourceRoot":"","sources":["../../src/errors/rejectedField.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAKxC,MAAM,QAAQ,GAAG,MAAM,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC;AACtG,MAAM,MAAM,GAAG,MAAM,CAAC,mBAAmB,CAAC,QAAQ,CAAC,CAAC;AACpD,MAAM,UAAU,aAAa,CAAC,KAAc;IAC3C,OAAO,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC;AAChF,CAAC"}
@@ -41,5 +41,15 @@ declare const AuthUnavailable_base: Schema.Class<AuthUnavailable, Schema.TaggedS
41
41
  }>, import("effect/Cause").YieldableError>;
42
42
  export declare class AuthUnavailable extends AuthUnavailable_base {
43
43
  }
44
+ declare const InternalServerError_base: Schema.Class<InternalServerError, Schema.TaggedStruct<"InternalServerError", {
45
+ message: Schema.String;
46
+ }>, import("effect/Cause").YieldableError>;
47
+ export declare class InternalServerError extends InternalServerError_base {
48
+ }
49
+ declare const ServiceUnavailable_base: Schema.Class<ServiceUnavailable, Schema.TaggedStruct<"ServiceUnavailable", {
50
+ message: Schema.String;
51
+ }>, import("effect/Cause").YieldableError>;
52
+ export declare class ServiceUnavailable extends ServiceUnavailable_base {
53
+ }
44
54
  export {};
45
55
  //# sourceMappingURL=taxonomy.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"taxonomy.d.ts","sourceRoot":"","sources":["../../src/errors/taxonomy.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;;;;AAKhC,qBAAa,QAAS,SAAQ,aAAqD;CAAG;;;;AAEtF,qBAAa,YAAa,SAAQ,iBAA6D;CAAG;;;;AAElG,qBAAa,SAAU,SAAQ,cAAuD;CAAG;;;;;AAEzF,qBAAa,UAAW,SAAQ,eAAwD;CAAG;;;;;AAE3F,qBAAa,QAAS,SAAQ,aAAoD;CAAG;;;;AAErF,qBAAa,kBAAmB,SAAQ,uBAAyE;CAAG;;;;AAEpH,qBAAa,eAAgB,SAAQ,oBAAmE;CAAG;;;;AAE3G,qBAAa,eAAgB,SAAQ,oBAAmE;CAAG"}
1
+ {"version":3,"file":"taxonomy.d.ts","sourceRoot":"","sources":["../../src/errors/taxonomy.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;;;;AAIhC,qBAAa,QAAS,SAAQ,aAAqD;CAAG;;;;AACtF,qBAAa,YAAa,SAAQ,iBAA6D;CAAG;;;;AAClG,qBAAa,SAAU,SAAQ,cAAuD;CAAG;;;;;AACzF,qBAAa,UAAW,SAAQ,eAAwD;CAAG;;;;;AAC3F,qBAAa,QAAS,SAAQ,aAAoD;CAAG;;;;AACrF,qBAAa,kBAAmB,SAAQ,uBAAyE;CAAG;;;;AACpH,qBAAa,eAAgB,SAAQ,oBAAmE;CAAG;;;;AAC3G,qBAAa,eAAgB,SAAQ,oBAAmE;CAAG;;;;AAC3G,qBAAa,mBAAoB,SAAQ,wBAA2E;CAAG;;;;AACvH,qBAAa,kBAAmB,SAAQ,uBAAyE;CAAG"}
@@ -17,4 +17,8 @@ export class TooManyRequests extends Schema.TaggedError()("TooManyRequests", des
17
17
  }
18
18
  export class AuthUnavailable extends Schema.TaggedError()("AuthUnavailable", described) {
19
19
  }
20
+ export class InternalServerError extends Schema.TaggedError()("InternalServerError", described) {
21
+ }
22
+ export class ServiceUnavailable extends Schema.TaggedError()("ServiceUnavailable", described) {
23
+ }
20
24
  //# sourceMappingURL=taxonomy.js.map