@protobase/cli 0.66.6 → 0.66.8

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 (57) hide show
  1. package/dist/build/config-bundle.js +11 -1
  2. package/dist/build/first-party-code.js +22 -0
  3. package/dist/build/plugin-build.js +3 -1
  4. package/dist/build/shell.js +3 -1
  5. package/dist/cli/package.js +1 -1
  6. package/dist/dev/entry.js +1 -1
  7. package/dist/dev/run.js +2 -1
  8. package/dist/dev/server-package.js +8 -6
  9. package/dist/doctor/built-in-models.js +26 -0
  10. package/dist/doctor/run.js +11 -4
  11. package/dist/main.js +3 -2
  12. package/dist/mobile/android-signing.js +8 -1
  13. package/dist/mobile/load-project.js +4 -1
  14. package/dist/project/cli-modules.js +30 -0
  15. package/dist/project/import-project-file.js +9 -9
  16. package/package.json +8 -7
  17. package/skills/protobase/references/features/ai.md +10 -7
  18. package/skills/protobase/references/features/build-and-run.md +1 -0
  19. package/skills/protobase/references/features/compatibility.md +6 -6
  20. package/skills/protobase/references/features/data-model.md +1 -1
  21. package/skills/protobase/references/features/email-and-public-forms.md +2 -0
  22. package/skills/protobase/references/features/events-webhooks-and-jobs.md +2 -0
  23. package/skills/protobase/references/features/files-and-images.md +1 -0
  24. package/skills/protobase/references/features/history-comments-and-live-updates.md +1 -0
  25. package/skills/protobase/references/features/look-and-language.md +1 -0
  26. package/skills/protobase/references/features/navigation.md +5 -4
  27. package/skills/protobase/references/features/sign-in-and-accounts.md +7 -0
  28. package/skills/protobase/references/features/start-a-project.md +2 -1
  29. package/skills/protobase/references/features/your-own-code.md +4 -2
  30. package/skills/protobase/references/getting-started.md +10 -2
  31. package/skills/protobase/references/guides/api-functions.md +2 -2
  32. package/skills/protobase/references/guides/mobile-app.md +2 -2
  33. package/skills/protobase/references/reference/api.md +2 -2
  34. package/skills/protobase/references/reference/app-identity.md +1 -1
  35. package/skills/protobase/references/reference/assistant.md +22 -2
  36. package/skills/protobase/references/reference/auth.md +42 -11
  37. package/skills/protobase/references/reference/cli.md +9 -5
  38. package/skills/protobase/references/reference/client.md +1 -1
  39. package/skills/protobase/references/reference/comments.md +2 -2
  40. package/skills/protobase/references/reference/custom-components.md +7 -1
  41. package/skills/protobase/references/reference/data-config.md +1 -1
  42. package/skills/protobase/references/reference/email.md +1 -1
  43. package/skills/protobase/references/reference/files.md +4 -0
  44. package/skills/protobase/references/reference/history.md +1 -1
  45. package/skills/protobase/references/reference/jobs.md +3 -1
  46. package/skills/protobase/references/reference/languages.md +2 -2
  47. package/skills/protobase/references/reference/live.md +4 -0
  48. package/skills/protobase/references/reference/mcp.md +22 -2
  49. package/skills/protobase/references/reference/plugins.md +6 -4
  50. package/skills/protobase/references/reference/sidebar.md +6 -0
  51. package/skills/protobase/references/reference/supabase-realtime.md +5 -1
  52. package/skills/protobase/references/reference/supabase-rest.md +5 -1
  53. package/skills/protobase/references/reference/supabase-storage.md +5 -1
  54. package/skills/protobase/references/reference/theme.md +1 -1
  55. package/skills/protobase/references/reference/ui-config.md +6 -2
  56. package/skills/protobase/references/reference/versioning.md +5 -2
  57. package/skills/protobase/references/reference/webhooks.md +2 -0
@@ -43,6 +43,10 @@ It is live data, not multiplayer editing: two people editing the same record sti
43
43
 
44
44
  Connecting again with `?after=<cursor>` catches up on what was missed while disconnected. [`client.live({ onChanges, onResync })`](client.md) of `@protobase/client` follows the stream and connects again by itself, as the app does. However many callers of one client follow it, they share one stream, so a page with many live views holds one connection, which matters under HTTP/1.1, where a browser opens at most six to a host.
45
45
 
