@softr/sdk 0.0.45-alpha.0 → 0.0.45-alpha.3

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 (36) hide show
  1. package/dist/auth/context.d.ts +1 -1
  2. package/dist/auth/isomorphic.d.ts +2 -19
  3. package/dist/auth/isomorphic.js +2 -24
  4. package/dist/auth/permissions.d.ts +1 -1
  5. package/dist/auth/session.js +1 -15
  6. package/dist/boot-error.js +10 -5
  7. package/dist/datasource/chart.js +3 -3
  8. package/dist/datasource/record.js +13 -8
  9. package/dist/default-error-overlay.js +8 -3
  10. package/dist/index.d.ts +5 -4
  11. package/dist/index.js +5 -4
  12. package/dist/package.js +1 -1
  13. package/dist/server.js +1 -1
  14. package/dist/visibility/audience.d.ts +31 -0
  15. package/dist/visibility/audience.js +25 -0
  16. package/dist/visibility/route.d.ts +71 -0
  17. package/dist/visibility/route.js +83 -0
  18. package/dist/visibility/visibility.d.ts +76 -0
  19. package/dist/visibility/visibility.js +63 -0
  20. package/package.json +10 -33
  21. package/skills/softr-sdk/SKILL.md +11 -12
  22. package/skills/softr-sdk/auth/SKILL.md +2 -2
  23. package/skills/softr-sdk/charts-and-metrics/SKILL.md +2 -2
  24. package/skills/softr-sdk/current-user/SKILL.md +12 -31
  25. package/skills/softr-sdk/file-uploads/SKILL.md +1 -1
  26. package/skills/softr-sdk/integrations/SKILL.md +2 -2
  27. package/skills/softr-sdk/record-mutations/SKILL.md +2 -2
  28. package/skills/softr-sdk/record-queries/SKILL.md +3 -3
  29. package/skills/softr-sdk/region-visibility/SKILL.md +63 -0
  30. package/skills/softr-sdk/routes-and-visibility/SKILL.md +33 -28
  31. package/skills/softr-sdk/workflows/SKILL.md +2 -2
  32. package/dist/mcp.d.ts +0 -36
  33. package/dist/mcp.js +0 -581
  34. package/dist/routes/visibility.d.ts +0 -142
  35. package/dist/routes/visibility.js +0 -115
  36. package/skills/softr-sdk/mcp/SKILL.md +0 -74
@@ -0,0 +1,63 @@
1
+ //#region src/visibility/visibility.ts
2
+ /**
3
+ * Why a visitor was denied.
4
+ *
5
+ * - `unauthenticated` — the route needs a signed-in visitor and there is none.
6
+ * - `forbidden` — signed in, but in none of the groups the route names.
7
+ * - `alreadyAuthenticated` — the route is for signed-out visitors only.
8
+ */
9
+ const ACCESS_REASONS = [
10
+ "unauthenticated",
11
+ "forbidden",
12
+ "alreadyAuthenticated"
13
+ ];
14
+ /** The decision and, when a visitor is denied, the reason. Throws nothing. */
15
+ function checkAccess(visibility, context) {
16
+ const { user } = context;
17
+ switch (visibility.userGroup) {
18
+ case "ALL_USERS": return { ok: true };
19
+ case "NON_LOGGED_IN_USERS": return user === null ? { ok: true } : {
20
+ ok: false,
21
+ reason: "alreadyAuthenticated"
22
+ };
23
+ case "LOGGED_IN_USERS": {
24
+ if (user === null) return {
25
+ ok: false,
26
+ reason: "unauthenticated"
27
+ };
28
+ const required = visibility.userGroupIds ?? [];
29
+ if (required.length === 0) return { ok: true };
30
+ return required.some((id) => user.userGroupIds.includes(id)) ? { ok: true } : {
31
+ ok: false,
32
+ reason: "forbidden"
33
+ };
34
+ }
35
+ }
36
+ }
37
+ /**
38
+ * Declares who may see a route or a region of the page.
39
+ *
40
+ * At module scope in a route file, with a literal rule: the build reads these
41
+ * declarations out of the source to publish the app's route visibility.
42
+ *
43
+ * @example
44
+ * const visibility = defineVisibility({ userGroup: "LOGGED_IN_USERS" })
45
+ *
46
+ * export const Route = createFileRoute("/about")({
47
+ * beforeLoad: guardRoute(visibility),
48
+ * component: About,
49
+ * })
50
+ */
51
+ function defineVisibility(rule) {
52
+ return rule;
53
+ }
54
+ /**
55
+ * Whether the visitor passes the rule, narrowing `context.user` where the rule
56
+ * allows it. Throws nothing, so it is safe in a component.
57
+ */
58
+ function isAuthorized(visibility, context) {
59
+ return checkAccess(visibility, context).ok;
60
+ }
61
+
62
+ //#endregion
63
+ export { ACCESS_REASONS, checkAccess, defineVisibility, isAuthorized };
package/package.json CHANGED
@@ -1,37 +1,16 @@
1
1
  {
2
2
  "name": "@softr/sdk",
3
- "version": "0.0.45-alpha.0",
3
+ "version": "0.0.45-alpha.3",
4
4
  "type": "module",
5
5
  "exports": {
6
- ".": {
7
- "types": "./dist/index.d.ts",
8
- "import": "./dist/index.js"
9
- },
10
- "./client": {
11
- "types": "./dist/client.d.ts",
12
- "import": "./dist/client.js"
13
- },
14
- "./server": {
15
- "types": "./dist/server.d.ts",
16
- "import": "./dist/server.js"
17
- },
18
- "./vite": {
19
- "types": "./dist/vite.d.ts",
20
- "import": "./dist/vite.js"
21
- }
22
- },
23
- "bin": {
24
- "softr-sdk": "./dist/cli.js"
25
- },
26
- "files": [
27
- "dist",
28
- "schema",
29
- "skills",
30
- "!skills/_artifacts"
31
- ],
32
- "publishConfig": {
33
- "access": "public"
34
- },
6
+ ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" },
7
+ "./client": { "types": "./dist/client.d.ts", "import": "./dist/client.js" },
8
+ "./server": { "types": "./dist/server.d.ts", "import": "./dist/server.js" },
9
+ "./vite": { "types": "./dist/vite.d.ts", "import": "./dist/vite.js" }
10
+ },
11
+ "bin": { "softr-sdk": "./dist/cli.js" },
12
+ "files": ["dist", "schema", "skills", "!skills/_artifacts"],
13
+ "publishConfig": { "access": "public" },
35
14
  "scripts": {
36
15
  "build": "tsdown",
37
16
  "test": "node --import ./src/test-utils/setup.ts --test",
@@ -63,9 +42,7 @@
63
42
  "optional": true
64
43
  }
