ras-stack 0.30.1 → 0.32.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 (44) hide show
  1. package/README.md +64 -534
  2. package/dist/build/cli.d.ts +1 -2
  3. package/dist/build/cli.js +8 -9
  4. package/dist/build/cli.js.map +1 -1
  5. package/dist/cli.d.ts +18 -0
  6. package/dist/cli.js +29 -0
  7. package/dist/cli.js.map +1 -0
  8. package/dist/conformance/index.d.ts +13 -0
  9. package/dist/conformance/index.js +43 -0
  10. package/dist/conformance/index.js.map +1 -1
  11. package/dist/policy/cli.d.ts +1 -2
  12. package/dist/policy/cli.js +18 -17
  13. package/dist/policy/cli.js.map +1 -1
  14. package/dist/posthog/client.d.ts +13 -0
  15. package/dist/posthog/client.js +31 -0
  16. package/dist/posthog/client.js.map +1 -0
  17. package/dist/posthog/config.d.ts +12 -0
  18. package/dist/posthog/config.js +49 -0
  19. package/dist/posthog/config.js.map +1 -0
  20. package/dist/posthog/coverage.d.ts +18 -0
  21. package/dist/posthog/coverage.js +25 -0
  22. package/dist/posthog/coverage.js.map +1 -0
  23. package/dist/posthog/index.d.ts +6 -0
  24. package/dist/posthog/index.js +4 -0
  25. package/dist/posthog/index.js.map +1 -0
  26. package/dist/posthog/proxy.d.ts +25 -0
  27. package/dist/posthog/proxy.js +25 -0
  28. package/dist/posthog/proxy.js.map +1 -0
  29. package/dist/posthog/react.d.ts +19 -0
  30. package/dist/posthog/react.js +18 -0
  31. package/dist/posthog/react.js.map +1 -0
  32. package/dist/posthog/request.d.ts +14 -0
  33. package/dist/posthog/request.js +22 -0
  34. package/dist/posthog/request.js.map +1 -0
  35. package/dist/posthog/server.d.ts +4 -0
  36. package/dist/posthog/server.js +17 -0
  37. package/dist/posthog/server.js.map +1 -0
  38. package/dist/preview/cli.d.ts +1 -2
  39. package/dist/preview/cli.js +5 -4
  40. package/dist/preview/cli.js.map +1 -1
  41. package/dist/runtime/dev-cli.d.ts +1 -2
  42. package/dist/runtime/dev-cli.js +8 -7
  43. package/dist/runtime/dev-cli.js.map +1 -1
  44. package/package.json +38 -6
package/README.md CHANGED
@@ -1,580 +1,110 @@
1
- # ras-stack
1
+ <div align="center">
2
2
 
3
- **The small pieces of full-stack plumbing I reuse across my TypeScript applications.**
3
+ # 🧱 ras-stack
4
4
 
