@voltro/cli 0.38.0 → 0.39.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.
- package/CHANGELOG.md +224 -0
- package/THIRD-PARTY-NOTICES.md +80 -45
- package/dist/{addCommand-BNeoeSxe.js → addCommand-C05L9tZh.js} +2 -2
- package/dist/addCommand-C3MdJG-a.js +2 -0
- package/dist/{agentsMd-mhQMF1bx.js → agentsMd-7zI2h5l9.js} +3 -3
- package/dist/agentsMd-BFCXh2gl.js +2 -0
- package/dist/apiBuild-2GvK8CUB.js +2 -0
- package/dist/{apiBuild-CvacV4zA.js → apiBuild-DGalUk9v.js} +3 -3
- package/dist/baselineCommand-CXv680Dc.js +2 -0
- package/dist/{baselineCommand-C6NMt-oa.js → baselineCommand-Ck2Xm8M7.js} +18 -18
- package/dist/bin.js +1 -1
- package/dist/{build-_LoTC1s0.js → build-CfF6t0UM.js} +107 -100
- package/dist/{capabilitiesCommand-nq_pz5xd.js → capabilitiesCommand-D5gucwYa.js} +8 -8
- package/dist/{checkCommand-wibtQx-N.js → checkCommand-BV56Pcc1.js} +1 -1
- package/dist/{checkCommand-Pjk5sBl2.js → checkCommand-CBtynBKW.js} +34 -34
- package/dist/{cloudCmd-NQSwe_Qk.js → cloudCmd-C42gaO8s.js} +16 -16
- package/dist/{clusterCmd-D5wsCmA_.js → clusterCmd-DrVFCzSj.js} +8 -4
- package/dist/{codegen-GYYcdpCg.js → codegen-CbpGCWLG.js} +1 -1
- package/dist/codegen-i8QGcHsi.js +2 -0
- package/dist/codegenCommand-W7SDdiAQ.js +129 -0
- package/dist/{codemodRunner-CXgQ-ecJ.js → codemodRunner-D-jTyvWo.js} +622 -551
- package/dist/{commands-DaZVi9wi.js → commands-BD9eBRY3.js} +48 -48
- package/dist/{connectionConfig-UFlIEiys.js → connectionConfig-Bk9IC7D0.js} +1 -1
- package/dist/{dashboardCommand-Dxy_kmoW.js → dashboardCommand-7qGylm0F.js} +3 -3
- package/dist/{dataCommand-D6-Ha2lf.js → dataCommand-C_F2DYxe.js} +14 -11
- package/dist/{dbCommand-C4UW8cVN.js → dbCommand-DSwGv9wS.js} +50 -49
- package/dist/dbCommand-DcqEyxju.js +2 -0
- package/dist/dev-alhkKoEX.js +3 -0
- package/dist/{dev-E7Eo5GHc.js → dev-gqpnzVhI.js} +2260 -2184
- package/dist/{dialectDriver-CgXnDfec.js → dialectDriver-czCHYpeH.js} +2 -1
- package/dist/{doctorCommand-gmDyALFL.js → doctorCommand-Bvs-BQrM.js} +242 -238
- package/dist/doctorCommand-CVXfRrng.js +2 -0
- package/dist/{dormancyCommand-CE6wG8lj.js → dormancyCommand-B-PPHc9Q.js} +1 -1
- package/dist/{e2eCmd-BRabZww-.js → e2eCmd-uFHig1hV.js} +8 -2
- package/dist/{embeddingsCommand-C3uAuf6N.js → embeddingsCommand-OKY6XjUf.js} +6 -6
- package/dist/{envCommand-DzuWh8ya.js → envCommand-B1-Zm85H.js} +19 -19
- package/dist/{evolveCommand-DPLqcYb2.js → evolveCommand-C4-NlFbd.js} +6 -8
- package/dist/{generateCommand-DbgcUpGw.js → generateCommand-CcyvH2ve.js} +1 -1
- package/dist/index.js +1 -1
- package/dist/{infoCommand-CmczGj_l.js → infoCommand-CTo8Jnhq.js} +7 -7
- package/dist/inspect-B7U7Cl_Z.js +1192 -0
- package/dist/inspect-DUze25t0.js +2 -0
- package/dist/{inspectCmd-EHFZ9yYu.js → inspectCmd-niF97fAq.js} +5 -5
- package/dist/inspectGateHint-BjnFubmH.js +7 -0
- package/dist/{logFileSink-C_D2wRN1.js → logFileSink-B4uP8pvf.js} +1 -1
- package/dist/{logsCmd-D36xK7Zu.js → logsCmd-B6oNsfaZ.js} +10 -7
- package/dist/{manifestBuild-hpPLaGxV.js → manifestBuild-BK42hu0k.js} +1 -1
- package/dist/manifestBuild-j0n109tt.js +2 -0
- package/dist/{metaCommands-CIoQNlaQ.js → metaCommands-DUYR--Ts.js} +1 -1
- package/dist/{migrate-RwgUWXfc.js → migrate-BnPw2zC8.js} +7 -4
- package/dist/{packageCommand-Cug_3Ogl.js → packageCommand-9SVyvsSo.js} +3 -2
- package/dist/{probeCommand-BPEpwT32.js → probeCommand-C9gazU0H.js} +35 -24
- package/dist/{projectScaffold-CxgtJvlb.js → projectScaffold-BEhhHjPr.js} +1 -1
- package/dist/{projectScaffold-DDgWbNLe.js → projectScaffold-BvhLOrLq.js} +17 -15
- package/dist/{runtimeTrace-6jfznSzr.js → runtimeTrace-DzZOCd94.js} +1 -1
- package/dist/{scheduleManifestCmd-D2x0CTTY.js → scheduleManifestCmd-kmWzrO_w.js} +6 -9
- package/dist/{sdkgen-CEJZZQUA.js → sdkgen-C4roLErM.js} +4 -4
- package/dist/{seedRunner-ZmLSqNe2.js → seedRunner-IdHEprqf.js} +1 -4
- package/dist/serveCommand-B6TATyCj.js +1770 -0
- package/dist/serveCommand-CNR0gI9V.js +2 -0
- package/dist/serveEntry.js +2 -2
- package/dist/{serverlessCommand-CfJZy6dS.js → serverlessCommand-DbJ7Plg9.js} +3 -3
- package/dist/{start-CidwCcjl.js → start-BNuTWdRd.js} +1 -1
- package/dist/{start-RCh6qKFe.js → start-ft_KzFTd.js} +257 -254
- package/dist/startEntry.js +1 -1
- package/dist/{test-DIQ0jlkQ.js → test-CLYXrZ2F.js} +1 -1
- package/dist/{tracesCmd-DStmCJPi.js → tracesCmd-DgtgOUdi.js} +7 -4
- package/dist/{typecheckCommand-BlsWiCNq.js → typecheckCommand-BuCtT92o.js} +7 -7
- package/dist/updateCommand-6FMU2klq.js +2 -0
- package/dist/{updateCommand-CmksX7m_.js → updateCommand-CHBmCB17.js} +5 -3
- package/dist/{webDev-BKdD7c_a.js → webDev-CTpSY-e_.js} +813 -775
- package/dist/webDev-id3I5PvG.js +2 -0
- package/dist/{webhookDiscovery-D7VaeMlz.js → webhookDiscovery-CphsQe59.js} +4 -6
- package/dist/{webhookDiscovery-CrGAfhIG.js → webhookDiscovery-il9ti-HE.js} +1 -1
- package/dist/{webhooksCommand-BHeaF1ZJ.js → webhooksCommand-BVvOtZ1B.js} +1 -1
- package/package.json +27 -21
- package/templates/AGENTS.md +2 -2
- package/templates/agent-docs/_index.md +2 -2
- package/templates/agent-docs/_manifest.json +2 -2
- package/templates/agent-docs/authentication.md +30 -0
- package/templates/agent-docs/cli.md +17 -13
- package/templates/agent-docs/data.md +44 -1
- package/templates/agent-docs/database/migrations.md +2 -2
- package/templates/agent-docs/database/seedsdialects.md +2 -2
- package/templates/agent-docs/deployment.md +21 -7
- package/templates/agent-docs/internationalization.md +10 -3
- package/templates/agent-docs/local-first-mobile.md +141 -20
- package/templates/agent-docs/plugins/mail.md +1 -1
- package/templates/agent-docs/plugins/ratelimit.md +1 -1
- package/templates/agent-docs/plugins.md +3 -1
- package/templates/agent-docs/releases.md +130 -1
- package/templates/agent-docs/routing.md +34 -2
- package/templates/agent-docs/templates/apibackends.md +1 -1
- package/templates/agent-docs/templates/mobile.md +1 -1
- package/templates/agent-docs/whats-new.md +181 -49
- package/templates/apps/api-ai/package.json +7 -7
- package/templates/apps/api-auth/package.json +8 -8
- package/templates/apps/api-backend/package.json +7 -7
- package/templates/apps/api-backend-deactivation/package.json +7 -7
- package/templates/apps/api-backend-mail/package.json +8 -8
- package/templates/apps/api-backend-mariadb/package.json +9 -9
- package/templates/apps/api-backend-sqlite/package.json +8 -8
- package/templates/apps/api-backend-storage/package.json +8 -8
- package/templates/apps/api-cms/package.json +10 -10
- package/templates/apps/api-collab/package.json +8 -8
- package/templates/apps/api-data-advanced/package.json +8 -8
- package/templates/apps/api-durable/package.json +8 -8
- package/templates/apps/api-feature-flags/package.json +9 -9
- package/templates/apps/api-governance/package.json +8 -8
- package/templates/apps/api-kv/package.json +8 -8
- package/templates/apps/api-moderation/package.json +8 -8
- package/templates/apps/api-observability/package.json +8 -8
- package/templates/apps/api-ratelimit/package.json +8 -8
- package/templates/apps/api-rbac/authz.ts +16 -1
- package/templates/apps/api-rbac/package.json +8 -8
- package/templates/apps/api-rest/package.json +7 -7
- package/templates/apps/api-saas/package.json +11 -11
- package/templates/apps/api-saas-starter/package.json +10 -10
- package/templates/apps/api-search/package.json +8 -8
- package/templates/apps/api-status/package.json +8 -8
- package/templates/apps/api-versioning/package.json +8 -8
- package/templates/apps/api-webhooks/package.json +9 -9
- package/templates/apps/changelog/package.json +6 -6
- package/templates/apps/edge-functions/package.json +2 -2
- package/templates/apps/frontend-admin/package.json +8 -8
- package/templates/apps/frontend-app/package.json +9 -9
- package/templates/apps/frontend-auth/package.json +8 -8
- package/templates/apps/frontend-blank/package.json +7 -7
- package/templates/apps/frontend-cms/package.json +9 -9
- package/templates/apps/frontend-collab/package.json +10 -10
- package/templates/apps/frontend-contact/package.json +7 -7
- package/templates/apps/frontend-dashboard/package.json +7 -7
- package/templates/apps/frontend-docs/package.json +7 -7
- package/templates/apps/frontend-i18n/package.json +6 -6
- package/templates/apps/frontend-landing/package.json +7 -7
- package/templates/apps/frontend-portal/package.json +8 -8
- package/templates/apps/frontend-saas/package.json +8 -8
- package/templates/apps/frontend-spa/package.json +7 -7
- package/templates/apps/frontend-ssr/package.json +7 -7
- package/templates/apps/frontend-ssr-api/package.json +8 -8
- package/templates/apps/frontend-static-blog/package.json +6 -6
- package/templates/apps/frontend-status/package.json +8 -8
- package/templates/apps/mobile-app/README.md +41 -17
- package/templates/apps/mobile-app/app.config.ts +1 -1
- package/templates/apps/mobile-app/package.json +15 -11
- package/templates/apps/mobile-app/src/app/_layout.tsx +44 -13
- package/templates/apps/mobile-app/src/app/index.tsx +1 -1
- package/templates/apps/mobile-app/src/app/settings.tsx +4 -1
- package/templates/apps/mobile-app/src/client.ts +74 -57
- package/templates/apps/mobile-app/src/lib/api.ts +4 -4
- package/templates/apps/mobile-app/src/lib/deeplinks.ts +6 -6
- package/templates/apps/mobile-app/src/persistence.ts +24 -3
- package/templates/apps/mobile-app/tsconfig.json +17 -3
- package/dist/addCommand-aXSQveak.js +0 -2
- package/dist/agentsMd-BTchIZku.js +0 -2
- package/dist/apiBuild-ttSDDOGp.js +0 -2
- package/dist/baselineCommand-Cfw2Afwm.js +0 -2
- package/dist/codegen-uYrrDzQv.js +0 -2
- package/dist/codegenCommand-C60m_LCp.js +0 -30
- package/dist/dbCommand-CC-qNzpz.js +0 -2
- package/dist/dev-CAOoXOkc.js +0 -3
- package/dist/doctorCommand-Cjl8Yii8.js +0 -2
- package/dist/inspect-CjTYzAs_.js +0 -1190
- package/dist/inspect-P4pxoMaV.js +0 -2
- package/dist/inspectGateHint-BF6608UT.js +0 -4
- package/dist/manifestBuild-COkJoyAr.js +0 -2
- package/dist/serveCommand-DNRF2nDI.js +0 -2
- package/dist/serveCommand-DnY4VS3D.js +0 -1767
- package/dist/updateCommand-B0J_T9Vs.js +0 -2
- package/dist/webDev-CcJfdSjl2.js +0 -2
|
@@ -341,16 +341,88 @@ code already speaks:
|
|
|
341
341
|
_"@voltro/react-native — the credential-free mobile plumbing: registerDevice + the _voltro_devices table, defineDeepLink + its matcher, useBackgroundSync, and offline-first client defaults + connection status."_
|
|
342
342
|
|
|
343
343
|
The React-client bindings are **import-safe** in React Native — every DOM touch
|
|
344
|
-
in `@voltro/client` is `typeof window`-guarded, so nothing crashes at import
|
|
345
|
-
|
|
346
|
-
`
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
344
|
+
in `@voltro/client` is `typeof window`-guarded, so nothing crashes at import —
|
|
345
|
+
and the **client runtime now builds on RN**: `@voltro/client`'s
|
|
346
|
+
[`buildApiRuntime`](/docs/react-native/overview#building-the-client-on-react-native)
|
|
347
|
+
constructs the `ApiHandle` (runtime + subscription cache + rpc client) over a
|
|
348
|
+
WebSocket **you** inject, so RN passes its own `globalThis.WebSocket` and gets
|
|
349
|
+
the same client stack the web app uses, without pulling in `@voltro/web`.
|
|
350
|
+
|
|
351
|
+
Still open before the loop is proven end-to-end on a device: codegen emitting the
|
|
352
|
+
api's rpc group for a mobile app, an RN persistence adapter, a NetInfo connection
|
|
353
|
+
signal, and a reconnect supervisor. `@voltro/react-native` ships the
|
|
354
|
+
mobile-specific plumbing around that, limited to the parts that need **no
|
|
355
|
+
per-tenant credentials and no native runtime**: device registration,
|
|
356
|
+
background-sync scheduling, offline-first defaults, a connection-status surface,
|
|
357
|
+
and the deep-link declaration shape.
|
|
358
|
+
|
|
359
|
+
## Building the client on React Native
|
|
360
|
+
|
|
361
|
+
`startMobileApis()` connects every api your app declares and keeps them
|
|
362
|
+
connected — over the **same** supervisor the web client uses, not a mobile copy
|
|
363
|
+
of it: exponential backoff, generation tracking, and the stale-seed gate that
|
|
364
|
+
must never carry one subject's rows into the next one's screens.
|
|
365
|
+
|
|
366
|
+
```ts
|
|
367
|
+
import { startMobileApis, toApiHandles } from '@voltro/react-native'
|
|
368
|
+
import { mobileApis } from './.framework/mobileApis.generated' // from `voltro codegen`
|
|
369
|
+
|
|
370
|
+
const apis = mobileApis(() => 'ws://192.168.1.20:4000/ws')
|
|
371
|
+
|
|
372
|
+
const supervisor = startMobileApis({
|
|
373
|
+
apis,
|
|
374
|
+
onChange: (clients) => setHandles(toApiHandles(apis, clients)),
|
|
375
|
+
})
|
|
376
|
+
// later: supervisor.dispose() — or supervisor.reconnect() after a sign-in
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
`buildApiRuntime()` from `@voltro/client` is the layer underneath, if you want
|
|
380
|
+
one connection without supervision:
|
|
381
|
+
|
|
382
|
+
```ts
|
|
383
|
+
import { buildApiRuntime } from '@voltro/client'
|
|
384
|
+
|
|
385
|
+
const built = await buildApiRuntime({
|
|
386
|
+
name: 'app',
|
|
387
|
+
wsUrl: 'ws://192.168.1.20:4000/ws', // your machine's LAN address
|
|
388
|
+
group: rpcGroup, // from codegen
|
|
389
|
+
// RN provides a global WebSocket; the web client injects a tracked one.
|
|
390
|
+
webSocketConstructor: (url, protocols) => new globalThis.WebSocket(url, protocols as string[]),
|
|
391
|
+
})
|
|
392
|
+
// built = { runtime, cache, client, errorBus } → an ApiHandle for the provider
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
### The api binding is generated
|
|
396
|
+
|
|
397
|
+
`voltro codegen` reads the app's `voltro.mobile.ts` and writes
|
|
398
|
+
`.framework/mobileApis.generated.ts` — which apis this app talks to, and where
|
|
399
|
+
each one's rpc group and descriptors come from. The template's `pnpm ios` /
|
|
400
|
+
`pnpm start` scripts run it, so there is no separate step.
|
|
401
|
+
|
|
402
|
+
```ts
|
|
403
|
+
// voltro.mobile.ts
|
|
404
|
+
export default {
|
|
405
|
+
apis: { app: { package: '@acme/api' } },
|
|
406
|
+
}
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
Only the BINDING is generated. The procedure types ride the import of the api
|
|
410
|
+
package's own `rpcGroup`, so a schema change needs no regeneration here.
|
|
411
|
+
|
|
412
|
+
### `localhost` on a phone is the phone
|
|
413
|
+
|
|
414
|
+
The ws URL is a runtime parameter, never baked into the generated file. A device
|
|
415
|
+
pointed at `ws://localhost:4000/ws` connects to itself, times out and retries
|
|
416
|
+
forever — which reads as a broken framework rather than a wrong host.
|
|
417
|
+
`resolveDevWsUrl()` takes the LAN host Expo already knows:
|
|
418
|
+
|
|
419
|
+
```ts
|
|
420
|
+
import Constants from 'expo-constants'
|
|
421
|
+
import { resolveDevWsUrl } from '@voltro/react-native'
|
|
422
|
+
|
|
423
|
+
const wsUrl = process.env.EXPO_PUBLIC_API_WS_URL
|
|
424
|
+
?? resolveDevWsUrl(Constants.expoConfig?.hostUri, 4000)
|
|
425
|
+
```
|
|
354
426
|
|
|
355
427
|
> **Scaffold a mobile app.** `voltro create-project acme --api=api-backend
|
|
356
428
|
> --mobile` (or `voltro add-app mobile --template mobile-app`) scaffolds an Expo
|
|
@@ -362,6 +434,34 @@ The package **root is RN-safe** — no `node:*`, no `@voltro/database`, and Reac
|
|
|
362
434
|
is reached only through the hooks (an optional peer). The `_voltro_devices` table
|
|
363
435
|
declaration is server-side and lives at `@voltro/react-native/schema`.
|
|
364
436
|
|
|
437
|
+
## Persisted stores on a device
|
|
438
|
+
|
|
439
|
+
`defineStore({ persist })` reads during RENDER, and a render cannot await — so a
|
|
440
|
+
device's async storage cannot back it directly. `createAsyncStoragePersistence()`
|
|
441
|
+
hydrates the keys into memory once, then serves reads from memory and writes
|
|
442
|
+
through asynchronously.
|
|
443
|
+
|
|
444
|
+
```ts
|
|
445
|
+
import AsyncStorage from '@react-native-async-storage/async-storage'
|
|
446
|
+
import { setStoreStorage } from '@voltro/client'
|
|
447
|
+
import { createAsyncStoragePersistence } from '@voltro/react-native'
|
|
448
|
+
|
|
449
|
+
const persistence = createAsyncStoragePersistence({ storage: AsyncStorage })
|
|
450
|
+
|
|
451
|
+
await persistence.hydrate() // BEFORE the first render
|
|
452
|
+
setStoreStorage(persistence.provider)
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
**`hydrate()` must be awaited before rendering.** An app that renders first shows
|
|
456
|
+
empty state and flickers into the saved state a frame later; the template holds
|
|
457
|
+
the Expo splash screen until it resolves. Writes to one key coalesce per tick, so
|
|
458
|
+
a store written on every keystroke costs one round trip, and a failed write is
|
|
459
|
+
reported through `onError` rather than thrown into the `set()` that caused it.
|
|
460
|
+
|
|
461
|
+
A storage with no `getAllKeys()` and no explicit `keys: [...]` is a REFUSAL, not
|
|
462
|
+
an empty hydration — an empty cache is indistinguishable from a first run, which
|
|
463
|
+
is the hardest persistence bug there is to attribute.
|
|
464
|
+
|
|
365
465
|
## Device registration
|
|
366
466
|
|
|
367
467
|
A device is registered **after** the OS issues its push token (APNs on iOS, FCM
|
|
@@ -425,7 +525,7 @@ host away, so a universal link, an App Link, and a custom-scheme URL all match
|
|
|
425
525
|
the same path-only pattern:
|
|
426
526
|
|
|
427
527
|
```ts
|
|
428
|
-
import {
|
|
528
|
+
import { dispatchDeepLink, matchDeepLink } from '@voltro/react-native'
|
|
429
529
|
import orderLink from './orders.deepLink'
|
|
430
530
|
|
|
431
531
|
matchDeepLink('/orders/:id', '/orders/42') // → { id: '42' }
|
|
@@ -433,14 +533,22 @@ matchDeepLink('/orders/:id', '/orders/42/edit') // → null
|
|
|
433
533
|
|
|
434
534
|
// Until `*.deepLink.ts` file discovery lands, register links by hand —
|
|
435
535
|
// declaration order wins, so list more-specific patterns first.
|
|
436
|
-
const
|
|
437
|
-
|
|
536
|
+
const params = dispatchDeepLink([orderLink], 'myapp://orders/42')
|
|
537
|
+
params?.id // '42' — and the winning handler has already run
|
|
438
538
|
```
|
|
439
539
|
|
|
540
|
+
**Use `dispatchDeepLink` for a TABLE, `runDeepLink` for one descriptor.**
|
|
541
|
+
`matchFirstDeepLink` also exists and only inspects: because a table is a
|
|
542
|
+
heterogeneous array, the descriptor it returns has an erased pattern, so its
|
|
543
|
+
handler's declared params (`Record<string, never>`) reject the params returned
|
|
544
|
+
alongside it. Matching and invoking in two steps therefore does not typecheck —
|
|
545
|
+
which is why the dispatching version exists rather than being left to every
|
|
546
|
+
caller to cast around.
|
|
547
|
+
|
|
440
548
|
> **Seam — file discovery.** Wiring `*.deepLink.ts` into codegen (so the router
|
|
441
549
|
> auto-collects every declared link) is one additive file, landing after the
|
|
442
550
|
> current release settles. The descriptor shape above is **final**, so register
|
|
443
|
-
> links via `
|
|
551
|
+
> links via `dispatchDeepLink()` until then.
|
|
444
552
|
|
|
445
553
|
## Background sync
|
|
446
554
|
|
|
@@ -471,21 +579,33 @@ function SyncIndicator() {
|
|
|
471
579
|
`offlineFirstDefaults` is the mobile posture as a value you spread into your
|
|
472
580
|
client config: local-first ON, optimistic mutations, sync-on-foreground, a
|
|
473
581
|
5-minute cadence, and a retry backoff schedule. `useMobileConnectionStatus()`
|
|
474
|
-
surfaces a `connected | degraded | offline` status
|
|
475
|
-
|
|
582
|
+
surfaces a `connected | degraded | offline` status.
|
|
583
|
+
|
|
584
|
+
**Where "online" comes from is injected, not detected.** The default source reads
|
|
585
|
+
`navigator.onLine`, which React Native does not have — so on a device it answers
|
|
586
|
+
"online" forever, airplane mode included. Pass `netInfoOnlineSource(NetInfo)`:
|
|
476
587
|
|
|
477
588
|
```tsx
|
|
478
|
-
import
|
|
589
|
+
import NetInfo from '@react-native-community/netinfo'
|
|
590
|
+
import { netInfoOnlineSource, offlineFirstDefaults, useMobileConnectionStatus } from '@voltro/react-native'
|
|
591
|
+
|
|
592
|
+
// Module scope: an inline call is a new object every render, and the hook would
|
|
593
|
+
// resubscribe on each one.
|
|
594
|
+
const onlineSource = netInfoOnlineSource(NetInfo)
|
|
479
595
|
|
|
480
|
-
// Spread the mobile posture into your client config.
|
|
481
596
|
const config = { ...offlineFirstDefaults, url }
|
|
482
597
|
|
|
483
598
|
function ConnectionPill() {
|
|
484
|
-
const { status, reportFailure, reportSuccess } = useMobileConnectionStatus()
|
|
599
|
+
const { status, reportFailure, reportSuccess } = useMobileConnectionStatus({ onlineSource })
|
|
485
600
|
return <span data-status={status}>{status}</span>
|
|
486
601
|
}
|
|
487
602
|
```
|
|
488
603
|
|
|
604
|
+
`isInternetReachable` is believed only when it is a boolean. NetInfo reports
|
|
605
|
+
`null` while its probe is outstanding, and reading that as `false` flashes
|
|
606
|
+
"offline" on every cold start and every network change — so a `null` falls back
|
|
607
|
+
to the link-layer `isConnected`.
|
|
608
|
+
|
|
489
609
|
## What's shipped vs. a seam
|
|
490
610
|
|
|
491
611
|
This package ships the credential-free plumbing above. The parts that need
|
|
@@ -498,4 +618,5 @@ not built here:
|
|
|
498
618
|
| **Native module bindings** (camera, biometrics, secure token storage) | Need a native runtime this TS package cannot provide. |
|
|
499
619
|
| **Swift / Kotlin SDK generators** | **Built + golden-tested** — `voltro build api --target swift\|kotlin` emits a native SDK package. What is deferred is *compiling* the emitted package (`swiftc` / Gradle): that is a mobile-CI step, no cross-language toolchain lives in the framework repo. |
|
|
500
620
|
| **Universal-links / App-Links file automation** (`apple-app-site-association`, `assetlinks.json`) | A deployment-layer concern, not a client primitive. |
|
|
501
|
-
| **`*.deepLink.ts` codegen discovery** | One additive file after the release settles; the descriptor shape is final, so register links via `
|
|
621
|
+
| **`*.deepLink.ts` codegen discovery** | One additive file after the release settles; the descriptor shape is final, so register links via `dispatchDeepLink()` today. |
|
|
622
|
+
| **Booting on a device** | Everything above is unit-tested without a simulator, and a simulator is the only thing that can prove the loop runs under Metro. That is an Expo/EAS CI step — the framework repository has no iOS/Android toolchain, and we say so rather than implying coverage we do not have. |
|
|
@@ -331,4 +331,4 @@ mailPlugin({ provider: 'resend', from: '…', allowlist: ['me@acme.com'] })
|
|
|
331
331
|
|
|
332
332
|
- [Webhooks](/docs/plugins/webhooks) — mounting the provider bounce/complaint query
|
|
333
333
|
- [Rate limiting](/docs/plugins/ratelimit) — the sibling interceptor plugin
|
|
334
|
-
- [Multi-tenancy](/docs/multi-tenancy) — where per-tenant suppression scoping comes from
|
|
334
|
+
- [Multi-tenancy](/docs/multi-tenancy/overview) — where per-tenant suppression scoping comes from
|
|
@@ -280,4 +280,4 @@ Bring your own backend entirely by passing any object that satisfies the
|
|
|
280
280
|
## See also
|
|
281
281
|
|
|
282
282
|
- [Mutations](/docs/data/mutations) — the interceptor chain rate limiting hooks into
|
|
283
|
-
- [Multi-tenancy](/docs/multi-tenancy) — where `subject.tenantId` comes from
|
|
283
|
+
- [Multi-tenancy](/docs/multi-tenancy/overview) — where `subject.tenantId` comes from
|
|
@@ -67,6 +67,8 @@ Status legend: ✓ shipped · ◐ partial · — planned.
|
|
|
67
67
|
| `@voltro/plugin-rbac` | ✓ | Roles compile to scopes + the `permission()` handler guard + typed `ScopeError` |
|
|
68
68
|
| `@voltro/plugin-ratelimit` | ✓ | Per-endpoint / per-subject / per-tenant limits; sliding-window / fixed-window / token-bucket; memory / postgres / redis stores |
|
|
69
69
|
| `@voltro/plugin-billing` | ✓ | Subscriptions, plans, entitlements + usage metering over a pluggable provider (Stripe + mock); seat-based billing; proration, failed-payment retries, tax and the checkout seat stepper are Stripe's, via the official SDK; `requireEntitlement()` guard + `enforce` interceptor; `/billing/webhook` via plugin-webhooks; money as integer minor units |
|
|
70
|
+
| `@voltro/plugin-licensing` | ✓ | Offline-verified EdDSA license keys + cloud-issued entitlement snapshots that feed plugin-billing; plan entitlements + pricing decided server-side, never baked into a published version. [→ details](/docs/plugins/licensing) |
|
|
71
|
+
| `@voltro/plugin-ai-flows` | ✓ | Durable multi-step AI pipelines — deterministic or agentic, with human-in-the-loop, chaining and cadence; author flows in code (`defineFlow`) or as data (visual-editor rows), one engine runs both. [→ details](/docs/plugins/ai-flows) |
|
|
70
72
|
| `@voltro/plugin-mail` | ✓ | Transactional email — Resend / Postmark / SendGrid / SES / Mailgun / SMTP, *.email.tsx templates, per-tenant suppression, send-time scheduling, bulk/batch send, per-send idempotency, durable via workflows |
|
|
71
73
|
| `@voltro/plugin-storage` | ✓ | File storage — public (CDN-direct) + private (access policy + per-object grants), S3 / R2 / GCS / MinIO / filesystem providers, presigned URLs, `listRefs` browse/search, HTTP Range (206) serving, dashboard browser |
|
|
72
74
|
| `@voltro/plugin-postgis` | ✓ | Postgres-native `geography` / `geometry` columns + spatial predicates (`ST_DWithin`, `ST_Contains`, `ST_Intersects`); GiST indexes via `.expressionIndex(..., { kind: 'gist' })`. No `ST_Distance` projection yet. Postgres-only by design (fails loud elsewhere). [→ details](/docs/plugins/postgis) |
|
|
@@ -432,7 +434,7 @@ Absent `framework` field → no compat check. Suitable for in-tree plugins that
|
|
|
432
434
|
- **Mutate other plugins' state.** Plugins don't talk to each other directly. If two plugins need to coordinate, it's via the rpc layer (one plugin's interceptor sees the other's `subject.metadata`, for example).
|
|
433
435
|
- **Bypass tenant scoping.** Interceptors run AFTER the runtime's tenant predicate merge. A mutation interceptor can't query rows from another tenant by manipulating `subject.tenantId`.
|
|
434
436
|
- **Access raw secrets directly.** Plugins get config via their own factory function's options object. `lifecycle.env` exposes `process.env` but only the plugin's own factory chooses which env vars to read.
|
|
435
|
-
- **Modify the core schema DSL or query builder.** Schema extension happens through schema mixins (
|
|
437
|
+
- **Modify the core schema DSL or query builder.** Schema extension happens through schema mixins (`defineMixin` from `@voltro/database`); not through plugin runtime hooks.
|
|
436
438
|
|
|
437
439
|
These boundaries hold for v1; some may relax for verified plugins once the marketplace ships.
|
|
438
440
|
|
|
@@ -1,6 +1,135 @@
|
|
|
1
1
|
# Releases
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> 0.35 through 0.38 in one pass — the boot-breaking access declarations, the stricter input handling, and the runtime behaviour that moved underneath you.
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
<!-- source: en/releases/upgrading-to-0-38.md -->
|
|
10
|
+
## Upgrading to 0.38.0
|
|
11
|
+
|
|
12
|
+
_0.35 through 0.38 in one pass — the boot-breaking access declarations, the stricter input handling, and the runtime behaviour that moved underneath you._
|
|
13
|
+
|
|
14
|
+
0.35, 0.36, 0.37 and 0.38 landed within two days of each other, so this page covers
|
|
15
|
+
them as one upgrade. It is much smaller than [0.34](/docs/releases/upgrading-to-0-34) —
|
|
16
|
+
but two of the changes will stop your app from booting until you make a decision, and
|
|
17
|
+
one changes how every request is validated.
|
|
18
|
+
|
|
19
|
+
Run the upgrade first, fix the boot, then read what moved at runtime.
|
|
20
|
+
|
|
21
|
+
## 1. Run `voltro update`
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
voltro update
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Every breaking change below either ships a codemod that rewrites your source or prints
|
|
28
|
+
written steps during the update. What a codemod cannot do is make an access decision on
|
|
29
|
+
your behalf — that part is yours.
|
|
30
|
+
|
|
31
|
+
## 2. What breaks your boot
|
|
32
|
+
|
|
33
|
+
### A declared event must decide who may listen (0.35)
|
|
34
|
+
|
|
35
|
+
`defineEvent`'s `guards:` was optional, and an empty list was skipped — so under
|
|
36
|
+
`security.defaultDeny` an event with **no access declaration was subscribable by anyone
|
|
37
|
+
who could open the socket**, while the identical shape was already refused for every
|
|
38
|
+
procedure. The boot gate now covers `defineEvent` too.
|
|
39
|
+
|
|
40
|
+
Declare an access decision on each event: a `guards:` list, or an explicit
|
|
41
|
+
`openAccess: '<reason>'` when it genuinely is public.
|
|
42
|
+
|
|
43
|
+
### Twelve first-party plugin routes now require a scope (0.35)
|
|
44
|
+
|
|
45
|
+
Every first-party plugin rpc route now declares an access decision, and `defaultDeny` is
|
|
46
|
+
enforced in the dispatch spine as defense in depth. Previously-open routes that now need
|
|
47
|
+
a scope:
|
|
48
|
+
|
|
49
|
+
| Route | Scope |
|
|
50
|
+
|---|---|
|
|
51
|
+
| `billing.startCheckout` / `portalUrl` / `previewChange` / `changePlan` / `changeSeats` / `invoices` | `billing:manage` |
|
|
52
|
+
| `billing.reportUsage` | `billing:report` |
|
|
53
|
+
| `governance.export` / `erase` | `admin:full` (was already enforced in-handler, now declared) |
|
|
54
|
+
| `storage.mintUploadUrl` / `ingestUrl` | `storage:manage` |
|
|
55
|
+
| `storage.listRefs` | `storage:browse` |
|
|
56
|
+
|
|
57
|
+
**Migration:** grant each scope to the role or subjects that legitimately hold that
|
|
58
|
+
capability — via an rbac role, `resolveScopes`, or api-key scopes. The codemod lists
|
|
59
|
+
every affected route plus the open-by-design surfaces that did **not** change.
|
|
60
|
+
|
|
61
|
+
### The framework table set no longer reads a runtime flag (0.35)
|
|
62
|
+
|
|
63
|
+
`CDC`, `VOLTRO_UNDO` and `VOLTRO_TRACING_PERSIST` used to move the *declared* table set,
|
|
64
|
+
which meant the migrate job's env and the pod's env could disagree — a green apply
|
|
65
|
+
followed by a crash loop. `app.config.ts` gained `schema: { traces?, undo? }` to declare
|
|
66
|
+
the two that still need a decision.
|
|
67
|
+
|
|
68
|
+
### A failing `*.startup.ts` now refuses the boot (0.38)
|
|
69
|
+
|
|
70
|
+
It used to warn and let the server come up. If your startup module is allowed to fail,
|
|
71
|
+
handle the failure inside it — the framework will no longer serve traffic behind a
|
|
72
|
+
startup that did not complete.
|
|
73
|
+
|
|
74
|
+
## 3. What changed at runtime
|
|
75
|
+
|
|
76
|
+
### An undeclared input field now rejects the call (0.37)
|
|
77
|
+
|
|
78
|
+
A field a procedure's input schema does not declare **rejects** the request. It used to
|
|
79
|
+
be silently discarded and the call ran with what was left. This is the change most likely
|
|
80
|
+
to surface in a client you did not update in lockstep: an extra property that used to be
|
|
81
|
+
ignored is now an error. Check any hand-built request payloads.
|
|
82
|
+
|
|
83
|
+
### `IMPERSONATION_METADATA_KEY` moved to `@voltro/protocol` (0.38)
|
|
84
|
+
|
|
85
|
+
It names the one reserved key in `Subject.metadata`, and `Subject` is protocol's type —
|
|
86
|
+
two packages need it (plugin-auth writes the mark, plugin-audit reads it) and a plugin
|
|
87
|
+
must not depend on another plugin. The codemod repoints the import, preserving an alias
|
|
88
|
+
and the type-only form. Everything else stays where it was: `impersonationOf`,
|
|
89
|
+
`isImpersonated`, `ImpersonationMark` and `impersonationAuditRedactor` are still exported
|
|
90
|
+
from `@voltro/plugin-auth`.
|
|
91
|
+
|
|
92
|
+
An audit row now also records when an action was taken through an **impersonated**
|
|
93
|
+
session, on the default settings, and no redactor can remove that mark.
|
|
94
|
+
|
|
95
|
+
### Upstream 401s report a different code (0.36)
|
|
96
|
+
|
|
97
|
+
A 401 from an upstream now produces `code: 'unauthorized'`, not `code: 'session_expired'`.
|
|
98
|
+
The connection vault's own failure — the case where we *do* know the credential is
|
|
99
|
+
unusable — becomes `code: 'credential_unusable'`. If you branch on these codes, update
|
|
100
|
+
the branch.
|
|
101
|
+
|
|
102
|
+
### `RunStepStatus` gained `'skipped'` (0.35, plugin-ai-flows)
|
|
103
|
+
|
|
104
|
+
A sixth member means an exhaustive `switch` stops compiling and a status-keyed lookup has
|
|
105
|
+
a hole. A manual codemod fires on any app that names the type or its literals. Everything
|
|
106
|
+
else in that release is additive.
|
|
107
|
+
|
|
108
|
+
### The analytics CDC mirror versions from the change, not the clock (0.35)
|
|
109
|
+
|
|
110
|
+
Under `changeScope: 'fleet'` (postgres CDC, mysql binlog) the mirror's version is derived
|
|
111
|
+
from the change's own position in the totally-ordered fleet stream instead of each
|
|
112
|
+
replica's clock. N replicas still issue N writes per change — that is the transport — but
|
|
113
|
+
they are now **byte-identical**, so the sinks' existing guards dedupe them for free.
|
|
114
|
+
|
|
115
|
+
This closed a real defect: under clock skew larger than the gap between two changes to one
|
|
116
|
+
row, a peer's duplicate of the *older* image could take the higher version and win in the
|
|
117
|
+
warehouse permanently and silently.
|
|
118
|
+
|
|
119
|
+
**If you wrote a custom sink**, `AnalyticsMirrorImpl.maxVersion` is now **required** — a
|
|
120
|
+
replica joining mid-stream seeds each key from the warehouse's high-water mark. All
|
|
121
|
+
shipped warehouse sinks implement it; the codemod note covers a custom one. New tunable:
|
|
122
|
+
`VOLTRO_ANALYTICS_MIRROR_VERSION_STATE_LIMIT` (default 100000).
|
|
123
|
+
|
|
124
|
+
## The short checklist
|
|
125
|
+
|
|
126
|
+
1. `voltro update` — let the codemods run.
|
|
127
|
+
2. Declare access on every `defineEvent`.
|
|
128
|
+
3. Grant the twelve plugin-route scopes to the roles that should hold them.
|
|
129
|
+
4. Move `CDC` / undo / traces decisions into `app.config.ts`'s `schema: { … }`.
|
|
130
|
+
5. Make sure your `*.startup.ts` cannot fail unintentionally.
|
|
131
|
+
6. Audit client payloads for fields your input schemas do not declare.
|
|
132
|
+
7. If you have a custom analytics sink, implement `maxVersion`.
|
|
4
133
|
|
|
5
134
|
|
|
6
135
|
|
|
@@ -758,7 +758,18 @@ That last case is how dynamic `static` routes work in dev / when `getStaticPaths
|
|
|
758
758
|
|
|
759
759
|
_Server-side data fetch via `loader`, page-level `<head>` tags via `meta`, and how the build pipeline runs both._
|
|
760
760
|
|
|
761
|
-
A **loader** is the page's
|
|
761
|
+
A **loader** is the page's data hook. It runs before the React render (during SSR, during SSG, or per-request for ISR/SSR), and its result lands in `useLoaderData<T>()`. **Meta** is a sibling export that produces `<title>` + `<meta>` tags.
|
|
762
|
+
|
|
763
|
+
> **It runs on the server AND again in the browser.** A loader is not a
|
|
764
|
+
> server-only hook: it runs during SSR for the first paint, and runs AGAIN, in
|
|
765
|
+
> the browser, on every in-app navigation to the route. Same function, different
|
|
766
|
+
> environment — so anything server-only in it must be guarded with
|
|
767
|
+
> `ctx.isServer`.
|
|
768
|
+
>
|
|
769
|
+
> This is the single most expensive thing to learn late, because a client-only
|
|
770
|
+
> failure is invisible to every probe that does not NAVIGATE: a fresh page load,
|
|
771
|
+
> a `curl`, any SSR check all take the server path and pass. Only clicking a
|
|
772
|
+
> link inside the running app reaches the other one.
|
|
762
773
|
|
|
763
774
|
Both are static module exports — the framework discovers them, the build pipeline runs them.
|
|
764
775
|
|
|
@@ -780,7 +791,8 @@ export const loader = async ({ params, headers }: {
|
|
|
780
791
|
params: { id: string }
|
|
781
792
|
headers: Readonly<Record<string, string>>
|
|
782
793
|
}): Promise<Note> => {
|
|
783
|
-
//
|
|
794
|
+
// Runs during SSR *and* again in the browser on in-app navigation —
|
|
795
|
+
// guard anything server-only with `ctx.isServer`.
|
|
784
796
|
const res = await fetch(`${INTERNAL_API}/notes/${params.id}`, {
|
|
785
797
|
headers: { cookie: headers.cookie ?? '' },
|
|
786
798
|
})
|
|
@@ -1026,6 +1038,7 @@ React alone.
|
|
|
1026
1038
|
export const loader = async (ctx: {
|
|
1027
1039
|
readonly params: Readonly<Record<string, string>> // URL params from [name] segments
|
|
1028
1040
|
readonly pathname: string // matched path (no query string)
|
|
1041
|
+
readonly isServer: boolean // true during SSR/SSG, false in the browser
|
|
1029
1042
|
readonly search: string // raw query string incl. `?`, or '' — filled on every path
|
|
1030
1043
|
readonly signal: AbortSignal // Aborts if the client disconnects mid-render
|
|
1031
1044
|
readonly headers?: Readonly<Record<string, string>> // Request headers (SSR/ISR only — empty for SSG/client)
|
|
@@ -1036,6 +1049,25 @@ export const loader = async (ctx: {
|
|
|
1036
1049
|
}) => Promise<unknown>
|
|
1037
1050
|
```
|
|
1038
1051
|
|
|
1052
|
+
### Branch on `isServer`, not on what happens to be missing
|
|
1053
|
+
|
|
1054
|
+
`isServer` is the supported way to ask which invocation this is. The two things
|
|
1055
|
+
that look like they answer the same question do not:
|
|
1056
|
+
|
|
1057
|
+
- **`query` is absent in the browser**, so `if (ctx.query)` appears to work. It
|
|
1058
|
+
branches on the ABSENCE OF A FUNCTION, which says nothing about why it is
|
|
1059
|
+
absent and breaks the moment anything else becomes conditional.
|
|
1060
|
+
- **`headers` is `{}` in the browser, not `undefined`** — so `if (ctx.headers)`
|
|
1061
|
+
is TRUE on both paths. A consumer wrote exactly that check and it silently did
|
|
1062
|
+
nothing.
|
|
1063
|
+
|
|
1064
|
+
```ts
|
|
1065
|
+
export const loader = async (ctx: LoaderContext) => {
|
|
1066
|
+
if (ctx.isServer) seedStore(prefs, await ctx.query!('prefs.get'))
|
|
1067
|
+
return null
|
|
1068
|
+
}
|
|
1069
|
+
```
|
|
1070
|
+
|
|
1039
1071
|
The loader context carries `pathname` and `search`, not a `request` object.
|
|
1040
1072
|
|
|
1041
1073
|
`pathname` is deliberately query-free — a loader keyed on `?tab=2` would cache badly. `search` carries the raw query string (with its leading `?`, or `''`), filled identically on client navigation, `voltro dev` SSR and `voltro start` SSR. Parse it with `new URLSearchParams(ctx.search)`.
|
|
@@ -1006,7 +1006,7 @@ export default async ({ store, log, onShutdown, id }: StartupContext) => {
|
|
|
1006
1006
|
}
|
|
1007
1007
|
```
|
|
1008
1008
|
|
|
1009
|
-
A throw here
|
|
1009
|
+
A throw here REFUSES the boot, naming the file and the cause — a startup is where an app arms things the request path depends on, and "serving without them" is the failure nobody sees. Catch the error inside the function if a failure is genuinely acceptable (a cache warm), so the decision sits where somebody made it. The function must also RETURN: start the work, hand back teardown via `onShutdown`, return — one that never returns refuses the boot after `VOLTRO_STARTUP_TIMEOUT_MS` (default 60 s), while a slow-but-successful startup is simply awaited. Distinct from `*.seed.ts` (runs once and RETURNS) and `*.cron.tsx` (periodic): a startup HOLDS a resource until shutdown. `StartupContext` is imported from the `@voltro/cli/startup` subpath.
|
|
1010
1010
|
|
|
1011
1011
|
## Try it
|
|
1012
1012
|
|
|
@@ -51,7 +51,7 @@ Deep links are typed and go through one table: a universal link, a custom-scheme
|
|
|
51
51
|
|
|
52
52
|
## Honest status — read before you expect the loop on a device
|
|
53
53
|
|
|
54
|
-
The **pure logic** (`src/lib/*`) is real and unit-tested (`pnpm test`, no simulator)
|
|
54
|
+
The **pure logic** (`src/lib/*`) is real and unit-tested (`pnpm test`, no simulator), and the **client wiring now exists**: `src/client.ts` calls `@voltro/client`'s `buildApiRuntime` with RN's `globalThis.WebSocket`, so `buildApiHandle()` returns a live handle. What it still waits on is **codegen emitting this app's rpc group + descriptor map** (on web those come from the generated entry) — until that lands, `buildApiHandles()` returns an empty map and the screens show their loading state, deliberately rather than faking one. Its header spells out the remaining step.
|
|
55
55
|
|
|
56
56
|
Booting the app on a device (`expo run:ios`) is your Expo/EAS CI step — there is no iOS/Android toolchain in the framework repo.
|
|
57
57
|
|