@protobase/cli 0.66.7 → 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.
- package/dist/cli/package.js +1 -1
- package/dist/dev/entry.js +1 -1
- package/dist/dev/run.js +2 -1
- package/dist/main.js +3 -2
- package/package.json +7 -7
- package/skills/protobase/references/features/ai.md +10 -7
- package/skills/protobase/references/features/build-and-run.md +1 -0
- package/skills/protobase/references/features/compatibility.md +6 -6
- package/skills/protobase/references/features/data-model.md +1 -1
- package/skills/protobase/references/features/email-and-public-forms.md +2 -0
- package/skills/protobase/references/features/events-webhooks-and-jobs.md +2 -0
- package/skills/protobase/references/features/files-and-images.md +1 -0
- package/skills/protobase/references/features/look-and-language.md +1 -0
- package/skills/protobase/references/features/navigation.md +5 -4
- package/skills/protobase/references/features/sign-in-and-accounts.md +6 -0
- package/skills/protobase/references/features/your-own-code.md +4 -2
- package/skills/protobase/references/getting-started.md +8 -2
- package/skills/protobase/references/guides/api-functions.md +2 -2
- package/skills/protobase/references/reference/api.md +1 -1
- package/skills/protobase/references/reference/app-identity.md +1 -1
- package/skills/protobase/references/reference/assistant.md +22 -2
- package/skills/protobase/references/reference/auth.md +33 -6
- package/skills/protobase/references/reference/cli.md +7 -3
- package/skills/protobase/references/reference/client.md +1 -1
- package/skills/protobase/references/reference/comments.md +1 -1
- package/skills/protobase/references/reference/custom-components.md +7 -1
- package/skills/protobase/references/reference/data-config.md +1 -1
- package/skills/protobase/references/reference/email.md +1 -1
- package/skills/protobase/references/reference/files.md +4 -0
- package/skills/protobase/references/reference/jobs.md +3 -1
- package/skills/protobase/references/reference/languages.md +2 -2
- package/skills/protobase/references/reference/mcp.md +21 -1
- package/skills/protobase/references/reference/plugins.md +6 -4
- package/skills/protobase/references/reference/sidebar.md +6 -0
- package/skills/protobase/references/reference/supabase-realtime.md +5 -1
- package/skills/protobase/references/reference/supabase-rest.md +5 -1
- package/skills/protobase/references/reference/supabase-storage.md +5 -1
- package/skills/protobase/references/reference/theme.md +1 -1
- package/skills/protobase/references/reference/ui-config.md +6 -2
- package/skills/protobase/references/reference/versioning.md +2 -0
package/dist/cli/package.js
CHANGED
package/dist/dev/entry.js
CHANGED
|
@@ -55,7 +55,7 @@ const ui = {
|
|
|
55
55
|
icons: projectIcons(projectDir)
|
|
56
56
|
};
|
|
57
57
|
const { admin, jobs } = projectAdmin(project, db, ui, reporter, upgradeWebSocket);
|
|
58
|
-
await devJobs(db, jobs, admin.decryptField, project.mailer);
|
|
58
|
+
if (process.env.PROTOBASE_DEV_JOBS !== "false") await devJobs(db, jobs, admin.decryptField, project.mailer);
|
|
59
59
|
devRealtimePrune(admin);
|
|
60
60
|
var entry_default = withDevServiceWorker(new Hono(), Boolean(project.notifications)).route("/", admin);
|
|
61
61
|
/** Whether the auth store has a user yet, for the banner's sign-in line; undefined without one. */
|
package/dist/dev/run.js
CHANGED
|
@@ -25,6 +25,7 @@ const runDev = async (options, out) => {
|
|
|
25
25
|
process.env.PROTOBASE_PROJECT = options.projectDir;
|
|
26
26
|
process.env.PROTOBASE_EXTENSIONS = JSON.stringify(extensions);
|
|
27
27
|
process.env.PROTOBASE_DATABASE_URL = url;
|
|
28
|
+
process.env.PROTOBASE_DEV_JOBS = String(options.jobs ?? true);
|
|
28
29
|
const onApiExit = (code) => {
|
|
29
30
|
process.stderr.write(`error: the API process of protobase dev ended with exit code ${code}\n`);
|
|
30
31
|
server.close().then(() => process.exit(code || 1));
|
|
@@ -63,7 +64,7 @@ const runDev = async (options, out) => {
|
|
|
63
64
|
const started = await apiChild.start(`http://localhost:${port}`).catch(closeAndThrow);
|
|
64
65
|
apiReady(started);
|
|
65
66
|
const { anyUsers, authDrift } = started;
|
|
66
|
-
out(`protobase dev ready at http://localhost:${port}\n project ${options.projectDir}\n api http://localhost:${port}/api/v1 (docs at /api/docs)\n` + loginLine(anyUsers, process.env.npm_config_user_agent) + authDriftLines(authDrift));
|
|
67
|
+
out(`protobase dev ready at http://localhost:${port}\n project ${options.projectDir}\n api http://localhost:${port}/api/v1 (docs at /api/docs)\n` + (options.jobs === false ? " jobs off: no jobs run and no events are delivered (--no-jobs)\n" : "") + loginLine(anyUsers, process.env.npm_config_user_agent) + authDriftLines(authDrift));
|
|
67
68
|
return server;
|
|
68
69
|
};
|
|
69
70
|
//#endregion
|
package/dist/main.js
CHANGED
|
@@ -60,7 +60,7 @@ program.command("doctor").description("Check a config folder against the databas
|
|
|
60
60
|
}, out);
|
|
61
61
|
if (errors > 0) process.exitCode = 1;
|
|
62
62
|
});
|
|
63
|
-
program.command("dev").description("Serve the admin app and API for the project in the current directory").option("--port <port>", "port for the app and API", "5173").option("--env <variable>", "environment variable holding the database URL", "DATABASE_URL").option("--allowed-hosts <hosts>", "extra Host headers to accept, comma separated; a leading dot matches subdomains (.trycloudflare.com)").option("--cache-dir <dir>", "Vite's dependency cache, so parallel dev servers do not share one").option("--extend <dir>", "merge the protobase.config.ts and protobase.ui.tsx in this folder before the project's, repeatable", collect, []).action(async (opts) => {
|
|
63
|
+
program.command("dev").description("Serve the admin app and API for the project in the current directory").option("--port <port>", "port for the app and API", "5173").option("--env <variable>", "environment variable holding the database URL", "DATABASE_URL").option("--allowed-hosts <hosts>", "extra Host headers to accept, comma separated; a leading dot matches subdomains (.trycloudflare.com)").option("--cache-dir <dir>", "Vite's dependency cache, so parallel dev servers do not share one").option("--extend <dir>", "merge the protobase.config.ts and protobase.ui.tsx in this folder before the project's, repeatable", collect, []).option("--no-jobs", "run none of the app's jobs and deliver none of its events, such as against its hosted database").action(async (opts) => {
|
|
64
64
|
const [{ runDev }, { resolveExtensions }, { writeProjectTypes }] = await Promise.all([
|
|
65
65
|
import("./dev/run.js"),
|
|
66
66
|
import("./project/extensions.js"),
|
|
@@ -73,7 +73,8 @@ program.command("dev").description("Serve the admin app and API for the project
|
|
|
73
73
|
envVar: opts.env,
|
|
74
74
|
projectDir: process.cwd(),
|
|
75
75
|
cacheDir: opts.cacheDir && path.resolve(opts.cacheDir),
|
|
76
|
-
allowedHosts: parseHosts(opts.allowedHosts)
|
|
76
|
+
allowedHosts: parseHosts(opts.allowedHosts),
|
|
77
|
+
jobs: opts.jobs
|
|
77
78
|
}, out);
|
|
78
79
|
warn(placeholderSecretWarning(process.env));
|
|
79
80
|
process.once("SIGINT", () => void server.close().then(() => process.exit(0)));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@protobase/cli",
|
|
3
|
-
"version": "0.66.
|
|
3
|
+
"version": "0.66.8",
|
|
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.8",
|
|
47
|
+
"@protobase/layout": "0.66.8",
|
|
48
|
+
"@protobase/query": "0.66.8",
|
|
49
|
+
"@protobase/schema": "0.66.8",
|
|
50
|
+
"@protobase/server": "0.66.8",
|
|
51
|
+
"@protobase/ui": "0.66.8",
|
|
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",
|
|
@@ -6,16 +6,19 @@ Fields and actions filled by a model, the assistant beside the admin, and MCP.
|
|
|
6
6
|
|
|
7
7
|
- [AI fields](../reference/ai.md#ai-fields): A model fills a field from a prompt that uses the record's other fields, after each change.
|
|
8
8
|
- [AI actions](../reference/ai.md#ai-actions): A button asks a model to write a field now, such as a suggested priority or a translation.
|
|
9
|
-
- [Assistant](../reference/assistant.md): A chat beside the admin that answers questions about the app, reads its data and proposes changes for approval.
|
|
9
|
+
- [Assistant](../reference/assistant.md) (beta): A chat beside the admin that answers questions about the app, reads its data and proposes changes for approval.
|
|
10
10
|
- [Choice of model](../reference/assistant.md#which-backend): The assistant runs on OpenRouter or any OpenAI-compatible endpoint, also a local one.
|
|
11
|
-
- [Assistant tools](../reference/assistant.md#the-apps-own-tools): Your own tools for the assistant, acting on records as the user.
|
|
12
|
-
- [
|
|
11
|
+
- [Assistant tools](../reference/assistant.md#the-apps-own-tools) (beta): Your own tools for the assistant, acting on records as the user.
|
|
12
|
+
- [Assistant widgets](../reference/assistant.md#widget-parts): Your own React components in the chat, showing a record as it is now and taking decisions on it, from last month's chat too.
|
|
13
|
+
- [Assistant backend of your own](../reference/assistant.md#another-backend): The chat beside the admin talks to a backend elsewhere, with its own prompt and tools, built from the same pieces as the built-in one.
|
|
14
|
+
- [MCP server](../reference/mcp.md#the-tools) (beta): AI clients connect to the app over MCP as the signed-in person: they list the resources in a small first call, describe one resource's fields when they need them, and find and read records within that person's access, soft-deleted ones too when asked.
|
|
13
15
|
- [MCP read fields](../reference/mcp.md#reading-only-what-is-needed): AI clients read only the fields they name, a plain list returns the columns of the app's table, and the server tells clients how to keep reads small.
|
|
14
|
-
- [MCP counts](../reference/mcp.md#the-tools): AI clients count the records a filter finds, in all or per value of a field such as a status, without reading the records.
|
|
15
|
-
- [MCP app icon](../reference/mcp.md#the-apps-icon): AI clients show the app's own icon beside its name when someone adds its MCP endpoint.
|
|
16
|
-
- [MCP
|
|
16
|
+
- [MCP counts](../reference/mcp.md#the-tools) (beta): AI clients count the records a filter finds, in all or per value of a field such as a status, without reading the records.
|
|
17
|
+
- [MCP app icon](../reference/mcp.md#the-apps-icon) (beta): AI clients show the app's own icon beside its name when someone adds its MCP endpoint.
|
|
18
|
+
- [MCP SQL for admins](../reference/mcp.md#sql-for-admins): People with the `admin` or `ai` role run read-only SQL over MCP when you turn it on.
|
|
19
|
+
- [MCP writes](../reference/mcp.md#writes) (beta): When turned on, for every resource or only those you list, AI clients create, update and delete records after the person approves each write.
|
|
17
20
|
- [Small MCP write results](../reference/mcp.md#what-a-write-returns): A write over MCP returns the record's key, new ETag, the fields written and the read-only fields that changed, not the whole record; `fields` asks for more.
|
|
18
|
-
- [MCP writes without approval](../reference/mcp.md#writes-without-approval): Roles you name, such as service accounts, write over MCP without being asked to approve each write; everyone else is still asked.
|
|
21
|
+
- [MCP writes without approval](../reference/mcp.md#writes-without-approval) (beta): Roles you name, such as service accounts, write over MCP without being asked to approve each write; everyone else is still asked.
|
|
19
22
|
- [MCP comments](../reference/mcp.md#comments): AI clients read a record's comments, and with MCP writes on add one after the person approves it.
|
|
20
23
|
- [MCP batch writes](../reference/mcp.md#batch-writes): When MCP writes are on, AI clients make many creates, updates and deletes across resources in one transaction, after the person approves the batch.
|
|
21
24
|
- [Connected apps](../reference/auth.md#oauth-for-mcp-clients): People allow an AI client to sign in, and disconnect it later.
|
|
@@ -6,6 +6,7 @@ Develop, build and run an app, on servers, in containers and on phones.
|
|
|
6
6
|
|
|
7
7
|
- [Node and Bun](../reference/cli.md#how-it-runs): The `protobase` command runs on Node 22.12 or later or on Bun, and a bundle runs on both unless it is built for Bun only.
|
|
8
8
|
- [Dev server](../reference/cli.md#dev): The admin app and the API on one port, with changes shown without a restart.
|
|
9
|
+
- [Dev server without jobs](../reference/cli.md#dev): `protobase dev --no-jobs` runs no jobs and delivers no events, webhooks or pushes, to develop against a hosted app's database without doing its work twice.
|
|
9
10
|
- [Editor types](../reference/cli.md#types): `protobase types` gives the editor and `tsc` the types of the CLI that builds the app, with nothing installed.
|
|
10
11
|
- [Project scripts](../reference/cli.md#run): `protobase run` runs a seed or a migration script on the CLI's packages.
|
|
11
12
|
- [Migrate command](../reference/cli.md#migrate): `protobase migrate` applies the project's SQL migrations in one transaction, with nothing installed.
|
|
@@ -6,10 +6,10 @@ Apps written for other systems that keep working on Protobase, and moving their
|
|
|
6
6
|
|
|
7
7
|
## Supabase
|
|
8
8
|
|
|
9
|
-
- [Supabase import](../reference/cli.md#import-supabase): Moves a Supabase project into Protobase, with schema, data, users, functions, storage and scheduled jobs.
|
|
10
|
-
- [Create a project on a Supabase database](../getting-started.md#on-
|
|
11
|
-
- [Supabase client API](../reference/supabase-rest.md): supabase-js `from()` and `rpc()` calls keep working, under the tables' RLS policies.
|
|
12
|
-
- [Supabase Auth](../reference/auth.md#supabase-js-and-supabasessr): `supabase.auth` and `@supabase/ssr` calls keep working.
|
|
13
|
-
- [Supabase Storage](../reference/supabase-storage.md): supabase-js storage calls keep working, over the app's file storage.
|
|
14
|
-
- [Supabase Realtime](../reference/supabase-realtime.md): Database changes, broadcast and presence through supabase-js channels, private ones authorized by the policies on `realtime.messages`, broadcasts sent from a server without joining a channel, and broadcasts from the database with `realtime.send()` and `realtime.broadcast_changes()`, across [several processes](../reference/supabase-realtime.md#several-processes) serving one app.
|
|
9
|
+
- [Supabase import](../reference/cli.md#import-supabase) (beta): Moves a Supabase project into Protobase, with schema, data, users, functions, storage and scheduled jobs, and a coding agent's skill finishes what its report leaves.
|
|
10
|
+
- [Create a project on a Supabase database](../getting-started.md#on-a-supabase-database) (beta): your Supabase users sign in to the new project with their own passwords, and Supabase's own auth schema stays as Supabase set it up.
|
|
11
|
+
- [Supabase client API](../reference/supabase-rest.md) (beta): supabase-js `from()` and `rpc()` calls keep working, under the tables' RLS policies.
|
|
12
|
+
- [Supabase Auth](../reference/auth.md#supabase-js-and-supabasessr) (beta): `supabase.auth` and `@supabase/ssr` calls keep working.
|
|
13
|
+
- [Supabase Storage](../reference/supabase-storage.md) (beta): supabase-js storage calls keep working, over the app's file storage.
|
|
14
|
+
- [Supabase Realtime](../reference/supabase-realtime.md) (beta): Database changes, broadcast and presence through supabase-js channels, private ones authorized by the policies on `realtime.messages`, broadcasts sent from a server without joining a channel, and broadcasts from the database with `realtime.send()` and `realtime.broadcast_changes()`, across [several processes](../reference/supabase-realtime.md#several-processes) serving one app.
|
|
15
15
|
- [Edge Functions](../guides/api-functions.md#functions-moved-from-supabase): Functions moved from Supabase answer at their old addresses.
|
|
@@ -18,7 +18,7 @@ Describe the tables of an existing Postgres database as resources, with their fi
|
|
|
18
18
|
- [Record validation](../reference/data-config.md#resource): A rule over the whole record runs on every create and update and reports each problem on the field it concerns.
|
|
19
19
|
- [Rollups and formulas](../reference/data-config.md#rollups-and-formulas) (beta): Fields that hold counts, sums, averages, minimums or maximums of related records, or values calculated from the record's own numbers, kept current by the database.
|
|
20
20
|
- [Sensitive fields](../reference/data-config.md#sensitive-fields): Values such as bank account numbers are left out of every response and shown masked. A permitted user reveals one value at a time, and each reveal is audited.
|
|
21
|
-
- [Recent strong sign-in](../reference/data-config.md#recent-strong-sign-in): A sensitive field can ask for a passkey or a second step in the last minutes before it is revealed. The record page asks the person to confirm it's them, then shows it.
|
|
21
|
+
- [Recent strong sign-in](../reference/data-config.md#recent-strong-sign-in): A sensitive field can ask for a passkey or a second step in the last minutes before it is revealed, or an emailed code for an account with neither. The record page asks the person to confirm it's them, then shows it.
|
|
22
22
|
- [Computed reveals](../reference/data-config.md#computed-reveals) (beta): A secret kept elsewhere, or built from several values, is revealed like a sensitive field, with the same access, audit and recent sign-in check, but made when it is asked for.
|
|
23
23
|
- [Encrypted fields](../reference/data-config.md#encrypted-fields): A sensitive text field is encrypted with the app's own key before it is stored, so dumps, backups and SQL readers see only ciphertext. A command encrypts the values a column already holds.
|
|
24
24
|
- [Encrypted fields in jobs and functions](../reference/data-config.md#reading-in-jobs-and-functions) (beta): A job or an API function reads an encrypted value the app must use itself, such as a share's password, and each read is audited like a reveal.
|
|
@@ -7,6 +7,8 @@ Mail the app sends, the PDF documents it makes, and forms for people without an
|
|
|
7
7
|
- [Email](../reference/email.md#configuration): The app sends mail through one SMTP server or a mailer you supply.
|
|
8
8
|
- [Built-in emails](../reference/email.md#protobases-own-mail): Sign-in codes, password resets, invitations and mentions are sent in the app's name.
|
|
9
9
|
- [Email templates](../reference/email.md#templates): Your own emails are React components with typed data, sent from jobs, hooks or functions.
|
|
10
|
+
- [Protobase's mail look](../reference/email.md#the-look-of-protobases-own-mail): Your emails use the layout, button, code and note that Protobase's own mail is built from.
|
|
11
|
+
- [Unsubscribe headers](../reference/email.md#templates): Your emails carry header fields of their own, such as a one-click unsubscribe link for bulk mail.
|
|
10
12
|
- [Attachments](../reference/email.md#attachments): A message carries files, such as an invoice's PDF, through the app's mail or someone's own mailbox.
|
|
11
13
|
- [PDF documents](../reference/pdf.md#documents): PDF files such as invoices and quotes are React components with typed data, rendered from jobs, hooks or functions and attached to mail as they are.
|
|
12
14
|
- [Mail from your own mailbox](../reference/email.md#from-someones-own-mailbox) (beta): People link their Gmail on the account page, and the app's functions send mail from it, with a clear error when the link is missing or expired.
|
|
@@ -10,10 +10,12 @@ React to changes, deliver them to other systems, and run work in the background.
|
|
|
10
10
|
- [Signed webhooks](../reference/webhooks.md#what-a-receiver-gets): Deliveries are signed under the Standard Webhooks spec.
|
|
11
11
|
- [Delivery retries](../reference/webhooks.md#delivery-and-retries): Failed deliveries are retried for about three days.
|
|
12
12
|
- [Webhooks in the admin app](../reference/webhooks.md#subscriptions-in-the-admin-app): Users create subscriptions, send test events and resend failures in the app.
|
|
13
|
+
- [Webhook delivery log](../reference/webhooks.md#subscriptions-in-the-admin-app): Every try of the admin app's subscriptions, successes too, is kept for 30 days with its status and timing, when the app turns it on.
|
|
13
14
|
- [Background jobs](../reference/jobs.md#enqueueing): Jobs are kept in the app's own Postgres and can be queued in the same transaction as a write, also from SQL.
|
|
14
15
|
- [Typed jobs](../reference/jobs.md#typed-names-and-payloads): Code that queues a job, and the job itself, are checked against the names and arguments the config declares as they compile.
|
|
15
16
|
- [Retries](../reference/jobs.md#failures): Failed jobs are retried with growing waits, and duplicates can be prevented with a key.
|
|
16
17
|
- [Ordered groups](../reference/jobs.md#ordered-groups): Jobs in one group run one at a time, in order.
|
|
17
18
|
- [Recurring jobs](../reference/jobs.md#recurring-jobs): Jobs run on a cron schedule in a chosen time zone.
|
|
18
19
|
- [Failed jobs page](../reference/jobs.md#failed-jobs-page): Admins see jobs that failed for good and retry or delete them.
|
|
20
|
+
- [Worker process](../reference/jobs.md#where-jobs-run): Jobs and event deliveries run beside the API, or in a worker process of their own from the same bundle.
|
|
19
21
|
- [Jobs on hosts that sleep](../reference/jobs.md#hosts-that-sleep): A host that stops idle apps can wake them when a job is due.
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
Uploaded files and images, where they are kept and how they are served.
|
|
6
6
|
|
|
7
7
|
- [File fields](../reference/files.md#what-the-column-holds): A text column holds an uploaded file, with limits on type and size.
|
|
8
|
+
- [File uploads in forms](../reference/files.md#in-forms): Forms upload a dropped or chosen file at once with a progress bar, and replace or remove it; tables show its name and a thumbnail.
|
|
8
9
|
- [File storage](../reference/files.md#providers): Files are kept on disk, or in a bucket of your own.
|
|
9
10
|
- [S3-compatible buckets](../reference/files.md#s3-compatible-buckets) (beta): Files are kept in any bucket that speaks the S3 API, when the app runs on Bun.
|
|
10
11
|
- [Files from a seed](../reference/files.md#writing-files-from-a-seed): A seed or another script writes files into the app's providers, read from its config and environment as the server reads them.
|
|
@@ -8,6 +8,7 @@ The app's colours, name, icon, language and installation.
|
|
|
8
8
|
- [Dark mode](../reference/theme.md#light-dark-or-the-system): Each user picks light, dark or the system setting.
|
|
9
9
|
- [App name, icon and colours](../reference/app-identity.md): The app's own name, icon and colours in the sidebar, browser tabs, `/favicon.ico`, home-screen icon and mail.
|
|
10
10
|
- [Logo and favicon](../reference/theme.md#logo-and-favicon): Your own favicon or logo replaces Protobase's.
|
|
11
|
+
- [Named icons](../reference/layouts.md#named-actions): Actions, key facts and card meta show an icon picked by name from a set, such as `play`, `bed` or `cpu`.
|
|
11
12
|
- [English and Dutch](../reference/languages.md): The built-in screens come in English and Dutch, with numbers and dates written to match.
|
|
12
13
|
- [Installable app](../reference/installable-app.md): The admin installs as an app on phones and computers, and says when the server cannot be reached.
|
|
13
14
|
- Reduced motion: When the system asks for less motion, animations and pulsing dots stop.
|
|
@@ -4,14 +4,15 @@
|
|
|
4
4
|
|
|
5
5
|
The sidebar, the user menu, the bottom bar on phones and the global search.
|
|
6
6
|
|
|
7
|
-
- [Sidebar](../reference/sidebar.md): Resources and pages in groups, in the order you set.
|
|
7
|
+
- [Sidebar](../reference/sidebar.md): Resources and pages in groups, in the order you set, and a drawer on phones.
|
|
8
8
|
- [Sidebar icons](../reference/sidebar.md#sidebar-icons): Each resource shows the icon its view names in the sidebar, bottom bar and profile menu.
|
|
9
|
-
- [Recent records](../reference/sidebar.md#recent-records): A sidebar entry lists a few records under it, with status dots or as titles alone, and marked per reader, such as unread ones in bold with a count.
|
|
9
|
+
- [Recent records](../reference/sidebar.md#recent-records) (beta): A sidebar entry lists a few records under it, with status dots or as titles alone, and marked per reader, such as unread ones in bold with a count.
|
|
10
10
|
- [Create link](../reference/sidebar.md#create-link): A sidebar entry links to its create form, such as "New message" under Direct messages, for whoever may create.
|
|
11
11
|
- [The current organization](../reference/sidebar.md#the-current-organization): A sidebar entry opens its list on the organization you work in, which you can clear to see every organization you are a member of.
|
|
12
12
|
- [User menu](../reference/user-menu.md): Account pages, help links, security settings, setting a status, the theme switch and Sign out.
|
|
13
13
|
- [Bottom bar](../reference/bottom-bar.md): On phones, tabs along the bottom for each role's main screens.
|
|
14
14
|
- [Global search](../reference/client.md#global-search): A search box, also on Cmd+K or Ctrl+K, finds records in every resource and jumps to pages and New forms.
|
|
15
|
-
- Breadcrumbs: The top bar shows the group, list and record you are on, each a link back.
|
|
16
|
-
-
|
|
15
|
+
- [Breadcrumbs](../reference/ui-config.md#breadcrumbs): The top bar shows the group, list and record you are on, each a link back, and on a phone a back button to the one before.
|
|
16
|
+
- [Tab titles](../reference/ui-config.md#names-and-columns): Browser tabs name the record, list or page they show, then the app.
|
|
17
|
+
- [Update notice](../reference/versioning.md#a-new-version-under-an-open-page): When the server runs a newer Protobase version, open pages offer a reload so the user can save their work first.
|
|
17
18
|
- Config refresh: When the config or the user's roles change, open pages load the new menus, columns and permissions without a reload.
|
|
@@ -5,11 +5,13 @@
|
|
|
5
5
|
Sign-in methods, users, roles, organizations and tokens.
|
|
6
6
|
|
|
7
7
|
- [Built-in sign-in](../reference/auth.md): Password sign-in, sessions and short-lived tokens, with no separate identity service.
|
|
8
|
+
- [Sign-in page](../reference/client.md#the-admin-app): The app signs people in at its own address, where the browser's Back returns to the step before, and signing in comes back to the page they were on.
|
|
8
9
|
- [First admin](../reference/auth.md#first-run): No account can be made over the web while the app has no users. You create the first admin from the terminal, and the last active admin cannot be deleted, demoted or blocked.
|
|
9
10
|
- [Users from a script](../reference/auth.md#first-run): A seed or another script of the project creates and lists users, and mints their tokens, on the app's own sign-in.
|
|
10
11
|
- [Versioned sign-in tables](../reference/auth.md#migrations): The sign-in tables come from numbered migrations that ship with Protobase, written as the project's own and recorded in the database, so a deploy checks them without comparing tables. Every feature has its tables from the start, so turning one on or off needs no migration.
|
|
11
12
|
- [Existing users table](../reference/auth.md#an-existing-users-table): Sign-in can use the users table another system keeps in the same database.
|
|
12
13
|
- [Users page](../reference/auth.md#users-page): Admins find people, set roles, block them, add accounts and invite people by email.
|
|
14
|
+
- [Account page](../reference/auth.md#account-page): Everyone signed in has an account page for their profile, sign-in and security, devices and connected apps, and you add, hide or reorder its tabs.
|
|
13
15
|
- [Password reset](../reference/auth.md#password-reset): People reset a forgotten password through an emailed link.
|
|
14
16
|
- [Password changes](../reference/auth.md#name-password-and-devices): Changing the password signs out every other device and MCP client, and emails the account that its password changed. Guesses at the current password are limited for each account.
|
|
15
17
|
- [Leaked passwords](../reference/auth.md#leaked-passwords): New passwords that appear in a known data breach are refused, when the app turns the check on.
|
|
@@ -19,8 +21,12 @@ Sign-in methods, users, roles, organizations and tokens.
|
|
|
19
21
|
- [Open sign-up](../reference/auth.md#open-sign-up): Anyone can create an account after confirming an emailed code.
|
|
20
22
|
- [Passkeys](../reference/auth.md#passkeys): People sign in with their device's face or fingerprint unlock, or with a security key.
|
|
21
23
|
- [Two-factor authentication](../reference/auth.md#two-factor-authentication): A second step with an authenticator app, backup codes or emailed codes.
|
|
24
|
+
- [Emailed confirmation code](../reference/auth.md#emailed-confirmation-code): Someone without a passkey or a second step confirms it's them with a code emailed on request, where they are, such as before a secret is shown.
|
|
25
|
+
- [Confirming an account change](../reference/auth.md#confirming-an-account-change): Adding a passkey, unlinking a provider or deleting the account from an older session asks to confirm it's them in place, never to sign out and sign in again.
|
|
22
26
|
- [Sign-in policy](../reference/auth.md#sign-in-policy): Admins choose which sign-in methods are on, off or required, and the server refuses a policy that would lock people out.
|
|
23
27
|
- [GitHub and social sign-in](../reference/auth.md#sign-in-with-github): People sign in with GitHub or another social provider.
|
|
28
|
+
- [Sign-in links](../reference/auth.md#sign-in-links-and-connected-accounts): A link signs someone in with a sign-in provider at once, without the sign-in page.
|
|
29
|
+
- [Connected accounts](../reference/auth.md#sign-in-links-and-connected-accounts): People link GitHub or another sign-in provider to their account from the account page.
|
|
24
30
|
- [SAML sign-in](../reference/auth.md#sign-in-with-saml): People sign in through any SAML provider.
|
|
25
31
|
- [OpenID Connect provider](../reference/auth.md#platform-sign-in-provider): A host adds an OpenID Connect provider for people who already have an account.
|
|
26
32
|
- [Staff sign-in](../reference/auth.md#staff-sign-in): The team that runs an app signs in as a person to help them, with a reason and a log entry.
|
|
@@ -11,11 +11,13 @@ React components, field types, pages and handlers of your own, loaded as plugins
|
|
|
11
11
|
- [Custom field types](../reference/plugins.md#field-types): Register how a kind of value is shown and edited, and pick it per field in a view.
|
|
12
12
|
- [Custom create pages](../reference/custom-components.md#create-pages): Replace a resource's create form with your own page, such as a booking flow.
|
|
13
13
|
- [Form parts](../reference/custom-components.md#form-parts): Build your own forms from the app's fields, pickers, switches, buttons and dialogs.
|
|
14
|
+
- [Markdown](../reference/custom-components.md#components): Show markdown in your own components, such as a chat message, as the app's markdown fields draw it.
|
|
15
|
+
- [Polling lists](../reference/custom-components.md#record-sections): A list your own component reads can refetch on an interval you set, to stay current.
|
|
14
16
|
- [Image carousel](../reference/custom-components.md#image-carousel): Step through screenshots or a record's files in your own components by swiping or with arrows, as cards step through photos.
|
|
15
17
|
- [Assistant parts](../reference/assistant.md#in-the-ui): The assistant's panel, chat messages, composer and action cards for your own components, with a frameless action card for a page that is already framed.
|
|
16
18
|
- [Chat parts](../reference/custom-components.md#chat-parts): A chat between people from the app's parts: messages with their author, avatar, time and a status line, day and new-message lines, a thread that loads earlier messages, keeps the reader's place, opens on the new-message line, says when the reader has read to the end and jumps to the end when they send, and a composer of several lines with @mentions and slots for an attach button and a tray of picked files, which sends files without text and takes pasted and dropped files.
|
|
17
|
-
- [Members' names](../reference/custom-components.md#members-names): The names and photos of the organization's members by user id, for message authors and @mentions in your own components; every member reads them, without email addresses.
|
|
18
|
-
- [Signed-in user](../reference/custom-components.md#
|
|
19
|
+
- [Members' names](../reference/custom-components.md#members-names) (beta): The names and photos of the organization's members by user id, for message authors and @mentions in your own components; every member reads them, without email addresses.
|
|
20
|
+
- [Signed-in user](../reference/custom-components.md#signed-in-user): The signed-in person's user id and name in your own components, such as for a filter on their own messages.
|
|
19
21
|
- [Action handlers](../reference/custom-components.md#action-handlers): Your own code for a named action, with the record, the API client and the action's inputs.
|
|
20
22
|
- [Shell slots](../reference/custom-components.md#shell-slots): Your own components in the top bar, in a panel beside every page, or as the sidebar logo.
|
|
21
23
|
- [Plugins](../reference/plugins.md#how-a-plugin-stays-working): Your React code is a plugin that the app loads at start and that keeps working across Protobase releases without changes.
|
|
@@ -61,8 +61,6 @@ Then it runs that `scaffold` in the project and prints the next steps. Without a
|
|
|
61
61
|
|
|
62
62
|
It never writes to the database. The first of the next steps does: `bun run db:migrate` adds the `auth` schema that sign-in keeps its users in, and the `migrations` schema, where the script records the migrations it applied in `migrations.applied`. A `public.schema_migrations` another migration tool keeps is left as it is.
|
|
63
63
|
|
|
64
|
-
On a Supabase database, which it knows by its `auth` and `storage` schemas and its `supabase_admin` role, as [`import supabase`](reference/cli.md#import-supabase) does, the `auth` schema is Supabase Auth's, and the project changes nothing in it. It leaves out `auth`, `storage` and the platform's other schemas, such as `realtime` and `supabase_migrations`, as Supabase's own tables. The project reads Supabase's users through a view over `auth.users` and keeps only its own sign-in data, in a schema of its own, `app_auth`: it points `protobase.config.ts` at the view, with [`users.keyTable`](reference/auth.md#a-view-over-another-systems-users), and writes migrations in place of those of Protobase's own user table: Better Auth's tables in `app_auth`, pointing at `auth.users`, then the view, its side table of the columns only Better Auth has, and each user's account with their Supabase password and social providers. `bun run db:migrate` applies them as the `postgres` role a Supabase connection string signs in as, and Supabase's users sign in with their own passwords, under their own ids. You can create, disable and delete users from either side, and a name changed in Supabase shows at once. <gap>A user who signs up through Supabase Auth later is a user of the project at once but cannot sign in with a password because sharing passwords with Supabase Auth is not implemented.</gap> Supabase's phone-only and anonymous users are not users of the project, since sign-in needs an email. A ban set in the project stays the project's own, while a ban set in Supabase holds in the project too.
|
|
65
|
-
|
|
66
64
|
```sh
|
|
67
65
|
cd shop
|
|
68
66
|
bun run db:migrate # the first write to the database
|
|
@@ -70,6 +68,14 @@ bun run protobase users create you@example.com --generate-password
|
|
|
70
68
|
bun run dev # http://localhost:5173
|
|
71
69
|
```
|
|
72
70
|
|
|
71
|
+
#### On a Supabase database
|
|
72
|
+
|
|
73
|
+
:::caution[Beta]
|
|
74
|
+
A user who signs up through Supabase Auth later cannot sign in to the project with a password yet, as below.
|
|
75
|
+
:::
|
|
76
|
+
|
|
77
|
+
On a Supabase database, which it knows by its `auth` and `storage` schemas and its `supabase_admin` role, as [`import supabase`](reference/cli.md#import-supabase) does, the `auth` schema is Supabase Auth's, and the project changes nothing in it. It leaves out `auth`, `storage` and the platform's other schemas, such as `realtime` and `supabase_migrations`, as Supabase's own tables. The project reads Supabase's users through a view over `auth.users` and keeps only its own sign-in data, in a schema of its own, `app_auth`: it points `protobase.config.ts` at the view, with [`users.keyTable`](reference/auth.md#a-view-over-another-systems-users), and writes migrations in place of those of Protobase's own user table: Better Auth's tables in `app_auth`, pointing at `auth.users`, then the view, its side table of the columns only Better Auth has, and each user's account with their Supabase password and social providers. `bun run db:migrate` applies them as the `postgres` role a Supabase connection string signs in as, and Supabase's users sign in with their own passwords, under their own ids. You can create, disable and delete users from either side, and a name changed in Supabase shows at once. <gap>A user who signs up through Supabase Auth later is a user of the project at once but cannot sign in with a password because sharing passwords with Supabase Auth is not implemented.</gap> Supabase's phone-only and anonymous users are not users of the project, since sign-in needs an email. A ban set in the project stays the project's own, while a ban set in Supabase holds in the project too.
|
|
78
|
+
|
|
73
79
|
## Admin over an existing database
|
|
74
80
|
|
|
75
81
|
For a Postgres database you already have, `npm create protobase <database-url>` makes the project, as [above](#on-an-existing-database). [Admin over an existing database](guides/existing-database.md) does it by hand, in a folder of your own: install the packages, scaffold the config from the tables, check it with `doctor`, add sign-in and its tables, create the first admin and run `protobase dev`.
|
|
@@ -419,9 +419,9 @@ export default defineFunction({ longQueries: true }, async (request, { db }) =>
|
|
|
419
419
|
|
|
420
420
|
[`protobase import supabase`](../reference/cli.md#import-supabase) converts `supabase/functions` into `functions/`. A converted function keeps Supabase's shape, `defineFunction({ supabase: true }, { fetch })`, and keeps working with `@supabase/server` and supabase-js:
|
|
421
421
|
|
|
422
|
-
- **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
|
|
422
|
+
- **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
423
|
- **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
|
-
- **Supabase's variables:** `protobase dev` sets `SUPABASE_URL` to the app's address with `/api/sb`, where supabase-js finds it, `SUPABASE_ANON_KEY
|
|
424
|
+
- **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
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).
|
|
426
426
|
|
|
427
427
|
## What functions are not
|
|
@@ -115,7 +115,7 @@ Get responses carry `permissions: { update, delete, fields }`, where `fields` ma
|
|
|
115
115
|
|
|
116
116
|
A field declared [`.sensitive()`](data-config.md#sensitive-fields) is in `/meta` (marked `sensitive: true`), in record `permissions.fields` and in create and update bodies, but in no list, get, `:search`, `:seek`, write or batch response, and `filter`, `order_by`, `fields` and the aggregates refuse it like an unknown field. `POST /{resource}/{key}:reveal` with `{ "field": "<name>" }` returns one record's value. It needs read access to the record and the field: another tenant's record, a row outside the caller's row filter or a key that does not exist is a `404`, a field the caller cannot read or that is not sensitive a `400`. An [encrypted field](data-config.md#encrypted-fields)'s reveal decrypts its value, and an empty one reveals `null`; a value that does not decrypt is a `500` reported to the operator, without an audit event. An update that changes the key of a record with encrypted fields is a `400` `key-change-unsupported`.
|
|
117
117
|
|
|
118
|
-
A field with [`.strongSignIn()`](data-config.md#recent-strong-sign-in) is a `403` `strong-sign-in-required`, whose problem carries `minutes`, until the caller's sign-in proved a passkey or a second step that recently. The same request reveals a resource's [computed reveals](data-config.md#computed-reveals) by name, whose value the server makes when asked.
|
|
118
|
+
A field with [`.strongSignIn()`](data-config.md#recent-strong-sign-in) is a `403` `strong-sign-in-required`, whose problem carries `minutes`, until the caller's sign-in proved a passkey or a second step that recently, or, for an account with neither, an [emailed confirmation code](auth.md#emailed-confirmation-code). The same request reveals a resource's [computed reveals](data-config.md#computed-reveals) by name, whose value the server makes when asked.
|
|
119
119
|
|
|
120
120
|
Before answering, the server publishes an audit event to the app's audit queue, and when publishing fails the reveal fails (`500`), so no value leaves without its event. The event never holds the value:
|
|
121
121
|
|
|
@@ -30,7 +30,7 @@ export default defineConfig({
|
|
|
30
30
|
|
|
31
31
|
## The icons
|
|
32
32
|
|
|
33
|
-
`protobase.favicon.svg` is the one source of the app's icon: the tab shows it, the [web manifest](installable-app.md#the-manifest) names it, and with it the sidebar's logo draws it
|
|
33
|
+
`protobase.favicon.svg` is the one source of the app's icon: the tab shows it, the [web manifest](installable-app.md#the-manifest) names it, and with it the sidebar's logo draws it in place of Protobase's mark, beside the app's name. A project's own [`shell.logo`](custom-components.md#shell-slots) still wins.
|
|
34
34
|
|
|
35
35
|
Some platforms take no SVG, so a project may put PNGs of the same icon beside it, each square at its size:
|
|
36
36
|
|
|
@@ -4,6 +4,10 @@
|
|
|
4
4
|
|
|
5
5
|
The chat beside the admin, who sees it, and its built-in backend on OpenRouter or another OpenAI-compatible endpoint.
|
|
6
6
|
|
|
7
|
+
:::caution[Beta]
|
|
8
|
+
A user cannot keep several conversations yet: each has one, shared by their tabs, as [the app's own tools](#the-apps-own-tools) says.
|
|
9
|
+
:::
|
|
10
|
+
|
|
7
11
|
The admin app can show an assistant: an **Assistant** button at the end of the top bar opens a chat beside the page. The app only draws what a backend sends and sends back what the user types and clicks; the backend decides everything else. Protobase ships one backend, built in, which answers questions about the app, reads its data and proposes changes the user approves. Another backend can take its place by URL.
|
|
8
12
|
|
|
9
13
|
## Who sees it
|
|
@@ -66,6 +70,10 @@ Making the query role needs `CREATEROLE` once, for the role that applies the mig
|
|
|
66
70
|
|
|
67
71
|
### The app's own tools
|
|
68
72
|
|
|
73
|
+
:::caution[Beta]
|
|
74
|
+
A user cannot keep several conversations yet: each has one, shared by their tabs, as below.
|
|
75
|
+
:::
|
|
76
|
+
|
|
69
77
|
`assistant/tools.ts` adds tools of the app's own, which the built-in backend offers after its two: its default export is an array of tools, found by that path when the app starts. A tool is `defineTool({ name, description, parameters, run })` from `@protobase/server`: `parameters` is the JSON Schema of its arguments, and `run(args, context)` returns the text the model reads. A name must be 1 to 64 letters, digits, `_` or `-`, and a name taken twice stops the server at startup. A backend elsewhere has its own tools, so the file does nothing there, nor with `assistant: false`. A tool's `annotations` are MCP's tool hints (`readOnlyHint`, `destructiveHint`, ...), which the dock ignores. The file serves the dock only, not the app's MCP endpoint: tools for AI clients over MCP go in `mcp/tools.ts`, which can export these too (see [The app's own tools](mcp.md#the-apps-own-tools) on MCP).
|
|
70
78
|
|
|
71
79
|
`context` holds the user's `session`, `locale` (the language they read, `'en'` or `'nl'`, for the texts a tool shows them such as its approvals; set over MCP, absent in the dock), `show(part)` to add a part to the answer, `approve(request)` for an approval card, `conversation` (the dock's, absent over MCP), `resources()` (the resources the user may read, restricted to their fields, with their views), and `records`: `list(resource, params)` with the REST API's list parameters, `aggregate(resource, { group_by, filter?, limit?, label? })` with its counts per value as `:aggregate` gives them, `get(resource, key)`, `create(resource, data)`, `update(resource, key, data, { etag? })` (without an ETag it updates any version), `delete(resource, key, { etag? })` (soft on a resource with `.softDelete()`) and `softDeleteField(resource)` (the field a delete sets, or `undefined` when a delete removes the record) on the app's resources **as the user**, through their access rules, tenant scope, validation and write hooks, as in the REST API. `get`, `create`, `update` and `delete` each answer `{ record, etag }`, and `list` a page of records. `batch(operations)` runs the operations of a REST [batch write](api.md#batch-writes) in one transaction and answers its `results`; a refusal or a missing record rejects with an `HttpProblem`, whose `detail` a tool can return to the model as text, such as with `.catch(refused)`: `refused` from `@protobase/server` turns a refusal into `{ "error": <slug>, "message": <detail> }` and throws anything else again.
|
|
@@ -108,13 +116,25 @@ export const TaskProposal = ({ taskId }: { taskId: number }) => {
|
|
|
108
116
|
|
|
109
117
|
The tool and its component live apart, joined by the name, since the server config cannot import `@protobase/ui` and the browser must not get server code; put shared prop types in a file both import with `import type`. Whoever may update the task decides on it, now or next month, from the chat or the task's own page, and a write hook can stamp who did from `event.user`; when two decide at once, the ETag makes the second write fail and the widget shows the first decision. The chat is not told: the model learns of the change by reading the record again, which the description asks it to do.
|
|
110
118
|
|
|
111
|
-
Each message carries the page the user is on, such as `/orders/42`, which the model reads with it.
|
|
119
|
+
Each message carries the page the user is on, such as `/orders/42`, which the model reads with it. A message is at most 4,000 characters, and a turn takes at most 8 steps of tool calls, after which the answer ends with `Stopped after 8 steps.`
|
|
112
120
|
|
|
113
121
|
<gap>There is one conversation per user, shared by their tabs.</gap> The server keeps it as a file, `<chats>/<user>/chat.jsonl` (see `PROTOBASE_ASSISTANT_CHATS`), with the chat and everything the model read, so a restart keeps it and the app only ever sends the new message. The directory is made on the first message and must be writable: in a container, point it at a volume. With several instances, send each user's requests to the same one.
|
|
114
122
|
|
|
115
123
|
## Another backend
|
|
116
124
|
|
|
117
|
-
A backend elsewhere replaces the built-in one through `PROTOBASE_ASSISTANT_URL`. It receives the user's API token as `Authorization: Bearer <token>`, verifies it against the app's key set (`/api/auth/jwks`), and allows the app's origin with CORS. It can be built from the building blocks `@protobase/server` exports (the protocol's routes, the turn loop with tool calling, tools, approvals, the query tools and a conversation store
|
|
125
|
+
A backend elsewhere replaces the built-in one through `PROTOBASE_ASSISTANT_URL`. It receives the user's API token as `Authorization: Bearer <token>`, verifies it against the app's key set (`/api/auth/jwks`), and allows the app's origin with CORS. It can be built from the building blocks `@protobase/server` exports (the protocol's routes, `assistantProtocolRoutes`, the turn loop with tool calling, `runTurn`, tools, approvals, the query tools, `withCacheBreakpoints`, which marks a chat's messages for prompt caching, and `createConversations` over a conversation store, `fileConversationStore` or one of your own that keeps chats elsewhere, such as in a database) with its own prompt and tools; the built-in backend's source, `packages/server/src/assistant/built-in-assistant.ts`, is the reference.
|
|
126
|
+
|
|
127
|
+
### The protocol
|
|
128
|
+
|
|
129
|
+
The dock and a backend speak three requests on the backend's URL, each with the user's bearer token:
|
|
130
|
+
|
|
131
|
+
| Request | What it does |
|
|
132
|
+
| --- | --- |
|
|
133
|
+
| `GET <url>/events` | Server-sent events, each a JSON `AssistantEvent`: `state` replaces the whole chat and always comes first on a connection, `message` adds a message or replaces the one with its id, `part` adds a part to a message or replaces the one with its id, and `patch` changes `replying`, `status` or the composer's `placeholder`. |
|
|
134
|
+
| `POST <url>/messages` | `{ text, page? }`: what the user wrote, and the page they are on. The built-in backend answers `202` and replies with events, or `409` while it still answers the last message. |
|
|
135
|
+
| `POST <url>/actions` | `{ partId, actionId }`: the user clicked a button of a card. |
|
|
136
|
+
|
|
137
|
+
The types are exported from `@protobase/schema` (`AssistantEvent`, `AssistantState`, `AssistantMessage`, `AssistantPart` and its text, table, card and widget parts, `AssistantMessageRequest`, `AssistantActionRequest`). `createAssistantClient({ url, token })` from `@protobase/client`, where `token` is a function that gives the user's token, follows the event stream, reconnecting by itself, and sends the two requests; the dock uses it.
|
|
118
138
|
|
|
119
139
|
## Widget parts
|
|
120
140
|
|
|
@@ -288,7 +288,7 @@ auth: { accountDeletion: false }
|
|
|
288
288
|
|
|
289
289
|
It is Better Auth's own deletion, `POST /api/auth/delete-user` with `{ password }`, which the client has as `session.deleteAccount(password?)` and `GET /api/auth/status` offers as `"accountDeletion": true`. It deletes the user with their sessions, sign-in methods and linked accounts, as an admin's [removal](#users-page) does, along with what the app keeps of them elsewhere, such as their [devices for push](../guides/push-notifications.md), and their [MCP clients](#oauth-for-mcp-clients)' grants; the app's own records that point at the user stay. This device is signed out, and the address can no longer sign in.
|
|
290
290
|
|
|
291
|
-
- Without a password, as for someone who signs in with [emailed codes](#emailed-sign-in-codes) or a passkey,
|
|
291
|
+
- Without a password, as for someone who signs in with [emailed codes](#emailed-sign-in-codes) or a passkey, a session that signed in more than a day ago must [confirm it's them](#confirming-an-account-change) first, else it is `403 STRONG_SIGN_IN_REQUIRED`; the dialog asks for it in place. A wrong password is `INVALID_PASSWORD`.
|
|
292
292
|
- The last active admin cannot delete themselves (`400 LAST_ADMIN`), nor can an account in `PROTOBASE_LOCKED_ACCOUNTS` (`403 ACCOUNT_LOCKED`); nothing is deleted then.
|
|
293
293
|
- A [staff session](#staff-sign-in) cannot delete the account (`STAFF_CANNOT_CHANGE_SIGN_IN`).
|
|
294
294
|
- With `accountDeletion: false` the endpoint answers `404` and the page offers nothing. Better Auth's emailed confirmation link (`/delete-user/callback`) is always off.
|
|
@@ -325,6 +325,8 @@ With mail configured, the account page's Profile tab offers "Change email": the
|
|
|
325
325
|
|
|
326
326
|
With mail configured, the sign-in page offers "Email me a sign-in code": the person enters their address, gets a 6-digit code, and signs in with it instead of a password. It is Better Auth's [email OTP](https://www.better-auth.com/docs/plugins/email-otp) plugin: `POST /api/auth/email-otp/send-verification-otp` with `{ email, type: "sign-in" }`, then `POST /api/auth/sign-in/email-otp` with `{ email, otp }`.
|
|
327
327
|
|
|
328
|
+
The code goes in a box per digit, which takes a pasted code and the system's code autofill, and is checked as soon as its last digit is in; **Send a new code** waits 30 seconds after each code that went out. The [sign-up](#open-sign-up) code page works the same; a new address's code is checked once full too, and a two-factor code goes in the same boxes.
|
|
329
|
+
|
|
328
330
|
By default the page leads with the password and offers the code below it. For an app whose people mostly never set a password, `signInFirst: 'emailCode'` leads with the code instead: the page asks for the address and "Email me a sign-in code", and the password field, with "Forgot password?", waits behind "Use a password". It applies while the [sign-in policy](#sign-in-policy) leaves both on; `/api/auth/status` then answers `signInFirst: "emailCode"`.
|
|
329
331
|
|
|
330
332
|
```ts
|
|
@@ -355,6 +357,7 @@ The sign-in page then offers "Create an account", and so do [sign-in prompts](ui
|
|
|
355
357
|
- The password follows the [sign-in policy](#sign-in-policy) as for an invitation: none while passwords are off, optional while emailed codes are on, 12 characters or more. What the policy requires, such as two-factor authentication, is set up after the first sign-in, as for every account.
|
|
356
358
|
- The three requests are limited per minute and client like sign-in (`signInPerMinute`, default 5), and refused (`NO_ADMIN_YET`) while the store has no user.
|
|
357
359
|
- The `onUserCreated` [hook](#hooks) runs for every account made this way.
|
|
360
|
+
- `createAuthSession` from `@protobase/client` has the three steps as `sendSignUpCode(email)`, `verifySignUpCode(email, code)`, which resolves to the ticket, and `signUp({ ticket, name, password? })`, which resolves to the signed-in user.
|
|
358
361
|
|
|
359
362
|
## Passkeys
|
|
360
363
|
|
|
@@ -366,7 +369,7 @@ People add passkeys (Face ID, Touch ID, Windows Hello, a phone or a security key
|
|
|
366
369
|
- On an app with several origins, every passkey works on each of them through [Related Origin Requests](https://passkeys.dev/docs/advanced/related-origins/) (Chrome and Edge 128, Safari 18, Firefox 152; at most 5 registrable domains).
|
|
367
370
|
- A browser asks for the passkeys of one RP ID at a time. When people have passkeys for more than one of the app's hosts, `GET /api/auth/status` lists them as `passkeyDomains`, the passkey domain first, and the sign-in page adds "Use a passkey made for {host}" for each of the others. `GET /api/auth/passkey/generate-authenticate-options?rpId=<host>` asks for one of them.
|
|
368
371
|
- A passkey sign-in asks for no second step: the passkey is something you have and something you are or know.
|
|
369
|
-
- Adding a passkey
|
|
372
|
+
- A session of any age adds a first passkey. Adding another to an account with a passkey or a [second step](#two-factor-authentication), from a session that signed in more than a day ago, asks to [confirm it's them](#confirming-an-account-change) first, and the account page asks for it in place.
|
|
370
373
|
- Passkeys can be renamed and removed on the account page; removing one here leaves it on the device.
|
|
371
374
|
|
|
372
375
|
## Two-factor authentication
|
|
@@ -386,7 +389,27 @@ Some values ask for a sign-in that proved a passkey or a second step not long ag
|
|
|
386
389
|
|
|
387
390
|
The app's authenticator gives a token of the person's own session `signIn.strongAt()`, which reads that time when a request needs it, so other requests do no extra lookup. An app with its own authenticator (`auth/authenticate.ts`) sets `signIn: { staff: false, strongAt }` on the sessions it returns, `strongAt` resolving to a `Date` or `undefined`; without it, such values are refused.
|
|
388
391
|
|
|
389
|
-
Confirming it's them later counts from then: a passkey signs the browser in anew, and a code from the authenticator app,
|
|
392
|
+
Confirming it's them later counts from then: a passkey signs the browser in anew, and a code from the authenticator app, a backup code, or the emailed code of an account whose second step is email (`/two-factor/send-otp`, then `verify-otp`), confirms the session it has. `createAuthSession` from `@protobase/client` does both, with `signInWithPasskey()` and `verifyTwoFactor({ method: 'totp', code })`.
|
|
393
|
+
|
|
394
|
+
### Emailed confirmation code
|
|
395
|
+
|
|
396
|
+
An account with neither a passkey nor a second step has nothing stronger to confirm with, so when the app has mail it confirms with a code emailed on request instead, and the record page and the account page offer it in place: "Email me a code", then the code from the email. `POST /api/auth/account/confirmation-code/send` emails a new code to the signed-in person, and `POST /api/auth/account/confirmation-code/verify` with `{ code }` checks it and notes the strong sign-in for that session, as a second step does. The client has them as `session.account.sendConfirmationCode()` and `confirmWithCode(code)`.
|
|
397
|
+
|
|
398
|
+
- It is a code of its own kind: it never signs anyone in, and a sign-in code never confirms.
|
|
399
|
+
- It works only in the session that asked for it, of the user that asked, and only through the session cookie: an [MCP client](#oauth-for-mcp-clients) or an app's token cannot ask for one or use it. A [staff session](#staff-sign-in) cannot either (`STAFF_CANNOT_CHANGE_SIGN_IN`).
|
|
400
|
+
- It works for 10 minutes and once. A new one ends the one before, and three wrong codes end it (`TOO_MANY_ATTEMPTS`); asking and checking are each limited to 5 a minute and client.
|
|
401
|
+
- When the email cannot be sent, the request fails and no code works.
|
|
402
|
+
- An account with a passkey or a second step is refused (`403 CONFIRMATION_CODE_NOT_OFFERED`) and keeps confirming with those.
|
|
403
|
+
|
|
404
|
+
### Confirming an account change
|
|
405
|
+
|
|
406
|
+
Better Auth's own check of a fresh session, which could only be met by signing out and in again, is off (`freshAge: 0`). In its place, a session that signed in more than a day ago confirms it's them in place, as above, before a change that needs it, else the change is `403 STRONG_SIGN_IN_REQUIRED`; the confirmation counts for 15 minutes:
|
|
407
|
+
|
|
408
|
+
- adding a passkey to an account that has a passkey or a second step (a first passkey needs none);
|
|
409
|
+
- unlinking a sign-in provider (`/unlink-account`);
|
|
410
|
+
- [deleting the account](#deleting-the-account) without a password.
|
|
411
|
+
|
|
412
|
+
Listing the account's devices (`/list-sessions`) needs no confirmation.
|
|
390
413
|
|
|
391
414
|
## Sign-in policy
|
|
392
415
|
|
|
@@ -473,7 +496,7 @@ auth: { operator: false }
|
|
|
473
496
|
|
|
474
497
|
## Sign-in with GitHub
|
|
475
498
|
|
|
476
|
-
Projects have no sign-in provider of their own by default (a platform can add [one](#platform-sign-in-provider)). `auth.socialProviders` adds Better Auth's [social providers](https://www.better-auth.com/docs/authentication/github), and the sign-in page shows "Continue with GitHub"
|
|
499
|
+
Projects have no sign-in provider of their own by default (a platform can add [one](#platform-sign-in-provider)). `auth.socialProviders` adds Better Auth's [social providers](https://www.better-auth.com/docs/authentication/github), and the sign-in page shows "Continue with {name}" for each, such as "Continue with GitHub" with GitHub's mark for `github`:
|
|
477
500
|
|
|
478
501
|
```ts
|
|
479
502
|
export default defineConfig(({ env }) => ({
|
|
@@ -622,7 +645,7 @@ It is Better Auth's organization plugin, with its tables named in snake_case (`o
|
|
|
622
645
|
|
|
623
646
|
**Deleting** an organization (Better Auth's `delete`, owners only) is refused with `409 ORGANIZATION_HAS_RECORDS` while a `.tenant` resource has rows in it; `deleteRecords: 'cascade'` leaves it to the project's foreign keys. The `beforeOrganizationDeleted` [hook](#hooks) may refuse too.
|
|
624
647
|
|
|
625
|
-
**Ids.** The `generateOrganizationId` [hook](#hooks) makes a new organization's id, by default a UUID; `.tenant` columns hold it, so `text` or `uuid`. An app whose tenant columns are integers takes the next value of its own organizations sequence instead, and adds its own row in `onOrganizationCreated`, as the ERP example does (`tests/apps/erp/auth/hooks.ts`). An app that ran as one organization keeps its tenant value: `protobase organizations create "Acme" --owner you@example.com --id 1 --members all` makes that organization, with every user in it: superusers as owners, everyone else as a member, their global roles moved onto the membership.
|
|
648
|
+
**Ids.** The `generateOrganizationId` [hook](#hooks) makes a new organization's id, by default a UUID; `.tenant` columns hold it, so `text` or `uuid`. An app whose tenant columns are integers takes the next value of its own organizations sequence instead, and adds its own row in `onOrganizationCreated`, as the ERP example does (`tests/apps/erp/auth/hooks.ts`). An app that ran as one organization keeps its tenant value: `protobase organizations create "Acme" --owner you@example.com --id 1 --members all` makes that organization, with every user in it: superusers as owners, everyone else as a member, their global roles moved onto the membership. The first user `createUser` makes also becomes the owner of every organization without members, such as those a seed made before anyone could own them.
|
|
626
649
|
|
|
627
650
|
**On the host.** The [`protobase organizations`](cli.md#organizations) commands call these functions of `@protobase/server`, which a script of the project's own, such as a seed, calls with the sign-in of [`loadProjectAuth`](#first-run). The host acts as a superuser, so it may give any role; `organization` is a slug or an id:
|
|
628
651
|
|
|
@@ -701,6 +724,10 @@ Their tokens are verified against that app's keys at `<issuer>/api/auth/jwks`; a
|
|
|
701
724
|
|
|
702
725
|
## supabase-js and @supabase/ssr
|
|
703
726
|
|
|
727
|
+
:::caution[Beta]
|
|
728
|
+
MFA, passkeys, SSO, phone and anonymous sign-in, the admin API and changing the address are not offered yet, as the list below says.
|
|
729
|
+
:::
|
|
730
|
+
|
|
704
731
|
An app moving from Supabase keeps its `supabase.auth` calls and its `@supabase/ssr` session checks: the app serves GoTrue's endpoints at `/api/sb/auth/v1`, over the same Better Auth sign-in, so `createClient(<the app's origin>/api/sb, <publishable key>)` signs in, refreshes, reads the user and signs out as on Supabase.
|
|
705
732
|
|
|
706
733
|
- `signInWithPassword`, `signInWithOtp` then `verifyOtp({ email, token, type: 'email' })`, `signInWithOAuth` with one of the app's [sign-in providers](#sign-in-with-github) (PKCE, as `@supabase/ssr` uses, or the implicit flow), `refreshSession`, `getUser`, `getClaims`, `updateUser({ password, data })` and `signOut` work. Each goes through Better Auth's own endpoint, with its rate limits and the [sign-in policy](#sign-in-policy).
|
|
@@ -751,7 +778,7 @@ auth: { mcp: { path: '/api/mcp', connectPage: '/-/connect' } }
|
|
|
751
778
|
- The scopes are `mcp` and `offline_access`, which is added when a client leaves it out. Access tokens last 15 minutes, carry the MCP resource as their audience and `typ: at+jwt`, and work for MCP only: the app's API refuses them, and the MCP endpoint refuses the app's own tokens. Refresh tokens roll for 30 days. The MCP endpoint verifies them, and checks on every request that the person still allows the client, still exists unbanned and still holds each role the token carries, so a disconnected client, a ban, a lowered role or a deleted user stops its access tokens at the next request rather than at their expiry; a lookup that fails refuses the request. A client that binds its tokens to a key (DPoP) sends a proof with each, checked against the resource's URL.
|
|
752
779
|
- A grant keeps the role and organization the person worked in when they allowed the client. A refresh is refused once they no longer hold that role or may no longer enter that organization, once the [sign-in policy](#sign-in-policy) requires something they have not set up, and while they are banned; access tokens already issued last out their 15 minutes. A grant made without a role is refused.
|
|
753
780
|
- Someone who has not set up what the sign-in policy requires is sent to the setup page first. Staff signed in as a person cannot allow a client, nor disconnect one. An account whose address is not verified cannot connect a client until it signs in once with an emailed code: a provider sign-up whose provider did not vouch for the address, or a password account created before Protobase created accounts verified. With password-only sign-in, or without mail, it cannot.
|
|
754
|
-
- The connect page shows the client's name as the client's own claim, who published it for a client with an https id, where the code goes (a website by its host, an app on this computer by its port, or an app on the device by its whole address), and the signed-in address. It cannot be used inside another site's frame, and Allow takes a click only once the page has been in front for half a second. `protobase serve` and `protobase dev` send `X-Frame-Options: DENY` and `Content-Security-Policy: frame-ancestors 'none'` with every page of the UI; a host that serves `public/` itself should send them too.
|
|
781
|
+
- The connect page shows the client's name as the client's own claim, who published it for a client with an https id, where the code goes (a website by its host, an app on this computer by its port, or an app on the device by its whole address), and the signed-in address. It cannot be used inside another site's frame, and Allow takes a click only once the page has been in front for half a second. `protobase serve` and `protobase dev` send `X-Frame-Options: DENY` and `Content-Security-Policy: frame-ancestors 'none'` with every page of the UI; a host that serves `public/` itself should send them too. A host's own consent screen arms its Allow button the same way with the guard `@protobase/ui` exports (see [the plugin API](plugins.md#api)).
|
|
755
782
|
- The account page lists the clients a person allowed, under Connected apps, and disconnects any one: its consent is forgotten and its tokens revoked, access tokens already issued included, so it has to ask again. A client published at a URL has one id for everyone, so two copies of it allowed on one account with the same role show as one entry, and disconnecting it disconnects both.
|
|
756
783
|
- Signing out everywhere, signing out the other devices, a password change, a password reset, an admin ending the person's sessions, banning them, setting their password or marking their address unverified, and `protobase users disable` disconnect every client and forget which were allowed, so each has to ask again; enabling a disabled user brings none back. Through the [endpoints for supabase-js](#supabase-js-and-supabasessr) the same holds for signing out with the `global` or `others` scope and for every new password set with `PUT /user`, since a password reset ends there too. Signing out of one browser, or a session expiring, disconnects nothing.
|
|
757
784
|
- A refresh token presented again after it was rotated, outside the provider's 30-second window for a retried request, is taken for stolen: that client is disconnected for the person, its access tokens stop at the next request, and it has to ask again. Their other clients keep working.
|
|
@@ -133,7 +133,7 @@ Exit code is 1 when there are errors. Run it after the project's migrations and
|
|
|
133
133
|
## `dev`
|
|
134
134
|
|
|
135
135
|
```sh
|
|
136
|
-
protobase dev [--port 5173] [--env DATABASE_URL] [--cache-dir <dir>] [--allowed-hosts <hosts>] [--extend <dir>] # run inside a project, e.g. bun run --cwd tests/apps/erp dev
|
|
136
|
+
protobase dev [--port 5173] [--env DATABASE_URL] [--cache-dir <dir>] [--allowed-hosts <hosts>] [--extend <dir>] [--no-jobs] # run inside a project, e.g. bun run --cwd tests/apps/erp dev
|
|
137
137
|
```
|
|
138
138
|
|
|
139
139
|
One port, two processes: Vite serves the admin app from `@protobase/ui`'s `src/app/` (`dist/app/` when installed; `index.html` and `main.tsx`, or `main.js`; the command fails with a clear message when they are missing) and hands `/api`, supabase-js's `/api/sb` included, to the Hono app from `createAdmin`, which runs in a process of its own; the CLI's process never loads `@protobase/server`. Open `http://localhost:<port>`; the API reference is at `/api/docs`.
|
|
@@ -146,7 +146,7 @@ One port, two processes: Vite serves the admin app from `@protobase/ui`'s `src/a
|
|
|
146
146
|
- **Cache:** Vite's dependency cache is `node_modules/.vite`; two dev servers sharing it can disturb each other's optimisation, so pass `--cache-dir` for a second one (the integration test does).
|
|
147
147
|
- **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.
|
|
148
148
|
- **Mobile releases:** see [below](#mobile-releases).
|
|
149
|
-
- **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.
|
|
149
|
+
- **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`.
|
|
150
150
|
- **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
151
|
- **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.
|
|
152
152
|
- **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.
|
|
@@ -575,6 +575,10 @@ Run inside a project with sign-in on, against its database with the project's mi
|
|
|
575
575
|
|
|
576
576
|
## `import supabase`
|
|
577
577
|
|
|
578
|
+
:::caution[Beta]
|
|
579
|
+
MFA factors and passkeys do not carry over yet, as the second run says.
|
|
580
|
+
:::
|
|
581
|
+
|
|
578
582
|
```sh
|
|
579
583
|
protobase import supabase [<connection> | --source-env <variable>] [--env DATABASE_URL] [--dir db/migrations] [--config config]
|
|
580
584
|
[--storage-url <url>] [--storage-key-env SUPABASE_SERVICE_ROLE_KEY] [--yes]
|
|
@@ -589,7 +593,7 @@ Turns a Supabase project's database into the project in the current directory, i
|
|
|
589
593
|
|
|
590
594
|
5. **Storage's files**: with `--storage-url https://<ref>.supabase.co` and the project's secret key in the variable `--storage-key-env` names, `SUPABASE_SERVICE_ROLE_KEY` by default, the second run, or any run after it, copies the bytes of every object in `storage.objects` from the Supabase project's Storage API into the app's [Storage file provider](supabase-storage.md#where-things-are-kept), where [`/api/sb/storage/v1`](supabase-storage.md) serves them, with the type and cache control the rows recorded. It streams each object, checks its byte count against the size Supabase recorded and its MD5 against the ETag where that is one, and deletes a copy that does not match. It first prints each bucket's size and stops when what is left to copy and what the provider holds exceed the provider's `max_bytes`, unless `--yes`. An object that fails does not stop the rest; the run then fails, listing them in `supabase-import/storage.md`, and a rerun copies only what the provider does not hold yet. The key goes only in the requests' headers, never in a file. Plain `http://` is accepted only for a local Supabase on `localhost`, `127.0.0.1` or `[::1]`, such as `supabase start`'s `http://127.0.0.1:54321`, since the key bypasses every Storage policy; a download Supabase answers with a redirect is not followed, so the key goes to no other host, and that object fails.
|
|
591
595
|
|
|
592
|
-
An agent with the `supabase-move` skill of [`skill install`](#skill) finishes what the report leaves: it rewrites each item, checking the project's database for the extensions an item needs, and runs `doctor` and the typecheck until both pass, leaving the values of Vault's secrets for you to enter.
|
|
596
|
+
An agent with the `supabase-move` skill of [`skill install`](#skill) finishes what the report leaves: it rewrites each item, checking the project's database for the extensions an item needs, and runs `doctor` and the typecheck until both pass, leaving the values of Vault's secrets for you to enter. It keeps `supabase/functions`, the Deno originals the app no longer runs, out of the typecheck through the `exclude` of `tsconfig.json`, and removes the folder only when you say so.
|
|
593
597
|
|
|
594
598
|
Remove `supabase-import/` to import again.
|
|
595
599
|
|
|
@@ -17,7 +17,7 @@ const { record, etag } = await api.get(orders, id)
|
|
|
17
17
|
await api.update(orders, id, { status: 'shipped' }, etag) // 412 rejects with PreconditionFailedError
|
|
18
18
|
```
|
|
19
19
|
|
|
20
|
-
Pass a resource builder to get its record type ([`InferRecord`](https://docs.protobase.net/api/schema/type-aliases/inferrecord/)), or a name for loose records. Filters are AIP-160 text or [`where`](https://docs.protobase.net/api/schema/variables/where/) results (printed with [`printFilter`](https://docs.protobase.net/api/schema/functions/printfilter/)). Errors are `ApiError` (`status`, `slug`, `errors`, `filterErrors` with spans, `blockedBy` for a delete other records point at), which `isApiError(error)` tells apart. Also: `search`, `create`, `remove`, `facets`, `series`, `histogram`, `map(resource, { latitude, longitude, zoom, filter?, orderBy?, fields? })` (the records in a map's view, or buckets of them, see [`:map`](api.md#maps)), `seek`, `reveal(resource, key, field)` (a [sensitive field](data-config.md#sensitive-fields)'s value), `generate(resource, key, action)` (runs an [AI action](ai.md#ai-actions) and resolves to the record as written), `meta(etag?, { signal? })`, `invoke(name, { body?, method?, path?, query? })` (one of the app's [API functions](../guides/api-functions.md#calling-a-function); its answer as parsed JSON, text, or a `Blob` for any other type), `upload(resource, field, file, { onProgress?, signal? })` (a file for a [file field](files.md#uploading); its `value` goes in the next write, and `onProgress` uses XMLHttpRequest, since fetch reports no upload progress), `live({ onChanges, onResync, onRetry? })` (hears which records of the [live](live.md) resources change until the function it returns is called, connecting again by itself; every caller of one client shares one stream), `channel(resource, key)` (a record's [channel](channels.md#from-a-page), with its presence and broadcasts, every channel of the client on one socket).
|
|
20
|
+
Pass a resource builder to get its record type ([`InferRecord`](https://docs.protobase.net/api/schema/type-aliases/inferrecord/)), or a name for loose records. Filters are AIP-160 text or [`where`](https://docs.protobase.net/api/schema/variables/where/) results (printed with [`printFilter`](https://docs.protobase.net/api/schema/functions/printfilter/)). Errors are `ApiError` (`status`, `slug`, `errors`, `filterErrors` with spans, `blockedBy` for a delete other records point at), which `isApiError(error)` tells apart. Also: `search`, `create`, `remove`, `facets`, `series`, `histogram`, `map(resource, { latitude, longitude, zoom, filter?, orderBy?, fields? })` (the records in a map's view, or buckets of them, see [`:map`](api.md#maps)), `seek`, `reveal(resource, key, field)` (a [sensitive field](data-config.md#sensitive-fields)'s value), `generate(resource, key, action)` (runs an [AI action](ai.md#ai-actions) and resolves to the record as written), `meta(etag?, { signal? })`, `invoke(name, { body?, method?, path?, query? })` (one of the app's [API functions](../guides/api-functions.md#calling-a-function); its answer as parsed JSON, text, or a `Blob` for any other type), `history(resource, key, { pageSize?, pageToken? })` (a page of a record's [changes](history.md), newest first), `comments(resource, key, { pageSize?, pageToken? })`, `comment(resource, key, { body, mentions? })` and `mentionable(resource, key, query)` (a record's [comments](comments.md#the-endpoints), adding one, and the people it may mention), `form(name)`, `formChallenge(name)` and `submitForm(name, values, proof)` (a [public form](forms.md#routes)), `upload(resource, field, file, { onProgress?, signal? })` (a file for a [file field](files.md#uploading); its `value` goes in the next write, and `onProgress` uses XMLHttpRequest, since fetch reports no upload progress), `live({ onChanges, onResync, onRetry? })` (hears which records of the [live](live.md) resources change until the function it returns is called, connecting again by itself; every caller of one client shares one stream), `channel(resource, key)` (a record's [channel](channels.md#from-a-page), with its presence and broadcasts, every channel of the client on one socket).
|
|
21
21
|
|
|
22
22
|
With an absolute `baseUrl`, such as another app's `https://desk.example.com/api/v1`, the `url` of each [file](files.md) in a record the client answers is absolute too, on that API's origin, where the API answers a path on its own origin. `routedClient(own, sourceOf)` is one client over several APIs: each call about a resource goes to the client `sourceOf(resource)` gives, else to `own`, as the admin app does for [remote resources](remote-resources.md), and `sourceMeta(meta, source)` keeps the part of another API's `/meta` an app draws.
|
|
23
23
|
|
|
@@ -54,7 +54,7 @@ Whoever may read the record may read its comments and add one: the same tenant,
|
|
|
54
54
|
|
|
55
55
|
## The endpoints
|
|
56
56
|
|
|
57
|
-
`GET /api/v1/{resource}/{key}:comments` reads them newest first, `POST` to the same path adds one, and `GET /api/v1/{resource}/{key}:mentionable?q=` lists whom a comment may mention. See [REST API](api.md#record-comments).
|
|
57
|
+
`GET /api/v1/{resource}/{key}:comments` reads them newest first, `POST` to the same path adds one, and `GET /api/v1/{resource}/{key}:mentionable?q=` lists whom a comment may mention. See [REST API](api.md#record-comments). From code, [`@protobase/client`](client.md) has `comments(resource, key, { pageSize?, pageToken? })`, `comment(resource, key, { body, mentions? })`, where `mentions` holds the ids of the users mentioned, and `mentionable(resource, key, query)`.
|
|
58
58
|
|
|
59
59
|
An AI client reads and adds them over the app's [MCP endpoint](mcp.md#comments), with `list_comments` and `add_comment`, under the same rules.
|
|
60
60
|
|
|
@@ -239,7 +239,7 @@ A component that asks for input looks like the app's own forms when it is built
|
|
|
239
239
|
| `RecordPicker` | Records of another resource, picked as the create form picks a relation: type to narrow them by their search fields or title, scroll for more, or use the keyboard. `resource` names the resource, whose model it reads from the app; `value` is the chosen record's key as text and `onChange` gets the new one. With `multiple`, `value` and `onChange` hold several keys, such as the objectives a task serves through a join resource: each shows as a token that can be taken off. |
|
|
240
240
|
| `Segmented`, `MenuSelect` | A switch between a few short options in a row (`size`, `iconOnly`), and the same options in a menu. |
|
|
241
241
|
| `Button`, `buttonClass` | A button (`variant` `primary`, `secondary`, `ghost` or `danger`, `size`, `loading`), and its classes for a link that looks like one. |
|
|
242
|
-
| `Dialog`, `ResponsiveDialog` | A modal with a `title`, `description`, `actions` and a body, which Escape and a click outside close with `onClose`; the responsive one becomes a sheet
|
|
242
|
+
| `Dialog`, `ResponsiveDialog` | A modal with a `title`, `description`, `actions` and a body, which Escape and a click outside close with `onClose`; the responsive one becomes a sheet from the bottom of a phone, which a swipe down on its header closes too. |
|
|
243
243
|
|
|
244
244
|
```tsx
|
|
245
245
|
// components/book-repair.tsx, exported from protobase.ui.tsx
|
|
@@ -335,6 +335,10 @@ export const Channel = ({ messages, sent, members, onSend, onLoadEarlier }: Chan
|
|
|
335
335
|
|
|
336
336
|
### Members' names
|
|
337
337
|
|
|
338
|
+
:::caution[Beta]
|
|
339
|
+
The whole member list loads in the browser, with no paging or search on the server, and former members' names do not show.
|
|
340
|
+
:::
|
|
341
|
+
|
|
338
342
|
The author is a user id, and `useMembers()` from `@protobase/ui` turns it into a name: the members of the organization the signed-in person works in, `{ id, name, image? }` by name, with `id` the user id that `.author()` stores and that `onSend`'s mentions hold. Every member reads it, owners, admins and members alike; it holds no email addresses. It is a TanStack Query result (`data`, `isLoading`, `error`) with `member(id)`, which finds one and, for an id it does not know, such as a newcomer's first message, reads the list again once it is 10 seconds old; it is also read again when the window regains focus and the list is 30 seconds old. It needs [organizations](auth.md#organizations): without them, `error` says so. In an app with [status](status.md#in-the-app), each member carries their `status` too, read again every 15 seconds.
|
|
339
343
|
|
|
340
344
|
```tsx
|
|
@@ -358,6 +362,8 @@ export const ChannelComposer = ({ onSend }: { onSend: (text: string, mentions: s
|
|
|
358
362
|
|
|
359
363
|
`searchPeople` filters the list in the browser and shows at most 8, as comments' mentions do. <gap>`useMembers()` cannot page or search on the server because a server-side people resource is not implemented, so the whole list loads in the browser, which suits a team, not thousands of members.</gap> <gap>Old messages of someone who has left the organization cannot show their name because looking up former members is not implemented.</gap> A person who works in an organization without being a member of it, through a global role, gets no list.
|
|
360
364
|
|
|
365
|
+
### Signed-in user
|
|
366
|
+
|
|
361
367
|
`useSignedInUser()` from `@protobase/ui` gives the signed-in person as `{ id, name? }`, `id` being the user id `.author()` stores and mentions hold, such as for a filter on their own messages (`author_id != "<id>"`) or mentions of them; it is `undefined` while the session loads, for a visitor and when nobody is signed in.
|
|
362
368
|
|
|
363
369
|
## Components in the chat
|
|
@@ -224,7 +224,7 @@ A [webhook subscription](webhooks.md) delivering `as` a role runs that role's ac
|
|
|
224
224
|
|
|
225
225
|
### Recent strong sign-in
|
|
226
226
|
|
|
227
|
-
A field such as a recovery code or an API secret can ask for more than read access: `.strongSignIn()` reveals it only to someone whose sign-in proved a passkey or a second step in the last 15 minutes, or `minutes` you give. It is sensitive too, so all of the above applies, and `/meta` marks it `strongSignIn: { minutes }`.
|
|
227
|
+
A field such as a recovery code or an API secret can ask for more than read access: `.strongSignIn()` reveals it only to someone whose sign-in proved a passkey or a second step in the last 15 minutes, or `minutes` you give; an account with neither confirms with an [emailed code](auth.md#emailed-confirmation-code) instead. It is sensitive too, so all of the above applies, and `/meta` marks it `strongSignIn: { minutes }`.
|
|
228
228
|
|
|
229
229
|
```ts
|
|
230
230
|
recoveryCode: f.text().encrypted().strongSignIn({ minutes: 10 }),
|
|
@@ -89,7 +89,7 @@ The template that comes back has two methods, both typed by the component's prop
|
|
|
89
89
|
|
|
90
90
|
React and React Email's renderer are loaded on the first email rendered, not when the app starts.
|
|
91
91
|
|
|
92
|
-
Mail an app sends in bulk, such as alerts or newsletters, should carry a one-click unsubscribe link, which
|
|
92
|
+
Mail an app sends in bulk, such as alerts or newsletters, should carry a one-click unsubscribe link, which large mailbox providers expect of bulk senders ([RFC 8058](https://www.rfc-editor.org/rfc/rfc8058)): a `List-Unsubscribe` header with the recipient's own link, and `List-Unsubscribe-Post`. The link must stop the mail on a `POST` without a sign-in, such as an [API function](../guides/api-functions.md) that takes a token of the recipient's in its address:
|
|
93
93
|
|
|
94
94
|
```ts
|
|
95
95
|
headers: ({ unsubscribeUrl }: Digest) => ({ 'List-Unsubscribe': `<${unsubscribeUrl}>`, 'List-Unsubscribe-Post': 'List-Unsubscribe=One-Click' }),
|
|
@@ -168,6 +168,10 @@ Uploads go through Protobase's own route, so the type is checked and processors
|
|
|
168
168
|
|
|
169
169
|
A write takes only a ticket, `null` to clear the field, or the stored `uri` unchanged, and in a field with `.externalUrls()` an https URL. Anything else is refused with `400`, so a row can only point at a file its writer uploaded for that field, or at a URL elsewhere where the field allows one. From the client, `client.upload(resource, field, file, { onProgress })` does step 1; see [Client](client.md).
|
|
170
170
|
|
|
171
|
+
### In forms
|
|
172
|
+
|
|
173
|
+
In the admin app's create and edit forms, a file field takes a dropped or chosen file, limited to the field's `.accept()` types, and uploads it at once with a progress bar; Save then stores it with the record. A stored file shows its thumbnail or an icon, its name linking to the file, its type and size, and the type it was sent as when the server corrected it. Replace picks another file, and Remove, for an optional field, clears it. Tables show the file's name, beside a small thumbnail for an image.
|
|
174
|
+
|
|
171
175
|
### How the type is decided
|
|
172
176
|
|
|
173
177
|
An upload carries three hints: what the **content** is (C, from its magic bytes), what the **name's extension** says (E), and what the **client declared** (H, the request's or the file part's `Content-Type`; `application/octet-stream` counts as none).
|
|
@@ -152,13 +152,15 @@ Each run is a job of its own, enqueued ahead for its time by the workers: its id
|
|
|
152
152
|
|
|
153
153
|
Because the next run waits in the database like any delayed job, [`jobs.next_due()`](#hosts-that-sleep) sees it, and a host that wakes the app for due jobs wakes it for its recurring jobs too. That is why Protobase does not use pg-boss's own scheduler, which keeps no next run in the database and works the time out in a running process.
|
|
154
154
|
|
|
155
|
+
`isSchedule(schedule, timezone)` from `@protobase/server` tells whether a recurring job takes a schedule in a time zone, for a tool that writes the config, such as an import.
|
|
156
|
+
|
|
155
157
|
## Failed jobs page
|
|
156
158
|
|
|
157
159
|
With jobs, the admin app has a **Failed jobs** page, for admins only: each job that used up its attempts or failed for good, with its payload and last error. **Retry** gives a job one more attempt, from now, and **Delete** removes it. It reads the `jobs.failed` view, which a project's SQL can query too.
|
|
158
160
|
|
|
159
161
|
## Where jobs run
|
|
160
162
|
|
|
161
|
-
- **`protobase dev`** runs them in its process. An edited job runs its new code from the next job on.
|
|
163
|
+
- **`protobase dev`** runs them in its process. An edited job runs its new code from the next job on. `protobase dev --no-jobs` runs none, for a dev server on a hosted app's database.
|
|
162
164
|
- **`protobase serve`** runs them in its process, beside the API, unless `PROTOBASE_JOBS=false`; it enqueues them either way. On SIGTERM it stops taking jobs and gives running ones 30 seconds to finish. A job still running then is failed, to be retried.
|
|
163
165
|
- **[`protobase worker <bundle>`](cli.md#worker)** runs the same jobs from the same bundle in a process of its own, with the same shutdown, for a `serve` with `PROTOBASE_JOBS=false`.
|
|
164
166
|
- Each worker checks every job name for due jobs every 2 seconds: one indexed query per job name and interval. Once a minute, one of the workers also counts the queues and retries the jobs whose worker died (their `timeout` passed); once a day it deletes completed and failed jobs older than 7 days.
|
|
@@ -24,8 +24,8 @@ Numbers, amounts and dates use the conventions of the language: English as writt
|
|
|
24
24
|
|
|
25
25
|
## What is translated
|
|
26
26
|
|
|
27
|
-
Every built-in screen is translated: the shell with its sidebar, top bar, search, notification bell, profile menu and bottom bar; the list page with all its displays (table, cards, board, calendar, timeline and map), filters, search, sort, imports and exports; the record page with its actions, related lists, history, comments, gallery, saving, conflicts and deleting; the create page and composed pages; saved searches and favorites; the sign-in, sign-up and two-factor screens, the account and sign-in policy pages, and the organization, people and invitation pages; public forms; and the messages, toasts and confirmations around them. So are counts, relative times ("5 minutes ago"), yes, no and the other values the app writes itself, and the notices shown while the app loads.
|
|
27
|
+
Every built-in screen is translated: the shell with its sidebar, top bar, search, notification bell, profile menu and bottom bar; the list page with all its displays (table, cards, board, calendar, timeline and map), filters, search, sort, imports and exports; the record page with its actions, related lists, history, comments, gallery, saving, conflicts and deleting; the create page and composed pages; saved searches and favorites; the sign-in, sign-up and two-factor screens, the account and sign-in policy pages, and the organization, people and invitation pages; public forms; the assistant's dock and a record's [lines](ui-config.md#record-lines); the notice naming a plugin that is off and why; and the messages, toasts and confirmations around them. The parts `@protobase/ui` gives your own components, such as the [form parts](custom-components.md#form-parts), the [chat parts](custom-components.md#chat-parts) and the markdown editor, come in the app's language too. So are counts, relative times ("5 minutes ago"), yes, no and the other values the app writes itself, and the notices shown while the app loads.
|
|
28
28
|
|
|
29
29
|
The approvals an AI client asks for over [MCP](mcp.md#writes) are translated too, with their buttons and the refusal for a client that cannot ask: in the app's `locale`, or else in the language the client's request accepts.
|
|
30
30
|
|
|
31
|
-
The other texts a plugin or the server writes itself, such as an error message the API sends back
|
|
31
|
+
The other texts a plugin or the server writes itself, such as an error message the API sends back, show as they are written.
|
|
@@ -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
|
|
@@ -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
|
|
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
|
|
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`, `
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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({
|
|
@@ -53,6 +53,8 @@ Every commit message follows [Conventional Commits](https://www.conventionalcomm
|
|
|
53
53
|
|
|
54
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.
|
|
55
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
|
+
|
|
56
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.
|
|
57
59
|
|
|
58
60
|
## Bundled and locked projects
|