5
- [![npm](https://img.shields.io/npm/v/ras-stack)](https://www.npmjs.com/package/ras-stack) [![Build](https://img.shields.io/github/actions/workflow/status/richardsolomou/ras-stack/ci.yml?branch=main)](https://github.com/richardsolomou/ras-stack/actions/workflows/ci.yml) [![License](https://img.shields.io/github/license/richardsolomou/ras-stack)](LICENSE)
6
-
7
- I build several applications with the same TypeScript stack. They kept growing slightly different copies of the same code for session settings, origin checks, email delivery, realtime publication, resumable uploads, health checks, and project configuration.
8
-
9
- `ras-stack` is the shared home for that plumbing. It is a personal library, published in case the code or the way it is split up is useful to someone else.
10
-
11
- ## What it is
12
-
13
- The package contains independent helpers for common application infrastructure:
14
-
15
- - **Authentication:** secure defaults and utilities for sessions, rate limits, secrets, social providers, tokens, and trusted origins.
16
- - **Server requests:** same-origin mutation guards, error-normalizing RPC wrappers, canonical-host redirects, and health responses.
17
- - **Realtime:** Centrifugo client lifecycle, token signing, presence synchronization, and a bounded publisher with retries and graceful shutdown.
18
- - **Email:** SMTP environment parsing and a small Nodemailer delivery interface.
19
- - **Database:** standard Better SQLite lifecycle and Drizzle migration mechanics that preserve the native typed database.
20
- - **Uploads:** a promise-based wrapper around resumable `tus-js-client` uploads.
21
- - **Project configuration:** shared TypeScript and Oxlint bases.
22
-
23
- Each area has its own import path. An application can use one without adopting the rest.
24
-
25
- ## What it is not
26
-
27
- This is not a framework, starter, application template, or complete authentication system. It does not own an application's database schema, migrations, routes, authorization rules, email templates, upload policy, or realtime event names.
28
-
29
- The libraries underneath remain available normally. Applications still configure and call Better Auth, TanStack Start, Drizzle, Nodemailer, `tus-js-client`, and Centrifugo directly. The helpers only centralize the parts that would otherwise be copied unchanged.
30
-
31
- ## Install
32
-
33
- `ras-stack` requires Node 24.
34
-
35
- ```sh
36
- pnpm add ras-stack
37
- ```
38
-
39
- Nodemailer, Centrifuge, Better SQLite/Drizzle, and `tus-js-client` are optional peer dependencies. Install them only when using their integrations:
40
-
41
- ```sh
42
- pnpm add nodemailer
43
- pnpm add centrifuge
44
- pnpm add tus-js-client
45
- pnpm add better-sqlite3 drizzle-orm
46
- ```
47
-
48
- ## Authentication and request security
49
-
50
- The auth entrypoint provides options and utilities rather than an auth factory. The application keeps its complete Better Auth configuration:
51
-
52
- ```ts
53
- import { betterAuth } from 'better-auth'
54
- import { configuredProviderOptions, standardRateLimitOptions, standardSessionOptions, trustedOrigins } from 'ras-stack/auth'
55
-
56
- const auth = betterAuth({
57
- database,
58
- plugins,
59
- socialProviders: configuredProviderOptions(['google', 'discord']),
60
- session: standardSessionOptions(),
61
- rateLimit: standardRateLimitOptions({ '/sign-up/email': { window: 60, max: 10 } }),
62
- trustedOrigins: trustedOrigins({
63
- configured: [process.env.APP_URL],
64
- trustForwardedHeaders: true,
65
- }),
66
- })
67
- ```
68
-
69
- The optional TanStack entrypoints bind the shared primitives to TanStack Start's ambient request and provide the common Query client default:
70
-
71
- ```ts
72
- import { createStackQueryClient } from 'ras-stack/tanstack/query'
73
- import {
74
- betterAuthHandlers,
75
- canonicalHostMiddleware,
76
- createTanStackRpc,
77
- requireTanStackMutationOrigin,
78
- tanStackHealthHandler,
79
- } from 'ras-stack/tanstack/server'
80
-
81
- export const { rpc, mutationRpc } = createTanStackRpc({
82
- requireMutation: (request) =>
83
- requireTanStackMutationOrigin(
84
- {
85
- configured: [process.env.APP_URL],
86
- trustForwardedHeaders: true,
87
- },
88
- request,
89
- ),
90
- })
91
-
92
- export const queryClient = createStackQueryClient()
93
-
94
- export const authHandlers = betterAuthHandlers(() => app().auth)
95
- export const healthHandler = tanStackHealthHandler(() => app().database.get(sql`SELECT 1`))
96
- export const canonicalHost = canonicalHostMiddleware(() => ({ canonicalUrl: process.env.APP_URL }))
97
- ```
98
-
99
- Applications still own their auth clients, authorization, file-route declarations, router, logging, health-check work, and Query configuration. Both integrations remain optional, and their upstream libraries remain directly accessible.
100
-
101
- Only enable `trustForwardedHeaders` behind a proxy that replaces incoming forwarded headers. Otherwise a client could choose the origin used by the check.
102
-
103
- Infrastructure boundaries can keep approved client output separate from private causes:
104
-
105
- ```ts
106
- import { InfrastructureError, infrastructureDiagnostic, infrastructureFailure } from 'ras-stack/server'
107
-
108
- throw new InfrastructureError('smtp_unavailable', 'email is temporarily unavailable', {
109
- cause: transportError,
110
- retryable: true,
111
- })
112
-
113
- const publicFailure = infrastructureFailure(error, {
114
- code: 'internal_error',
115
- message: 'something went wrong',
116
- retryable: false,
117
- })
118
- logger.error(infrastructureDiagnostic(error), 'infrastructure request failed')
119
- ```
120
-
121
- Only `InfrastructureError.publicMessage` is treated as approved for clients. Unknown failures use the caller's fallback; diagnostics are for application-owned logging and telemetry, never response serialization.
122
-
123
- Browser auth flows can share failure classification and pending/error state without sharing forms or navigation:
124
-
125
- ```tsx
126
- import { classifySignInFailure } from 'ras-stack/auth/client'
127
- import { useAuthAction } from 'ras-stack/auth/react'
128
-
129
- const signIn = useAuthAction({ failureMessage: (failure) => messageFor(classifySignInFailure(failure)) })
130
- const result = await signIn.run(() => authClient.signIn.email({ email, password }))
131
- if (!result.error) await navigateAfterSignIn()
132
- ```
133
-
134
- Applications retain field models, validation, password-reset disclosure policy, two-factor transitions, telemetry, copy, and success navigation.
135
-
136
- ## Database lifecycle
137
-
138
- The SQLite entrypoint owns the native client lifecycle, standard safety PRAGMAs, and optional Drizzle migrations while returning the upstream typed database:
139
-
140
- ```ts
141
- import { bundledDirectory } from 'ras-stack/database'
142
- import { openDrizzleSqlite } from 'ras-stack/database/sqlite'
143
-
144
- const database = openDrizzleSqlite({
145
- file,
146
- schema,
147
- migrationsFolder: bundledDirectory({
148
- developmentUrl: new URL('../../drizzle', import.meta.url),
149
- production: import.meta.env.PROD,
150
- name: 'drizzle',
151
- }),
152
- })
153
- ```
154
-
155
- Applications retain their schema, migrations, database path, repositories, transactions, and driver selection. Dual-database applications can use `openSqliteClient()` and `configureSqlite()` beneath their own wrapper without forcing PostgreSQL through a shared database interface.
156
-
157
- The PostgreSQL entrypoint applies the same boundary to Postgres.js and Drizzle while returning both native objects:
158
-
159
- ```ts
160
- import { databaseTarget } from 'ras-stack/database'
161
- import { closeDrizzlePostgres, migrateDrizzlePostgres, openDrizzlePostgres } from 'ras-stack/database/postgres'
162
-
163
- const target = databaseTarget({ databaseUrl: process.env.DATABASE_URL, sqliteFile })
164
- if (target.provider === 'postgres') {
165
- const connection = openDrizzlePostgres({ url: target.url, schema })
166
- await migrateDrizzlePostgres(connection, postgresMigrationsFolder)
167
- await closeDrizzlePostgres(connection)
168
- }
169
- ```
170
-
171
- The package does not make SQLite and PostgreSQL queries look identical. Applications keep driver-specific transaction and compatibility behavior while sharing target validation, pool defaults, numeric parsing, migrations, credential-safe display URLs, and shutdown.
172
-
173
- Consumer tests can verify the real provider selection and provider-specific safety settings without sharing a schema or repository:
174
-
175
- ```ts
176
- import { assertDatabaseTargetConformance, assertSqliteConformance } from 'ras-stack/conformance'
177
-
178
- await assertDatabaseTargetConformance(databaseTarget)
179
- await assertSqliteConformance((name) => sqliteClient.pragma(name, { simple: true }))
180
- ```
181
-
182
- ## Realtime updates
183
-
184
- Applications choose their channel names, authorize subscriptions, and define payloads. `ras-stack` handles Centrifugo's HTTP publication, signed tokens, and repeated browser lifecycle mechanics:
185
-
186
- ```ts
187
- import { CentrifugoPublisher, signRealtimeToken } from 'ras-stack/realtime'
188
- import {
189
- connectRealtimeClient,
190
- createSameOriginRealtimeClient,
191
- openRealtimeSubscription,
192
- requestRealtimeTicket,
193
- watchSubscriptionPresence,
194
- } from 'ras-stack/realtime/client'
195
-
196
- const publisher = new CentrifugoPublisher({
197
- apiUrl,
198
- apiKey,
199
- maxConcurrentChannels: 8,
200
- maxPendingChannels: 1024,
201
- onError: (error, channel) => logger.error({ error, channel }, 'realtime publication failed'),
202
- })
203
-
204
- publisher.publish(`battle:${battle.id}`, { type: 'change' })
205
-
206
- const token = signRealtimeToken(user.id, { channel: `battle:${battle.id}`, info: presence }, { secret })
207
-
208
- await publisher.close()
209
-
210
- const client = createSameOriginRealtimeClient({
211
- getToken: () => requestRealtimeTicket('/api/realtime/token', { parse: (value) => (value as { token: string }).token }),
212
- })
213
- const channelToken = (channel: string) =>
214
- requestRealtimeTicket('/api/realtime/token', {
215
- init: { method: 'POST', body: JSON.stringify({ channel }) },
216
- parse: (value) => (value as { token: string }).token,
217
- })
218
- const disconnect = connectRealtimeClient(client)
219
- const live = openRealtimeSubscription(client, channel, { getToken: ({ channel }) => channelToken(channel) }, (subscription) =>
220
- watchSubscriptionPresence(subscription, setClients),
221
- )
222
-
223
- live.close()
224
- disconnect()
225
- ```
226
-
227
- React applications can keep transport ownership equally small while retaining the native client and subscription:
228
-
229
- ```tsx
230
- import { useCallback } from 'react'
231
- import { useConnectedRealtimeClient, useRealtimePresence, useRealtimeSubscription } from 'ras-stack/realtime/react'
232
-
233
- const createClient = useCallback(() => createSameOriginRealtimeClient({ getToken }), [workspaceId])
234
- const client = useConnectedRealtimeClient(createClient)
235
- const subscription = useRealtimeSubscription({ client, channel, options, configure })
236
- const clients = useRealtimePresence(subscription)
237
- ```
238
-
239
- The client factory may return a client or a promise, so applications can fetch and validate an initial ticket before connecting. Pass `onError` as the third argument to handle asynchronous ticket failures. A client that resolves after unmount is disconnected without ever connecting. The factory, error handler, subscription options, and configure callback should have stable identities and change only when their corresponding lifecycle should restart.
240
-
241
- The client helpers return the underlying Centrifuge client and subscription. React ownership, channel conventions, ticket validation, event parsing, presence models, and query invalidation remain application code. `publish()` returns `false` when the publisher is closed, disabled, or at capacity. `close()` rejects new work and waits for accepted publications and their bounded retries to finish.
242
-
243
- ## Email and uploads
244
-
245
- The optional integrations return the underlying library objects when an application needs more control:
246
-
247
- ```ts
248
- import { createSmtpDelivery, createSmtpTransport, smtpConfigFromEnvironment } from 'ras-stack/email'
249
- import { createTusUpload, startTusUpload } from 'ras-stack/uploads'
250
-
251
- const smtp = smtpConfigFromEnvironment()
252
- const email = smtp ? createSmtpDelivery(smtp) : undefined
253
-
254
- const upload = createTusUpload({
255
- endpoint: '/api/upload',
256
- file,
257
- metadata,
258
- shouldRetry: (status) => status !== 423,
259
- onProgress,
260
- })
5
+ **A practical TypeScript stack with strong defaults and room to make it yours.**
261
6
 
262
- await startTusUpload(upload)
263
- ```
7
+ TanStack Start · React · Better Auth · Drizzle · SQLite/PostgreSQL · Centrifugo · Caddy · PostHog
264
8
 
265
- Applications retain ownership of email templates, missing-email behavior, upload metadata, authorization, quotas, and completion processing.
9
+ [![npm](https://img.shields.io/npm/v/ras-stack)](https://www.npmjs.com/package/ras-stack) [![Build](https://img.shields.io/github/actions/workflow/status/richardsolomou/ras-stack/ci.yml?branch=main)](https://github.com/richardsolomou/ras-stack/actions/workflows/ci.yml) [![License](https://img.shields.io/github/license/richardsolomou/ras-stack)](LICENSE)
266
10
 
267
- Production server assets can be declared in `ras-stack.assets.json` instead of appended shell copy chains:
11
+ </div>
268
12
 
269
- ```json
270
- {
271
- "outputDirectory": ".output/server",
272
- "assets": [
273
- { "source": "drizzle", "destination": "drizzle" },
274
- { "source": "drizzle-postgres", "destination": "drizzle-postgres" }
275
- ]
276
- }
277
- ```
13
+ I kept rebuilding the same boring parts: secure sessions, origin checks, database startup, realtime connections, process shutdown, CI, previews, and releases. `ras-stack` solves them once with the libraries I would choose anyway.
278
14
 
279
- Run `ras-stack-assets sync` after the application build and `ras-stack-assets check` before packaging. Sync replaces each declared destination, and check compares the exact file set and content. Sources stay inside the repository, destinations stay inside the output directory, overlapping destinations and symbolic links are rejected, and the application retains ownership of asset generation and contents.
15
+ It is opinionated about security, lifecycle, failure handling, and supply-chain checks, but not product behavior. Use one helper or the whole stack, override what differs, and keep access to the library underneath.
280
16
 
281
- Stateful development servers can reuse one typed resource across HMR without application-specific `globalThis` casts:
17
+ ## The idea 💡
282
18
 
283
- ```ts
284
- import { clearGlobalSingleton, globalAsyncSingleton } from 'ras-stack/server'
19
+ TanStack handles the web application, Better Auth handles authentication, Drizzle handles typed data, and Centrifugo handles realtime delivery. `ras-stack` connects them; it does not replace them.
285
20
 
286
- export const app = () => globalAsyncSingleton('my-app.instance', createApp)
287
- export const resetApp = () => clearGlobalSingleton('my-app.instance', (instance) => instance.close())
288
- ```
21
+ A helper belongs here when it removes a repeated decision or failure mode without hiding the underlying tool. Helpers return native objects, accept overrides, and live behind narrow entrypoints so applications can always drop down a level.
289
22
 
290
- Rejected async initialization removes only its own pending value so a later request can retry. Clearing deletes the key before awaiting initialization or disposal, allowing replacement startup without reusing a closing resource.
23
+ ## What you get 📦
291
24
 
292
- ## Shared project configuration
25
+ It ships in four forms:
293
26
 
294
- Extend the supplied configuration and override anything specific to the application:
27
+ - **TypeScript modules** under narrow import paths such as `ras-stack/database/sqlite`, `ras-stack/realtime/react`, and `ras-stack/tanstack/server`.
28
+ - **Command-line tools** for generated policy, production assets, preview status, and a local Centrifugo container.
29
+ - **GitHub Actions and reusable workflows** for toolchain setup, checks, browser tests, previews, and Changesets releases.
30
+ - **A separate OCI image** containing verified Caddy and Centrifugo binaries for production images.
295
31
 
296
- ```json
297
- {
298
- "extends": ["./node_modules/ras-stack/config/oxlint.json"],
299
- "rules": {
300
- "application-specific-rule": "off"
301
- }
302
- }
303
- ```
32
+ An application can use one surface without adopting the others. The npm package has no runtime dependency on the web, database, email, or realtime libraries; those integrations are optional peers.
304
33
 
305
- ```json
306
- {
307
- "extends": "ras-stack/config/typescript/tanstack",
308
- "include": ["src", "vite.config.ts"]
309
- }
310
- ```
311
-
312
- TypeScript bases are also available at `ras-stack/config/typescript/browser` and `ras-stack/config/typescript/library`.
34
+ ## The stack 🧰
313
35
 
314
- The TypeScript configs compose by runtime role:
36
+ These combinations are tested here and in production. [Sealed Lists](https://github.com/richardsolomou/sealed-lists), [Praetorium](https://github.com/richardsolomou/praetorium.gg), and [STL Quest](https://github.com/richardsolomou/stl.quest) use the application and runtime pieces. [BaseKit](https://github.com/richardsolomou/basekit) and [tro.gg](https://github.com/richardsolomou/tro.gg) use only the tooling that fits their different architectures.
315
37
 
316
- - `base` contains runtime-neutral strictness and module-safety options.
317
- - `bundler` adds ESM bundler resolution without assuming DOM or Node globals.
318
- - `browser` adds DOM, JSX, and no-emit defaults.
319
- - `tanstack` adds Vite and Node types to the browser role.
320
- - `node-bundler` targets bundled Node 24 workers and scripts.
321
- - `library` uses NodeNext resolution and declaration-friendly strictness.
38
+ | Layer | Supported technology | What `ras-stack` centralizes |
39
+ | ------------------- | ------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
40
+ | Runtime and tooling | Node, ESM TypeScript, pnpm, Just, Oxlint | Compiler/linter bases, setup actions, and version synchronization |
41
+ | Web application | TanStack Start, React, TanStack Query | Request binding, mutation-origin checks, canonical hosts, health handlers, and Query defaults |
42
+ | Authentication | Better Auth | Secure option builders, origins, secrets, tokens, failure classification, and React action state |
43
+ | Data | Drizzle, `better-sqlite3`, Postgres.js | Connection lifecycle, safety defaults, migrations, target selection, and conformance checks |
44
+ | Realtime | Centrifuge, Centrifugo, Caddy | Publishing, tokens, browser/React lifecycle, presence, proxy configuration, binaries, and supervision |
45
+ | Email and uploads | Nodemailer, `tus-js-client` | SMTP configuration/delivery and promise-based resumable uploads |
46
+ | Observability | PostHog JS, React, and Node SDKs | Initialization, error defaults, request correlation, proxy routes, shutdown, and coverage decisions |
47
+ | Delivery | GitHub Actions, Changesets, Dokploy, Docker | Checks, releases, preview lifecycle/status, production assets, and runtime binaries |
322
48
 
323
- Oxlint applications can extend the strict default plus independent layers for shared application preferences and generated TanStack files:
324
-
325
- ```json
326
- {
327
- "extends": ["./node_modules/ras-stack/config/oxlint/application.json", "./node_modules/ras-stack/config/oxlint/tanstack.json"],
328
- "rules": {
329
- "application-specific-rule": "off"
330
- }
331
- }
332
- ```
49
+ Applications still configure every upstream library directly. This table describes what is tested together, not a replacement API.
333
50
 
334
- These configs do not set include paths, aliases, generated directories outside TanStack's route tree, or framework-specific worker globals. Keep those differences in the consuming repository.
335
-
336
- ## Repository policy
337
-
338
- Policy files which cannot inherit can stay committed while being checked against the shared source. Select only the policies a repository wants in `ras-stack.policy.json`:
339
-
340
- ```json
341
- {
342
- "changesets": {
343
- "overrides": {
344
- "access": "restricted",
345
- "privatePackages": { "version": true, "tag": true }
346
- }
347
- },
348
- "dependabot": true,
349
- "pnpm": {},
350
- "adoption": {
351
- "minimumRasStackVersion": "0.29.0",
352
- "node": ">=24 <25",
353
- "pnpm": "11.15.0",
354
- "just": "1.58.0"
355
- }
356
- }
357
- ```
51
+ ## Where it stops 🧭
358
52
 
359
- Then generate or verify the effective files:
53
+ `ras-stack` owns mechanics that should behave the same everywhere: safe database startup, mutation-origin checks, realtime tokens, process supervision, and preview status.
360
54
 
361
- ```sh
362
- pnpm exec ras-stack-policy sync
363
- pnpm exec ras-stack-policy check
364
- pnpm exec ras-stack-policy sync adoption
365
- pnpm exec ras-stack-policy check adoption
366
- ```
55
+ The application keeps schemas, migrations, repositories, routes, authorization, templates, upload rules, realtime payloads, storage, deployment topology, and UI. There is no shared application factory or giant configuration object.
367
56
 
368
- `changesets` and `dependabot` produce deterministic complete files, with optional deep overrides. The pnpm policy changes only `minimumReleaseAge` in the existing `pnpm-workspace.yaml`, preserving local package layout, build approvals, dependency overrides, exclusions, and comments. Its default is seven days; set `"minimumReleaseAge": 0` only as an explicit repository exception. Commit both the selection and generated files so policy changes remain visible in review.
57
+ The [`examples/full-stack`](examples/full-stack) workspace shows the boundaries together and tests them through `workspace:*`. It is an integration contract and reference, not a starter to copy.
369
58
 
370
- Adoption synchronization updates older ras-stack package and workflow references plus the declared Node, pnpm, and Just versions. It preserves package range style, explicit workflow-version exceptions, newer ras-stack versions, application dependencies, and workflow structure. Check mode reports the exact files that would change without writing them. Shared config references remain check-only because adding them requires application-specific include paths and overrides.
59
+ ## Pick what you need 🧩
371
60
 
372
- The same adoption policy can produce a read-only fleet report from public repository metadata. Declare each repository's supported versions and required shared config references in `ras-stack.fleet.json`, then run:
61
+ `ras-stack` requires Node 24.
373
62
 
374
63
  ```sh
375
- GITHUB_TOKEN="$(gh auth token)" pnpm exec ras-stack-policy fleet
376
- ```
377
-
378
- The command reads only `package.json`, root toolchain configs, and GitHub workflow metadata. It prints Markdown, exits unsuccessfully when drift exists, and never writes to a consumer repository. Omit an expectation when a repository intentionally does not share that surface. The included manually dispatched workflow writes the result to the Actions summary and retains it as an artifact.
379
-
380
- ## GitHub Actions
381
-
382
- The JavaScript setup action reads the Node version from `engines.node` and the pnpm version from `packageManager` in the consuming repository:
383
-
384
- ```yaml
385
- steps:
386
- - uses: actions/checkout@v7
387
- - uses: richardsolomou/ras-stack/actions/setup-js@v0.29.0
388
- - run: pnpm check
389
- ```
390
-
391
- Just is independent of the application language and is installed separately when a repository uses it:
392
-
393
- ```yaml
394
- - uses: richardsolomou/ras-stack/actions/setup-just@v0.29.0
395
- with:
396
- version: '1.58.0'
397
- ```
398
-
399
- Applications using Changesets can call the reusable release workflow after their own required checks:
400
-
401
- ```yaml
402
- release:
403
- if: github.ref == 'refs/heads/main' && github.event_name == 'push'
404
- needs: [check]
405
- permissions:
406
- contents: write
407
- uses: richardsolomou/ras-stack/.github/workflows/release-changesets.yml@v0.29.0
408
- secrets: inherit
409
- ```
410
-
411
- Browser jobs can cache the pinned Playwright payload through the shared setup action. Production-container E2E can use the reusable workflow, while repository-specific preparation and the actual test command remain inputs:
412
-
413
- ```yaml
414
- e2e:
415
- uses: richardsolomou/ras-stack/.github/workflows/check-container-browser.yml@v0.29.0
416
- with:
417
- image: my-app-e2e
418
- cache-scope: my-app-e2e
419
- prepare-command: just prepare-e2e
420
- command: just e2e-run
421
- just-version: '1.58.0'
422
- ```
423
-
424
- The loaded image tag is also available to the command as `RAS_STACK_TEST_IMAGE`. Build and browser durations are written to the job summary, and failure artifacts remain configurable. Applications that need extra caches, services, registry publication, or a different PR/main topology can use `actions/build-container` and `actions/setup-playwright` inside their own job instead.
425
-
426
- Dokploy previews can share the application/domain/image/environment/deploy/health/delete/prune lifecycle through `ras-stack/preview/dokploy`. Product-specific Stripe, storage, seed, and verification work stays around the manager's configure and cleanup hooks.
427
-
428
- Preview comments and commit checks can use the same state transition without carrying a GitHub API client in every repository:
429
-
430
- ```ts
431
- import { reportPreviewStatus } from 'ras-stack/preview/github'
432
-
433
- await reportPreviewStatus(
434
- { repository, token, marker: '<!-- app-preview -->', note: 'Preview data is disposable.' },
435
- { state: 'ready', prNumber, sha, previewUrl, runUrl },
436
- )
437
- ```
438
-
439
- The reporter keeps one marked comment and one named check run, preserves the last ready commit while a replacement builds, bounds comment pagination, and validates repository, pull request, commit, marker, and URL inputs. Applications retain their preview hostname, access note, seed credentials, and product cleanup hooks.
440
-
441
- Trusted preview wrappers can delegate each status transition to the reusable workflow instead of checking out and running a repository-owned comment script:
442
-
443
- ```yaml
444
- mark-preview-ready:
445
- permissions:
446
- contents: read
447
- checks: write
448
- issues: write
449
- uses: richardsolomou/ras-stack/.github/workflows/report-preview-status.yml@v0.29.0
450
- with:
451
- state: ready
452
- pr-number: ${{ github.event.workflow_run.pull_requests[0].number }}
453
- sha: ${{ github.event.workflow_run.head_sha }}
454
- preview-url: https://pr-${{ github.event.workflow_run.pull_requests[0].number }}.example.com
455
- run-url: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
456
- marker: <!-- app-preview -->
457
- note: Preview data is disposable.
458
- secrets:
459
- token: ${{ secrets.GITHUB_TOKEN }}
460
- ```
461
-
462
- The workflow uses the `ras-stack-preview` CLI from the consuming repository's pinned package. It owns only GitHub check/comment reporting; the caller retains the trusted event conditions, permissions, deployment, deletion, secret mapping, and product verification.
463
-
464
- The reusable `build-preview-image.yml` workflow publishes same-repository pull requests directly but turns fork builds into one-day artifacts without exposing a token or secret. A trusted `workflow_run` job can publish that artifact with `actions/publish-preview-image` before running its repository-owned deployment command. The event wrapper and secret-to-environment mapping remain in each application so the trust boundary is visible locally.
465
-
466
- Self-hosted images that run the app, Centrifugo, and Caddy together can share the lifecycle without sharing a Dockerfile:
467
-
468
- ```ts
469
- import { runRealtimeStack } from 'ras-stack/runtime'
470
-
471
- await runRealtimeStack({
472
- app: { command: 'node', args: ['.output/server/index.mjs'], env: { ...process.env, PORT: '3001' } },
473
- centrifugo: {
474
- configPath: '/app/realtime.json',
475
- env: process.env,
476
- environment: realtime,
477
- },
478
- caddy: {
479
- configPath: '/tmp/app/Caddyfile',
480
- env: process.env,
481
- },
482
- })
483
- ```
484
-
485
- `runRealtimeStack()` creates the Caddy configuration and supervises the standard app, Centrifugo, and Caddy topology. Any unexpected child exit stops its siblings; orchestrator signals receive a graceful window before remaining children are force-killed. Lower-level configuration and supervision functions remain available when a topology differs. Applications retain base images, namespaces, ports, volumes, secrets, per-process environment inheritance, preview seeding, and distributed-mode policy.
486
-
487
- The separately released `ghcr.io/richardsolomou/ras-stack-runtime-binaries` image provides verified static Caddy and Centrifugo binaries without imposing an application base image. Copy the binaries from an immutable release and pin its digest:
488
-
489
- ```dockerfile
490
- FROM ghcr.io/richardsolomou/ras-stack-runtime-binaries:runtime-v1.0.0@sha256:... AS runtime-binaries
491
- COPY --from=runtime-binaries /usr/local/bin/caddy /usr/local/bin/caddy
492
- COPY --from=runtime-binaries /usr/local/bin/centrifugo /usr/local/bin/centrifugo
64
+ pnpm add ras-stack
493
65
  ```
494
66
 
495
- For local development, the package can run the same pinned Centrifugo binary in Docker while the application and its Vite proxy remain local:
67
+ Nodemailer, Centrifuge, `better-sqlite3`, Drizzle, Postgres.js, and `tus-js-client` are optional peer dependencies. Install them only when using their integrations:
496
68
 
497
69
  ```sh
498
- ras-stack-realtime \
499
- --config realtime.json \
500
- --name example-realtime \
501
- --port 8000 \
502
- --origin http://localhost:3000 \
503
- --secret development-secret
504
- ```
505
-
506
- The foreground command follows terminal signals and leaves an existing named container alone. Add `--detach` to replace that named development container and return after startup. The host binding defaults to `127.0.0.1`; container-based callers that must reach Centrifugo through the Docker host can explicitly pass `--bind-address 0.0.0.0`. Applications with a Centrifugo connect proxy can pass its Docker-reachable URL through `--connect-proxy-endpoint`; channel definitions, proxy authorization, application environment, and Vite configuration remain in the application.
507
-
508
- `runtime/VERSION` and `runtime/Dockerfile` own the release and source versions. Runtime tags publish independently from npm releases so binary changes must pass the full-stack production-container gate before a `runtime-v*` tag is created.
509
-
510
- Read-only containers can pass writable `configHome` and `dataHome` paths to `caddyRuntimeEnvironment()`; both default to isolated directories under `/tmp`.
511
-
512
- The workflow consumes pending changesets, commits the resulting versions and changelogs, pushes the commit and tag atomically, and creates a GitHub Release. It does nothing when no versioned changeset is present. The caller owns its checks, Changesets configuration, release policy, and any deployment that follows the release.
513
-
514
- Pin actions and reusable workflows to a release tag and let Dependabot propose upgrades.
515
-
516
- Reusable workflows cannot refer to an action at their own dynamic release tag. Their implementations therefore pin ras-stack actions to an older independently published bootstrap tag and advance that pin only when the action contract changes. Consumer examples and direct action calls should use the fleet baseline above.
517
-
518
- The JavaScript setup action and shared check workflow reject Dependabot branches that do not contain the base commit recorded by the pull request event. Custom dependency workflows can apply the same guard directly:
519
-
520
- ```yaml
521
- - uses: richardsolomou/ras-stack/actions/require-current-base@v0.29.0
522
- if: github.event_name == 'pull_request' && startsWith(github.head_ref, 'dependabot/')
523
- with:
524
- base-sha: ${{ github.event.pull_request.base.sha }}
525
- head-sha: ${{ github.event.pull_request.head.sha }}
70
+ pnpm add nodemailer
71
+ pnpm add centrifuge
72
+ pnpm add tus-js-client
73
+ pnpm add better-sqlite3 drizzle-orm
74
+ pnpm add postgres drizzle-orm
526
75
  ```
527
76
 
528
- Keep strict up-to-date branch protection enabled for the required check. When the guard fails, update or rebase the dependency branch onto current `main`, regenerate its lockfile, and let the normal policy check run against that refreshed result. Do not merge it with an administrative bypass.
77
+ Start with the narrowest public entrypoint that owns the repeated mechanic:
529
78
 
530
- The reusable check workflow owns checkout and toolchain setup while the repository keeps its check command:
79
+ | Need | Entrypoint or command | The application still owns |
80
+ | --------------------------------------------------------- | ------------------------------------------------------------------------------- | -------------------------------------------------------------- |
81
+ | Authentication defaults and browser action state | `ras-stack/auth`, `ras-stack/auth/client`, `ras-stack/auth/react` | Better Auth configuration, forms, policy, and navigation |
82
+ | RPC, mutation-origin, health, and canonical-host handling | `ras-stack/server`, `ras-stack/tanstack/server` | Routes, authorization, logging, and health work |
83
+ | SQLite or PostgreSQL lifecycle | `ras-stack/database/*` | Schemas, migrations, repositories, and transactions |
84
+ | Realtime publication and browser lifecycle | `ras-stack/realtime/*` | Channels, tickets, payloads, presence models, and invalidation |
85
+ | Email, uploads, and production assets | `ras-stack/email`, `ras-stack/uploads`, `ras assets` | Templates, metadata, quotas, storage, and asset contents |
86
+ | Production or development realtime runtime | `ras-stack/runtime`, `ras realtime` | Images, ports, secrets, volumes, and distributed policy |
87
+ | Compiler, lint, CI, release, and preview mechanics | `ras-stack/config/*`, `actions/*`, `.github/workflows/*`, `ras-stack/preview/*` | Triggers, permissions, services, deployment, and verification |
88
+ | Generated repository policy and adoption checks | `ras policy` | Which policies apply and every declared exception |
531
89
 
532
- ```yaml
533
- jobs:
534
- check:
535
- uses: richardsolomou/ras-stack/.github/workflows/check-js.yml@v0.29.0
536
- with:
537
- command: just check
538
- just-version: '1.58.0'
539
- ```
540
-
541
- Simple Playwright jobs can also share browser installation and failure artifacts:
542
-
543
- ```yaml
544
- jobs:
545
- end-to-end:
546
- uses: richardsolomou/ras-stack/.github/workflows/check-browser.yml@v0.29.0
547
- with:
548
- prepare-command: pnpm build
549
- command: pnpm test:e2e:run
550
- artifact-path: test-results
551
- ```
90
+ ## Guides 📚
552
91
 
553
- Callers continue to own workflow triggers, concurrency, required-job dependencies, permissions, services, caches, custom setup, and release or deployment policy. Keep jobs local when they need steps beyond these stable shapes.
92
+ | Guide | What it covers |
93
+ | -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
94
+ | [Application primitives](docs/application-primitives.md) | Authentication, request security, databases, realtime clients, email, uploads, and stateful development resources |
95
+ | [Repository tooling](docs/repository-tooling.md) | TypeScript and Oxlint configuration, generated policy, fleet checks, GitHub Actions, previews, releases, and production runtime composition |
96
+ | [PostHog integration](docs/posthog.md) | Browser/server setup, identity and session correlation, ingest proxying, shutdown, coverage declarations, and source-map responsibility |
97
+ | [Full-stack example](docs/full-stack-example.md) | The `workspace:*` integration contract, local development, production container, and two-browser journey |
554
98
 
555
- ## Development
99
+ ## Development 🛠️
556
100
 
557
- Development requires Node 24 and pnpm 11.15.0.
101
+ Development requires Node 24, pnpm 11.15.0, and Just 1.58.0.
558
102
 
559
103
  ```sh
560
104
  just install
561
105
  just check
562
106
  ```
563
107
 
564
- ### Full-stack example
565
-
566
- `examples/full-stack` is a private workspace application whose `ras-stack` dependency is `workspace:*`. It imports only public package entrypoints, so the repository gate catches integration breakage before a release reaches consumers.
567
-
568
- The example builds a real TanStack application and production image. Its browser journey signs two independent users in, writes through mutation-protected RPC into SQLite, uploads a bounded text file through TUS, and observes a cross-context message through Centrifugo and Caddy. The same app also consumes the shared query client, React realtime hooks, persisted auth secret, SMTP environment parsing, health response, singleton, TypeScript, and Oxlint contracts.
569
-
570
- Run the compile and unit boundary with:
571
-
572
- ```sh
573
- pnpm --filter @ras-stack/example-full-stack check
574
- ```
575
-
576
- The CI `Full-stack example` job builds the container with a read-only root, starts the complete Node/Centrifugo/Caddy runtime, and runs the Playwright journey. Dokploy and GitHub preview reporting remain workflow integrations because exercising them requires repository credentials rather than application behavior.
577
-
578
108
  See [CONTRIBUTING.md](CONTRIBUTING.md) for release instructions. Report vulnerabilities privately as described in [SECURITY.md](SECURITY.md).
579
109
 
580
110
  ## License
@@ -1,2 +1 @@
1
- #!/usr/bin/env node
2
- export {};
1
+ export declare function runAssetsCli(arguments_: string[]): Promise<void>;