65
44
  },
66
- "keywords": [
67
- "tanstack-intent"
68
- ],
45
+ "keywords": ["tanstack-intent"],
69
46
  "intent": {
70
47
  "version": 1,
71
48
  "repo": "@softr/sdk",
@@ -11,7 +11,7 @@ description: >-
11
11
  metadata:
12
12
  type: core
13
13
  library: softr-sdk
14
- library_version: 0.0.43
14
+ library_version: 0.0.45
15
15
  ---
16
16
 
17
17
  # Softr SDK core
@@ -19,8 +19,8 @@ metadata:
19
19
  `@softr/sdk` is how a Softr application talks to Softr. The visitor's identity, who may see a route, records in a datasource, signing in: all of it goes through this package. Everything else is an ordinary TanStack Start app. Routes, server functions, loaders and middleware are Start's, not Softr's.
20
20
 
21
21
  > **CRITICAL**: The SDK is split across four entrypoints and the split is enforced. `ds` and `auth` exist only on `@softr/sdk/server` and only run inside a `createServerFn` handler. Importing them anywhere else fails the build.
22
- > **CRITICAL**: Every route file must declare its audience with `defineVisibility`. There is no default and nothing is inherited from a sibling. A route file without a declaration is a lint error.
23
- > **CRITICAL**: The session wiring is already in place. `src/start.ts`, `src/router.tsx` and `src/routes/__root.tsx` register `softrAuthMiddleware` and resolve the visitor. Do not add a second registration of it. `softrMcpMiddleware` is the one thing an app adds to `src/start.ts` itself — load the `softr-sdk/mcp` sub-skill before doing so.
22
+ > **CRITICAL**: Every route file must declare its audience with `defineVisibility` and guard itself with that rule in `beforeLoad`. There is no default and nothing is inherited from a sibling.
23
+ > **CRITICAL**: The app is already wired. `src/start.ts`, `src/router.tsx` and `src/routes/__root.tsx` register the middleware and resolve the session. Do not add a second registration.
24
24
  > **CRITICAL**: `workflow.trigger` lives on `@softr/sdk/server` and only runs inside a `createServerFn` handler. Use `defineWorkflow` from `@softr/sdk` to declare the action, then pass it to `workflow.trigger`. Load the `softr-sdk/workflows` sub-skill for the full pattern.
25
25
 
26
26
  Each sub-skill stands alone for its task and repeats the boundary above, so most work needs **one** sub-skill and not this file. Load this one when you are unsure which sub-skill applies, or when the wiring below is what you are touching. Add a second sub-skill only when the work genuinely crosses that boundary: a page that lists records behind a group restriction needs `record-queries` plus `routes-and-visibility`.
@@ -29,7 +29,7 @@ Each sub-skill stands alone for its task and repeats the boundary above, so most
29
29
 
30
30
  | Import | Runs | Holds |
31
31
  | --- | --- | --- |
32
- | `@softr/sdk` | Both | `defineQuery`, `defineMutation`, `defineVisibility`, `useCurrentUser`, `usePermissions`, `useUserTimezone`, `getSoftrContext`, `initialSoftrContext`, `softrAuthMiddleware`, the error predicates, and every type |
32
+ | `@softr/sdk` | Both | `defineQuery`, `defineMutation`, `defineVisibility`, `guardRoute`, `assertRouteAccess`, `isAuthorized`, `checkAccess`, `Audience`, `useCurrentUser`, `useUserTimezone`, `getSoftrContext`, `initialSoftrContext`, `softrAuthMiddleware`, the error predicates, and every type |
33
33
  | `@softr/sdk/server` | Server only | `ds`, `auth`, `workflow`, `integration` |
34
34
  | `@softr/sdk/client` | Browser only | Nothing yet, reserved for browser-only utilities |
35
35
  | `@softr/sdk/vite` | Build | `softr()`, already in `vite.config.ts` |
@@ -39,14 +39,14 @@ Each sub-skill stands alone for its task and repeats the boundary above, so most
39
39
  | Task | Sub-Skill |
40
40
  | --- | --- |
41
41
  | Add a route, or change who can see one | [routes-and-visibility/SKILL.md](./routes-and-visibility/SKILL.md) |
42
- | Read the signed-in visitor or what they may do | [current-user/SKILL.md](./current-user/SKILL.md) |
42
+ | Show part of a page to some visitors only | [region-visibility/SKILL.md](./region-visibility/SKILL.md) |
43
+ | Read the signed-in visitor | [current-user/SKILL.md](./current-user/SKILL.md) |
43
44
  | Fetch records from a datasource | [record-queries/SKILL.md](./record-queries/SKILL.md) |
44
45
  | Chart, aggregate or count records | [charts-and-metrics/SKILL.md](./charts-and-metrics/SKILL.md) |
45
46
  | Create, update or delete a record | [record-mutations/SKILL.md](./record-mutations/SKILL.md) |
46
47
  | Trigger a Softr workflow | [workflows/SKILL.md](./workflows/SKILL.md) |
47
48
  | Sign a visitor in, out, or up | [auth/SKILL.md](./auth/SKILL.md) |
48
49
  | Call a connected third-party API (Slack, Stripe, Gmail, …) | [integrations/SKILL.md](./integrations/SKILL.md) |
49
- | Open the app to AI assistants over MCP | [mcp/SKILL.md](./mcp/SKILL.md) |
50
50
 
51
51
  ## Quick Decision Tree
52
52
 
@@ -54,7 +54,10 @@ Each sub-skill stands alone for its task and repeats the boundary above, so most
54
54
  Adding a page, or changing who may see one?
55
55
  load softr-sdk/routes-and-visibility
56
56
 
57
- Showing the visitor's name, or hiding UI they cannot use?
57
+ Hiding a region of a page from some visitors?
58
+ load softr-sdk/region-visibility
59
+
60
+ Showing the visitor's name, email or groups?
58
61
  load softr-sdk/current-user
59
62
 
60
63
  Putting records on a page, in a table, a list or a detail view?
@@ -75,16 +78,13 @@ Wiring a login, signup, or sign-out?
75
78
  Calling a connected third-party API (Slack, Stripe, Gmail, Google Calendar, …)?
76
79
  load softr-sdk/integrations
77
80
 
78
- Opening the app to an AI assistant over MCP?
79
- load softr-sdk/mcp
80
-
81
81
  Writing a server function, loader, or middleware in general?
82
82
  not a Softr concern. Use the TanStack Start skills.
83
83
  ```
84
84
 
85
85
  ## The wiring
86
86
 
87
- Already present in the app. Read it to understand where the visitor comes from. Do not recreate it. `softrMcpMiddleware` is the exception: it is not wired by default and is added here when the app opens itself to AI assistants.
87
+ Already present in the app. Read it to understand where the visitor comes from. Do not recreate it.
88
88
 
89
89
  ```ts
90
90
  // src/start.ts: resolves the visitor once per request
@@ -127,7 +127,6 @@ export function getRouter() {
127
127
  ```ts
128
128
  type SoftrContext = {
129
129
  user: User | null // null means signed out
130
- permissions: { datasources: Record<string, DatasourcePermissions> }
131
130
  }
132
131
 
133
132
  type User = {
@@ -16,10 +16,10 @@ description: >-
16
16
  metadata:
17
17
  type: sub-skill
18
18
  library: softr-sdk
19
- library_version: 0.0.43
19
+ library_version: 0.0.45
20
20
  ---
21
21
 
22
- **Boundary.** `auth` lives on `@softr/sdk/server` and runs only inside a `createServerFn` handler — importing it into a component fails the build. `defineVisibility`, the hooks and every type come from `@softr/sdk`. The app is already wired (`src/start.ts`, `src/router.tsx`, `src/routes/__root.tsx`); never add a second registration. Load the core `softr-sdk` skill only if you are unsure this is the right sub-skill.
22
+ **Boundary.** `auth` lives on `@softr/sdk/server` and runs only inside a `createServerFn` handler — importing it into a component fails the build.
23
23
 
24
24
  # Auth
25
25
 
@@ -12,10 +12,10 @@ description: >-
12
12
  metadata:
13
13
  type: sub-skill
14
14
  library: softr-sdk
15
- library_version: 0.0.43
15
+ library_version: 0.0.45
16
16
  ---
17
17
 
18
- **Boundary.** `ds` lives on `@softr/sdk/server` and runs only inside a `createServerFn` handler or a server-side route hook — importing it into a component fails the build. `defineQuery` and every type come from `@softr/sdk` and can be imported anywhere. The app is already wired (`src/start.ts`, `src/router.tsx`, `src/routes/__root.tsx`); never add a second registration. Load the core `softr-sdk` skill only if you are unsure this is the right sub-skill.
18
+ **Boundary.** `ds` lives on `@softr/sdk/server` and runs only inside a `createServerFn` handler or a server-side route hook — importing it into a component fails the build. `defineQuery` and every type come from `@softr/sdk` and can be imported anywhere.
19
19
 
20
20
  # Charts and metrics
21
21
 
@@ -1,20 +1,17 @@
1
1
  ---
2
2
  name: current-user
3
3
  description: >-
4
- Read the signed-in Softr visitor and what they may do: useCurrentUser and the
5
- User type, usePermissions and DatasourcePermissions, context.user in a route's
6
- beforeLoad, and context.user inside a server function handler, where Start
7
- types it from the app's registered request middleware. Covers the null guest,
8
- SoftrUserGroups module augmentation for narrowing group ids, and why
9
- permissions are a rendering hint rather than an authorization check.
4
+ Read the signed-in Softr visitor: useCurrentUser and the User type,
5
+ context.user in a route's beforeLoad, and context.user inside a server
6
+ function handler, where Start types it from the app's registered request
7
+ middleware. Covers the null guest and the SoftrUserGroups module augmentation
8
+ that narrows group ids to your own.
10
9
  metadata:
11
10
  type: sub-skill
12
11
  library: softr-sdk
13
- library_version: 0.0.43
12
+ library_version: 0.0.45
14
13
  ---
15
14
 
16
- **Boundary.** `useCurrentUser`, `usePermissions` and every type come from `@softr/sdk` and work anywhere; `ds` and `auth` live on `@softr/sdk/server` and run only inside a `createServerFn` handler. The app is already wired — `src/start.ts` registers `softrAuthMiddleware`, `src/routes/__root.tsx` publishes the visitor as route context — never add a second registration. Load the core `softr-sdk` skill only if you are unsure this is the right sub-skill.
17
-
18
15
  # The current user
19
16
 
20
17
  One visitor is resolved once per request and published three ways: as a hook in components, as route context in `beforeLoad`, and on the `context` argument inside a server function. All three read the same `SoftrContext`. Which one you reach for depends only on where the code runs.
@@ -24,7 +21,7 @@ One visitor is resolved once per request and published three ways: as a hook in
24
21
  Nothing to install or register, since the app is already wired. Import from the root entrypoint:
25
22
 
26
23
  ```ts
27
- import { useCurrentUser, usePermissions } from '@softr/sdk'
24
+ import { useCurrentUser } from '@softr/sdk'
28
25
  import type { User } from '@softr/sdk'
29
26
  ```
30
27
 
@@ -56,12 +53,12 @@ It returns `User | null` and never suspends or fetches. It selects off root rout
56
53
 
57
54
  ```tsx
58
55
  export const Route = createFileRoute('/_app/invoices')({
59
- beforeLoad: visibility.guard(),
56
+ beforeLoad: guardRoute(visibility),
60
57
  loader: ({ context }) => fetchInvoicesFor({ data: { userId: context.user!.id } }),
61
58
  })
62
59
  ```
63
60
 
64
- A `guard()` in `beforeLoad` guarantees a visitor at runtime, but it does not change what the compiler sees. `context.user` is still `User | null` further down the route. `visibility.isAuthorized(context)` is the check that narrows it, so prefer that over `!`. See [routes-and-visibility](../routes-and-visibility/SKILL.md).
61
+ A `guardRoute` in `beforeLoad` guarantees a visitor at runtime, but it does not change what the compiler sees. `context.user` is still `User | null` further down the route. `isAuthorized(visibility, context)` is the check that narrows it, so prefer that over `!`. See [routes-and-visibility](../routes-and-visibility/SKILL.md).
65
62
 
66
63
  ### In a server function: `context`
67
64
 
@@ -85,24 +82,7 @@ const getMyInvoices = createServerFn().handler(async ({ context }) => {
85
82
  })
86
83
  ```
87
84
 
88
- `context.user` and `context.permissions` are typed. Start derives a handler's context from the app's registered request middleware as well as the server function's own, so there is nothing to import and nothing to cast.
89
-
90
- ### Hiding UI the visitor cannot use
91
-
92
- ```tsx
93
- import { usePermissions } from '@softr/sdk'
94
-
95
- const INVOICES = '00000000-0000-0000-0000-000000000001' // the registered datasource's id
96
-
97
- function NewInvoiceButton() {
98
- const { create } = usePermissions(INVOICES)
99
- return create ? <button>New invoice</button> : null
100
- }
101
- ```
102
-
103
- `usePermissions(datasourceId)` returns `{ read, create, update, delete }` for one datasource. The id is the opaque UUID from the table's registration on the application, the same one `ds.*` takes — not the table id, and never built from the table's name. It hides a control. It does not protect what the control does.
104
-
105
- Pass an id the visitor's session has no entry for and it throws while rendering, so a typo takes the page down rather than returning four falses.
85
+ `context.user` is typed. Start derives a handler's context from the app's registered request middleware as well as the server function's own, so there is nothing to import and nothing to cast.
106
86
 
107
87
  ### Narrowing group ids to your own
108
88
 
@@ -122,6 +102,7 @@ With this declaration, `user.userGroupIds` and every `userGroupIds` the SDK acce
122
102
 
123
103
  ## References
124
104
 
125
- - [routes-and-visibility/SKILL.md](../routes-and-visibility/SKILL.md): `isAuthorized()` narrows `context.user` while applying the route's rule
105
+ - [routes-and-visibility/SKILL.md](../routes-and-visibility/SKILL.md): `isAuthorized` narrows `context.user` while applying the route's rule
106
+ - [region-visibility/SKILL.md](../region-visibility/SKILL.md): rendering a region only to the visitors a rule admits
126
107
  - [record-queries/SKILL.md](../record-queries/SKILL.md): filtering a query by the visitor, and who really authorizes the read
127
108
  - [auth/SKILL.md](../auth/SKILL.md): what changes the value `useCurrentUser` returns
@@ -10,7 +10,7 @@ description: >-
10
10
  metadata:
11
11
  type: sub-skill
12
12
  library: softr-sdk
13
- library_version: 0.0.43
13
+ library_version: 0.0.45
14
14
  ---
15
15
 
16
16
  **Boundary.** `upload` lives on `@softr/sdk/server` and runs inside server functions. `useFileUpload` and `FileItem` live on `@softr/sdk` and run in components. `FileAttachment` comes from `@softr/sdk` and can be imported anywhere.
@@ -9,10 +9,10 @@ description: >-
9
9
  metadata:
10
10
  type: sub-skill
11
11
  library: softr-sdk
12
- library_version: 0.0.43
12
+ library_version: 0.0.45
13
13
  ---
14
14
 
15
- **Boundary.** `integration` lives on `@softr/sdk/server` and runs only inside a `createServerFn` handler — importing it into a component fails the build. `defineIntegration` and every type come from `@softr/sdk` and can be imported anywhere. Load the core `softr-sdk` skill only if you are unsure this is the right sub-skill.
15
+ **Boundary.** `integration` lives on `@softr/sdk/server` and runs only inside a `createServerFn` handler — importing it into a component fails the build. `defineIntegration` and every type come from `@softr/sdk` and can be imported anywhere.
16
16
 
17
17
  # Integrations
18
18
 
@@ -11,10 +11,10 @@ description: >-
11
11
  metadata:
12
12
  type: sub-skill
13
13
  library: softr-sdk
14
- library_version: 0.0.43
14
+ library_version: 0.0.45
15
15
  ---
16
16
 
17
- **Boundary.** `ds` lives on `@softr/sdk/server` and runs only inside a `createServerFn` handler — importing it into a component fails the build. `defineMutation` and every type come from `@softr/sdk` and can be imported anywhere. The app is already wired (`src/start.ts`, `src/router.tsx`, `src/routes/__root.tsx`); never add a second registration. Load the core `softr-sdk` skill only if you are unsure this is the right sub-skill.
17
+ **Boundary.** `ds` lives on `@softr/sdk/server` and runs only inside a `createServerFn` handler — importing it into a component fails the build. `defineMutation` and every type come from `@softr/sdk` and can be imported anywhere.
18
18
 
19
19
  # Writing records
20
20
 
@@ -13,10 +13,10 @@ description: >-
13
13
  metadata:
14
14
  type: sub-skill
15
15
  library: softr-sdk
16
- library_version: 0.0.43
16
+ library_version: 0.0.45
17
17
  ---
18
18
 
19
- **Boundary.** `ds` lives on `@softr/sdk/server` and runs only inside a `createServerFn` handler or a server-side route hook — importing it into a component fails the build. `defineQuery` and every type come from `@softr/sdk` and can be imported anywhere. The app is already wired (`src/start.ts`, `src/router.tsx`, `src/routes/__root.tsx`); never add a second registration. Load the core `softr-sdk` skill only if you are unsure this is the right sub-skill.
19
+ **Boundary.** `ds` lives on `@softr/sdk/server` and runs only inside a `createServerFn` handler or a server-side route hook — importing it into a component fails the build. `defineQuery` and every type come from `@softr/sdk` and can be imported anywhere.
20
20
 
21
21
  # Reading records
22
22
 
@@ -179,7 +179,7 @@ ds.list(products).where((q) => q.status.is(selectedStatusId))
179
179
 
180
180
  Skip this request when the options are predefined and fixed — hardcode them instead.
181
181
 
182
- A read the datasource turns down throws rather than coming back empty, and it throws a plain `Error` carrying the status. There is no typed predicate to narrow it with. Handle it where you would handle any other failed fetch: an `errorComponent` on the route, or a check in the handler.
182
+ A read the datasource turns down throws rather than coming back empty, and it throws a plain `Error` carrying the status, followed by the reason the datasource gives when it sends one. There is no typed predicate to narrow it with. Handle it where you would handle any other failed fetch: an `errorComponent` on the route, or a check in the handler.
183
183
 
184
184
  ## Typing
185
185
 
@@ -0,0 +1,63 @@
1
+ ---
2
+ name: region-visibility
3
+ description: >-
4
+ Show part of a page only to the visitors a rule admits: the Audience
5
+ component, its children and fallback, where a region's rule may be declared,
6
+ and why hiding a control is not authorization. Use when a button, a column, a
7
+ nav item or a panel should appear for some visitors and not others, on a page
8
+ everyone may open. Gating the whole route is routes-and-visibility.
9
+ metadata:
10
+ type: sub-skill
11
+ library: softr-sdk
12
+ library_version: 0.0.45
13
+ ---
14
+
15
+ **Boundary.** `Audience` and `defineVisibility` come from `@softr/sdk` and work anywhere.
16
+
17
+ # Region visibility
18
+
19
+ A route's rule decides who may open the page. A region's rule decides what they see once they are on it. The two are independent: an admin panel on a page every signed-in visitor may open is a region, and so is a refund button only staff should reach for.
20
+
21
+ ## Setup
22
+
23
+ Wrap the region in `Audience` and give it a rule:
24
+
25
+ ```tsx
26
+ import { Audience, defineVisibility } from '@softr/sdk'
27
+
28
+ const staffOnly = defineVisibility({
29
+ userGroup: 'LOGGED_IN_USERS',
30
+ userGroupIds: ['00000000-0000-0000-0000-00000000000a'],
31
+ })
32
+
33
+ function InvoiceActions({ invoice }: { invoice: Invoice }) {
34
+ return (
35
+ <Audience visibility={staffOnly} fallback={<p>Contact your account manager to refund.</p>}>
36
+ <RefundButton invoice={invoice} />
37
+ </Audience>
38
+ )
39
+ }
40
+ ```
41
+
42
+ `Audience` renders its children for the visitors the rule admits and `fallback` for everyone else. Without a `fallback` it renders nothing.
43
+
44
+ The rules are the same four a route uses, and [routes-and-visibility](../routes-and-visibility/SKILL.md) covers them.
45
+
46
+ ## Core Patterns
47
+
48
+ ### Where the rule is declared
49
+
50
+ A region's rule is an ordinary value. It can sit beside the component, or come from a module the whole app shares so several regions read the same one. A route's rule cannot. Softr reads that one out of the route file, so it has to be declared there.
51
+
52
+ Two files cannot hold a region's rule. Any declaration in `__root.tsx` is a build error. In the lazy half of a split route, Softr takes the first declaration as the route's own. Put the rule in the component's file or a shared module and import it.
53
+
54
+ A route file may declare a region's rule alongside its own. The route's rule is the one `beforeLoad` names.
55
+
56
+ ### Hiding a control is not authorization
57
+
58
+ `Audience` decides what to render and nothing else. The server function behind a hidden button is still reachable on its own, and Softr authorizes that request whatever the page rendered. If a region hides something sensitive, declare the action behind it with the permissions it should have.
59
+
60
+ ## References
61
+
62
+ - [routes-and-visibility/SKILL.md](../routes-and-visibility/SKILL.md): the rule shapes, and gating a whole page
63
+ - [current-user/SKILL.md](../current-user/SKILL.md): reading the visitor a rule has admitted
@@ -3,19 +3,20 @@ name: routes-and-visibility
3
3
  description: >-
4
4
  Add a route file to a Softr app and declare who may see it: defineVisibility
5
5
  with ALL_USERS, LOGGED_IN_USERS or NON_LOGGED_IN_USERS, userGroupIds, and the
6
- four ways to enforce a rule (guard, assert, isAuthorized, check). Covers
7
- per-reason overrides, the redirect and throw strategies, rendering an access
8
- error with isAccessError, isUnauthenticatedError, isForbiddenError and
9
- isAlreadyAuthenticatedError, guarding a layout, and the lint rule that
10
- requires a declaration alongside the rules that reject a duplicate, computed
11
- or root-level one.
6
+ ways to apply a rule: guardRoute and assertRouteAccess in a route,
7
+ isAuthorized and checkAccess anywhere. Covers per-reason overrides, the
8
+ redirect and throw strategies, rendering an access error with
9
+ isAccessError, isUnauthenticatedError, isForbiddenError and
10
+ isAlreadyAuthenticatedError, guarding a layout, and the lint rule that requires a
11
+ declaration alongside the rules that reject a duplicate, computed or
12
+ root-level one.
12
13
  metadata:
13
14
  type: sub-skill
14
15
  library: softr-sdk
15
- library_version: 0.0.43
16
+ library_version: 0.0.45
16
17
  ---
17
18
 
18
- **Boundary.** `defineVisibility`, the error predicates and every type come from `@softr/sdk` and work anywhere; `ds` and `auth` live on `@softr/sdk/server` and run only inside a `createServerFn` handler. The app is already wired — `src/start.ts` registers `softrAuthMiddleware`, `src/routes/__root.tsx` publishes the visitor as route context — never add a second registration. Load the core `softr-sdk` skill only if you are unsure this is the right sub-skill.
19
+ **Boundary.** `defineVisibility`, the access functions and the error predicates come from `@softr/sdk` and work anywhere.
19
20
 
20
21
  # Route visibility
21
22
 
@@ -27,12 +28,12 @@ Every route file, without exception, looks like this:
27
28
 
28
29
  ```tsx
29
30
  import { createFileRoute } from '@tanstack/react-router'
30
- import { defineVisibility } from '@softr/sdk'
31
+ import { defineVisibility, guardRoute } from '@softr/sdk'
31
32
 
32
33
  const visibility = defineVisibility({ userGroup: 'LOGGED_IN_USERS' })
33
34
 
34
35
  export const Route = createFileRoute('/_app/billing')({
35
- beforeLoad: visibility.guard(),
36
+ beforeLoad: guardRoute(visibility),
36
37
  component: Billing,
37
38
  })
38
39
 
@@ -41,12 +42,13 @@ function Billing() {
41
42
  }
42
43
  ```
43
44
 
44
- One `defineVisibility` call per file, at module scope, with an inline object literal as its argument.
45
+ The route's rule is one `defineVisibility` call at module scope, with an inline object literal as its argument.
45
46
 
46
- Three rules about that shape, because Softr reads the file rather than running it:
47
+ Four constraints on that shape, because Softr reads the file rather than running it:
47
48
 
48
49
  - The call must initialise a top-level `const`.
49
50
  - The import must be a direct named import from `@softr/sdk`.
51
+ - The route guards itself in `beforeLoad`, naming a rule declared in the same file. A rule imported from elsewhere is rejected.
50
52
  - Never declare on `__root.tsx`. The root cannot carry an audience, so a declaration there would gate nothing while looking like it does. Softr rejects it. Put the rule on a layout below the root instead.
51
53
 
52
54
  The four rules:
@@ -70,28 +72,28 @@ Absent or empty `userGroupIds` means **any** signed-in visitor, not "nobody".
70
72
  ```tsx
71
73
  // src/routes/_app.tsx: guards every route under /_app
72
74
  import { Outlet, createFileRoute } from '@tanstack/react-router'
73
- import { defineVisibility } from '@softr/sdk'
75
+ import { defineVisibility, guardRoute } from '@softr/sdk'
74
76
 
75
77
  const visibility = defineVisibility({ userGroup: 'LOGGED_IN_USERS' })
76
78
 
77
79
  export const Route = createFileRoute('/_app')({
78
- beforeLoad: visibility.guard(),
80
+ beforeLoad: guardRoute(visibility),
79
81
  component: () => <Outlet />,
80
82
  })
81
83
  ```
82
84
 
83
- The whole match chain runs, so a layout guards everything beneath it. Sibling routes do not guard each other. `/admin/index.tsx` does nothing for `/admin/users.tsx` unless an `admin.tsx` layout exists. Routes below a guarded layout still declare their own rule, because the declaration is what Studio reads.
85
+ The whole match chain runs, so a layout guards everything beneath it. Sibling routes do not guard each other. `/admin/index.tsx` does nothing for `/admin/users.tsx` unless an `admin.tsx` layout exists. A route below a guarded layout declares and guards its own rule too.
84
86
 
85
87
  A thrown error renders in place of the guarded route's component. A shell layout guarded with `LOGGED_IN_USERS`, where `unauthenticated` redirects, sends a signed-out visitor away before anything renders there. A rule that can *throw* — a `userGroupIds` rule with `forbidden` set to throw — should not sit on the shell itself, or the error replaces the nav and chrome. Put it on a layout inside the shell, or on the restricted routes, so the error renders inside the chrome.
86
88
 
87
89
  ### Change what a denied visitor gets
88
90
 
89
- `guard()` takes per-reason overrides. A bare string is shorthand for a redirect.
91
+ `guardRoute` takes per-reason overrides as a second argument. A bare string is shorthand for a redirect.
90
92
 
91
93
  ```ts
92
- beforeLoad: visibility.guard({ unauthenticated: '/marketing/sign-up' }),
93
- beforeLoad: visibility.guard({ forbidden: { strategy: 'throw' } }),
94
- beforeLoad: visibility.guard({
94
+ beforeLoad: guardRoute(visibility, { unauthenticated: '/marketing/sign-up' }),
95
+ beforeLoad: guardRoute(visibility, { forbidden: { strategy: 'throw' } }),
96
+ beforeLoad: guardRoute(visibility, {
95
97
  unauthenticated: { strategy: 'redirect', to: '/marketing/sign-up' },
96
98
  forbidden: { strategy: 'throw' },
97
99
  }),
@@ -122,13 +124,13 @@ All three reasons are required, because nothing fills in a reason the file leave
122
124
  | `forbidden` | Signed in, in none of the named groups |
123
125
  | `alreadyAuthenticated` | Signed-in visitor on a signed-out-only route |
124
126
 
125
- That file covers every route at once; a route's own `guard()` overrides win over it. `ALL_USERS` admits everyone, so it never redirects and never throws. Only `strategy: 'throw'` reaches an `errorComponent`. A redirect navigates away and the route never renders.
127
+ That file covers every route at once; a route's own overrides win over it. `ALL_USERS` admits everyone, so it never redirects and never throws. Only `strategy: 'throw'` reaches an `errorComponent`. A redirect navigates away and the route never renders.
126
128
 
127
129
  ### Render the error in place
128
130
 
129
131
  ```tsx
130
132
  import { createFileRoute } from '@tanstack/react-router'
131
- import { defineVisibility, isAccessError, isForbiddenError } from '@softr/sdk'
133
+ import { defineVisibility, guardRoute, isAccessError, isForbiddenError } from '@softr/sdk'
132
134
 
133
135
  const visibility = defineVisibility({
134
136
  userGroup: 'LOGGED_IN_USERS',
@@ -136,7 +138,7 @@ const visibility = defineVisibility({
136
138
  })
137
139
 
138
140
  export const Route = createFileRoute('/_app/billing')({
139
- beforeLoad: visibility.guard(),
141
+ beforeLoad: guardRoute(visibility),
140
142
  component: Billing,
141
143
  errorComponent: ({ error }: { error: unknown }) => {
142
144
  if (isForbiddenError(error)) return <UpgradePrompt />
@@ -152,24 +154,27 @@ Use the predicates: `isAccessError` for any of the three, and `isUnauthenticated
152
154
 
153
155
  ### Run the check and then keep going
154
156
 
155
- `guard()` covers almost everything. When a route needs its own logic afterwards, `assert()` applies the same rule and throws the same access error, using `softr.json` for each reason. It takes no overrides.
157
+ `guardRoute` covers almost everything. When a route needs its own logic afterwards, `assertRouteAccess(visibility, context)` applies the same rule and throws the same access error, using the default behaviour for each reason. It takes no overrides.
156
158
 
157
- `assert()` guarantees the visitor at runtime but does not narrow `context.user` for the compiler. `isAuthorized()` is the one that narrows. It is a type predicate, so inside its `true` branch `context.user` is `User` under `LOGGED_IN_USERS` and `null` under `NON_LOGGED_IN_USERS`.
159
+ It also narrows `context.user` for the rest of the handler: `User` under `LOGGED_IN_USERS`, `null` under `NON_LOGGED_IN_USERS`. `guardRoute` cannot, because it hands `beforeLoad` a function rather than running inside one.
158
160
 
159
161
  ```ts
160
162
  import { redirect } from '@tanstack/react-router'
163
+ import { assertRouteAccess } from '@softr/sdk'
161
164
 
162
165
  beforeLoad: ({ context }) => {
163
- if (visibility.isAuthorized(context)) {
164
- if (context.user.email.endsWith('@example.com')) throw redirect({ to: '/internal' })
165
- }
166
+ assertRouteAccess(visibility, context)
167
+ if (context.user.email.endsWith('@example.com')) throw redirect({ to: '/internal' })
166
168
  },
167
169
  ```
168
170
 
169
- Reach for `assert()` when the route wants the error thrown for it, and `check(context)` when it needs to branch on why. `check` returns `{ ok: true } | { ok: false, reason }` and throws nothing. All of them are synchronous and make no request.
171
+ Reach for `assertRouteAccess` when the route wants the error thrown for it, `isAuthorized(visibility, context)` when it wants the same narrowing without a throw, and `checkAccess(visibility, context)` when it needs to branch on why. `checkAccess` returns `{ ok: true } | { ok: false, reason }` and throws nothing. All of them are synchronous and make no request.
172
+
173
+ `guardRoute` and `assertRouteAccess` throw router redirects, so they belong in a route and nowhere else. That is what `Route` in their names marks. `isAuthorized` and `checkAccess` throw nothing and answer anywhere, in a loader, a component or a server function.
170
174
 
171
175
  ## References
172
176
 
173
177
  - [current-user/SKILL.md](../current-user/SKILL.md): reading the visitor a guard has just admitted
174
178
  - [record-queries/SKILL.md](../record-queries/SKILL.md): a guard covers the page, not the server function behind it
175
179
  - [auth/SKILL.md](../auth/SKILL.md): `NON_LOGGED_IN_USERS` and the sign-in flow it fronts
180
+ - [region-visibility/SKILL.md](../region-visibility/SKILL.md): showing part of a page to some of the visitors the route admits
@@ -9,10 +9,10 @@ description: >-
9
9
  metadata:
10
10
  type: sub-skill
11
11
  library: softr-sdk
12
- library_version: 0.0.43
12
+ library_version: 0.0.45
13
13
  ---
14
14
 
15
- **Boundary.** `workflow` lives on `@softr/sdk/server` and runs only inside a `createServerFn` handler — importing it into a component fails the build. `defineWorkflow` and every type come from `@softr/sdk` and can be imported anywhere. Load the core `softr-sdk` skill only if you are unsure this is the right sub-skill.
15
+ **Boundary.** `workflow` lives on `@softr/sdk/server` and runs only inside a `createServerFn` handler — importing it into a component fails the build. `defineWorkflow` and every type come from `@softr/sdk` and can be imported anywhere.
16
16
 
17
17
  # Triggering workflows
18
18