@protobase/cli 0.66.14 → 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.
@@ -1,4 +1,4 @@
1
1
  //#region package.json
2
- var version = "0.66.14";
2
+ var version = "0.66.15";
3
3
  //#endregion
4
4
  export { version };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@protobase/cli",
3
- "version": "0.66.14",
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.14",
47
- "@protobase/layout": "0.66.14",
48
- "@protobase/query": "0.66.14",
49
- "@protobase/schema": "0.66.14",
50
- "@protobase/server": "0.66.14",
51
- "@protobase/ui": "0.66.14",
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.
@@ -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 full-text search, lists of values and relative dates.
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): Reads that would scan a large table without an index are refused, with the index to create.
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
- - **`waitUntil(promise)`** from `@protobase/server` takes the place of `EdgeRuntime.waitUntil`. The response goes out at once, and the process waits for the work when it shuts down gracefully. Work still running when the host kills the process is lost. 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).
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; 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).
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. <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>
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.
@@ -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.