46
+ ## Polling, then a socket
47
+
48
+ `pollThenSocket({ poll, onError, socket, onMessage, pollMs?, switchAfterMs?, socketFirst? })` of `@protobase/client`, in the browser and on Node, keeps up with anything that changes, until the function it returns is called. It calls `poll` at once and again `pollMs` (5 seconds) after each call ends, also after one that rejected, whose error goes to `onError`, and after `switchAfterMs` (30 seconds) opens the WebSocket `socket()` returns, such as `() => new WebSocket(url)`. Once the socket is open, polling stops: `poll` runs once more, for what changed before the socket opened, and `onMessage` gets each message's data. When the socket closes, or never opens, it polls again at once and opens a new socket `switchAfterMs` later. Polling goes on while a socket connects, so nothing waits on it. A short visit thus never opens a socket; `socketFirst: true` opens one at once instead, for a process that stays, and polls only while it is down.
49
+
46
50
  Who is on a record's page and what they send each other, which no change of a record says, goes over the record's [channel](channels.md).
47
51
 
48
52
  ## The table
@@ -14,7 +14,7 @@ An app can serve an MCP endpoint: a person adds its address to an AI client that
14
14
  export default defineConfig({ auth: { mcp: true } })
15
15
  ```
16
16
 
17
- Every auth store has the OAuth tables, so turning it on needs no migration. The endpoint is `https://<app>/api/mcp`, or the path `auth.mcp.path` names. How clients register, how people sign in and allow them, and how they disconnect them is in [OAuth for MCP clients](auth.md#oauth-for-mcp-clients).
17
+ Every auth store has the OAuth tables, so turning it on needs no migration. The endpoint verifies its tokens with the app's own sign-in, so an app whose authenticator is its own, `auth/authenticate.ts`, does not start with MCP on. The endpoint is `https://<app>/api/mcp`, or the path `auth.mcp.path` names. How clients register, how people sign in and allow them, and how they disconnect them is in [OAuth for MCP clients](auth.md#oauth-for-mcp-clients).
18
18
 
19
19
  ## Connecting a client
20
20
 
@@ -33,10 +33,18 @@ Give the client the endpoint's address. It finds the app's sign-in from there, o
33
33
 
34
34
  ## The app's icon
35
35
 
36
+ :::caution[Beta]
37
+ Clients that draw no SVG get no icon for an app without PNGs of its own, as below.
38
+ :::
39
+
36
40
  The endpoint names the app's [icon](app-identity.md#the-icons) to the client when it connects (`serverInfo.icons`), at absolute addresses on the app's origin: the project's 192 pixel, touch and 512 pixel PNGs it has, then its `protobase.favicon.svg`, or Protobase's without one. A client that draws server icons shows it beside the app's name. <gap>An app without PNGs of its own cannot offer clients that draw no SVG an icon because PNG versions of Protobase's icon are not implemented.</gap>
37
41
 
38
42
  ## The tools
39
43
 
44
+ :::caution[Beta]
45
+ `count_records` cannot count soft-deleted records per value yet, as below.
46
+ :::
47
+
40
48
  Each tool reads as the signed-in person: their role's access rules, row filters, tenant and hidden fields apply as in the [REST API](api.md#access).
41
49
 
42
50
  | Tool | What it does |
@@ -90,6 +98,10 @@ It runs one `SELECT` in a read-only transaction, as the app's database role, **o
90
98
 
91
99
  ### Writes
92
100
 
101
+ :::caution[Beta]
102
+ An approval cannot be answered on another server instance or after a restart yet, as below.
103
+ :::
104
+
93
105
  `mcpEndpoint.writes` also offers four tools that change records, to everyone signed in:
94
106
 
95
107
  ```ts
@@ -100,7 +112,7 @@ export default defineConfig({ auth: { mcp: true }, mcpEndpoint: { writes: true }
100
112
  | --- | --- |
101
113
  | `create_record` | Creates a record of a resource from `data`, field names to values, and returns its key and ETag with the fields it wrote, see [What a write returns](#what-a-write-returns). |
102
114
  | `update_record` | Changes the fields in `data` of one record by its key, and returns its key and new ETag with the fields it set. With the `etag` that `get_record` gave, it fails when someone changed the record since. It does not set the field a soft delete sets: a delete goes through `delete_record`. |
103
- | `delete_record` | Deletes one record by its key and the `etag` that `get_record` gave, and fails when someone changed the record since. A resource with a [soft delete](data-config.md) keeps the record, deleted; any other loses it for good. |
115
+ | `delete_record` | Deletes one record by its key. With the `etag` that `get_record` gave, it fails when someone changed the record since; without one, or with `*`, it deletes the record whatever its version. A resource with a [soft delete](data-config.md) keeps the record, deleted; any other loses it for good. |
104
116
  | `batch_write` | Creates, updates and deletes records across resources in one transaction, see [Batch writes](#batch-writes). |
105
117
 
106
118
  Each write goes through the REST API as the person: their role's access rules, `validate`, write hooks, events and webhooks apply, so the client can do only what the person may do in the app.
@@ -129,6 +141,10 @@ A write returns its record small, so changing one field of a large record does n
129
141
 
130
142
  #### Writes without approval
131
143
 
144
+ :::caution[Beta]
145
+ A deleted record cannot be restored over MCP yet, as below.
146
+ :::
147
+
132
148
  An app that runs service accounts over MCP, such as a backend job or an agent with no person present, names their roles in `mcpEndpoint.writes`:
133
149
 
134
150
  ```ts
