Supabase
Sign users in with Supabase Auth and read and write tables through PostgREST.
@chelone/supabase gives an admin two providers for a Supabase project: an
auth provider over Supabase Auth and a data provider over the project's
PostgREST API. The package is framework-free, so the same code serves
@chelone/react and @chelone/vue. Only the callback page wiring in
Recovery and invite links differs.
This page covers the package. The login, forgot password and set password pages are a separate part of the Supabase integration and have their own guide.
Install
pnpm add @chelone/supabase @supabase/supabase-js@supabase/supabase-js is a peer dependency (^2.48.1), so your app owns the
version. Create the client once and share it with both providers:
import { createClient } from "@supabase/supabase-js";
export const supabase = createClient(
import.meta.env.VITE_SUPABASE_URL,
import.meta.env.VITE_SUPABASE_ANON_KEY,
);Auth provider
supabaseAuthProvider(client, options?) returns an
auth provider backed by client.auth.
import { supabaseAuthProvider } from "@chelone/supabase";
import { supabase } from "./supabase";
<Admin authProvider={supabaseAuthProvider(supabase)} requireAuth>
...
</Admin>;In Vue, pass the same value as the authProvider prop of <Admin>.
| Method | What it does |
|---|---|
login | { email, password } calls signInWithPassword; { provider } starts OAuth. Any other params throw. |
logout | signOut. |
checkAuth | Rejects when getSession returns no session. |
checkError | Rejects on a 401, a 403, or a 400 AuthSessionMissingError, which logs the user out. |
getIdentity | Reads the signed-in user, see Identity. |
getPermissions | Resolves options.getPermissions(user), or undefined without that option. |
setPassword | { access_token, refresh_token, password }: sets the session, then the password. |
resetPassword | { email, redirectTo?, captchaToken? }: sends the reset email. |
handleCallback | Resolves a recovery or invite link, see below. |
Options
getIdentity(user)replaces the identity read from the user's metadata. It may return a promise.getPermissions(user)receives the signed-in user. Its result is whatgetPermissionsresolves.redirectTois where the OAuth provider sends the user back. AredirectTopassed tologinwins. It is also the path prefix of the set-password redirect thathandleCallbackreturns.
supabaseAuthProvider(supabase, {
redirectTo: `${window.location.origin}/auth-callback`,
getPermissions: (user) => user.app_metadata.role,
});OAuth sign-in
Pass the provider name to login:
await authProvider.login({ provider: "github" });SupabaseOAuthProvider lists the names the type accepts: apple, azure,
bitbucket, discord, facebook, github, gitlab, google, keycloak,
linkedin, notion, slack, spotify, twitch, twitter and workos.
Supabase sends the user to the provider and back to redirectTo. Set that URL
to the app's /auth-callback route so handleCallback runs when the user
returns.
Identity
Without a getIdentity option, the identity is built from the user:
idis the user'sid.fullNameisuser_metadata.full_name, falling back to the email.avatarisuser_metadata.avatar_url.
Recovery and invite links
A password recovery or invite email carries the user's tokens in a link. Both need the same setup:
- In the Supabase dashboard, under Authentication, URL Configuration, add the
app's
/auth-callbackroute to the redirect URLs, for examplehttps://admin.example.com/auth-callback. Pass the same URL asredirectTotoresetPassword. - Mount the callback page. Chelone's gate ignores a redirect on a
checkAuthrejection, so an unauthenticated user cannot be redirected to the callback page. The route is public and sits outside the gate.
In React, set the authCallback prop to an element:
import { AuthCallback } from "@/components/admin/auth/auth-callback";
<Admin
authProvider={supabaseAuthProvider(supabase)}
authCallback={<AuthCallback />}
>
...
</Admin>;In Vue, fill the #authCallback slot of <Admin> with your callback page. The
route is /auth-callback, outside the layout and the auth gate.
handleCallback reads access_token, refresh_token and type from the URL,
whether they sit in the query or in the hash.
- For
typeofrecoveryorinvite, it returns a redirect to/set-password(behind theredirectTooption, if set) with the tokens as query params.forgotPasswordPathandsetPasswordPathexport the two paths. - For anything else it returns nothing. An OAuth return is read from the URL by
supabase-jsitself. - A recovery or invite link without an access or refresh token is an error:
handleCallbackrejects with "Missing access_token or refresh_token for a recovery or invite link". The callback page shows it.
readSupabaseUrlParams(location?) is the reader handleCallback uses. It
returns { accessToken, refreshToken, type }, each null when absent.
Messages
The Supabase pages read their text from message keys. The package ships
supabaseEnglishMessages and supabaseFrenchMessages. Merge them into the
catalog you give createI18n, after defaultMessages:
import { createI18n, defaultMessages } from "@chelone/core";
import {
supabaseEnglishMessages,
supabaseFrenchMessages,
} from "@chelone/supabase";
export const i18n = createI18n({
locale: "en",
messages: {
en: { ...defaultMessages, ...supabaseEnglishMessages },
fr: { ...defaultMessages, ...supabaseFrenchMessages },
},
fallbackLocale: "en",
});Keys keep the ra-supabase. prefix (for example ra-supabase.auth.email), so
a catalog written for ra-supabase carries over. %{provider} placeholders are
written {provider}. See Translation for the
catalog itself.
Data provider
supabaseDataProvider(options) is a data provider
over <instanceUrl>/rest/v1.
import { supabaseDataProvider } from "@chelone/supabase";
import { supabase } from "./supabase";
export const dataProvider = supabaseDataProvider({
instanceUrl: import.meta.env.VITE_SUPABASE_URL,
apiKey: import.meta.env.VITE_SUPABASE_ANON_KEY,
client: supabase,
});Options
instanceUrlis the project URL.apiKeyis a legacy anon key or asb_publishable_key.clientsupplies the session. Each call sends the user's access token, so row level security applies. With noclient, or no session, noAuthorizationheader goes out and the key is the only credential.defaultListOpis the operator for a filter key with no@opsuffix. It defaults toeq.primaryKeysmaps a resource to its key columns. A resource not listed is keyed byid.schemais a schema name, or a function returning one, for projects that expose a schema other thanpublic.sortOrdersets where nulls go:AscendingNullsLastDescendingNullsFirst(the default),AscendingNullsFirstDescendingNullsLast,AscendingNullsFirstDescendingNullsFirstorAscendingNullsLastDescendingNullsLast.
Content-Range
getList asks PostgREST for an exact count and reads the total from the
Content-Range response header. A missing header is an error. When the browser
calls a project from another origin, the API must expose it with
Access-Control-Expose-Headers: Content-Range. A total of * becomes an
undefined total.
Meta
Each call takes these keys in meta:
meta.columnsis a column list, as a string or an array. The default is*.meta.embedis a list of related tables. Each becomesname(*)in the select, soembed: ["comments"]selects*,comments(*).meta.schemaoverrides theschemaoption for this call. Reads send it asAccept-Profile, writes asContent-Profile.meta.nullsfirstandmeta.nullslasttaketruefor every sort column or a list of columns, and win oversortOrder.meta.headersadds request headers and wins over the ones the provider sets.
<List meta={{ columns: ["id", "title"], embed: ["author"] }} />Compound keys
List the key columns in primaryKeys:
supabaseDataProvider({
instanceUrl,
apiKey,
client: supabase,
primaryKeys: { order_items: ["order_id", "line"] },
});Chelone needs one id per record. For a compound key the provider adds an id
that is the JSON array of the key values, such as [12,3], and removes it again
before a write. A sort on id sorts on every key column. An id that does not
decode to that many values is an error.
Filters
A filter key is a column name, optionally followed by @ and an operator:
field@op. Without a suffix the key uses defaultListOp.
const filter = {
"status@in": ["draft", "review"],
"views@gte": 100,
"title@ilike": "launch",
};| Operator | PostgREST meaning |
|---|---|
eq, neq | equal, not equal |
gt, gte, lt, lte | greater, greater or equal, less, less or equal |
like, ilike | pattern match, case-insensitive pattern match |
match, imatch | regular expression match, case-insensitive |
in | one of a list |
is | null, true, false |
isdistinct | not equal, treating null as a value |
fts, plfts, phfts, wfts | full-text search: default, plain, phrase, web |
cs, cd, ov | contains, contained in, overlaps (arrays, ranges, JSON) |
sl, sr, nxr, nxl, adj | range: left of, right of, no extend right, no extend left, adjacent |
not, all, any | the PostgREST modifiers |
likeandilikesplit the value on spaces and add one condition per word, each wrapped in*: a search forlaunch planmatches rows containing both words.- An array value with
inbecomesin.(a,b). Withcs,cdorovit becomes a Postgres array literal,{a,b}. With another operator the items join with commas. nullwitheqbecomesis.null, and withneqbecomesnot.is.null.- A
Datebecomes its ISO string. An object value on a key with an operator is sent as JSON. An object value on a plain key nests the column path:{ author: { name: "Ada" } }filtersauthor.name. - A value of
undefinedis skipped.
or and and
The keys or and and take an object of conditions and produce a PostgREST
logic group. Values inside the group are quoted when they hold a comma,
parenthesis, quote or backslash.
{
or: {
"status@eq": "draft",
"and": { "views@gte": 100, "views@lt": 500 },
},
}This sends or=(status.eq.draft,and(views.gte.100,views.lt.500)).
Errors
A 401 or 403 from PostgREST is an HttpError. Chelone passes it to
checkError on supabaseAuthProvider, which rethrows it and logs the user out.
Pass supabaseAuthProvider to <Admin> alongside the data provider for this to
apply.
getOne, update and delete turn PostgREST's 406 for a missing row into a
404.
What is not supported
- Cursor pagination. A
getListwith a cursor throws "supabaseDataProvider does not support cursor pagination". Use page andperPage. rpc/resources. Only tables and views are read and written.- Guessers.
ListGuesser,EditGuesserandShowGuesserhave no column information to guess from, so write the fields and inputs. - A realtime transport. The package has no Supabase Realtime adapter. To add live updates, supply your own transport as described in Realtime.