@plutocms/supabase 0.29.0 → 0.31.0

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/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.31.0](https://github.com/plutocms/supabase/compare/v0.30.0...v0.31.0) (2026-11-12)
4
+
5
+
6
+ ### Features
7
+
8
+ * admin UI round 5: tooltips, a playground site, new tab links; core 0.68 ([#302](https://github.com/plutocms/supabase/issues/302)) ([441d322](https://github.com/plutocms/supabase/commit/441d322a52cf31712cd7510b0876ca885698fda7))
9
+
10
+ ## [0.30.0](https://github.com/plutocms/supabase/compare/v0.29.0...v0.30.0) (2026-11-10)
11
+
12
+
13
+ ### Features
14
+
15
+ * the post editor look of core, a line icon for the backend badge, shop translations ([#293](https://github.com/plutocms/supabase/issues/293)) ([96f582b](https://github.com/plutocms/supabase/commit/96f582b261524fa9511d1f32451ae8a801dcc3b1))
16
+
17
+
18
+ ### Bug Fixes
19
+
20
+ * declare the MIT license of supabase and supabase-shop; short READMEs ([#289](https://github.com/plutocms/supabase/issues/289)) ([d9b0745](https://github.com/plutocms/supabase/commit/d9b07459d1315bc75e8188535c8db0d2f9212399))
21
+
3
22
  ## [0.29.0](https://github.com/plutocms/supabase/compare/v0.28.0...v0.29.0) (2026-11-08)
4
23
 
5
24
 
package/README.md CHANGED
@@ -1,11 +1,13 @@
1
1
  # Supabase Layer for Pluto
2
2
 
3
- This layer integrates Supabase as a backend service for Pluto, providing a robust and scalable solution for managing content and data.
3
+ This layer makes Supabase the backend of Pluto: the database, the sign-in, the users, and the roles.
4
+
5
+ **Docs:** [Supabase](https://pluto.vulmix.dev/docs/supabase)
4
6
 
5
7
  ## Installation
6
8
 
7
9
  > [!NOTE]
8
- > Make sure you have a Supabase project set up. You will need the project ID, URL, and API key. You can find these in your [Supabase dashboard](https://supabase.com/dashboard).
10
+ > Make sure you have a Supabase project set up. You need the URL and the publishable key of the project. You can find these in your [Supabase dashboard](https://supabase.com/dashboard).
9
11
 
10
12
  1. Create a `.env` file in the root of your project and add the following environment variables:
11
13
 
@@ -30,117 +32,15 @@ export default defineNuxtConfig({
30
32
  })
31
33
  ```
32
34
 
33
- ## Features
34
-
35
- ### Composables
36
-
37
- This layer provides the following composables:
38
-
39
- - `useAuth`: Sign in, request a password reset, and update the password, with Supabase.
40
-
41
- ### Admin shell
42
-
43
- Pluto core owns the admin shell: the `/admin` route, the sign-in page, the route guard and the
44
- top navbar. This layer registers an auth driver, and core renders the shell from it. See
45
- `.claude/skills/auth-driver/SKILL.md`.
46
-
47
- An app that overrode `NavbarAdmin` or `pages/admin.vue` of an older version must move to registry
48
- entries. See "Migrating an app" in that skill.
49
-
50
- ### Invite users (access API)
51
-
52
- An admin invites a user by email and gives the user one or more roles. The routes are under
53
- `/api/_pluto/supabase/access/**`. Every write needs an admin. This needs the migration
54
- `008_users_and_roles_admin.sql`, which the admin applies from `/admin/migrations`.
55
-
56
- Invites need no setup. Check one thing: in the Supabase dashboard (Authentication, then URL
57
- Configuration), the **Site URL** is the URL of your site. Supabase accepts every link back to that
58
- host, so `/admin/accept-invite` needs no entry in the Redirect URLs. Add a Redirect URL only for
59
- another host (for example a preview deploy, or `www.` next to the bare domain).
60
-
61
- How the invite email goes out:
62
-
63
- - **Without the service role key** (the default): the server sends a passwordless sign-in link
64
- with the public key. Supabase creates the account and uses its sign-up or magic link email
65
- template. Public sign-up must be on (the Supabase default). Supabase sends one email per
66
- address every 60 seconds.
67
- - **With the service role key** (optional): set `NUXT_SUPABASE_SECRET_KEY` (the Supabase secret
68
- key) in the environment of the **running server**. Never put it in the build environment. The
69
- server then uses Supabase's invite API and the "Invite user" email template. This also works
70
- when public sign-up is off.
71
-
72
- Optional: a custom invite email template (Authentication, then Email Templates, then Invite
73
- user) makes the link work in every browser, because the page then reads a `token_hash` and not
74
- tokens in the URL hash:
75
-
76
- ```
77
- {{ .SiteURL }}/admin/accept-invite?token_hash={{ .TokenHash }}&type=invite
78
- ```
79
-
80
- The built-in Supabase mailer sends very few emails, and only to members of your Supabase
81
- organization. To invite any other address, set up custom SMTP (Project Settings, then
82
- Authentication, then SMTP). A production site needs it anyway, for every auth email. Without it,
83
- an invite to another address answers 400 with the hint `emailNotAuthorized`.
84
-
85
- The invited user opens the link, sets a password on `/admin/accept-invite`, and goes to the
86
- admin. An invite to an email that already has an account adds the roles and sends no email.
87
-
88
- #### Invites through Pluto mail (optional)
89
-
90
- With the service role key **and** a working Pluto mail driver, an invite does not use Supabase's
91
- mailer. The server makes a one-time link with `auth.admin.generateLink`, and Pluto mail sends the
92
- email. This lifts the team-only address rule and the Supabase email rate limit for invites. Set
93
- the mail variables in the environment of the **running server** (Resend is the first driver):
94
-
95
- ```
96
- NUXT_PLUTO_MAIL_RESEND_API_KEY=re_...
97
- NUXT_PLUTO_MAIL_FROM="Your Site <hello@your-domain.com>"
98
- ```
99
-
100
- `Settings > Email` in the admin guides the setup (account, key, domain, test email). The
101
- `mail` skill of `@plutocms/core` lists every `NUXT_PLUTO_MAIL_*` variable. If the send fails,
102
- the user and the roles stay, and the admin gets the invite link to send by hand.
103
-
104
- With the key, an admin can also copy the invite link (Users, then the row menu, or the invite
105
- form). The link works once and expires. Sign-up, password reset and magic link emails still
106
- come from Supabase. The Email tab has a guide to send them through Resend too.
107
-
108
- ### API Routes
109
-
110
- The layer includes API routes for interacting with Supabase services, such as authentication and settings.
111
- Every route lives under `/api/_pluto/supabase/`. Core blocks a cross-site write to `/api/_pluto/**`, so
112
- a page of another site cannot call them. A failure is an HTTP error with a translation key, never a
113
- `200` with `success: false`. These routes serve the admin. They are not a public API.
114
-
115
- ### Public content API
116
-
117
- The public content of the Supabase layers is read through the public API of core,
118
- `GET /api/pluto/v1/content/<type>` (and `/<id or slug>`). See
119
- [the public API](https://pluto.vulmix.dev/docs/build/public-api). The routes are anonymous. This layer
120
- reads for them with an anon client that has no user session, so Row Level Security applies as for
121
- an anonymous visitor, and the answer is the same for everyone. A CDN can cache it. The adapter
122
- never uses the service role key for a read. The adapter also supports the `filters` of a public
123
- query, and puts the created, updated and published-at columns on each item, so a public list can
124
- sort by date.
125
-
126
- - GET and PUT `/api/_pluto/supabase/settings` (PUT needs `settings:manage`; it answers `{ message, data }`)
127
- - POST `/api/_pluto/supabase/auth/signup`
128
- - GET `/api/_pluto/supabase/migrations`, POST `/api/_pluto/supabase/migrations/run` (admin)
129
- - GET `/api/_pluto/supabase/me/capabilities`
130
- - GET and PUT `/api/_pluto/supabase/me/locale`
131
- - GET `/api/_pluto/supabase/setup` (the setup state), POST `/api/_pluto/supabase/setup` (runs the first setup)
132
- - GET `/api/_pluto/supabase/access/users`, GET `/api/_pluto/supabase/access/roles` (permission `users:read`)
133
- - POST `/api/_pluto/supabase/access/users/invite`, POST `/api/_pluto/supabase/access/users/:id/resend` (admin; `locale` is the
134
- optional language of the email)
135
- - POST `/api/_pluto/supabase/access/users/invite-link` (admin; needs the service role key; returns a one-time link
136
- and sends no email)
137
- - GET `/api/_pluto/supabase/access/capabilities` (admin; says whether the service key and Pluto mail are set)
138
- - PUT and DELETE `/api/_pluto/supabase/access/users/:id/roles` (admin; DELETE revokes the access)
139
- - POST `/api/_pluto/supabase/access/roles`, PUT and DELETE `/api/_pluto/supabase/access/roles/:key` (admin)
35
+ ## What it adds
140
36
 
141
- ### Types
37
+ - **Content in Postgres:** a content adapter for the content types of every layer, with history, trash, scheduled publish, and a server copy of unsaved changes. See [Content adapter](https://pluto.vulmix.dev/docs/supabase/content-adapter).
38
+ - **Sign-in:** email and password with Supabase Auth, and the auth driver of the admin. See [Auth](https://pluto.vulmix.dev/docs/supabase/auth).
39
+ - **Users and roles:** invite users by email, and give roles that hold capabilities. See [Users and roles](https://pluto.vulmix.dev/docs/supabase/users-and-roles) and [Invites and email](https://pluto.vulmix.dev/docs/supabase/invites-and-email).
40
+ - **Media:** the media library of core stores its files in Supabase Storage.
41
+ - **Migrations:** each layer ships its database changes. An admin applies them from `/admin/migrations`. See [Migrations](https://pluto.vulmix.dev/docs/supabase/migrations).
142
42
 
143
- - `Database`: Auto-imported type definitions for Supabase database interactions.
43
+ The public site reads the content through the [public API](https://pluto.vulmix.dev/docs/build/public-api) of core. The routes under `/api/_pluto/supabase/` serve the admin. They are not a public API.
144
44
 
145
45
  ## Development
146
46
 
@@ -43,7 +43,7 @@ async function copyEnvLine() {
43
43
  <UAlert
44
44
  :title="t('supabase.envWarning.title')"
45
45
  color="warning"
46
- variant="outline"
46
+ variant="subtle"
47
47
  icon="lucide:triangle-alert"
48
48
  >
49
49
  <template #description>
@@ -84,14 +84,16 @@ async function copyEnvLine() {
84
84
  {{ envLine }}
85
85
  </code>
86
86
 
87
- <UButton
88
- :aria-label="t('supabase.envWarning.copyLine')"
89
- icon="lucide:copy"
90
- variant="soft"
91
- color="neutral"
92
- size="sm"
93
- @click="copyEnvLine"
94
- />
87
+ <UTooltip :text="t('supabase.envWarning.copyLine')">
88
+ <UButton
89
+ :aria-label="t('supabase.envWarning.copyLine')"
90
+ icon="lucide:copy"
91
+ variant="soft"
92
+ color="neutral"
93
+ size="sm"
94
+ @click="copyEnvLine"
95
+ />
96
+ </UTooltip>
95
97
  </div>
96
98
  </div>
97
99
  </template>
@@ -0,0 +1,47 @@
1
+ <script setup lang="ts">
2
+ // The mark of Supabase, in its brand colors, for a page that connects Pluto to it (the database
3
+ // setup). Size it with a class, such as `size-6`.
4
+ const id = useId()
5
+ </script>
6
+
7
+ <template>
8
+ <svg viewBox="0 0 109 113" fill="none" xmlns="http://www.w3.org/2000/svg">
9
+ <path
10
+ :fill="`url(#${id}-a)`"
11
+ d="M63.7076 110.284C60.8481 113.885 55.0502 111.912 54.9813 107.314L53.9738 40.0627L99.1935 40.0627C107.384 40.0627 111.952 49.5228 106.859 55.9374L63.7076 110.284Z"
12
+ />
13
+ <path
14
+ :fill="`url(#${id}-b)`"
15
+ d="M63.7076 110.284C60.8481 113.885 55.0502 111.912 54.9813 107.314L53.9738 40.0627L99.1935 40.0627C107.384 40.0627 111.952 49.5228 106.859 55.9374L63.7076 110.284Z"
16
+ fill-opacity="0.2"
17
+ />
18
+ <path
19
+ d="M45.317 2.07103C48.1765 -1.53037 53.9745 0.442937 54.0434 5.041L54.4849 72.2922H9.83113C1.64038 72.2922 -2.92775 62.8321 2.1655 56.4175L45.317 2.07103Z"
20
+ fill="#3ECF8E"
21
+ />
22
+ <defs>
23
+ <linearGradient
24
+ :id="`${id}-a`"
25
+ x1="53.9738"
26
+ y1="54.974"
27
+ x2="94.1635"
28
+ y2="71.8295"
29
+ gradientUnits="userSpaceOnUse"
30
+ >
31
+ <stop stop-color="#249361" />
32
+ <stop offset="1" stop-color="#3ECF8E" />
33
+ </linearGradient>
34
+ <linearGradient
35
+ :id="`${id}-b`"
36
+ x1="36.1558"
37
+ y1="30.578"
38
+ x2="54.4844"
39
+ y2="65.0806"
40
+ gradientUnits="userSpaceOnUse"
41
+ >
42
+ <stop />
43
+ <stop offset="1" stop-opacity="0" />
44
+ </linearGradient>
45
+ </defs>
46
+ </svg>
47
+ </template>
@@ -43,7 +43,7 @@ const host = (() => {
43
43
  :label="t('supabase.backend.openDashboard')"
44
44
  :to="dashboard"
45
45
  color="neutral"
46
- icon="i-lucide-external-link"
46
+ trailing-icon="i-lucide-arrow-up-right"
47
47
  size="xs"
48
48
  target="_blank"
49
49
  variant="outline"
@@ -72,7 +72,7 @@ const dashboardLinks = computed(() =>
72
72
  key: 'project',
73
73
  label: t('supabase.mail.authEmails.dashboardHome'),
74
74
  to: links.value.project,
75
- icon: 'lucide:external-link',
75
+ icon: 'lucide:layout-dashboard',
76
76
  },
77
77
  ]
78
78
  : []
@@ -95,7 +95,7 @@ const dashboardLinks = computed(() =>
95
95
  :to="RESEND_SUPABASE_INTEGRATION_URL"
96
96
  target="_blank"
97
97
  rel="noopener noreferrer"
98
- icon="lucide:external-link"
98
+ trailing-icon="i-lucide-arrow-up-right"
99
99
  color="neutral"
100
100
  variant="outline"
101
101
  >
@@ -149,6 +149,7 @@ const dashboardLinks = computed(() =>
149
149
  :icon="link.icon"
150
150
  target="_blank"
151
151
  rel="noopener noreferrer"
152
+ trailing-icon="i-lucide-arrow-up-right"
152
153
  color="neutral"
153
154
  variant="outline"
154
155
  size="sm"
@@ -188,17 +188,20 @@ async function applyMigrations(useForm: boolean) {
188
188
  </script>
189
189
 
190
190
  <template>
191
- <AdminView>
192
- <h1 class="text-3xl font-bold lg:text-4xl">
193
- {{ t('supabase.migrations.title') }}
194
- </h1>
191
+ <PlutoViewAdminPanel>
192
+ <template #toolbar>
193
+ <PlutoViewToolbar
194
+ :title="t('supabase.migrations.title')"
195
+ icon="lucide:database-zap"
196
+ />
197
+ </template>
195
198
 
196
199
  <UAlert
197
200
  v-if="isReconnecting"
198
201
  :title="t('supabase.connection.restarting')"
199
202
  :description="t('supabase.migrations.reconnectingBanner')"
200
203
  color="info"
201
- variant="outline"
204
+ variant="subtle"
202
205
  icon="lucide:refresh-cw"
203
206
  />
204
207
 
@@ -218,7 +221,7 @@ async function applyMigrations(useForm: boolean) {
218
221
  :title="t('supabase.migrations.upToDate')"
219
222
  :description="t('supabase.migrations.upToDateDescription')"
220
223
  color="success"
221
- variant="outline"
224
+ variant="subtle"
222
225
  icon="lucide:check-circle"
223
226
  />
224
227
 
@@ -284,14 +287,12 @@ async function applyMigrations(useForm: boolean) {
284
287
  :message="t('supabase.connection.connectionStringHelp')"
285
288
  >
286
289
  <template #link>
287
- <ULink
290
+ <PlutoExternalLink
288
291
  :to="`https://supabase.com/dashboard/project/${projectId}/database/settings?showConnect=true&connectTab=direct&method=transaction`"
289
- target="_blank"
290
292
  class="underline"
291
- external
292
293
  >
293
294
  {{ t('supabase.connection.connectionStringHelpLink') }}
294
- </ULink>
295
+ </PlutoExternalLink>
295
296
  </template>
296
297
  </SupabaseMessageParts>
297
298
  </template>
@@ -310,19 +311,18 @@ async function applyMigrations(useForm: boolean) {
310
311
  <UAlert
311
312
  color="secondary"
312
313
  icon="lucide:badge-info"
313
- variant="outline"
314
+ variant="subtle"
314
315
  >
315
316
  <template #description>
316
317
  <SupabaseMessageParts
317
318
  :message="t('supabase.connection.passwordInfo')"
318
319
  >
319
320
  <template #link>
320
- <ULink
321
+ <PlutoExternalLink
321
322
  to="https://supabase.com/dashboard/project/_/database/settings"
322
- target="_blank"
323
323
  >
324
324
  {{ t('supabase.connection.passwordInfoLink') }}
325
- </ULink>
325
+ </PlutoExternalLink>
326
326
  </template>
327
327
  </SupabaseMessageParts>
328
328
  </template>
@@ -332,7 +332,7 @@ async function applyMigrations(useForm: boolean) {
332
332
  :title="t('supabase.connection.whyTitle')"
333
333
  :description="t('supabase.migrations.why')"
334
334
  color="warning"
335
- variant="outline"
335
+ variant="subtle"
336
336
  icon="lucide:info"
337
337
  />
338
338
 
@@ -341,7 +341,7 @@ async function applyMigrations(useForm: boolean) {
341
341
  :title="t('supabase.connection.restartTitle')"
342
342
  :description="t('supabase.migrations.restartDescription')"
343
343
  color="info"
344
- variant="outline"
344
+ variant="subtle"
345
345
  icon="lucide:refresh-cw"
346
346
  />
347
347
 
@@ -374,11 +374,13 @@ async function applyMigrations(useForm: boolean) {
374
374
  </UCard>
375
375
 
376
376
  <UCard v-if="lastRun">
377
- <div class="flex flex-col gap-y-4">
377
+ <template #header>
378
378
  <h2 class="font-semibold">
379
379
  {{ t('supabase.migrations.lastRun') }}
380
380
  </h2>
381
+ </template>
381
382
 
383
+ <div class="flex flex-col gap-y-4">
382
384
  <EnvPersistWarning
383
385
  v-if="lastRun.success && lastRun.persisted === false"
384
386
  :connection-string="lastConnectionString"
@@ -410,7 +412,7 @@ async function applyMigrations(useForm: boolean) {
410
412
  :title="t('supabase.migrations.loadFailed')"
411
413
  :description="t('supabase.migrations.loadFailedDescription')"
412
414
  color="error"
413
- variant="outline"
415
+ variant="subtle"
414
416
  icon="lucide:circle-x"
415
417
  >
416
418
  <template #actions>
@@ -424,5 +426,5 @@ async function applyMigrations(useForm: boolean) {
424
426
  </UButton>
425
427
  </template>
426
428
  </UAlert>
427
- </AdminView>
429
+ </PlutoViewAdminPanel>
428
430
  </template>