@protobase/cli 0.66.13 → 0.66.15
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/dist/cli/package.js +1 -1
- package/package.json +7 -7
- package/skills/protobase/references/features/api-functions.md +4 -2
- package/skills/protobase/references/features/composed-pages.md +1 -0
- package/skills/protobase/references/features/record-pages.md +1 -0
- package/skills/protobase/references/features/rest-api.md +3 -3
- package/skills/protobase/references/guides/api-functions.md +17 -1
- package/skills/protobase/references/reference/api.md +7 -3
- package/skills/protobase/references/reference/auth.md +1 -1
- package/skills/protobase/references/reference/blocks.md +9 -0
- package/skills/protobase/references/reference/cli.md +1 -1
- package/skills/protobase/references/reference/email.md +4 -0
- package/skills/protobase/references/reference/ui-config.md +8 -0
package/dist/cli/package.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@protobase/cli",
|
|
3
|
-
"version": "0.66.
|
|
3
|
+
"version": "0.66.15",
|
|
4
4
|
"description": "The protobase command: scaffold, doctor, dev, build and serve a Protobase project.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -43,12 +43,12 @@
|
|
|
43
43
|
"dependencies": {
|
|
44
44
|
"@better-auth/core": "^1.7.12",
|
|
45
45
|
"@hono/node-server": "^2.1.3",
|
|
46
|
-
"@protobase/config": "0.66.
|
|
47
|
-
"@protobase/layout": "0.66.
|
|
48
|
-
"@protobase/query": "0.66.
|
|
49
|
-
"@protobase/schema": "0.66.
|
|
50
|
-
"@protobase/server": "0.66.
|
|
51
|
-
"@protobase/ui": "0.66.
|
|
46
|
+
"@protobase/config": "0.66.15",
|
|
47
|
+
"@protobase/layout": "0.66.15",
|
|
48
|
+
"@protobase/query": "0.66.15",
|
|
49
|
+
"@protobase/schema": "0.66.15",
|
|
50
|
+
"@protobase/server": "0.66.15",
|
|
51
|
+
"@protobase/ui": "0.66.15",
|
|
52
52
|
"@react-email/render": "^2.1.0",
|
|
53
53
|
"@scalar/hono-api-reference": "^0.12.10",
|
|
54
54
|
"@sentry/vite-plugin": "^11.10.0",
|
|
@@ -7,8 +7,10 @@ Endpoints of your own beside the REST API.
|
|
|
7
7
|
- [API functions](../guides/api-functions.md#a-first-function): Each file in `functions/` becomes an endpoint for signed-in callers, reloaded while you work.
|
|
8
8
|
- [Records as the caller](../guides/api-functions.md#reading-and-writing-records-as-the-caller): Functions read and write records with the caller's access, validation and hooks.
|
|
9
9
|
- [Input checks](../guides/api-functions.md#checking-input): Request bodies are checked with zod, Valibot or another Standard Schema validator.
|
|
10
|
-
- [Roles for functions](../guides/api-functions.md#roles): A function can be limited to roles or a capability.
|
|
10
|
+
- [Roles for functions](../guides/api-functions.md#roles): A function can be limited to roles or a capability; in an app with roles, one with neither is open only to roles that hold everything.
|
|
11
11
|
- [Public functions and webhooks](../guides/api-functions.md#public-functions-and-webhooks): A function can be open to anyone, with helpers that check webhook signatures.
|
|
12
12
|
- [WebSockets](../guides/api-functions.md#websockets): A function can accept a WebSocket, and the caller is checked again every minute.
|
|
13
|
+
- [Calling a function](../guides/api-functions.md#calling-a-function): The typed client's `invoke` calls a function with the signed-in user's token, from an action or any page.
|
|
14
|
+
- [Work after the response](../guides/api-functions.md#work-after-the-response): A function can answer at once and finish its work afterwards, which a graceful stop waits for.
|
|
13
15
|
- [Hono apps](../guides/api-functions.md#hono-apps): A function with several routes can be a Hono app.
|
|
14
|
-
- [Long queries](../guides/api-functions.md#long-queries): A function or job that does bulk work may run queries for up to 30 seconds, on a connection kept for it.
|
|
16
|
+
- [Long queries](../guides/api-functions.md#long-queries): A function or job that does bulk work may run queries for up to 30 seconds, on a connection kept for it, and functions and jobs always leave a connection to page requests.
|
|
@@ -10,6 +10,7 @@ Pages of your own, built from blocks in a JSX layout.
|
|
|
10
10
|
- [App addresses](../reference/layouts.md#addresses): Links to lists, records, create forms and pages are typed addresses that can carry filters and prefilled values.
|
|
11
11
|
- [Per-user pages](../reference/layouts.md#access): Each user gets the page without the parts, fields and actions they cannot use.
|
|
12
12
|
- [Layout blocks](../reference/blocks.md#layout): Pages, responsive grids, cards and links.
|
|
13
|
+
- [Full-width pages](../reference/blocks.md#page): A page fills the whole area beside the sidebar instead of a centred column, for a dashboard with many blocks.
|
|
13
14
|
- [Data blocks](../reference/blocks.md#data): Counts, tables, single records, rows of cards, field values and progress bars.
|
|
14
15
|
- [Record rows](../reference/blocks.md#recordrow): A sideways row of cards for a filter, favorites or saved searches, with a count and new-record marks.
|
|
15
16
|
- [Record cards](../reference/blocks.md#recordcards): Full-width cards with fields and buttons, stacked or in a carousel.
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
The page of one record, with its sections, related records, galleries, charts and editing.
|
|
6
6
|
|
|
7
7
|
- [Record page layout](../reference/ui-config.md#the-record-page): Sections, a summary sidebar, related tables, maps, charts, galleries, history, comments and your own components.
|
|
8
|
+
- [Full-width record pages](../reference/ui-config.md#page-width): A view's record pages fill the whole area beside the sidebar instead of a centred column, with the sidebar cards still beside the sections.
|
|
8
9
|
- [Full-area record pages](../reference/ui-config.md#full-area-record-pages) (beta): A view draws its record pages with a component of its own that fills the area beside the sidebar and under the top bar, such as a chat channel, and links to the standard page.
|
|
9
10
|
- [Read view and edit](../reference/ui-config.md#read-view-and-edit): A record opens as a read-only fact sheet or a form, and Edit opens the form at its own address.
|
|
10
11
|
- [Unsaved edits](../reference/ui-config.md#editing-a-record): Unsaved changes to a record stay in the browser and can be resumed, and leaving a form, a dialog or a comment with changes asks first.
|
|
@@ -6,7 +6,7 @@ The API every resource gets, with filters, writes, errors and the typed client.
|
|
|
6
6
|
|
|
7
7
|
- [Generated REST API](../reference/api.md#routes): List, get, create, update and delete endpoints for every resource, following the AIP API design guidelines.
|
|
8
8
|
- [OpenAPI and reference](../reference/api.md#meta-and-openapi-per-caller): An OpenAPI 3.1 document and an API reference page, each showing only what the caller may see and do.
|
|
9
|
-
- [Filters](../reference/api.md#filters): AIP-160 filters and ordering, with
|
|
9
|
+
- [Filters](../reference/api.md#filters): AIP-160 filters and ordering, with word search across a resource's search fields, lists of values and relative dates.
|
|
10
10
|
- [Counts, facets and charts](../reference/api.md#routes): Endpoints for value counts, grouped sums and averages, records over time, histograms and paging to any row.
|
|
11
11
|
- [Map queries](../reference/api.md#maps): The records inside a map's view, or counts per area when there are many.
|
|
12
12
|
- [Related names and photos](../reference/api.md#related-names): A list response can carry the names of related records and each record's first photos.
|
|
@@ -14,11 +14,11 @@ The API every resource gets, with filters, writes, errors and the typed client.
|
|
|
14
14
|
- [Minimal responses](../reference/api.md#writes): A create or update can answer without the record, to save the transfer.
|
|
15
15
|
- [Batch writes](../reference/api.md#batch-writes): Up to 500 creates, updates, deletes and reorders across resources in one transaction.
|
|
16
16
|
- [Record permissions](../reference/api.md#record-permissions): Responses say which records and fields the caller may change.
|
|
17
|
-
- [Scan guard](../reference/api.md#safety):
|
|
17
|
+
- [Scan guard](../reference/api.md#safety): Lists, searches and facets on a column no index serves are refused, at any table size, with the index to create.
|
|
18
18
|
- [Statement timeouts](../reference/api.md#safety): Every request's queries stop after 1 second by default, with an error that names the request, and reads run read-only.
|
|
19
19
|
- [Errors](../reference/api.md#errors): Every error is a problem document with a stable type and per-field details.
|
|
20
20
|
- [Audit events](../reference/api.md#audit-events): Reveals of sensitive values and organization changes are published as audit events.
|
|
21
|
-
- [Error reporting](../reference/api.md#error-reporting): Server and admin app errors go to a Sentry-compatible error tracker when a DSN is set.
|
|
21
|
+
- [Error reporting](../reference/api.md#error-reporting) (beta): Server and admin app errors go to a Sentry-compatible error tracker when a DSN is set.
|
|
22
22
|
- [JWT authentication](../reference/api.md#authentication): The API accepts tokens from another identity provider, or a check of your own.
|
|
23
23
|
- [CORS](../reference/api.md#cors): Websites on other origins can call the API once their origins are listed.
|
|
24
24
|
- [Typed client](../reference/client.md): A typed fetch client and React hooks for the API.
|
|
@@ -291,6 +291,21 @@ The ticket only lets the socket open, and anyone who holds it may open one with
|
|
|
291
291
|
|
|
292
292
|
`protobase dev` closes every open socket with `1012` when you edit the project, so it reconnects to the rebuilt API. `protobase serve` closes them with `1001` when it stops, and drops any still open after 5 seconds. Clients that reconnect on these codes keep working across both.
|
|
293
293
|
|
|
294
|
+
## Work after the response
|
|
295
|
+
|
|
296
|
+
`waitUntil(promise)` from `@protobase/server` lets a function answer at once and finish its work afterwards, such as a slow call to another API:
|
|
297
|
+
|
|
298
|
+
```ts
|
|
299
|
+
import { defineFunction, waitUntil } from '@protobase/server'
|
|
300
|
+
|
|
301
|
+
export default defineFunction((_request, { session }) => {
|
|
302
|
+
waitUntil(fetch('https://hooks.example.com/notify', { method: 'POST', body: JSON.stringify({ user: session.user.id }) }))
|
|
303
|
+
return new Response(null, { status: 202 })
|
|
304
|
+
})
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
`protobase serve` waits for that work when it stops gracefully; work still running when the host kills the process is lost, and an error it throws is printed, since no response waits for it. On Protobase Cloud's Starter plan an app stops 30 seconds after its last request, with 10 seconds to finish, so move longer work into a [job](../reference/jobs.md).
|
|
308
|
+
|
|
294
309
|
## Secrets and other APIs
|
|
295
310
|
|
|
296
311
|
```ts title="functions/notify-customer.ts"
|
|
@@ -406,6 +421,7 @@ export default defineFunction({ longQueries: true }, async (request, { db }) =>
|
|
|
406
421
|
|
|
407
422
|
- **Up to 30 seconds** per query, the most any query of an app may run. Work longer than that belongs in steps, such as a [job](../reference/jobs.md) per batch, which takes `longQueries` too.
|
|
408
423
|
- **One connection kept for it.** Under `protobase dev` and `serve`, those queries run on a connection of their own, which asks for the longer timeout when it logs in, so long work never takes the connections pages need; a second long call waits for it. A PgBouncer of your own in front of the database must list `statement_timeout` in `track_extra_parameters` to pass that on; Protobase Cloud's does.
|
|
424
|
+
- **Room for pages.** Under `protobase dev` and `serve`, function calls and jobs together hold at most one connection less than [`PROTOBASE_DB_CONNECTIONS`](../reference/cli.md#serve), so a long import always leaves one to page and API requests; a call past that waits for a connection of its own.
|
|
409
425
|
- **`records` keep the API's limit,** [`statementTimeoutMs`](../reference/api.md#safety).
|
|
410
426
|
- **A query past its limit** is stopped, and the call answers `504 statement-timeout` naming the call, its limit and the way out, such as `POST /api/functions/import ran a query longer than the database's statement timeout, so the database stopped it. Make the query faster, such as with an index or a narrower filter, or give it longQueries: true, whose queries may run up to 30 s.` It goes to the app's error hook, `hooks/error.ts`, and the [error tracker](../reference/api.md#error-reporting).
|
|
411
427
|
|
|
@@ -422,7 +438,7 @@ export default defineFunction({ longQueries: true }, async (request, { db }) =>
|
|
|
422
438
|
- **At `/api/sb/functions/v1/<name>` too,** so `supabase.functions.invoke` callers do not change. Only a function with `supabase: true` answers there, and any other function answers `404`. There a protected function lets in a request with the app's publishable or secret key in the `apikey` header, and a user token of [`/api/sb/auth/v1`](../reference/auth.md#supabase-js-and-supabasessr) as the bearer, if any, as Supabase's `verify_jwt` does. A public function lets in anyone. The keys are the app's [API keys](../reference/supabase-rest.md#keys-and-roles): without them, only public functions answer there. As on Supabase, the request's path starts at the function's name (`/<name>/more/parts`), and a CORS preflight reaches the function, which answers it.
|
|
423
439
|
- **Its context there** is `{ name, db, caller() }`, where `caller()` resolves to who the key and token make the request: `{ role: 'anon' }` for the publishable key alone, `{ role: 'app' }` for the secret key, or `{ role: 'authenticated', claims }`. There is no `session` and no `records`. The publishable key alone lets callers in, so give `supabase: true` only to functions written for Supabase's `verify_jwt`. Together with `roles` or `capability`, which the address cannot check, it stops startup.
|
|
424
440
|
- **Supabase's variables:** `protobase dev` sets `SUPABASE_URL` to the app's address with `/api/sb`, where supabase-js finds it, `SUPABASE_ANON_KEY` and `SUPABASE_PUBLISHABLE_KEY` to the publishable key and `SUPABASE_PUBLISHABLE_KEYS` to `{"default":<publishable key>}`, `SUPABASE_SERVICE_ROLE_KEY` and `SUPABASE_SECRET_KEY` to the secret key and `SUPABASE_SECRET_KEYS` to `{"default":<secret key>}`, each only when the app has that key, `SUPABASE_JWKS_URL` to the key set of `/api/sb/auth/v1` when the app has sign-in, and `SUPABASE_DB_URL` to the database. A variable set in `.env` wins. Under `protobase serve`, set them where the host keeps its settings.
|
|
425
|
-
-
|
|
441
|
+
- **[`waitUntil(promise)`](#work-after-the-response)** from `@protobase/server` takes the place of `EdgeRuntime.waitUntil`.
|
|
426
442
|
|
|
427
443
|
## What functions are not
|
|
428
444
|
|
|
@@ -211,7 +211,7 @@ The API sends no CORS headers until it has something to allow: other websites in
|
|
|
211
211
|
|
|
212
212
|
## Errors
|
|
213
213
|
|
|
214
|
-
Every error is `application/problem+json` (RFC 9457) with `type` (`urn:protobase:problem:<slug>`), `title`, `status`, `detail`, `instance`. Filter and `order_by` errors add `parameter` and `errors: [{ code, message, hint, span: { start, end } }]`; invalid parameters add `errors: [{ parameter, message }]`. A create or update whose body fails the checks is `invalid-record` with `errors: [{ field, code, message, params }]`, `code` one of `required`, `invalid-value`, `read-only`, `unknown-field` and `invalid-record` (a `.validate()` rule); `params` is the limit an invalid value broke, such as `{ min }`, `{ max }` or a decimal's `{ precision, scale }`. Database constraint failures map to `409` or `400` without the raw message. A create or update that writes the position of a resource with `after` so a record ranks at or above one it depends on is a `409` `ranks-above-dependency`, as in a [`reorder`](#batch-writes). A create that repeats the values of a resource's [`.openExisting()`](data-config.md#opening-an-existing-record) unique key is a `409` `conflict` with `existing: { key }`, the key of the record holding them as its address writes it, only when the caller may read that record; otherwise it is the plain `409`, as any unique conflict is. A delete that other records still point at is a `409` `conflict` with `blockedBy: { resource, field? }`: their resource, when the caller may list it, and its relation field pointing at the record, when they may filter by it, so a client can open those records; the admin app's toast names them, such as "Orders still point at this company.", with a link to that list. Postgres names one table of them only, so `blockedBy` holds one resource
|
|
214
|
+
Every error is `application/problem+json` (RFC 9457) with `type` (`urn:protobase:problem:<slug>`), `title`, `status`, `detail`, `instance`. Filter and `order_by` errors add `parameter` and `errors: [{ code, message, hint, span: { start, end } }]`; invalid parameters add `errors: [{ parameter, message }]`. A create or update whose body fails the checks is `invalid-record` with `errors: [{ field, code, message, params }]`, `code` one of `required`, `invalid-value`, `read-only`, `unknown-field` and `invalid-record` (a `.validate()` rule); `params` is the limit an invalid value broke, such as `{ min }`, `{ max }` or a decimal's `{ precision, scale }`. Database constraint failures map to `409` or `400` without the raw message. A create or update that writes the position of a resource with `after` so a record ranks at or above one it depends on is a `409` `ranks-above-dependency`, as in a [`reorder`](#batch-writes). A create that repeats the values of a resource's [`.openExisting()`](data-config.md#opening-an-existing-record) unique key is a `409` `conflict` with `existing: { key }`, the key of the record holding them as its address writes it, only when the caller may read that record; otherwise it is the plain `409`, as any unique conflict is. A delete that other records still point at is a `409` `conflict` with `blockedBy: { resource, field? }`: their resource, when the caller may list it, and its relation field pointing at the record, when they may filter by it, so a client can open those records; the admin app's toast names them, such as "Orders still point at this company.", with a link to that list. Postgres names one table of them only, so `blockedBy` holds one resource. A write that Postgres rolls back because it locked the same records as another write in the opposite order (a deadlock) is a `409` `conflict` too: nothing was written, so send it again. Anything unexpected is a `500` with a fixed detail, reported to the app's error hook (`hooks/error.ts`, `createAdmin`'s `options.onUnhandledError`) and, when it is set, the [error tracker](#error-reporting).
|
|
215
215
|
|
|
216
216
|
| Status | Slugs |
|
|
217
217
|
| --- | --- |
|
|
@@ -219,13 +219,17 @@ Every error is `application/problem+json` (RFC 9457) with `type` (`urn:protobase
|
|
|
219
219
|
| 401 | `unauthenticated` |
|
|
220
220
|
| 403 | `access-denied`, `tenant-required` |
|
|
221
221
|
| 404 | `not-found` |
|
|
222
|
-
| 409 | `conflict
|
|
222
|
+
| 409 | `conflict`: a unique key, a record other records point at, or a deadlock with another write, which may be sent again |
|
|
223
223
|
| 412 / 428 | `precondition-failed` / `precondition-required` |
|
|
224
224
|
| 503 | `read-only`: the database refuses writes, as during a deploy's write freeze; send the write again after `Retry-After`. `connections-busy`: every database connection stayed busy for 2 seconds (see [Safety](#safety)) |
|
|
225
225
|
| 504 | `statement-timeout`: a query ran past its limit (see [Safety](#safety)) |
|
|
226
226
|
|
|
227
227
|
### Error reporting
|
|
228
228
|
|
|
229
|
+
:::caution[Beta]
|
|
230
|
+
<gap>An error raised only inside a plugin built by an earlier `build-plugin` cannot be reported because marking a plugin's scripts when the app loads them is not implemented; build the plugin again.</gap>
|
|
231
|
+
:::
|
|
232
|
+
|
|
229
233
|
An app can send its errors to an error tracker of its own: [Sentry](https://sentry.io) or one that speaks its protocol, such as [Bugsink](https://www.bugsink.com) or [GlitchTip](https://glitchtip.com). It is off until a DSN, the address the tracker gives a project, is set in `errorReporting` or `PROTOBASE_SENTRY_DSN`; without one nothing is loaded or sent.
|
|
230
234
|
|
|
231
235
|
```ts
|
|
@@ -235,6 +239,6 @@ export default defineConfig({
|
|
|
235
239
|
```
|
|
236
240
|
|
|
237
241
|
- **Server:** every `500` with the request it failed, and every `504 statement-timeout` and `503 connections-busy` with the request it names, under `dev`, `serve` and the runtime, besides the default export of `hooks/error.ts` or the console. Under `serve`, the runtime and `worker`, an uncaught exception or unhandled rejection too, which then still ends the process with exit code 1.
|
|
238
|
-
- **Admin app:** uncaught errors and the errors of components that fail to draw. `/api/meta` names the DSN to every signed-in caller, as a browser app always shows its own. Only the app's own errors are sent: `build`, `build-shell` and `build-plugin` mark every script they write as the app's, and an error none of whose stack frames is in a marked script, such as one raised by a browser extension or a script the browser injects, is dropped. Under `dev`, whose scripts are not built, every error is sent.
|
|
242
|
+
- **Admin app:** uncaught errors and the errors of components that fail to draw. `/api/meta` names the DSN to every signed-in caller, as a browser app always shows its own. Only the app's own errors are sent: `build`, `build-shell` and `build-plugin` mark every script they write as the app's, and an error none of whose stack frames is in a marked script, such as one raised by a browser extension or a script the browser injects, is dropped. Under `dev`, whose scripts are not built, every error is sent.
|
|
239
243
|
- Errors only, with no performance tracing. Before an event leaves, the values of authorization headers, cookies, passwords, tokens and other credentials, in fields and in URLs, are replaced by `[Filtered]`.
|
|
240
244
|
- `environment` names the deployment in the tracker; without it the tracker's default, `production`. `errorReporting: false` turns reporting off whatever the environment says. A DSN that is no `http(s)` URL with a key and a project id stops startup.
|
|
@@ -249,7 +249,7 @@ export default { send: async ({ to, subject, text }) => { /* deliver it */ } } s
|
|
|
249
249
|
|
|
250
250
|
- The email names the app and gives the link, as a button and in its plain-text part, that it works for 1 hour and once, and that it can be ignored (see [Protobase's own mail](email.md#protobases-own-mail)).
|
|
251
251
|
- The SMTP client greets the server with the sender's domain (`admin.example.com` above), not the container's `[127.0.0.1]`; a `?name=` on `PROTOBASE_SMTP_URL` overrides it.
|
|
252
|
-
- A request answers the same whether or not the address has an account, and the mail goes out after the answer, so neither the response nor its timing tells. A send that fails is logged by Better Auth.
|
|
252
|
+
- A request answers the same whether or not the address has an account, and the mail goes out after the answer, so neither the response nor its timing tells. A send that fails is logged by Better Auth; one the server does not finish within 30 seconds fails with `MAIL_SERVER_TIMEOUT` (see [When the server does not answer](email.md#when-the-server-does-not-answer)), so the failure is in the log while an emailed code still works.
|
|
253
253
|
- Requests are limited to 3 per minute and client. Setting a new password ends all of the user's sessions; the new password needs 12 characters like any other.
|
|
254
254
|
- `redirectTo` must be on a trusted origin (the app's `url`, one of its [other origins](#several-origins) or `auth.trustedOrigins`); the admin app sends its own page with a `password-reset` marker, so the link opens the set-password page there.
|
|
255
255
|
- The link is on the origin the reset was asked on, when that is one of the app's.
|
|
@@ -100,6 +100,15 @@ The root of every page, and only the root.
|
|
|
100
100
|
| `title` | Heading, and the page's name in the sidebar and breadcrumbs. Required. |
|
|
101
101
|
| `description` | A line under the title. |
|
|
102
102
|
| `actions` | Elements beside the title, such as a `ModalForm`. |
|
|
103
|
+
| `width` | `column`, the default, keeps the page in a centred column about 1200px wide; `full` fills the content pane beside the sidebar, for a dashboard with many blocks. On a phone both look the same. |
|
|
104
|
+
|
|
105
|
+
```tsx
|
|
106
|
+
<Page title="Today" width="full">
|
|
107
|
+
<Grid columns={4}>
|
|
108
|
+
...
|
|
109
|
+
</Grid>
|
|
110
|
+
</Page>
|
|
111
|
+
```
|
|
103
112
|
|
|
104
113
|
### Grid
|
|
105
114
|
|
|
@@ -148,7 +148,7 @@ One port, two processes: Vite serves the admin app from `@protobase/ui`'s `src/a
|
|
|
148
148
|
- **Login:** real sign-in only, with the config's `auth`, on by default, or its `authenticate` for tokens of an auth server elsewhere (see [protobase.config.ts](config.md) and [Login with Better Auth](auth.md)). A project with sign-in off and no authenticator fails at startup with a message pointing there, as does one with sign-in on and no `PROTOBASE_SECRET`, one whose auth store has not applied every migration of this version, naming them (see [`auth migration`](#auth-migration)), one with a resource over a table of the auth store, and one with a resource whose [events](events.md) or [history](history.md) lack their triggers, or whose [comments](comments.md) lack their table, or whose [rollup and formula fields](data-config.md#rollups-and-formulas) lack their columns or triggers, which it checks again on every reload (see [`migration`](#migration)), and one with an [AI field or action](ai.md#the-model) and no model key; there is no development login. The `login` line of the startup message says how to sign in: while the auth store has no users, it prints the exact command that creates the first admin, such as `bun run protobase users create you@example.com --generate-password` (the package manager that started `dev`, `npx` otherwise). Against a database that has users already, such as an app's hosted one, sign in as one of them or add one with a new email; its own password is set there. A `warning` line follows for each way the auth store's tables differ from what the app's sign-in needs, such as a column dropped by hand: the one place Protobase compares them, since `serve` reads the store's record of [migrations](auth.md#migrations) alone.
|
|
149
149
|
- **Mobile releases:** see [below](#mobile-releases).
|
|
150
150
|
- **Jobs:** with [jobs](jobs.md) in the config it runs them in the API's process, once the jobs schema checks out (see [`migration`](#migration)); an edited job runs its new code from the next job on. `--no-jobs` runs none of them and delivers none of the app's [events](events.md), webhooks or pushes, for a dev server on the database of a hosted app that already runs them; jobs are still enqueued, for that app to run, and the startup message says `jobs off`.
|
|
151
|
-
- **Tunnels and other hosts:** Vite only answers `localhost` by default. `--allowed-hosts .trycloudflare.com,app.example.com` adds Host headers (a leading dot matches all subdomains). Hot reload needs no extra setting: the Vite client takes protocol, host and port from the page, so behind an HTTPS tunnel it connects over `wss` on 443 as long as the tunnel forwards WebSockets.
|
|
151
|
+
- **Tunnels and other hosts:** Vite only answers `localhost` by default. `--allowed-hosts .trycloudflare.com,app.example.com` adds Host headers (a leading dot matches all subdomains). The API's WebSockets, such as an [API function](../guides/api-functions.md#websockets)'s, pass the same check: an upgrade with another Host header is refused with `403`. Hot reload needs no extra setting: the Vite client takes protocol, host and port from the page, so behind an HTTPS tunnel it connects over `wss` on 443 as long as the tunnel forwards WebSockets.
|
|
152
152
|
- **Project discovery**, by convention: `config/index.ts` exports the resources (and views), and every `config/*/ui.ts` export that is not already exported by the index is added as a view. Each `functions/<name>.ts` (or `functions/<name>/index.ts`) is an [API function](../guides/api-functions.md) at `/api/functions/<name>`. `protobase.config.ts` (or `protobase.config.json`) in the project root describes the rest as data, and the jobs, hooks and other code it names are found by their paths; see [protobase.config.ts](config.md). Without it, the app runs on the defaults.
|
|
153
153
|
- **Project UI:** the app imports `protobase.ui.tsx` from the project root, with hot reload, when it exists (see [Custom components](custom-components.md)): the same contract as the plugin `protobase build` makes of it, built into the dev page instead. Layout files are checked as they load, as `protobase build` checks them.
|
|
154
154
|
- **Theme:** the page links `/protobase.theme.css`, which the dev server answers from the project's [theme](theme.md), checked as the build checks it, on every request, so an edit shows on the next reload. It links `/protobase.favicon.svg` too, the project's [favicon](theme.md#logo-and-favicon) or Protobase's, and `/protobase.apple-touch-icon.png`, which it answers with the PNG [icons](app-identity.md#the-icons) the project has.
|
|
@@ -28,6 +28,10 @@ PROTOBASE_MAIL_FROM=noreply@shop.example.com
|
|
|
28
28
|
|
|
29
29
|
A project that delivers its mail another way exports a mailer, `{ send({ to, subject, text, html, headers }) }`, as the default of `auth/mailer.ts`: sign-in and the jobs (`mail` in their context) both send with it. `auth: { mailer: false }` turns sign-in's mail off whatever the environment says. A host that runs jobs itself passes a mailer to `startJobs`. A template's `send` takes one too, see [Templates](#templates).
|
|
30
30
|
|
|
31
|
+
### When the server does not answer
|
|
32
|
+
|
|
33
|
+
A send through `PROTOBASE_SMTP_URL` gets 30 seconds in all: looking up the server, connecting to each of its addresses, its greeting, TLS, the login and the message. One that has not finished by then rejects with a `MailServerTimeoutError` (exported by `@protobase/server`) whose `code` is `MAIL_SERVER_TIMEOUT`, as when a firewall drops the mail port's packets or the server never greets; it is not retried. Options on the URL, such as `?connectionTimeout=`, cannot lift the limit. A server that answers and refuses keeps its own error code, such as `EAUTH` for a refused login or `EENVELOPE` for a refused sender. <gap>A send that ran out cannot close its connection at the limit because cancelling a send in progress is not implemented in the SMTP client; the client's own timers, 10 seconds for each of the server's addresses, close it soon after.</gap>
|
|
34
|
+
|
|
31
35
|
## Protobase's own mail
|
|
32
36
|
|
|
33
37
|
The auth emails (sign-in, two-factor and sign-up codes, the note to an address that asked to sign up while it has an account, password reset, invitations and staff sign-in notices) and the email to someone a [comment mentions](comments.md#mentions) are built with React Email's components: a heading, the message, one code or button, and a footer naming the app.
|
|
@@ -671,6 +671,14 @@ Without a layout the record page shows one section with every field the user may
|
|
|
671
671
|
|
|
672
672
|
A record opened from a list, by its row or its card, shows the fields the list loaded at once, with placeholders for the others, and takes input once the whole record has loaded; actions, custom components, maps and galleries wait for it. What the page shows is read again whenever the browser tab or window regains focus, as are the lists, unless the record has unsaved changes, which keep the version they were made to.
|
|
673
673
|
|
|
674
|
+
### Page width
|
|
675
|
+
|
|
676
|
+
The record page sits in a centred column about 1100px wide. `.recordWidth('full')` makes it fill the content pane beside the sidebar instead, with the sidebar cards still on the right, for a record with wide sections or related tables; `'column'` is the default. On a phone both look the same.
|
|
677
|
+
|
|
678
|
+
```ts
|
|
679
|
+
view<typeof objectives>('objectives').recordWidth('full')
|
|
680
|
+
```
|
|
681
|
+
|
|
674
682
|
### Full-area record pages
|
|
675
683
|
|
|
676
684
|
`.record({ component })` makes the record page at `/<resource>/<key>` a [custom component](custom-components.md#full-area-record-pages) of the app's own, for a record that is used rather than filled in, such as a chat channel. The component fills the content area, at full width beside the sidebar and down to the bottom bar on a phone, and the page does not scroll, so the component scrolls its own parts, such as messages above a box that stays at the bottom. The sidebar, the top bar with its breadcrumb naming the record, the bottom bar and a right panel stay; the heading, Save, the action rail, the sections, the related records and the Details card are not drawn. A record that does not exist, or that the user may not read, shows the record page's usual notice, and the component is never drawn.
|