@@ -162,6 +178,10 @@ A batch is checked before the person is asked: an operation other than a create,
162
178
 
163
179
  ## The app's own tools
164
180
 
181
+ :::caution[Beta]
182
+ Nothing refuses a tool result larger than an AI client takes yet, as below.
183
+ :::
184
+
165
185
  `mcp/tools.ts` adds tools of the app's own, offered after the built-in ones to everyone signed in, whatever `mcpEndpoint.writes` says. Its default export is an array of `defineTool(...)` from `@protobase/server`, the same shape as the [assistant's own tools](assistant.md#the-apps-own-tools): a `name` of 1 to 64 letters, digits, `_` or `-`, a `description` the client's model reads, `parameters`, the JSON Schema of its arguments, which the endpoint checks each call against, `annotations`, MCP's tool hints such as `readOnlyHint`, optionally a `title` and `_meta`, which `tools/list` gives clients as they are, and `run(args, context)`, which returns the text the client reads. The file is found when the app starts, under `protobase dev` and `protobase serve` alike, and only with `auth.mcp` on; with `mcpEndpoint: false` it offers nothing.
166
186
 
167
187
  `context` holds the caller's `session`, the `locale` they read, `approve(request)`, `resources()`, the resources they may read, and `records`, the app's records **as that person**: their role's access rules, tenant, hidden fields, `validate` and write hooks apply, as in the REST API and the built-in tools. Its methods are the assistant's, listed [there](assistant.md#the-apps-own-tools); a refusal rejects with an `HttpProblem`. `show` does nothing over MCP, and there is no `conversation`.
@@ -42,8 +42,10 @@ Each place a plugin can fill is an extension point. The config names it, or a pl
42
42
  | [Action handler](custom-components.md#action-handlers) | `a.action('name')` in a view | `actions: { name }` | `ActionContext` | The button is disabled |
43
43
  | [Shell slot](custom-components.md#shell-slots) | Nothing: drawn on every page | `shell: { actions, rightPanel, logo, bell }` | Nothing; `logo` gets `compact`, `workspace`, `appName` and `appIcon` | Not drawn; Protobase's logo for `logo`, its notification bell for `bell` |
44
44
  | [Before sign-in](#before-sign-in) | `beforeSignIn: true` where the plugin is listed | `beforeSignIn` | `children`, `params`, `removeParams`, `signedIn` | The pages as they are without it |
45
+ | Rules and hooks a view names | A [drop rule](ui-config.md#drop-rules), [board drop](ui-config.md#board-drops), [title rule](ui-config.md#titles-per-reader) or [marks hook](sidebar.md#marks-per-reader) by name in a view | `dropRules`, `boardDrops`, `titles`, `marks` | What its page says | What its page says |
46
+ | [Presence](status.md#in-the-app) | Nothing: read on the members page and the sidebar | `presence` | Nothing; a hook | No one shows as online or away |
45
47
 
46
- Components and cards share one namespace of names, which `component('Name')` joins to the export; field types and action handlers each have their own. Props that name fields go through the same walker as everything else in `/api/meta`, so a user who may not read a field never gets it in a component's props; a field type's `props` are plain data, and a field the user may not read is left out of the view, field type and all.
48
+ Components and cards share one namespace of names, which `component('Name')` joins to the export; field types, action handlers, drop rules, board drops, title rules and marks hooks each have their own. Props that name fields go through the same walker as everything else in `/api/meta`, so a user who may not read a field never gets it in a component's props; a field type's `props` are plain data, and a field the user may not read is left out of the view, field type and all.
47
49
 
48
50
  New extension points are new optional keys of `definePlugin`, so adding one never changes what a plugin built before it does.
49
51
 
@@ -67,7 +69,7 @@ export { DraftOrders } from './components/draft-orders'
67
69
  export default definePlugin({ name: 'sales-tools', actions: { remindCustomer } })
68
70
  ```
69
71
 
70
- It is written as a project's `protobase.ui.tsx` is: each component exported under the PascalCase name the config declares, from a file of its own, and the rest in the default export, `definePlugin({ name, actions, fields, shell, beforeSignIn, extends })` (`defineUi` is the same function). Its components use the hooks and components of `@protobase/ui`, which read the API as the signed-in user, under the same access rules as everything else.
72
+ It is written as a project's `protobase.ui.tsx` is: each component exported under the PascalCase name the config declares, from a file of its own, and the rest in the default export, `definePlugin({ name, actions, fields, shell, beforeSignIn, extends, ... })` (`defineUi` is the same function), with the keys of [Extension points](#extension-points). Its components use the hooks and components of `@protobase/ui`, which read the API as the signed-in user, under the same access rules as everything else.
71
73
 
72
74
  **Imports.** `react`, `react/jsx-runtime` (the JSX), `react-dom` and `@protobase/ui`, by those names, come from the app. Everything else a plugin imports, its own files, icons or a chart library, is built into it. Other modules of those packages, such as `react-dom/client` or a file inside `@protobase/ui`, fail the build: a copy of its own would not work with the app's. Read data with `@protobase/ui`'s hooks (`useList`, `useRecord`, `useClient`), which share the app's cache: a data library of the plugin's own keeps a cache of its own.
73
75
 
@@ -235,7 +237,7 @@ When the user signs in, or before for a plugin that loads [before sign-in](#befo
235
237
  - The files in `assets/`, named by their content, forever (`Cache-Control: public, max-age=31536000, immutable`).
236
238
  - From another origin than the app's, with `Access-Control-Allow-Origin` (`*` is fine for public files), since the browser loads modules and `plugin.json` with CORS.
237
239
 
238
- The plugins are merged in the order listed. Two of them cannot define one name: a plugin that defines a component, action handler or field type an earlier one defines is turned off, and the earlier one keeps it. So the project's own names win over a platform's, and within a bundle the build already fails on such a name.
240
+ The plugins are merged in the order listed. Two of them cannot define one name: a plugin that defines a component, action handler, field type, drop rule, board drop, title rule or marks hook an earlier one defines is turned off, and the earlier one keeps it. So the project's own names win over a platform's, and within a bundle the build already fails on such a name. The logo, the bell and the presence hook come from the last one that sets them, so the project's own replace a plugin's.
239
241
 
240
242
  `protobase dev` loads plugins the same way, its page sharing the same four modules. The project's own `protobase.ui.tsx` is built into the dev page with hot reload instead, merged after the plugins, and keeps its names over theirs.
241
243
 
@@ -326,7 +328,7 @@ From `@protobase/ui`:
326
328
 
327
329
  | | |
328
330
  | --- | --- |
329
- | `definePlugin(config)` | Typed identity for a plugin's default export: `name`, `actions`, `fields`, `shell`, `beforeSignIn`, `extends`, as `defineUi`. |
331
+ | `definePlugin(config)` | Typed identity for a plugin's default export: `name`, `extends` and the keys of [Extension points](#extension-points), as `defineUi`. |
330
332
  | `BeforeSignInProps` | The props of a plugin's [`beforeSignIn`](#before-sign-in) component. |
331
333
  | `usePlugin()` | The plugin the calling component belongs to (see [above](#writing-a-plugin)). |
332
334
  | `useResourceClient(resource)` | The client of the API that serves a resource: the app's own, or a source's for a [remote resource](remote-resources.md). |
@@ -6,6 +6,8 @@ How sidebar entries are grouped and which icon each shows, how an entry lists re
6
6
 
7
7
  The admin's sidebar has one entry per resource view, grouped by `nav.group` (default: the database schema, with no group name for `public`) and sorted by `nav.order`, then by the order `config/index.ts` exports them, in `protobase dev` and in a built bundle alike. The app opens on the first entry. A view's [named lists](ui-config.md#more-than-one-list) are entries of their own, placed by their own `nav`. `nav({ hidden: true })` leaves a resource out, for line and link tables; a resource in the [user menu](user-menu.md) is left out too. [Composed pages](layouts.md#setup) have entries of their own, and groups that hold pages come first. A [remote resource](remote-resources.md) has its entry where its source's view places it; while its source cannot be reached, it sits under the source's label, marked "Cannot reach" with the source's name.
8
8
 
9
+ Below the `md` breakpoint (768px) the sidebar is a drawer, which the top bar's menu button opens, or **More** on a [bottom bar](bottom-bar.md).
10
+
9
11
  ## Sidebar icons
10
12
 
11
13
  Each resource shows the icon its view names, in the sidebar, the [bottom bar](bottom-bar.md) and the [user menu](user-menu.md), and its [named lists](ui-config.md#more-than-one-list) show the same icon:
@@ -49,6 +51,10 @@ export const directMessagesView = view<typeof directMessages>('direct_messages')
49
51
 
50
52
  ## Recent records
51
53
 
54
+ :::caution[Beta]
55
+ A marks hook that throws stops the whole sidebar, not only its own group.
56
+ :::
57
+
52
58
  An entry can list a few of its records under it, each with a status dot, so the ones that need attention are one click away:
53
59
 
54
60
  ```ts
@@ -4,6 +4,10 @@
4
4
 
5
5
  The Realtime-compatible /api/sb/realtime/v1 that supabase-js's channel() joins, for postgres_changes checked under the subscriber's RLS policies, broadcasts and presence, on public and private channels, and broadcasts from the database.
6
6
 
7
+ :::caution[Beta]
8
+ A channel cannot `select` columns, a policy cannot read `request.headers`, and a project that was not imported has no `realtime.send()` yet: see the limits in each section.
9
+ :::
10
+
7
11
  An app with API keys also serves `/api/sb/realtime/v1/websocket`, the socket that supabase-js's `channel()` joins. An app moved from Supabase keeps its `postgres_changes` listeners, broadcasts and presence: point the client at the app's `/api/sb` and give it the publishable key.
8
12
 
9
13
  ```ts
@@ -18,7 +22,7 @@ supabase
18
22
 
19
23
  ## Keys, roles and tokens
20
24
 
21
- The keys are `apiKeys` in `protobase.config.ts`, `{ publishable_key, secret_key }`, or else `PROTOBASE_PUBLISHABLE_KEY` and `PROTOBASE_SECRET_KEY`. The socket sends the key in its URL and is refused with `401` without a key of the app. Each channel joins with the client's access token: a signed-in user's token makes it `authenticated` with the token's claims, the publishable key alone `anon`, the secret key alone the app's own role. A token the client refreshes on an open socket replaces the channel's; a channel whose token expires is closed with a `Token has expired` system message, as on Supabase.
25
+ The keys are `apiKeys` in `protobase.config.ts`, `{ publishable_key, secret_key }`, or else `PROTOBASE_PUBLISHABLE_KEY` and `PROTOBASE_SECRET_KEY`. The socket sends the key in its URL and is refused with `401` without a key of the app. Each channel joins with the client's access token: a user's access token from [`/api/sb/auth/v1`](auth.md#supabase-js-and-supabasessr), which supabase-js sends once `supabase.auth` has signed in, makes it `authenticated` with the token's claims, the publishable key alone `anon`, the secret key alone the app's own role. A token the client refreshes on an open socket replaces the channel's; a channel whose token expires is closed with a `Token has expired` system message, as on Supabase.
22
26
 
23
27
  The server serves the socket when the host gives `createAdmin` its WebSocket upgrade: `protobase serve` does, on Bun and Node, and so does `protobase dev`, beside Vite's hot reload socket. An edit to the project under `protobase dev` closes the open sockets with code `1012`, and supabase-js reconnects to the rebuilt API.
24
28
 
@@ -4,6 +4,10 @@
4
4
 
5
5
  The PostgREST-compatible /api/sb/rest/v1 that supabase-js's from() and rpc() call, under the tables' RLS policies.
6
6
 
7
+ :::caution[Beta]
8
+ Some PostgREST features are not supported yet, such as many-to-many embeds, spreads, CSV, schemas other than `public` and aggregates: see [What it understands](#what-it-understands).
9
+ :::
10
+
7
11
  An app with API keys also serves `/api/sb/rest/v1`, the API that supabase-js's `from()` and `rpc()` call. An app moved from Supabase keeps its data calls: point the client at the app's `/api/sb` and give it the publishable key.
8
12
 
9
13
  ```ts
@@ -20,7 +24,7 @@ The keys are `apiKeys` in `protobase.config.ts`, `{ publishable_key, secret_key
20
24
  | The request sends | It runs as |
21
25
  | --- | --- |
22
26
  | the publishable key | `anon` |
23
- | either key and a signed-in user's token | `authenticated` |
27
+ | either key and a user's access token from [`/api/sb/auth/v1`](auth.md#supabase-js-and-supabasessr) | `authenticated` |
24
28
  | the secret key | the app's own role, which owns the tables |
25
29
 
26
30
  The transaction sets the role with `SET LOCAL ROLE` and the token's claims in `request.jwt.claims` and `request.jwt.claim.sub`, so Supabase's `auth.uid()`, `auth.role()` and `auth.jwt()` work, and the tables' RLS policies decide what the request may read and write. The resources' access rules (`.access(...)`) do not apply here: a table needs its own grants and policies, and one without a grant to `anon` or `authenticated` refuses them. The `role` claim is the database role, as on Supabase. The database needs the `anon` and `authenticated` roles, and the app's role must be allowed to `SET ROLE` to them.
@@ -4,6 +4,10 @@
4
4
 
5
5
  The Storage-compatible /api/sb/storage/v1 that supabase-js's storage calls, over the app's file provider, under the restored storage policies.
6
6
 
7
+ :::caution[Beta]
8
+ Image transforms, resumable uploads, the S3 protocol and object versioning are not supported yet: see [What it serves](#what-it-serves).
9
+ :::
10
+
7
11
  An app with API keys also serves `/api/sb/storage/v1`, the API that supabase-js's `storage` calls. An app moved from Supabase keeps its uploads, downloads, public URLs and signed URLs: point the client at the app's `/api/sb` and give it the publishable key.
8
12
 
9
13
  ```ts
@@ -24,7 +28,7 @@ The keys are `apiKeys` in `protobase.config.ts`, `{ publishable_key, secret_key
24
28
  | The request sends | It runs as |
25
29
  | --- | --- |
26
30
  | the publishable key | `anon` |
27
- | either key and a signed-in user's token | `authenticated` |
31
+ | either key and a user's access token from [`/api/sb/auth/v1`](auth.md#supabase-js-and-supabasessr) | `authenticated` |
28
32
  | the secret key | the app's own role, which owns the tables |
29
33
 
30
34
  The role is set with `SET LOCAL ROLE` and the token's claims in `request.jwt.claims` and `request.jwt.claim.sub`, so Supabase's `auth.uid()` works in the policies. The database needs the `anon` and `authenticated` roles, and the app's role must be allowed to `SET ROLE` to them. Every read and write of `storage.objects` and `storage.buckets` runs as that role, with the token's claims set, so the restored policies decide, as on Supabase:
@@ -123,7 +123,7 @@ The list is `packages/ui/src/tokens.css`, and a test fails when one of its token
123
123
 
124
124
  ## Logo and favicon
125
125
 
126
- The sidebar shows Protobase's logo, its mark alone when the sidebar shows only icons, or, for a project with its own favicon, that icon and the app's name ([App identity](app-identity.md)). A project draws something else with `shell.logo` in `protobase.ui.tsx`, a component drawn in its place ([Shell slots](custom-components.md#shell-slots)); it draws with these tokens like the rest of the app.
126
+ The sidebar's logo is the project's own favicon, or Protobase's mark without one, beside the app's [name](app-identity.md#the-name), with the organization the user works in under it, and the icon alone when the sidebar shows only icons. An app that keeps Protobase's name and has no favicon of its own shows Protobase's wordmark beside the mark. A project draws something else with `shell.logo` in `protobase.ui.tsx`, a component drawn in its place ([Shell slots](custom-components.md#shell-slots)); it draws with these tokens like the rest of the app.
127
127
 
128
128
  The browser tab shows Protobase's mark. A project shows its own with a `protobase.favicon.svg` at its root, beside `protobase.theme.css`: the app's `index.html` links `/protobase.favicon.svg`, which `protobase build` writes to the bundle's `public/`, the project's file or Protobase's without one, and which `protobase dev` serves the same way. An SVG can follow the system's light or dark with a `prefers-color-scheme` media query inside it, as Protobase's does. PNGs beside it serve the platforms that take no SVG, such as an iPhone's home screen ([App identity](app-identity.md#the-icons)).
129
129
 
@@ -60,7 +60,11 @@ export const repairsView = view<typeof repairs>('repairs')
60
60
  | `.signIn({ title, description, benefits?, icon?, signUp? })` | What a visitor who is not signed in is told about the resource, in place of its list and by a card's favorite button. See [Sign-in prompts](#sign-in-prompts). |
61
61
  | `.form((r) => ({ name, fields, thanks, title?, perMinute? }))` | A public form at `/-/forms/<name>` that creates a record for anyone, signed in or not, behind a rate limit and a bot check. See [Public forms](forms.md). |
62
62
 
63
- A browser tab names what it shows, then the app's name ([`name`](config/app.md#name)): `Nordlicht GmbH · Companies · Protobase` on a record page, the plural on a list, `New company · Companies · Protobase` on the create form, and a composed page's title on that page.
63
+ A browser tab names what it shows, then the app's name ([`name`](config/app.md#name)): `Nordlicht GmbH · Companies · Protobase` on a record page, the plural on a list, `New company · Companies · Protobase` on the create form, and a composed page's title on that page, after the label of its tab when it has tabs.
64
+
65
+ ### Breadcrumbs
66
+
67
+ The top bar shows where a page is: the sidebar group, the list and the record's title, each a link back. On a phone, a back button before them goes to the one just before the current page.
64
68
 
65
69
  ### Titles per reader
66
70
 
@@ -607,7 +611,7 @@ The create form at `/<resource>/new`, not a page of the app's own, can open the
607
611
 
608
612
  ## Sign-in prompts
609
613
 
610
- `.signIn({ title, description, benefits?, icon?, signUp? })` on a view tells a visitor who is not signed in what they could do with the resource once signed in. Where a signed-in user sees the list, the visitor sees the prompt: an icon (`icon`, as for field hints, a sign-in icon by default), `title`, `description` and a line per `benefits`, with **Sign in**, which opens the sign-in page and comes back to the same address. A card's [favorite button](#favorites) that saves to the resource, and a list's [Save search](#saved-searches) button that saves to it, open the same prompt as a dialog, with **Not now** to close it. With [open sign-up](auth.md#open-sign-up) on the server, the prompt also offers **Create an account**, which opens the app's own sign-up page. `signUp: { label, href }` names a page elsewhere instead, `https://`, `http://` or `mailto:`, opened in a new tab.
614
+ `.signIn({ title, description, benefits?, icon?, signUp? })` on a view tells a visitor who is not signed in what they could do with the resource once signed in. Where a signed-in user sees the list, the visitor sees the prompt: an icon (`icon`, as for field hints, a sign-in icon by default), `title`, `description` and a line per `benefits`, with **Sign in**, which opens the sign-in page and comes back to the same address. A card's [favorite button](#favorites) that saves to the resource, and a list's [Save search](#saved-searches) button that saves to it, open the same prompt as a dialog, with **Not now** to close it. With [open sign-up](auth.md#open-sign-up) on the server, the prompt also offers **Create an account**, which opens the app's own sign-up page. `signUp: { label, href }` names a page elsewhere instead, `https://`, `http://` or `mailto:`, opened in a new tab. A visitor is most likely new, so where the prompt offers to make an account, that is its primary action and **Sign in** the secondary one.
611
615
 
612
616
  ```ts
613
617
  view<typeof favorites>('favorites').signIn({
@@ -49,9 +49,12 @@ Every commit message follows [Conventional Commits](https://www.conventionalcomm
49
49
  3. The tag is pushed, and the GitHub release of the tag gets the notes.
50
50
  4. Beside the checks, a pack job takes the tag `bun run release --dry-run` predicts, and `scripts/stamp-version.mjs` writes its version into every `package.json` and `bun.lock` in the runner, never committed. `scripts/publish.mjs --pack --out` then builds every package to `dist` and packs it with Bun (the private `@protobase/presets` is not published). Once the tag is pushed, npm, under Node, publishes those tarballs with [trusted publishing](https://docs.npmjs.com/trusted-publishers) and provenance (`scripts/publish.mjs --from`); there is no npm token, and only the publish job holds the identity token it needs. A tag other than the version the tarballs were stamped with fails the publish before anything is uploaded, and a run that cut no release packs and publishes nothing.
51
51
  5. Once npm serves the packages, the tag's version is written into the `package.json` files the same way, and `bun run images:context <image>` writes each image's build context at the tag from them, and the images are built natively for `linux/amd64` and `linux/arm64` and pushed to GitHub's registry as `ghcr.io/prototeamhq/protobase:<version>` and `ghcr.io/prototeamhq/protobase-dev:<version>` (see [Deploy](../guides/deploy.md#images)). A release whose publish failed has no images.
52
+ 6. Once npm serves the packages, `scripts/bump-repos/main.ts` starts the `protobase-bump.yml` workflow, with the release's version, of each repository the `PROTOBASE_BUMP_REPOS` secret lists, using the `PROTOBASE_BUMP_TOKEN` secret; an unset or empty list bumps nothing. Each listed repository owns that workflow, with a `version` input: it moves the repository's `@protobase` packages to the release in a pull request of its own, which merges itself once its CI is green and otherwise stays open for a fix. A repository that cannot be started within a minute fails the job after the others were started, and nothing else waits on it.
52
53
 
53
54
  When `main` moved on while the checks ran, the run of the newer push releases both. `bun run release --dry-run` prints the next version and its notes without tagging anything.
54
55
 
56
+ A push made with GitHub Actions' own token, such as a pull request the AI review merged, [starts no workflow](https://docs.github.com/en/actions/concepts/security/github_token), but a run by hand always starts one. Running the workflow by hand from `main` without a tag (`gh workflow run release.yml`) releases `main`'s head as a push does, with the same checks, which then run in full, commitlint over the commits since the last `v<version>` tag, and the same release. A release started while another runs waits for it.
57
+
55
58
  Running the workflow by hand with a tag (`gh workflow run release.yml -f tag=v0.1.0`) only packs and publishes: the packages at that tag and its version, skipping every version npm already has, and the images, which repeats a missed or half-done publish. A publish that npm fails because it could not read the identity token, or because Sigstore's transparency log already holds its provenance (a 409 after npm's own retry), is tried again up to 3 times, and the 409 counts as published when npm has the version; any other failure fails the run. `node scripts/publish.mjs --dry-run` packs the packages and shows what `npm publish` would send, and `node scripts/publish.mjs --pack`, which CI runs, packs every package without asking npm or publishing. Each package on npm trusts `release.yml` of `PrototeamHQ/protobase` as its publisher; a new package has to be published once by hand before that can be set up. The workflow pushes the images with its own token; a new image on GitHub's registry starts out private and is made public once in its package settings.
56
59
 
57
60
  ## Bundled and locked projects
@@ -61,11 +64,11 @@ Where a project's `package.json` lists the `@protobase` packages decides whose c
61
64
  | `package.json` | Mode | Copies used by `dev`, `build` and `protobase run` | The bundle's `protobase` |
62
65
  | --- | --- | --- | --- |
63
66
  | `@protobase/*` in `devDependencies` | bundled (what the presets write) | the CLI's own | the CLI's version |
64
- | `@protobase/*` in `dependencies` | locked | the project's installed copies | the installed version |
67
+ | `@protobase/*` in `dependencies` | locked | the project's installed copies, but the build reads the config with the CLI's own | the installed version |
65
68
  | no `@protobase/*`, or no `package.json` | bundled | the CLI's own | the CLI's version |
66
69
 
67
70
  - **Bundled.** Nothing of Protobase has to be installed: a `protobase.config.ts` alone is a project. The CLI that runs, installed globally or as the project's dev dependency, supplies every `@protobase` package and every package the serve runtime supplies to a bundle (`kysely`, `pg`, `react`, ...), whatever the project installed: to `dev`'s API and page, to the build, to [`protobase run`](cli.md#run) and, through [`protobase types`](cli.md#types), to the editor. Installing a newer CLI that the specifier holds updates the app with no change to its files or lockfile: dev and the next build use it, and the bundle names its version. `@types/react` and `@types/node` stay the project's own dev dependencies, since the CLI carries no types for them.
68
- - **Locked.** Move every `@protobase` package to `dependencies` at one exact version, and install. Dev, the build and scripts use the installed copies, and the bundle names that version, so a host serves exactly it. A CLI installed elsewhere hands a locked project over to the `@protobase/cli` it installed (see [CLI](cli.md)). The version moves only when the specifier and the lockfile do.
71
+ - **Locked.** Move every `@protobase` package to `dependencies` at one exact version, and install. Dev and scripts use the installed copies, and the bundle names that version, so a host serves exactly it. The build reads the config with the building CLI's own `@protobase` packages, as a serve runtime supplies its own to the bundle, so it loads one copy of each. A CLI installed elsewhere hands a locked project over to the `@protobase/cli` it installed (see [CLI](cli.md)). The version moves only when the specifier and the lockfile do.
69
72
 
70
73
  Packages the runtime supplies, such as React, may be listed for their types, in either mode: in a bundle the runtime's copy runs, whichever the project installed. `dependencies` holds the packages the project's own code uses at runtime.
71
74
 
@@ -116,6 +116,8 @@ It adds two pages, over a `webhooks` schema that [`protobase migration`](cli.md#
116
116
  - **Webhook subscriptions** (`webhook_subscriptions`, the table `webhooks.subscriptions`): a name, the events (`orders.created orders.large`, or `orders.*`), the URL, a filter, extra headers, full or thin payload, and whether it is enabled. The server sets the tenant and the owner, the person who made it, and makes the secret, which only a [reveal](api.md#sensitive-fields-and-audit) shows. Saving checks the events and the filter against the resources, and refuses a URL whose host is a private, loopback or link-local address.
117
117
  - **Webhook deliveries** (`webhook_deliveries`, a view over the delivery jobs): the deliveries that wait for their next try or failed for good, with their last error. A delivery leaves the list once it succeeds.
118
118
 
119
+ The migration gives the subscriptions table an index for each sort, filter and search of its list, and creates the `pg_trgm` extension the search's indexes need (`create extension if not exists pg_trgm`, a trusted extension since Postgres 13, so any role that may create in the database can). A database that has an earlier version's schema gets the indexes from the next `protobase migration`; the server does not start until it is applied.
120
+
119
121
  Both are scoped to the tenant like any resource with `.tenant()`, and their access decides who may do what: `webhook_subscriptions.create`, `webhook_deliveries.update` and so on, as for the app's own resources. A role's wildcard over resources, such as `*.read`, grants none of them: their URLs and secrets let whoever reads them call the receivers, so with roles only admins (`*`) and roles that [name them](access.md#resources-that-need-named-grants) manage webhook subscriptions.
120
122
 
121
123
  A subscription made here **delivers as its owner**, with the role they act with now in its tenant (the [active role](auth.md#active-role) they last chose, else their first): only records and fields it may read. Letting people make subscriptions therefore grants them nothing they could not read already. The roles are read from the app's sign-in; a project that signs people in otherwise exports its own, `async ({ id, tenant }) => [...]`, as the default of `webhooks/owner-roles.ts`.