mirror of
https://github.com/supabase/supabase.git
synced 2026-10-19 08:09:11 +03:00
Merge branch 'master' into feat/event-collections
This commit is contained in:
commit
8f3a35eafd
1651 files changed
+24912
-16241
No files matched your search
@@ -0,0 +1,19 @@
|
||||
## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file.
|
||||
|
||||
YES/NO
|
||||
|
||||
## What kind of change does this PR introduce?
|
||||
|
||||
Bug fix, feature, docs update, ...
|
||||
|
||||
## What is the current behavior?
|
||||
|
||||
Please link any relevant issues here.
|
||||
|
||||
## What is the new behavior?
|
||||
|
||||
Feel free to include screenshots if it includes visual changes.
|
||||
|
||||
## Additional context
|
||||
|
||||
Add any other context or screenshots.
|
||||
@@ -8,5 +8,7 @@ mdx = md
|
||||
|
||||
[*]
|
||||
BasedOnStyles = Vale, Custom
|
||||
BlockIgnores = (?s) *(<StepHikeCompact>.*?</StepHikeCompact>)
|
||||
TokenIgnores = (href=".*?")
|
||||
BlockIgnores = (?s) *(<StepHikeCompact>.*?</StepHikeCompact>), \
|
||||
(?s) *(export\s+const\s+\S+\s*=\s*\[.*?\]), \
|
||||
(?s) *(<Image.*?/>)
|
||||
TokenIgnores = (href\s*([=:])\s*['"].*?\1), \
|
||||
@@ -0,0 +1,35 @@
|
||||
# CONTRIBUTING.md
|
||||
|
||||
Thank you for contributing to Supabase! We’re a big, exciting open source project and we’d love to have you contribute! Here’s some resources and guidance to help you get started:
|
||||
|
||||
[1. Getting Started](#getting-started)
|
||||
[2. Issues](#issues)
|
||||
[3. Pull Requests](#pull-requests)
|
||||
|
||||
## Getting Started
|
||||
|
||||
To ensure a positive and inclusive environment, please read our [code of conduct](https://github.com/supabase/.github/blob/main/CODE_OF_CONDUCT.md) before contributing. For help setting up the code in this repo, please follow our [DEVELOPERS.md](https://github.com/supabase/supabase/blob/master/DEVELOPERS.md) file. For the [docs](https://supabase.com/docs) site, follow this [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/apps/docs/CONTRIBUTING.md) guide.
|
||||
|
||||
## Issues
|
||||
|
||||
If you find a bug, please create an Issue and we’ll triage it.
|
||||
|
||||
- Please search [existing Issues](https://github.com/supabase/supabase) before creating a new one.
|
||||
- Please include a clear description of the problem along with steps to reproduce it. Exact steps with screenshots and urls really help here.
|
||||
|
||||
## Pull Requests
|
||||
|
||||
We actively welcome your Pull Requests! A couple of things to keep in mind before you submit:
|
||||
|
||||
- If you’re fixing an Issue, make sure someone else hasn’t already created a PR fixing the same issue. Likewise, make sure to link your PR to the related Issue(s).
|
||||
- We will always try to accept the first viable PR that resolves the Issue.
|
||||
- If you're new, we encourage you to take a look at issues tagged with [good first issue](https://github.com/supabase/supabase/pulls?q=is%3Aopen+is%3Apr+label%3A%22good+first+issue%22).
|
||||
- If you’re submitting a new feature, make sure you have opened a [Discussion](https://github.com/orgs/supabase/discussions/new/choose) to discuss the new feature before opening a PR. We’d love to accept your hard work, but unfortunately if a feature hasn’t gone through a proper design process, your PR will be closed.
|
||||
- Please use the PR message template and provide detailed context for quicker review. PRs without clear problem statements will be closed.
|
||||
|
||||
Prior to submitting your PR, please conduct the following pre-flight checks:
|
||||
|
||||
- Run `npm run build` locally to ensure that your code builds successfully without having to wait on us to approve Vercel Preview deploys.
|
||||
- Ensure that the Prettier tests run successfully on your PR.
|
||||
|
||||
Running these before you create the PR will help reduce back and forth with the team.
|
||||
@@ -1,18 +1,18 @@
|
||||
'use client'
|
||||
|
||||
import { useAppStateSnapshot } from '@/lib/state'
|
||||
import { DialogContent_Shadcn_, Dialog_Shadcn_ } from 'ui'
|
||||
import { DialogContent, Dialog } from 'ui'
|
||||
|
||||
const LoginDialog = ({ children }: { children: React.ReactNode }) => {
|
||||
const snap = useAppStateSnapshot()
|
||||
|
||||
return (
|
||||
<Dialog_Shadcn_
|
||||
<Dialog
|
||||
open={snap.loginDialogOpen}
|
||||
onOpenChange={() => snap.setLoginDialogOpen(!snap.loginDialogOpen)}
|
||||
>
|
||||
<DialogContent_Shadcn_ className="max-w-md">{children}</DialogContent_Shadcn_>
|
||||
</Dialog_Shadcn_>
|
||||
<DialogContent className="max-w-md">{children}</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import type { NextRequest } from 'next/server'
|
||||
|
||||
// Redirect to supabase.com until this site is live
|
||||
// when ready, remove the VERCEL env var at
|
||||
// https://vercel.com/supabase/database-new/settings/environment-variables
|
||||
export function middleware(request: NextRequest) {
|
||||
if (process.env.VERCEL === '1') {
|
||||
return NextResponse.redirect(new URL('https://supabase.com', request.url))
|
||||
}
|
||||
}
|
||||
@@ -42,7 +42,7 @@ const Favicons = () => {
|
||||
<link rel="shortcut icon" href={`${basePath}/favicon/favicon.ico`} />
|
||||
<link rel="icon" type="image/png" href={`${basePath}/favicon/favicon.ico`} />
|
||||
{/* misc */}
|
||||
<link rel="manifest" href={`${basePath}/favicon/site.webmanifest`} />
|
||||
<link rel="manifest" href={`${basePath}/favicon/manifest.json`} />
|
||||
<meta name="msapplication-config" content={`${basePath}/favicon/browserconfig.xml`} />
|
||||
<link rel="alternate" type="application/rss+xml" href={`${basePath}/feed.xml`} />
|
||||
</Head>
|
||||
|
||||
@@ -0,0 +1,204 @@
|
||||
import { useSupabaseClient } from '@supabase/auth-helpers-react'
|
||||
import { Check, MessageSquareQuote, X } from 'lucide-react'
|
||||
import { usePathname } from 'next/navigation'
|
||||
import {
|
||||
type CSSProperties,
|
||||
type MouseEventHandler,
|
||||
forwardRef,
|
||||
useReducer,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react'
|
||||
|
||||
import { useIsLoggedIn } from 'common'
|
||||
import { Button, cn } from 'ui'
|
||||
|
||||
import { useSendFeedbackMutation } from '~/lib/fetch/feedback'
|
||||
import { useSendTelemetryEvent } from '~/lib/telemetry'
|
||||
import { FeedbackModal, type FeedbackFields } from './FeedbackModal'
|
||||
|
||||
const FeedbackButton = forwardRef<
|
||||
HTMLButtonElement,
|
||||
{ isYes: boolean; onClick: MouseEventHandler; visible: boolean }
|
||||
>(({ isYes, onClick, visible }, ref) => {
|
||||
const isLoggedIn = useIsLoggedIn()
|
||||
if (!isLoggedIn) return null
|
||||
|
||||
return (
|
||||
<button
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'mt-0',
|
||||
'flex items-center gap-2',
|
||||
'text-xs text-foreground-lighter',
|
||||
'hover:text-foreground',
|
||||
!visible && 'opacity-0 invisible',
|
||||
'[transition-property:opacity,color]',
|
||||
'[transition-delay:700ms,0ms]'
|
||||
)}
|
||||
onClick={onClick}
|
||||
>
|
||||
{isYes ? <>What went well?</> : <>How can we improve?</>}
|
||||
<MessageSquareQuote size={14} strokeWidth={1.5} />
|
||||
</button>
|
||||
)
|
||||
})
|
||||
FeedbackButton.displayName = 'FeedbackButton'
|
||||
|
||||
type Response = 'yes' | 'no'
|
||||
|
||||
enum StateType {
|
||||
Unanswered = 'unanswered',
|
||||
Followup = 'followup',
|
||||
}
|
||||
|
||||
type State = { type: StateType.Unanswered } | { type: StateType.Followup; response: Response }
|
||||
|
||||
type Action = { event: 'VOTED'; response: Response }
|
||||
|
||||
const initialState = { type: StateType.Unanswered } satisfies State
|
||||
|
||||
function reducer(state: State, action: Action) {
|
||||
switch (action.event) {
|
||||
case 'VOTED':
|
||||
if (state.type === StateType.Unanswered)
|
||||
return { type: StateType.Followup, response: action.response }
|
||||
default:
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
||||
function Feedback() {
|
||||
const [state, dispatch] = useReducer(reducer, initialState)
|
||||
const [modalOpen, setModalOpen] = useState(false)
|
||||
const feedbackButtonRef = useRef<HTMLButtonElement>(null)
|
||||
|
||||
const pathname = usePathname()
|
||||
const sendTelemetryEvent = useSendTelemetryEvent()
|
||||
const { mutate: sendFeedbackComment } = useSendFeedbackMutation()
|
||||
const supabase = useSupabaseClient()
|
||||
|
||||
const unanswered = state.type === 'unanswered'
|
||||
const isYes = 'response' in state && state.response === 'yes'
|
||||
const isNo = 'response' in state && state.response === 'no'
|
||||
const showYes = unanswered || isYes
|
||||
const showNo = unanswered || isNo
|
||||
|
||||
async function sendFeedbackVote(response: Response) {
|
||||
const { error } = await supabase.from('feedback').insert({ vote: response, page: pathname })
|
||||
if (error) console.error(error)
|
||||
}
|
||||
|
||||
function handleVote(response: Response) {
|
||||
sendTelemetryEvent({
|
||||
category: 'docs',
|
||||
action: 'feedback_voted',
|
||||
label: response,
|
||||
})
|
||||
sendFeedbackVote(response)
|
||||
dispatch({ event: 'VOTED', response })
|
||||
// Focus so screen reader users are aware of the new element
|
||||
setTimeout(() => {
|
||||
feedbackButtonRef.current?.focus()
|
||||
// Wait for element to show up first
|
||||
}, 700)
|
||||
}
|
||||
|
||||
function refocusButton() {
|
||||
setTimeout(() => {
|
||||
feedbackButtonRef.current?.focus()
|
||||
// Wait for modal to disappear and button to become focusable again
|
||||
}, 100)
|
||||
}
|
||||
|
||||
async function handleSubmit({ page, comment }: FeedbackFields) {
|
||||
sendFeedbackComment({ message: comment, pathname: page })
|
||||
setModalOpen(false)
|
||||
refocusButton()
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="@container px-5 mb-6" aria-labelledby="feedback-title">
|
||||
<h3 id="feedback-title" className="block font-mono text-xs uppercase text-foreground mb-4">
|
||||
Is this helpful?
|
||||
</h3>
|
||||
<div className="relative flex flex-col gap-y-4 mb-2 @[12rem]:flex-row @[12rem]:items-center">
|
||||
<div
|
||||
style={{ '--container-flex-gap': '0.5rem' } as CSSProperties}
|
||||
className={`relative flex gap-[var(--container-flex-gap)] items-center`}
|
||||
>
|
||||
<Button
|
||||
type="outline"
|
||||
rounded
|
||||
className={cn(
|
||||
'px-1',
|
||||
!isYes && 'hover:text-brand-600 hover:border-brand-500',
|
||||
isYes && 'bg-brand text-brand-200 !border-brand disabled:opacity-100',
|
||||
!showYes && 'opacity-0 invisible',
|
||||
'[transition-property:opacity,transform,color] [transition-duration:150ms,300ms,300ms]',
|
||||
'motion-reduce:[transition-duration:150ms,1ms,300ms]'
|
||||
)}
|
||||
onClick={() => handleVote('yes')}
|
||||
disabled={state.type === StateType.Followup}
|
||||
>
|
||||
<Check size={14} strokeWidth={3} />
|
||||
<span className="sr-only">Yes</span>
|
||||
</Button>
|
||||
<Button
|
||||
type="outline"
|
||||
rounded
|
||||
className={cn(
|
||||
'px-1',
|
||||
!isNo && 'hover:text-warning-600 hover:border-warning-500',
|
||||
isNo &&
|
||||
`bg-warning text-warning-200 !border-warning -translate-x-[calc(100%+var(--container-flex-gap,0.5rem))] disabled:opacity-100`,
|
||||
!showNo && 'opacity-0 invisible',
|
||||
'[transition-property:opacity,transform,color] [transition-duration:150ms,300ms,300ms]',
|
||||
'motion-reduce:[transition-duration:150ms,1ms,300ms]'
|
||||
)}
|
||||
onClick={() => handleVote('no')}
|
||||
disabled={state.type === StateType.Followup}
|
||||
>
|
||||
<X size={14} strokeWidth={3} />
|
||||
<span className="sr-only">No</span>
|
||||
</Button>
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
'flex flex-col gap-1',
|
||||
'text-xs',
|
||||
'opacity-0 invisible',
|
||||
'text-left',
|
||||
'-translate-x-[0.25rem] @[12rem]:-translate-x-[1.25rem]',
|
||||
'[transition-property:opacity,transform] [transition-duration:150ms,300ms]',
|
||||
'motion-reduce:[transition-duration:150ms,1ms]',
|
||||
'delay-300',
|
||||
state.type === StateType.Followup &&
|
||||
'opacity-100 visible -translate-x-0 @[12rem]:-translate-x-[1rem]'
|
||||
)}
|
||||
>
|
||||
<span className="text-foreground-light">Thanks for your feedback!</span>
|
||||
{/**
|
||||
<FeedbackButton
|
||||
ref={feedbackButtonRef}
|
||||
onClick={() => setModalOpen(true)}
|
||||
isYes={isYes}
|
||||
visible={!unanswered}
|
||||
/>
|
||||
*/}
|
||||
</div>
|
||||
</div>
|
||||
<FeedbackModal
|
||||
visible={modalOpen}
|
||||
page={pathname}
|
||||
onCancel={() => {
|
||||
setModalOpen(false)
|
||||
refocusButton()
|
||||
}}
|
||||
onSubmit={handleSubmit}
|
||||
/>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
export { Feedback }
|
||||
@@ -0,0 +1,65 @@
|
||||
import { Button, Form, Input, Modal } from 'ui'
|
||||
|
||||
export type FeedbackFields = {
|
||||
page: string
|
||||
comment: string
|
||||
}
|
||||
|
||||
type FeedbackModalProps = {
|
||||
visible: boolean
|
||||
page: string
|
||||
onCancel: () => void
|
||||
onSubmit: (values: FeedbackFields) => void
|
||||
}
|
||||
|
||||
function FeedbackModal({ visible, page, onCancel, onSubmit }: FeedbackModalProps) {
|
||||
return (
|
||||
<Modal hideFooter header="Leave a comment" visible={visible} onEscapeKeyDown={onCancel}>
|
||||
<Form
|
||||
initialValues={{ page, comment: '' }}
|
||||
validateOnBlur
|
||||
validate={(vals) => {
|
||||
const errors: Partial<FeedbackFields> = {}
|
||||
|
||||
if (!vals.comment) {
|
||||
errors.comment = 'Required'
|
||||
}
|
||||
|
||||
return errors
|
||||
}}
|
||||
onReset={onCancel}
|
||||
onSubmit={onSubmit}
|
||||
>
|
||||
{({ isSubmitting }: { isSubmitting: boolean }) => (
|
||||
<>
|
||||
<Modal.Content className="pt-4 pb-2">
|
||||
<Input type="hidden" id="page" name="page" value={page} />
|
||||
<Input.TextArea
|
||||
label="Comment"
|
||||
id="comment"
|
||||
name="comment"
|
||||
size="medium"
|
||||
className="mb-2"
|
||||
textAreaClassName="resize-none"
|
||||
afterLabel=" (not anonymous)"
|
||||
/>
|
||||
</Modal.Content>
|
||||
<Modal.Separator />
|
||||
<Modal.Content className="pt-2 pb-4">
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<Button htmlType="reset" type="default" onClick={onCancel} disabled={isSubmitting}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button htmlType="submit" loading={isSubmitting} disabled={isSubmitting}>
|
||||
Submit feedback
|
||||
</Button>
|
||||
</div>
|
||||
</Modal.Content>
|
||||
</>
|
||||
)}
|
||||
</Form>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export { FeedbackModal }
|
||||
@@ -0,0 +1 @@
|
||||
export { Feedback } from './Feedback'
|
||||
@@ -1,111 +0,0 @@
|
||||
If you upload additional profile photos, they'll accumulate
|
||||
in the `avatars` bucket because of their random names with only the latest being referenced
|
||||
from `public.profiles` and the older versions getting orphaned.
|
||||
|
||||
To automatically remove obsolete storage objects, extend the database
|
||||
triggers. Note that it is not sufficient to delete the objects from the
|
||||
`storage.objects` table because that would orphan and leak the actual storage objects in
|
||||
the S3 backend. Instead, invoke the storage API within Postgres via the `http` extension.
|
||||
|
||||
Enable the [http extension for the `extensions` schema](https://supabase.com/dashboard/project/_/database/extensions) in the Dashboard.
|
||||
Then, define the following SQL functions in the SQL Editor to delete
|
||||
storage objects via the API:
|
||||
|
||||
```sql
|
||||
create or replace function delete_storage_object(bucket text, object text, out status int, out content text)
|
||||
returns record
|
||||
language 'plpgsql'
|
||||
security definer
|
||||
as $$
|
||||
declare
|
||||
project_url text := '<YOURPROJECTURL>';
|
||||
service_role_key text := '<YOURSERVICEROLEKEY>'; -- full access needed
|
||||
url text := project_url||'/storage/v1/object/'||bucket||'/'||object;
|
||||
begin
|
||||
select
|
||||
into status, content
|
||||
result.status::int, result.content::text
|
||||
FROM extensions.http((
|
||||
'DELETE',
|
||||
url,
|
||||
ARRAY[extensions.http_header('authorization','Bearer '||service_role_key)],
|
||||
NULL,
|
||||
NULL)::extensions.http_request) as result;
|
||||
end;
|
||||
$$;
|
||||
|
||||
create or replace function delete_avatar(avatar_url text, out status int, out content text)
|
||||
returns record
|
||||
language 'plpgsql'
|
||||
security definer
|
||||
as $$
|
||||
begin
|
||||
select
|
||||
into status, content
|
||||
result.status, result.content
|
||||
from public.delete_storage_object('avatars', avatar_url) as result;
|
||||
end;
|
||||
$$;
|
||||
|
||||
```
|
||||
|
||||
Next, add a trigger that removes any obsolete avatar whenever the
|
||||
profile is updated or deleted:
|
||||
|
||||
```sql
|
||||
create or replace function delete_old_avatar()
|
||||
returns trigger
|
||||
language 'plpgsql'
|
||||
security definer
|
||||
as $$
|
||||
declare
|
||||
status int;
|
||||
content text;
|
||||
avatar_name text;
|
||||
begin
|
||||
if coalesce(old.avatar_url, '') <> ''
|
||||
and (tg_op = 'DELETE' or (old.avatar_url <> coalesce(new.avatar_url, ''))) then
|
||||
-- extract avatar name
|
||||
avatar_name := old.avatar_url;
|
||||
select
|
||||
into status, content
|
||||
result.status, result.content
|
||||
from public.delete_avatar(avatar_name) as result;
|
||||
if status <> 200 then
|
||||
raise warning 'Could not delete avatar: % %', status, content;
|
||||
end if;
|
||||
end if;
|
||||
if tg_op = 'DELETE' then
|
||||
return old;
|
||||
end if;
|
||||
return new;
|
||||
end;
|
||||
$$;
|
||||
|
||||
create trigger before_profile_changes
|
||||
before update of avatar_url or delete on public.profiles
|
||||
for each row execute function public.delete_old_avatar();
|
||||
|
||||
```
|
||||
|
||||
Finally, delete the `public.profile` row before a user is deleted.
|
||||
If this step is omitted, you won't be able to delete users without
|
||||
first manually deleting their avatar image.
|
||||
|
||||
```sql
|
||||
create or replace function delete_old_profile()
|
||||
returns trigger
|
||||
language 'plpgsql'
|
||||
security definer
|
||||
as $$
|
||||
begin
|
||||
delete from public.profiles where id = old.id;
|
||||
return old;
|
||||
end;
|
||||
$$;
|
||||
|
||||
create trigger before_delete_user
|
||||
before delete on auth.users
|
||||
for each row execute function public.delete_old_profile();
|
||||
|
||||
```
|
||||
@@ -40,11 +40,7 @@ const NavigationMenuHome = () => {
|
||||
>
|
||||
{link?.icon && <HomeMenuIconPicker icon={link.icon} />}
|
||||
{link.label}
|
||||
{link.community && (
|
||||
<Badge size="small" color="scale">
|
||||
Community
|
||||
</Badge>
|
||||
)}
|
||||
{link.community && <Badge size="small">Community</Badge>}
|
||||
</li>
|
||||
</Link>
|
||||
)
|
||||
|
||||
@@ -264,7 +264,7 @@ export const gettingstarted: NavMenuConstant = {
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Web app tutorials',
|
||||
name: 'Web app demos',
|
||||
items: [
|
||||
{
|
||||
name: 'Next.js',
|
||||
@@ -492,6 +492,12 @@ export const PhoneLoginsItems = [
|
||||
isDarkMode: true,
|
||||
hasLightIcon: true,
|
||||
},
|
||||
{
|
||||
name: 'Textlocal SMS Login (Community Supported)',
|
||||
icon: '/docs/img/icons/textlocal-icon',
|
||||
url: '/guides/auth/phone-login/textlocal',
|
||||
linkDescription: 'Textlocal is a cloud-based SMS platform offering bulk messaging services.',
|
||||
},
|
||||
]
|
||||
|
||||
export const auth = {
|
||||
@@ -607,6 +613,10 @@ export const auth = {
|
||||
name: 'Column Level Security',
|
||||
url: '/guides/auth/column-level-security',
|
||||
},
|
||||
{
|
||||
name: 'Custom Claims & RBAC',
|
||||
url: '/guides/auth/custom-claims-and-role-based-access-control-rbac',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -690,6 +700,12 @@ export const database: NavMenuConstant = {
|
||||
{
|
||||
name: 'Connecting to your database',
|
||||
url: '/guides/database/connecting-to-postgres',
|
||||
items: [
|
||||
{
|
||||
name: 'Serverless Drivers',
|
||||
url: '/guides/database/connecting-to-postgres/serverless-drivers',
|
||||
},
|
||||
],
|
||||
},
|
||||
{ name: 'Importing data', url: '/guides/database/import-data' },
|
||||
{ name: 'Securing your data', url: '/guides/database/secure-data' },
|
||||
@@ -1118,6 +1134,10 @@ export const functions: NavMenuConstant = {
|
||||
name: 'Debugging Edge Functions',
|
||||
url: '/guides/functions/debugging',
|
||||
},
|
||||
{
|
||||
name: 'Logging and Using Headers',
|
||||
url: '/guides/functions/functions-headers',
|
||||
},
|
||||
{
|
||||
name: 'Testing your Edge Functions',
|
||||
url: '/guides/functions/unit-test',
|
||||
@@ -1187,6 +1207,10 @@ export const functions: NavMenuConstant = {
|
||||
name: 'Hugging Face',
|
||||
url: '/guides/ai/examples/huggingface-image-captioning',
|
||||
},
|
||||
{
|
||||
name: 'Monitoring with Sentry',
|
||||
url: '/guides/functions/examples/sentry-monitoring',
|
||||
},
|
||||
{ name: 'OpenAI API', url: '/guides/ai/examples/openai' },
|
||||
{
|
||||
name: 'Sending Emails with Resend',
|
||||
@@ -1387,6 +1411,7 @@ export const ai = {
|
||||
url: '/guides/ai/vector-indexes',
|
||||
items: vectorIndexItems,
|
||||
},
|
||||
{ name: 'Hybrid search', url: '/guides/ai/hybrid-search' },
|
||||
{
|
||||
name: 'Engineering for scale',
|
||||
url: '/guides/ai/engineering-for-scale',
|
||||
@@ -1579,6 +1604,15 @@ export const platform: NavMenuConstant = {
|
||||
{ name: 'Read Replicas', url: '/guides/platform/read-replicas' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Logging and observability',
|
||||
url: undefined,
|
||||
items: [
|
||||
{ name: 'Logging', url: '/guides/platform/logs' },
|
||||
{ name: 'Metrics', url: '/guides/platform/metrics' },
|
||||
{ name: 'Monitoring with Sentry', url: '/guides/platform/sentry-monitoring' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Platform Management',
|
||||
url: undefined,
|
||||
@@ -1594,8 +1628,6 @@ export const platform: NavMenuConstant = {
|
||||
name: 'HTTP Status Codes',
|
||||
url: '/guides/platform/http-status-codes',
|
||||
},
|
||||
{ name: 'Logging', url: '/guides/platform/logs' },
|
||||
{ name: 'Metrics', url: '/guides/platform/metrics' },
|
||||
{
|
||||
name: 'Migrating and Upgrading',
|
||||
url: '/guides/platform/migrating-and-upgrading-projects',
|
||||
|
||||
@@ -2,7 +2,8 @@ import { useTheme } from 'next-themes'
|
||||
import Image from 'next/legacy/image'
|
||||
import Link from 'next/link'
|
||||
import { FC } from 'react'
|
||||
import { Button, IconCommand, IconGitHub, IconSearch, SearchButton } from 'ui'
|
||||
import { Button, IconCommand, IconGitHub, IconSearch } from 'ui'
|
||||
import { SearchButton } from 'ui-patterns/Cmdk'
|
||||
import { useIsLoggedIn, useIsUserLoading } from 'common'
|
||||
import { ThemeToggle } from 'ui-patterns/ThemeToggle'
|
||||
|
||||
@@ -14,7 +15,7 @@ const TopNavBar: FC = () => {
|
||||
return (
|
||||
<nav
|
||||
aria-label="top bar"
|
||||
className="h-[60px] border-b backdrop-blur backdrop-filter bg bg-opacity-75"
|
||||
className="h-[var(--desktop-header-height,60px)] border-b backdrop-blur backdrop-filter bg bg-opacity-75"
|
||||
>
|
||||
<div className="px-5 max-w-7xl mx-auto flex gap-3 justify-between items-center h-full">
|
||||
<div className="lg:hidden">
|
||||
|
||||
@@ -58,7 +58,7 @@ const projectsStore = proxy({
|
||||
selectedProject: null as Project | null,
|
||||
setSelectedOrgProject: (org: Org | null, project: Project | null) => {
|
||||
projectsStore.selectedOrg = org
|
||||
storeOrRemoveNull('local', LOCAL_STORAGE_KEYS.SAVED_ORG, org?.id)
|
||||
storeOrRemoveNull('local', LOCAL_STORAGE_KEYS.SAVED_ORG, org?.id.toString())
|
||||
|
||||
projectsStore.selectedProject = project
|
||||
// @ts-ignore -- problem in OpenAPI spec -- project has ref property
|
||||
|
||||
@@ -33,11 +33,15 @@ export const prettyFormatVariable: Record<Variable, string> = {
|
||||
anonKey: 'Anon key',
|
||||
}
|
||||
|
||||
export function toDisplayNameOrgProject(org: Org, project: Project) {
|
||||
type DeepReadonly<T> = {
|
||||
readonly [P in keyof T]: DeepReadonly<T[P]>
|
||||
}
|
||||
|
||||
export function toDisplayNameOrgProject(org: DeepReadonly<Org>, project: DeepReadonly<Project>) {
|
||||
return `${org.name} / ${project.name}`
|
||||
}
|
||||
|
||||
export function toOrgProjectValue(org: Org, project: Project) {
|
||||
export function toOrgProjectValue(org: DeepReadonly<Org>, project: DeepReadonly<Project>) {
|
||||
return escapeDoubleQuotes(
|
||||
// @ts-ignore -- problem in OpenAPI spec -- project has ref property
|
||||
JSON.stringify([org.id, project.ref, removeDoubleQuotes(toDisplayNameOrgProject(org, project))])
|
||||
@@ -46,7 +50,7 @@ export function toOrgProjectValue(org: Org, project: Project) {
|
||||
|
||||
export function fromOrgProjectValue(
|
||||
maybeOrgProject: string
|
||||
): [string, string, string] | [null, null, null] {
|
||||
): [number, string, string] | [null, null, null] {
|
||||
try {
|
||||
// not restoring the double quotes on the display name is fine because it's only used for
|
||||
// command fuzzy search, not for exact/literal matching
|
||||
@@ -54,7 +58,7 @@ export function fromOrgProjectValue(
|
||||
if (!Array.isArray(data) || data.length !== 3) {
|
||||
throw Error("Shape of parsed JSON doesn't match form of org and project value")
|
||||
}
|
||||
return data as [string, string, string]
|
||||
return data as [number, string, string]
|
||||
} catch {
|
||||
return [null, null, null]
|
||||
}
|
||||
|
||||
@@ -41,7 +41,6 @@ import ProjectSetup from './MDX/project_setup.mdx'
|
||||
import QuickstartIntro from './MDX/quickstart_intro.mdx'
|
||||
import SocialProviderSettingsSupabase from './MDX/social_provider_settings_supabase.mdx'
|
||||
import SocialProviderSetup from './MDX/social_provider_setup.mdx'
|
||||
import StorageManagement from './MDX/storage_management.mdx'
|
||||
|
||||
// Icons
|
||||
import {
|
||||
@@ -153,7 +152,6 @@ const components = {
|
||||
SocialProviderSettingsSupabase,
|
||||
SocialProviderSetup,
|
||||
StepHikeCompact,
|
||||
StorageManagement,
|
||||
TabPanel: (props: any) => <Tabs.Panel {...props}>{props.children}</Tabs.Panel>,
|
||||
Tabs: (props: any) => <Tabs wrappable {...props} />,
|
||||
}
|
||||
|
||||
@@ -96,7 +96,7 @@ const ApiOperationSection = (props) => {
|
||||
>
|
||||
{operation.responseList.map((response: any, i: number) => (
|
||||
<Tabs.Panel key={i} id={response.responseCode} label={response.responseCode}>
|
||||
<p className="text-background">{response.description}</p>
|
||||
<p className="text-foreground">{response.description}</p>
|
||||
{response?.content && response?.content['application/json'] && (
|
||||
<div className="mt-8">
|
||||
<CodeBlock language="bash" className="relative">
|
||||
|
||||
@@ -22,7 +22,7 @@ const RefFunctionSection: React.FC<IRefFunctionSection> = (props) => {
|
||||
|
||||
const tsDefinition =
|
||||
hasTsRef && props.typeSpec ? extractTsDocNode(hasTsRef, props.typeSpec) : null
|
||||
const parameters = hasTsRef && tsDefinition ? generateParameters(tsDefinition) : ''
|
||||
const parameters = hasTsRef && tsDefinition ? generateParameters(tsDefinition) : item.params
|
||||
const shortText = hasTsRef && tsDefinition ? tsDefinition.signatures[0].comment.shortText : ''
|
||||
|
||||
return (
|
||||
@@ -51,24 +51,15 @@ const RefFunctionSection: React.FC<IRefFunctionSection> = (props) => {
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
)}
|
||||
{/* // parameters */}
|
||||
{/* parameters */}
|
||||
{parameters && (
|
||||
<div className="not-prose mt-12">
|
||||
<h5 className="mb-3 text-base text-foreground">Parameters</h5>
|
||||
<ul>
|
||||
{parameters.map((param) => {
|
||||
// grab override params from yaml file
|
||||
const overrideParams = item.overrideParams
|
||||
|
||||
// params from the yaml file can override the params from parameters if it matches the name
|
||||
const overide = overrideParams?.filter((x) => {
|
||||
return param.name === x.name
|
||||
})
|
||||
|
||||
const paramItem = overide?.length > 0 ? overide[0] : param
|
||||
return (
|
||||
<Param {...paramItem} key={param.name}>
|
||||
{paramItem.subContent && (
|
||||
<Param {...param} key={param.name}>
|
||||
{param.subContent && (
|
||||
<div className="mt-3">
|
||||
<Options>
|
||||
{param.subContent.map((param) => {
|
||||
|
||||
@@ -20,6 +20,8 @@ The toolkit includes:
|
||||
|
||||
Check out all of the AI [templates and examples](https://github.com/supabase/supabase/tree/master/examples/ai) in our GitHub repository.
|
||||
|
||||
{/* <!-- vale off --> */}
|
||||
|
||||
<div className="grid md:grid-cols-12 gap-4 not-prose">
|
||||
{examples.map((x) => (
|
||||
<div className="col-span-4" key={x.href}>
|
||||
@@ -66,8 +68,12 @@ export const examples = [
|
||||
},
|
||||
]
|
||||
|
||||
{/* <!-- vale on --> */}
|
||||
|
||||
## Integrations
|
||||
|
||||
{/* <!-- vale off --> */}
|
||||
|
||||
<div className="grid md:grid-cols-12 gap-4 not-prose">
|
||||
{integrations.map((x) => (
|
||||
<div className="col-span-4" key={x.href}>
|
||||
@@ -104,8 +110,12 @@ export const integrations = [
|
||||
},
|
||||
]
|
||||
|
||||
{/* <!-- vale on --> */}
|
||||
|
||||
## Case studies
|
||||
|
||||
{/* <!-- vale off --> */}
|
||||
|
||||
<div className="grid md:grid-cols-12 gap-4 not-prose">
|
||||
{[
|
||||
{
|
||||
@@ -134,3 +144,5 @@ export const integrations = [
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* <!-- vale on --> */}
|
||||
@@ -0,0 +1,228 @@
|
||||
---
|
||||
id: 'ai-hybrid-search'
|
||||
title: 'Hybrid search'
|
||||
description: 'Combine keyword search with semantic search to get both direct and contextual results.'
|
||||
subtitle: 'Combine keyword search with semantic search.'
|
||||
sidebar_label: 'Hybrid search'
|
||||
---
|
||||
|
||||
Hybrid search combines [full text search](/docs/guides/database/full-text-search) (searching by keyword) with [semantic search](/docs/guides/ai/concepts#what-are-embeddings) (searching by meaning) to identify results that are both directly and contextually relevant to the user's query.
|
||||
|
||||
## Why would I want to use hybrid search?
|
||||
|
||||
Sometimes a single search method doesn't quite capture what a user is really looking for. For example, if a user searches for "Italian recipes with tomato sauce" on a cooking app, a keyword search would pull up recipes that specifically mention "Italian," "recipes," and "tomato sauce" in the text. However, it might miss out on dishes that are quintessentially Italian and use tomato sauce but don't explicitly label themselves with these words, or use variations like "pasta sauce" or "marinara." On the other hand, a semantic search might understand the culinary context and find recipes that match the intent, such as a traditional "Spaghetti Marinara," even if they don't match the exact keyword phrase. However, it could also suggest recipes that are contextually related but not what the user is looking for, like a "Mexican salsa" recipe, because it understands the context to be broadly about tomato-based sauces.
|
||||
|
||||
Hybrid search combines the strengths of both these methods. It would ensure that recipes explicitly mentioning the keywords are prioritized, thus capturing direct hits that satisfy the keyword criteria. At the same time, it would include recipes identified through semantic understanding as being related in meaning or context, like different Italian dishes that traditionally use tomato sauce but might not have been tagged explicitly with the user's search terms. It identifies results that are both directly and contextually relevant to the user's query while ideally minimizing misses and irrelevant suggestions.
|
||||
|
||||
## When would I want to use hybrid search?
|
||||
|
||||
The decision to use hybrid search depends on what your users are looking for in your app. For a code repository where developers need to find exact lines of code or error messages, keyword search is likely ideal because it matches specific terms. In a mental health forum where users search for advice or experiences related to their feelings, semantic search may be better because it finds results based on the meaning of a query, not just specific words. For a shopping app where customers might search for specific product names yet also be open to related suggestions, hybrid search combines the best of both worlds - finding exact matches while also uncovering similar products based on the shopping context.
|
||||
|
||||
## How to combine search methods
|
||||
|
||||
Hybrid search merges keyword search and semantic search, but how does this process work?
|
||||
|
||||
First, each search method is executed separately. Keyword search, which involves searching by specific words or phrases present in the content, will yield its own set of results. Similarly, semantic search, which involves understanding the context or meaning behind the search query rather than the specific words used, will generate its own unique results.
|
||||
|
||||
Now with these separate result lists available, the next step is to combine them into a single, unified list. This is achieved through a process known as “fusion”. Fusion takes the results from both search methods and merges them together based on a certain ranking or scoring system. This system may prioritize certain results based on factors like their relevance to the search query, their ranking in the individual lists, or other criteria. The result is a final list that integrates the strengths of both keyword and semantic search methods.
|
||||
|
||||
## Reciprocal Ranked Fusion (RRF)
|
||||
|
||||
One of the most common fusion methods is Reciprocal Ranked Fusion (RRF). The key idea behind RRF is to give more weight to the top-ranked items in each individual result list when building the final combined list.
|
||||
|
||||
In RRF, we iterate over each record and assign a score (noting that each record could exist in one or both lists). The score is calculated as 1 divided by that record's rank in each list, summed together between both lists. For example, if a record with an ID of `123` was ranked third in the keyword search and ninth in semantic search, it would receive a score of $$\dfrac{1}{3} + \dfrac{1}{9} = 0.444$$. If the record was found in only one list and not the other, it would receive a score of 0 for the other list. The records are then sorted by this score to create the final list. The items with the highest scores are ranked first, and lowest scores ranked last.
|
||||
|
||||
This method ensures that items that are ranked high in multiple lists are given a high rank in the final list. It also ensures that items that are ranked high in only a few lists but low in others are not given a high rank in the final list. Placing the rank in the denominator when calculating score helps penalize the low ranking records.
|
||||
|
||||
### Smoothing constant `k`
|
||||
|
||||
To prevent extremely high scores for items that are ranked first (since we're dividing by the rank), a `k` constant is often added to the denominator to smooth the score:
|
||||
|
||||
$$\dfrac{1}{k+rank}$$
|
||||
|
||||
This constant can be any positive number, but is typically small. A constant of 1 would mean that a record ranked first would have a score of $$\dfrac{1}{1+1} = 0.5$$ instead of $$1$$. This adjustment can help balance the influence of items that are ranked very high in individual lists when creating the final combined list.
|
||||
|
||||
## Hybrid search in Postgres
|
||||
|
||||
Let's implement hybrid search in Postgres using `tsvector` (keyword search) and `pgvector` (semantic search).
|
||||
|
||||
First we'll create a `documents` table to store the documents that we will search over. This is just an example - adjust this to match the structure of your application.
|
||||
|
||||
```sql
|
||||
create table documents (
|
||||
id bigint primary key generated always as identity,
|
||||
content text,
|
||||
fts tsvector generated always as (to_tsvector('english', content)) stored,
|
||||
embedding vector(512)
|
||||
);
|
||||
```
|
||||
|
||||
The table contains 4 columns:
|
||||
|
||||
- `id` is an auto-generated unique ID for the record. We'll use this later to match records when performing RRF.
|
||||
- `content` contains the actual text we will be searching over.
|
||||
- `fts` is an auto-generated `tsvector` column that is generated using the text in `content`. We will use this for [full text search](/docs/guides/database/full-text-search) (search by keyword).
|
||||
- `embedding` is a [vector column](/docs/guides/ai/vector-columns) that stores the vector generated from our embedding model. We will use this for [semantic search](/docs/guides/ai/concepts#what-are-embeddings) (search by meaning). We chose 512 dimensions for this example, but adjust this to match the size of the embedding vectors generated from your preferred model.
|
||||
|
||||
Next we'll create indexes on the `fts` and `embedding` columns so that their individual queries will remain fast at scale:
|
||||
|
||||
```sql
|
||||
-- Create an index for the full-text search
|
||||
create index on documents using gin(fts);
|
||||
|
||||
-- Create an index for the semantic vector search
|
||||
create index on documents using hnsw (embedding vector_ip_ops);
|
||||
```
|
||||
|
||||
For full text search we use a [generalized inverted (GIN) index](https://www.postgresql.org/docs/current/gin-intro.html) which is designed for handling composite values like those stored in a `tsvector`.
|
||||
|
||||
For semantic vector search we use an [HNSW index](/docs/guides/ai/vector-indexes/hnsw-indexes), which is a high performing approximate nearest neighbor (ANN) search algorithm. Note that we are using the `vector_ip_ops` (inner product) operator with this index because we plan on using the inner product (`<#>`) operator later in our query. If you plan to use a different operator like cosine distance (`<=>`), be sure to update the index accordingly. For more information, see [distance operators](/docs/guides/ai/vector-indexes#distance-operators).
|
||||
|
||||
Finally we'll create our `hybrid_search` function:
|
||||
|
||||
```sql
|
||||
create or replace function hybrid_search(
|
||||
query_text text,
|
||||
query_embedding vector(512),
|
||||
match_count int,
|
||||
full_text_weight float = 1,
|
||||
semantic_weight float = 1,
|
||||
rrf_k int = 50
|
||||
)
|
||||
returns setof documents
|
||||
language sql
|
||||
as $$
|
||||
with full_text as (
|
||||
select
|
||||
id,
|
||||
-- Note: ts_rank_cd is not indexable but will only rank matches of the where clause
|
||||
-- which shouldn't be too big
|
||||
row_number() over(order by ts_rank_cd(fts, websearch_to_tsquery(query_text)) desc) as rank_ix
|
||||
from
|
||||
documents
|
||||
where
|
||||
fts @@ websearch_to_tsquery(query_text)
|
||||
order by rank_ix
|
||||
limit least(match_count, 30) * 2
|
||||
),
|
||||
semantic as (
|
||||
select
|
||||
id,
|
||||
row_number() over (order by doc_vector <#> query_embedding) as rank_ix
|
||||
from
|
||||
documents
|
||||
order by rank_ix
|
||||
limit least(match_count, 30) * 2
|
||||
)
|
||||
select
|
||||
documents.*
|
||||
from
|
||||
full_text
|
||||
full outer join semantic
|
||||
on full_text.id = semantic.id
|
||||
join documents
|
||||
on coalesce(full_text.id, semantic.id) = documents.id
|
||||
order by
|
||||
coalesce(1.0 / (rrf_k + full_text.rank_ix), 0.0) * full_text_weight +
|
||||
coalesce(1.0 / (rrf_k + semantic.rank_ix), 0.0) * semantic_weight
|
||||
desc
|
||||
limit
|
||||
least(match_count, 30)
|
||||
$$;
|
||||
```
|
||||
|
||||
Let's break this down:
|
||||
|
||||
- **Parameters:** The function accepts quite a few parameters, but the main (required) ones are `query_text`, `query_embedding`, and `match_count`.
|
||||
|
||||
- `query_text` is the user's query text (more on this shortly)
|
||||
- `query_embedding` is the vector representation of the user's query produced by the embedding model. We chose 512 dimensions for this example, but adjust this to match the size of the embedding vectors generated from your preferred model. This must match the size of the `embedding` vector on the `documents` table (and use the same model).
|
||||
- `match_count` is the number of records returned in the `limit` clause.
|
||||
|
||||
The other parameters are optional, but give more control over the fusion process.
|
||||
|
||||
- `full_text_weight` and `semantic_weight` decide how much weight each search method gets in the final score. These are both 1 by default which means they both equally contribute towards the final rank. A `full_text_weight` of 2 and `semantic_weight` of 1 would give full-text search twice as much weight as semantic search.
|
||||
- `rrf_k` is the `k` [smoothing constant](#smoothing-constant-k) added to the reciprocal rank. The default is 50.
|
||||
|
||||
- **Return type:** The function returns a set of records from our `documents` table.
|
||||
- **CTE:** We create two [common table expressions (CTE)](https://www.postgresql.org/docs/current/queries-with.html), one for full-text search and one for semantic search. These perform each query individually prior to joining them.
|
||||
- **RRF:** The final query combines the results from the two CTEs using [reciprocal rank fusion (RRF)](#reciprocal-ranked-fusion-rrf).
|
||||
|
||||
## Running hybrid search
|
||||
|
||||
To use this function in SQL, we can run:
|
||||
|
||||
```sql
|
||||
select
|
||||
*
|
||||
from
|
||||
hybrid_search(
|
||||
'Italian recipes with tomato sauce', -- user query
|
||||
'[...]'::vector(512), -- embedding generated from user query
|
||||
10
|
||||
);
|
||||
```
|
||||
|
||||
In practice, you will likely be calling this from the [Supabase client](/docs/reference/javascript/introduction) or through a custom backend layer. Here is a quick example of how you might call this from an [Edge Function](/docs/guides/functions) using JavaScript:
|
||||
|
||||
```tsx
|
||||
import { createClient } from 'npm:@supabase/supabase-js'
|
||||
import OpenAI from 'npm:openai'
|
||||
|
||||
const supabaseUrl = Deno.env.get('SUPABASE_URL')!
|
||||
const supabaseServiceRoleKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
|
||||
const openaiApiKey = Deno.env.get('OPENAI_API_KEY')!
|
||||
|
||||
Deno.serve(async (req) => {
|
||||
// Grab the user's query from the JSON payload
|
||||
const { query } = await req.json()
|
||||
|
||||
// Instantiate OpenAI client
|
||||
const openai = new OpenAI({ apiKey: openaiApiKey })
|
||||
|
||||
// Generate a one-time embedding for the user's query
|
||||
const embeddingResponse = await openai.embeddings.create({
|
||||
model: 'text-embedding-3-large',
|
||||
input: query,
|
||||
dimensions: 512,
|
||||
})
|
||||
|
||||
const [{ embedding }] = embeddingResponse.data
|
||||
|
||||
// Instantiate the Supabase client
|
||||
// (replace service role key with user's JWT if using Supabase auth and RLS)
|
||||
const supabase = createClient(supabaseUrl, supabaseServiceRoleKey)
|
||||
|
||||
// Call hybrid_search Postgres function via RPC
|
||||
const { data: documents } = await supabase.rpc('hybrid_search', {
|
||||
query_text: query,
|
||||
query_embedding: embedding,
|
||||
match_count: 10,
|
||||
})
|
||||
|
||||
return new Response(JSON.stringify(documents), {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
This uses OpenAI's `text-embedding-3-large` model to generate embeddings (shortened to 512 dimensions for faster retrieval). Swap in your preferred embedding model (and dimension size) accordingly.
|
||||
|
||||
To test this, make a `POST` request to the function's endpoint while passing in a JSON payload containing the user's query. Here is an example `POST` request using cURL:
|
||||
|
||||
```tsx
|
||||
curl -i --location --request POST \
|
||||
'http://127.0.0.1:54321/functions/v1/hybrid-search' \
|
||||
--header 'Authorization: Bearer <anonymous key>' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{"query":"Italian recipes with tomato sauce"}'
|
||||
```
|
||||
|
||||
For more information on how to create, test, and deploy edge functions, see [Getting started](/docs/guides/functions/quickstart).
|
||||
|
||||
## See also
|
||||
|
||||
- [Embedding concepts](/docs/guides/ai/concepts)
|
||||
- [Vector columns](/docs/guides/ai/vector-columns)
|
||||
- [Vector indexes](/docs/guides/ai/vector-indexes)
|
||||
- [Full text (keyword) search](/docs/guides/database/full-text-search)
|
||||
@@ -20,10 +20,10 @@ The keys are both long-lived JWTs. If you decode these keys, you will see that t
|
||||
|
||||
## The `anon` key
|
||||
|
||||
The `anon` key has very few privileges. You can use it in your [RLS policies](/docs/guides/auth/row-level-security) for "anonymous" access. For example, this policy will allow access to the `profiles` table:
|
||||
The `anon` key has very few privileges. You can use it in your [RLS policies](/docs/guides/auth/row-level-security) to allow unauthenticated access. For example, this policy will allow unauthenticated access to the `profiles` table:
|
||||
|
||||
```sql
|
||||
create policy "Allow anonymous access" on profiles to anon for
|
||||
create policy "Allow public access" on profiles to anon for
|
||||
select
|
||||
using (true);
|
||||
```
|
||||
@@ -31,7 +31,7 @@ select
|
||||
And similarity for disallowing access:
|
||||
|
||||
```sql
|
||||
create policy "Disallow anonymous access" on profiles to anon for
|
||||
create policy "Disallow public access" on profiles to anon for
|
||||
select
|
||||
using (false);
|
||||
```
|
||||
|
||||
@@ -59,9 +59,9 @@ We'll create a database table called `todos` for storing tasks. This creates a c
|
||||
|
||||
<StepHikeCompact.Step step={2}>
|
||||
|
||||
<StepHikeCompact.Details title="Allow anonymous access">
|
||||
<StepHikeCompact.Details title="Allow public access">
|
||||
|
||||
Let's turn on Row Level Security for this table and allow anonymous access.
|
||||
Let's turn on Row Level Security for this table and allow public access.
|
||||
|
||||
</StepHikeCompact.Details>
|
||||
|
||||
@@ -73,7 +73,7 @@ We'll create a database table called `todos` for storing tasks. This creates a c
|
||||
enable row level security;
|
||||
|
||||
-- Allow anonymous access
|
||||
create policy "Allow anonymous access"
|
||||
create policy "Allow public access"
|
||||
on todos
|
||||
for select
|
||||
to anon
|
||||
|
||||
@@ -125,6 +125,12 @@ export type Database = MergeDeep<
|
||||
>
|
||||
```
|
||||
|
||||
<Admonition type="note">
|
||||
|
||||
To use `MergeDeep`, set `compilerOptions.strictNullChecks` to `true` in your `tsconfig.json`.
|
||||
|
||||
</Admonition>
|
||||
|
||||
You can also override the type of an individual successful response if needed:
|
||||
|
||||
```ts
|
||||
|
||||
@@ -26,9 +26,9 @@ Supabase provides you with the option of adding captcha to your sign-in, sign-up
|
||||
queryGroup="captcha-method"
|
||||
>
|
||||
<TabPanel id="hcaptcha-1" label="HCaptcha">
|
||||
Go to the [hCaptcha](https://www.hcaptcha.com/) website and sign up for an account. On the welcome page, copy the **Sitekey** and **Secret key**.
|
||||
Go to the [hCaptcha](https://www.hcaptcha.com/) website and sign up for an account. On the Welcome page, copy the **Sitekey** and **Secret key**.
|
||||
|
||||
If you have already signed up and didn't copy this information from the welcome page, you can get the **Secret key** from the settings page.
|
||||
If you have already signed up and didn't copy this information from the Welcome page, you can get the **Secret key** from the Settings page.
|
||||
|
||||

|
||||
|
||||
@@ -43,7 +43,7 @@ In the Settings page, look for the **Sitekey** section and copy the key.
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel id="turnstile-1" label="Turnstile">
|
||||
Go to the [Cloudflare website](https://dash.cloudflare.com/login) and sign up for an account. On the welcome page, head to the Turnstile section and add a new site. Create a site and take note of the **Sitekey** and **Secret Key** as shown below
|
||||
Go to the [Cloudflare website](https://dash.cloudflare.com/login) and sign up for an account. On the Welcome page, head to the Turnstile section and add a new site. Create a site and take note of the **Sitekey** and **Secret Key** as shown below
|
||||

|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
@@ -220,7 +220,7 @@ const App = () => (
|
||||
)
|
||||
```
|
||||
|
||||
If you created your own theme, you may not need to override any of the them.
|
||||
If you created your own theme, you may not need to override any of them.
|
||||
|
||||
### Create your own theme [#create-theme]
|
||||
|
||||
@@ -470,3 +470,14 @@ Setting `showLinks` to `false` will hide the following links:
|
||||
- Already have an account? Sign in
|
||||
- Send a magic link email
|
||||
- Forgot your password?
|
||||
|
||||
### Sign in and Sign up views
|
||||
|
||||
Add `sign_in` or `sign_up` views with the `view` prop:
|
||||
|
||||
```
|
||||
<Auth
|
||||
supabaseClient={supabase}
|
||||
view="sign_up"
|
||||
/>
|
||||
```
|
||||
@@ -93,9 +93,9 @@ import { redirect } from '@remix-run/node'
|
||||
import { createServerClient } from '@supabase/auth-helpers-remix'
|
||||
|
||||
import type { Database } from 'db_types'
|
||||
import type { LoaderArgs } from '@remix-run/node'
|
||||
import type { LoaderFunctionArgs } from '@remix-run/node'
|
||||
|
||||
export const loader = async ({ request }: LoaderArgs) => {
|
||||
export const loader = async ({ request }: LoaderFunctionArgs) => {
|
||||
const response = new Response()
|
||||
const url = new URL(request.url)
|
||||
const code = url.searchParams.get('code')
|
||||
@@ -174,9 +174,9 @@ export const loader = async ({ request }) => {
|
||||
import { json } from '@remix-run/node' // change this import to whatever runtime you are using
|
||||
import { createServerClient } from '@supabase/auth-helpers-remix'
|
||||
|
||||
import type { LoaderArgs } from '@remix-run/node' // change this import to whatever runtime you are using
|
||||
import type { LoaderFunctionArgs } from '@remix-run/node' // change this import to whatever runtime you are using
|
||||
|
||||
export const loader = async ({ request }: LoaderArgs) => {
|
||||
export const loader = async ({ request }: LoaderFunctionArgs) => {
|
||||
const response = new Response()
|
||||
const supabaseClient = createServerClient(
|
||||
process.env.SUPABASE_URL!,
|
||||
@@ -246,9 +246,9 @@ export const action = async ({ request }) => {
|
||||
import { json } from '@remix-run/node' // change this import to whatever runtime you are using
|
||||
import { createServerClient } from '@supabase/auth-helpers-remix'
|
||||
|
||||
import type { ActionArgs } from '@remix-run/node' // change this import to whatever runtime you are using
|
||||
import type { ActionFunctionArgs } from '@remix-run/node' // change this import to whatever runtime you are using
|
||||
|
||||
export const action = async ({ request }: ActionArgs) => {
|
||||
export const action = async ({ request }: ActionFunctionArgs) => {
|
||||
const response = new Response()
|
||||
|
||||
const supabaseClient = createServerClient(
|
||||
@@ -351,7 +351,7 @@ And then we can share this instance across our application with Outlet Context.
|
||||
<TabPanel id="ts" label="TypeScript">
|
||||
|
||||
```tsx app/root.tsx
|
||||
export const loader = ({}: LoaderArgs) => {
|
||||
export const loader = ({}: LoaderFunctionArgs) => {
|
||||
const env = {
|
||||
SUPABASE_URL: process.env.SUPABASE_URL!,
|
||||
SUPABASE_ANON_KEY: process.env.SUPABASE_ANON_KEY!,
|
||||
@@ -441,7 +441,7 @@ export const loader = async ({ request }) => {
|
||||
<TabPanel id="ts" label="TypeScript">
|
||||
|
||||
```tsx app/root.tsx
|
||||
export const loader = async ({ request }: LoaderArgs) => {
|
||||
export const loader = async ({ request }: LoaderFunctionArgs) => {
|
||||
const env = {
|
||||
SUPABASE_URL: process.env.SUPABASE_URL!,
|
||||
SUPABASE_ANON_KEY: process.env.SUPABASE_ANON_KEY!,
|
||||
@@ -707,9 +707,9 @@ import type { Database } from 'db_types'
|
||||
|
||||
type Post = Database['public']['Tables']['posts']['Row']
|
||||
|
||||
import type { LoaderArgs } from '@remix-run/node'
|
||||
import type { LoaderFunctionArgs } from '@remix-run/node'
|
||||
|
||||
export const loader = async ({ request }: LoaderArgs) => {
|
||||
export const loader = async ({ request }: LoaderFunctionArgs) => {
|
||||
const response = new Response()
|
||||
const supabase = createServerClient<Database>(
|
||||
process.env.SUPABASE_URL!,
|
||||
|
||||
@@ -155,7 +155,7 @@ export const GET = async ({ url, locals: { supabase } }) => {
|
||||
await supabase.auth.exchangeCodeForSession(code)
|
||||
}
|
||||
|
||||
throw redirect(303, '/')
|
||||
redirect(303, '/')
|
||||
}
|
||||
```
|
||||
|
||||
@@ -175,7 +175,7 @@ export const GET = async ({ url, locals: { supabase } }) => {
|
||||
await supabase.auth.exchangeCodeForSession(code)
|
||||
}
|
||||
|
||||
throw redirect(303, '/')
|
||||
redirect(303, '/')
|
||||
}
|
||||
```
|
||||
|
||||
@@ -639,7 +639,7 @@ async function authorization({ event, resolve }) {
|
||||
const session = await event.locals.getSession()
|
||||
if (!session) {
|
||||
// the user is not signed in
|
||||
throw redirect(303, '/')
|
||||
redirect(303, '/')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -701,7 +701,7 @@ async function authorization({ event, resolve }) {
|
||||
const session = await event.locals.getSession()
|
||||
if (!session) {
|
||||
// the user is not signed in
|
||||
throw redirect(303, '/')
|
||||
redirect(303, '/')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -773,7 +773,7 @@ import { redirect } from '@sveltejs/kit'
|
||||
export const load = async ({ parent }) => {
|
||||
const { supabase, session } = await parent()
|
||||
if (!session) {
|
||||
throw redirect(303, '/')
|
||||
redirect(303, '/')
|
||||
}
|
||||
const { data: tableData } = await supabase.from('test').select('*')
|
||||
|
||||
@@ -819,12 +819,12 @@ export const actions = {
|
||||
})
|
||||
}
|
||||
|
||||
throw redirect(303, '/dashboard')
|
||||
redirect(303, '/dashboard')
|
||||
},
|
||||
|
||||
signout: async ({ locals: { supabase } }) => {
|
||||
await supabase.auth.signOut()
|
||||
throw redirect(303, '/')
|
||||
redirect(303, '/')
|
||||
},
|
||||
}
|
||||
```
|
||||
@@ -1109,7 +1109,7 @@ import { redirect } from '@sveltejs/kit'
|
||||
export const load: PageLoad = async (event) => {
|
||||
const { session, supabaseClient } = await getSupabase(event)
|
||||
if (!session) {
|
||||
throw redirect(303, '/')
|
||||
redirect(303, '/')
|
||||
}
|
||||
const { data: tableData } = await supabaseClient.from('test').select('*')
|
||||
|
||||
@@ -1145,7 +1145,7 @@ import { redirect } from '@sveltejs/kit'
|
||||
export const load: PageLoad = async ({ parent }) => {
|
||||
const { supabase, session } = await parent()
|
||||
if (!session) {
|
||||
throw redirect(303, '/')
|
||||
redirect(303, '/')
|
||||
}
|
||||
const { data: tableData } = await supabase.from('test').select('*')
|
||||
|
||||
@@ -1179,7 +1179,7 @@ import { json, redirect } from '@sveltejs/kit'
|
||||
export const GET: RequestHandler = async (event) => {
|
||||
const { session, supabaseClient } = await getSupabase(event)
|
||||
if (!session) {
|
||||
throw redirect(303, '/')
|
||||
redirect(303, '/')
|
||||
}
|
||||
const { data } = await supabaseClient.from('test').select('*')
|
||||
|
||||
@@ -1457,7 +1457,7 @@ import type { PageLoad } from './$types'
|
||||
|
||||
export const load: PageLoad = withAuth(async ({ session, getSupabaseClient }) => {
|
||||
if (!session.user) {
|
||||
throw redirect(303, '/')
|
||||
redirect(303, '/')
|
||||
}
|
||||
|
||||
const { data: tableData } = await getSupabaseClient().from('test').select('*')
|
||||
@@ -1489,7 +1489,7 @@ import { redirect } from '@sveltejs/kit'
|
||||
export const load: PageLoad = async (event) => {
|
||||
const { session, supabaseClient } = await getSupabase(event)
|
||||
if (!session) {
|
||||
throw redirect(303, '/')
|
||||
redirect(303, '/')
|
||||
}
|
||||
const { data: tableData } = await supabaseClient.from('test').select('*')
|
||||
|
||||
@@ -1526,7 +1526,7 @@ interface TestTable {
|
||||
|
||||
export const GET: RequestHandler = withAuth(async ({ session, getSupabaseClient }) => {
|
||||
if (!session.user) {
|
||||
throw redirect(303, '/')
|
||||
redirect(303, '/')
|
||||
}
|
||||
|
||||
const { data } = await getSupabaseClient().from<TestTable>('test').select('*')
|
||||
@@ -1546,7 +1546,7 @@ import { json, redirect } from '@sveltejs/kit'
|
||||
export const GET: RequestHandler = async (event) => {
|
||||
const { session, supabaseClient } = await getSupabase(event)
|
||||
if (!session) {
|
||||
throw redirect(303, '/')
|
||||
redirect(303, '/')
|
||||
}
|
||||
const { data } = await supabaseClient.from('test').select('*')
|
||||
|
||||
@@ -1886,7 +1886,7 @@ import type { PageLoad } from './$types'
|
||||
|
||||
export const load: PageLoad = withAuth(async ({ session, getSupabaseClient }) => {
|
||||
if (!session.user) {
|
||||
throw redirect(303, '/')
|
||||
redirect(303, '/')
|
||||
}
|
||||
|
||||
const { data: tableData } = await getSupabaseClient().from('test').select('*')
|
||||
@@ -1948,7 +1948,7 @@ interface TestTable {
|
||||
|
||||
export const GET: RequestHandler = withAuth(async ({ session, getSupabaseClient }) => {
|
||||
if (!session.user) {
|
||||
throw redirect(303, '/');
|
||||
redirect(303, '/');
|
||||
}
|
||||
|
||||
const { data } = await getSupabaseClient()
|
||||
|
||||
@@ -572,7 +572,7 @@ as $$
|
||||
is_admin boolean;
|
||||
begin
|
||||
-- Check if the user is marked as admin in the profiles table
|
||||
select is_admin into is_admin from profiles where user_id = event->>'user_id'::uuid;
|
||||
select is_admin into is_admin from profiles where user_id = (event->>'user_id')::uuid;
|
||||
|
||||
-- Proceed only if the user is an admin
|
||||
if is_admin then
|
||||
|
||||
@@ -7,7 +7,7 @@ subtitle: 'Set up redirect urls with Supabase Auth.'
|
||||
|
||||
## Overview
|
||||
|
||||
When using [passwordless sign-ins](/docs/reference/javascript/auth-signinwithotp) or [third-party providers](/docs/reference/javascript/auth-signinwithoauth#sign-in-using-a-third-party-provider-with-redirect), the Supabase client library methods provide a `redirectTo` parameter to specify where to redirect the user to after authentication. By default, the user will be redirected to the [`SITE_URL`](/docs/reference/auth/config#site_url) but you can modify the `SITE_URL` or add additional redirect URLs to the [allow list](https://supabase.com/dashboard/project/_/auth/url-configuration). Once you've added necessary URLs to the allow list, you can specify the URL you want the user to be redirected to in the `redirectTo` parameter.
|
||||
When using [passwordless sign-ins](/docs/reference/javascript/auth-signinwithotp) or [third-party providers](/docs/reference/javascript/auth-signinwithoauth#sign-in-using-a-third-party-provider-with-redirect), the Supabase client library methods provide a `redirectTo` parameter to specify where to redirect the user to after authentication. By default, the user will be redirected to the [`SITE_URL`](/docs/guides/auth/concepts/redirect-urls) but you can modify the `SITE_URL` or add additional redirect URLs to the [allow list](https://supabase.com/dashboard/project/_/auth/url-configuration). Once you've added necessary URLs to the allow list, you can specify the URL you want the user to be redirected to in the `redirectTo` parameter.
|
||||
|
||||
## Use wildcards in redirect URLs
|
||||
|
||||
@@ -81,3 +81,16 @@ const { data, error } = await supabase.auth.signInWithOAuth({
|
||||
For mobile applications you can use deep linking URIs. For example, for your `SITE_URL` you can specify something like `com.supabase://login-callback/` and for additional redirect URLs something like `com.supabase.staging://login-callback/` if needed.
|
||||
|
||||
Read more about deep linking and find code examples for different frameworks [here](/docs/guides/auth/native-mobile-deep-linking).
|
||||
|
||||
## Error Handling
|
||||
|
||||
When authentication fails, the user will still be redirected to the redirect URL provided. However, the error details will be returned as query fragments in the URL. You can parse these query fragments and show a custom error message to the user. For example:
|
||||
|
||||
```js
|
||||
const params = new URLSearchParams(window.location.hash.slice())
|
||||
|
||||
if (params.get('error_code').startsWith('4')) {
|
||||
// show error message if error is a 4xx error
|
||||
window.alert(params.get('error_description'))
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,278 @@
|
||||
---
|
||||
id: 'custom-claims-and-role-based-access-control-rbac'
|
||||
title: 'Custom Claims & Role-based Access Control (RBAC)'
|
||||
description: 'Use Auth Hooks to add custom claims for managing role-based access control.'
|
||||
---
|
||||
|
||||
Custom Claims are special attributes attached to a user that you can use to control access to portions of your application. For example:
|
||||
|
||||
```json
|
||||
{
|
||||
"user_role": "admin",
|
||||
"plan": "TRIAL",
|
||||
"user_level": 100,
|
||||
"group_name": "Super Guild!",
|
||||
"joined_on": "2022-05-20T14:28:18.217Z",
|
||||
"group_manager": false,
|
||||
"items": ["toothpick", "string", "ring"]
|
||||
}
|
||||
```
|
||||
|
||||
To implement Role-Based Access Control (RBAC) with `custom claims`, use a [Custom Access Token Auth Hook](/docs/guides/auth/auth-hooks#hook-custom-access-token). This hook runs before a token is issued. You can use it to add additional claims to the user's JWT.
|
||||
|
||||
This guide uses the [Slack Clone example](https://github.com/supabase/supabase/tree/master/examples/slack-clone/nextjs-slack-clone) to demonstrate how to add a `user_role` claim and use it in your [Row Level Security (RLS) policies](/docs/guides/auth/row-level-security).
|
||||
|
||||
## Create a table to track user roles and permissions
|
||||
|
||||
In this example, you will implement two user roles with specific permissions:
|
||||
|
||||
- `moderator`: A moderator can delete all messages but not channels.
|
||||
- `admin`: An admin can delete all messages and channels.
|
||||
|
||||
```sql supabase/migrations/init.sql
|
||||
-- Custom types
|
||||
create type public.app_permission as enum ('channels.delete', 'messages.delete');
|
||||
create type public.app_role as enum ('admin', 'moderator');
|
||||
|
||||
-- USER ROLES
|
||||
create table public.user_roles (
|
||||
id bigint generated by default as identity primary key,
|
||||
user_id uuid references public.users on delete cascade not null,
|
||||
role app_role not null,
|
||||
unique (user_id, role)
|
||||
);
|
||||
comment on table public.user_roles is 'Application roles for each user.';
|
||||
|
||||
-- ROLE PERMISSIONS
|
||||
create table public.role_permissions (
|
||||
id bigint generated by default as identity primary key,
|
||||
role app_role not null,
|
||||
permission app_permission not null,
|
||||
unique (role, permission)
|
||||
);
|
||||
comment on table public.role_permissions is 'Application permissions for each role.';
|
||||
```
|
||||
|
||||
<Admonition type="note">
|
||||
|
||||
For the [full schema](https://github.com/supabase/supabase/blob/master/examples/slack-clone/nextjs-slack-clone/README.md), see the example application on [GitHub](https://github.com/supabase/supabase/tree/master/examples/slack-clone/nextjs-slack-clone).
|
||||
|
||||
</Admonition>
|
||||
|
||||
You can now manage your roles and permissions in SQL. For example, to add the mentioned roles and permissions from above, run:
|
||||
|
||||
```sql supabase/seed.sql
|
||||
insert into public.role_permissions (role, permission)
|
||||
values
|
||||
('admin', 'channels.delete'),
|
||||
('admin', 'messages.delete'),
|
||||
('moderator', 'messages.delete');
|
||||
```
|
||||
|
||||
## Create Auth Hook to apply user role
|
||||
|
||||
The [Custom Access Token Auth Hook](/docs/guides/auth/auth-hooks#hook-custom-access-token) runs before a token is issued. You can use it edit the JWT.
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
size="small"
|
||||
type="underlined"
|
||||
defaultActiveId="plpgsql"
|
||||
queryGroup="language"
|
||||
>
|
||||
<TabPanel id="plpgsql" label="PL/pgSQL (best performance)">
|
||||
|
||||
```sql supabase/migrations/auth_hook.sql
|
||||
-- Create the auth hook function
|
||||
create or replace function public.custom_access_token_hook(event jsonb)
|
||||
returns jsonb
|
||||
language plpgsql
|
||||
immutable
|
||||
as $$
|
||||
declare
|
||||
claims jsonb;
|
||||
user_role public.app_role;
|
||||
begin
|
||||
-- Check if the user is marked as admin in the profiles table
|
||||
select role into user_role from public.user_roles where user_id = (event->>'user_id')::uuid;
|
||||
|
||||
claims := event->'claims';
|
||||
|
||||
if user_role is not null then
|
||||
-- Set the claim
|
||||
claims := jsonb_set(claims, '{user_role}', to_jsonb(user_role));
|
||||
else
|
||||
claims := jsonb_set(claims, '{user_role}', 'null');
|
||||
end if;
|
||||
|
||||
-- Update the 'claims' object in the original event
|
||||
event := jsonb_set(event, '{claims}', claims);
|
||||
|
||||
-- Return the modified or original event
|
||||
return event;
|
||||
end;
|
||||
$$;
|
||||
|
||||
grant usage on schema public to supabase_auth_admin;
|
||||
|
||||
grant execute
|
||||
on function public.custom_access_token_hook
|
||||
to supabase_auth_admin;
|
||||
|
||||
revoke execute
|
||||
on function public.custom_access_token_hook
|
||||
from authenticated, anon;
|
||||
|
||||
grant all
|
||||
on table public.user_roles
|
||||
to supabase_auth_admin;
|
||||
|
||||
revoke all
|
||||
on table public.user_roles
|
||||
from authenticated, anon;
|
||||
|
||||
create policy "Allow auth admin to read user roles" ON public.user_roles
|
||||
as permissive for select
|
||||
to supabase_auth_admin
|
||||
using (true)
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="plv8" label="JavaScript (PLV8)">
|
||||
|
||||
```sql supabase/migrations/auth_hook.sql
|
||||
-- Enable the "plv8" extension
|
||||
create extension plv8;
|
||||
|
||||
-- Create the auth hook function
|
||||
create or replace function custom_access_token_hook(event jsonb)
|
||||
returns jsonb
|
||||
language plv8
|
||||
as $$
|
||||
var user_role;
|
||||
|
||||
// Fetch the current user's user_role from the public user_roles table.
|
||||
var result = plv8.execute("select role from public.user_roles where user_id = $1", [event.user_id]);
|
||||
if (result.length > 0) {
|
||||
user_role = result[0].role;
|
||||
} else {
|
||||
// Assign null
|
||||
user_role = null;
|
||||
}
|
||||
|
||||
// Check if 'claims' exists in the event object; if not, initialize it
|
||||
if (!event.claims) {
|
||||
event.claims = {};
|
||||
}
|
||||
|
||||
// Update the level in the claims
|
||||
event.claims.user_role = user_role;
|
||||
|
||||
return event;
|
||||
$$;
|
||||
|
||||
grant usage on schema public to supabase_auth_admin;
|
||||
|
||||
grant execute
|
||||
on function public.custom_access_token_hook
|
||||
to supabase_auth_admin;
|
||||
|
||||
revoke execute
|
||||
on function public.custom_access_token_hook
|
||||
from authenticated, anon;
|
||||
|
||||
grant all
|
||||
on table public.user_roles
|
||||
to supabase_auth_admin;
|
||||
|
||||
revoke all
|
||||
on table public.user_roles
|
||||
from authenticated, anon;
|
||||
|
||||
create policy "Allow auth admin to read user roles" ON public.user_roles
|
||||
as permissive for select
|
||||
to supabase_auth_admin
|
||||
using (true)
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
### Enable the hook
|
||||
|
||||
In the dashboard, navigate to [`Authentication > Hooks (Beta)`](/dashboard/project/_/auth/hooks) and select the appropriate PostgreSQL function from the dropdown menu.
|
||||
|
||||
When developing locally, follow the [local development](/docs/guides/auth/auth-hooks#local-development) instructions.
|
||||
|
||||
<Admonition type="note">
|
||||
|
||||
To learn more about Auth Hooks, see the [Auth Hooks docs](/docs/guides/auth/auth-hooks).
|
||||
|
||||
</Admonition>
|
||||
|
||||
## Accessing custom claims in RLS policies
|
||||
|
||||
To utilize Role-Based Access Control (RBAC) in Row Level Security (RLS) policies, create an `authorize` method that reads the user's role from their JWT and checks the role's permissions:
|
||||
|
||||
```sql supabase/migrations/init.sql
|
||||
create function public.authorize(
|
||||
requested_permission app_permission,
|
||||
user_id uuid
|
||||
)
|
||||
returns boolean as $$
|
||||
declare
|
||||
bind_permissions int;
|
||||
begin
|
||||
select count(*)
|
||||
from public.role_permissions
|
||||
where role_permissions.permission = authorize.requested_permission
|
||||
and role_permissions.role = (auth.jwt() ->> 'user_role')::public.app_role
|
||||
into bind_permissions;
|
||||
|
||||
return bind_permissions > 0;
|
||||
end;
|
||||
$$ language plpgsql security definer set search_path = public;
|
||||
```
|
||||
|
||||
<Admonition type="note">
|
||||
|
||||
You can read more about using functions in RLS policies in the [RLS guide](/docs/guides/auth/row-level-security#using-functions).
|
||||
|
||||
</Admonition>
|
||||
|
||||
You can then use the `authorize` method within your RLS policies. For example, to enable the desired delete access, you would add the following policies:
|
||||
|
||||
```sql
|
||||
create policy "Allow authorized delete access" on public.channels for delete using ( authorize('channels.delete', auth.uid()) );
|
||||
create policy "Allow authorized delete access" on public.messages for delete using ( authorize('messages.delete', auth.uid()) );
|
||||
```
|
||||
|
||||
## Accessing custom claims in your application
|
||||
|
||||
The auth hook will only modify the access token JWT but not the auth response. Therefore, to access the custom claims in your application, e.g. your browser client, or server-side middleware, you will need to decode the `access_token` JWT on the auth session.
|
||||
|
||||
In a JavaScript client application you can for example use the [`jwt-decode` package](https://www.npmjs.com/package/jwt-decode):
|
||||
|
||||
```js
|
||||
import { jwtDecode } from 'jwt-decode'
|
||||
|
||||
const { subscription: authListener } = supabase.auth.onAuthStateChange(async (event, session) => {
|
||||
if (session) {
|
||||
const jwt = jwtDecode(session.access_token)
|
||||
const userRole = jwt.user_role
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
For server-side logic you can use packages like [express-jwt](https://github.com/auth0/express-jwt), [koa-jwt](https://github.com/stiang/koa-jwt), [PyJWT](https://github.com/jpadilla/pyjwt), [dart_jsonwebtoken](https://pub.dev/packages/dart_jsonwebtoken), [Microsoft.AspNetCore.Authentication.JwtBearer](https://www.nuget.org/packages/Microsoft.AspNetCore.Authentication.JwtBearer), etc.
|
||||
|
||||
## Conclusion
|
||||
|
||||
You now have a robust system in place to manage user roles and permissions within your database that automatically propagates to Supabase Auth.
|
||||
|
||||
## More resources
|
||||
|
||||
- [Auth Hooks](/docs/guides/auth/auth-hooks)
|
||||
- [Row Level Security](/docs/guides/auth/row-level-security)
|
||||
- [RLS Functions](/docs/guides/auth/row-level-security#using-functions)
|
||||
- [Next.js Slack Clone Example](https://github.com/supabase/supabase/tree/master/examples/slack-clone/nextjs-slack-clone)
|
||||
@@ -343,4 +343,58 @@ In certain auth scenarios you will need to handle linking back into your applica
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="kotlin" label="Android Kotlin">
|
||||
### Deep link config
|
||||
|
||||
1. Go to your [auth settings](https://supabase.com/dashboard/project/_/auth/url-configuration) page.
|
||||
2. Enter your app redirect URL in the `Additional Redirect URLs` field. This is the URL that the user gets redirected to after clicking a magic link.
|
||||
|
||||
The redirect callback url should have the format `[YOUR_SCHEME]://[YOUR_HOSTNAME]`. Here, `io.supabase.user-management://login-callback` is just an example. You can choose whatever you would like for `YOUR_SCHEME` and `YOUR_HOSTNAME` as long as the scheme is unique across the user's device. For this reason, typically a reverse domain of your website is used.
|
||||
|
||||
Now, edit the Android manifest to make sure the app opens when the user clicks on the magic link.
|
||||
|
||||
```xml
|
||||
<manifest ...>
|
||||
<!-- ... other tags -->
|
||||
<application ...>
|
||||
<activity ...>
|
||||
<!-- ... other tags -->
|
||||
|
||||
<!-- Deep Links -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<!-- Accepts URIs that begin with YOUR_SCHEME://YOUR_HOST -->
|
||||
<data
|
||||
android:scheme="YOUR_SCHEME"
|
||||
android:host="YOUR_HOSTNAME" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
</manifest>
|
||||
```
|
||||
|
||||
Check the [Android documentation](https://developer.android.com/training/app-links/deep-linking) for more information.
|
||||
|
||||
Next, specify the scheme and host in the Supabase Client:
|
||||
```kotlin
|
||||
install(Auth) {
|
||||
host = "login-callback"
|
||||
scheme = "io.supabase.user-management"
|
||||
}
|
||||
```
|
||||
|
||||
Finally, call `Auth#handleDeeplinks` when the app opens:
|
||||
|
||||
```kotlin
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
supabase.handleDeeplinks(intent)
|
||||
}
|
||||
```
|
||||
|
||||
The user will now be authenticated when your app receives a valid deeplink!
|
||||
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
@@ -108,7 +108,7 @@ curl -X POST 'https://cvwawazfelidkloqmbma.supabase.co/auth/v1/signup' \
|
||||
|
||||
The user receives an SMS with a 6-digit pin that you must [verify](/docs/guides/auth/phone-login#verify-a-user) within 60 seconds.
|
||||
|
||||
After their phone number is verified, the user can use their phone number and password to sign in with needing to reverify each time:
|
||||
Once a user's phone number is verified, they can sign in using their phone number and password without requiring phone number re-verification at each sign-in:
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
---
|
||||
id: 'auth-textlocal'
|
||||
title: 'Phone Auth with Textlocal'
|
||||
subtitle: 'How to set up and use Mobile OTP with Textlocal and Supabase.'
|
||||
description: 'How to set up and use Mobile OTP with Textlocal and Supabase.'
|
||||
---
|
||||
|
||||
<Admonition type="tip">
|
||||
|
||||
To manage costs related to SMS authentication, consider setting project rate limits and configuring CAPTCHA. For additional cost-control strategies, visit the Production Checklist.
|
||||
|
||||
Also check country-specific regulations around sending SMS messages, for example, India's TRAI DLT
|
||||
regulations.
|
||||
|
||||
</Admonition>
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before you begin, ensure you have the following:
|
||||
|
||||
- **A Textlocal account**: Sign up for [TextLocal](https://www.textlocal.com/signup) to start sending SMS messages.
|
||||
- **A Supabase project**: Necessary for SMS authentication integration. Create your project in the [Supabase Dashboard](https://supabase.com/dashboard).
|
||||
- **A mobile phone**: To receive SMS messages and test your setup.
|
||||
|
||||
## Setting up Textlocal as your SMS provider
|
||||
|
||||
To integrate Textlocal with Supabase:
|
||||
|
||||
1. [Get a Textlocal API key](#get-a-textlocal-api-key)
|
||||
2. [Customize your sender name](#customize-your-sender-name-optional)
|
||||
3. [Configure your Supabase project](#configure-supabase)
|
||||
|
||||
### Get a Textlocal API key
|
||||
|
||||
1. Log into your Textlocal account and go to `Settings` > `API Keys`.
|
||||
2. Generate a new API Key. Save your new API key in a safe location.
|
||||
|
||||
### Customize your sender name (Optional)
|
||||
|
||||
Textlocal defaults to `TXTLCL` as the sender name for all messages. You can customize this to better reflect your brand:
|
||||
|
||||
1. In your Textlocal dashboard, go to `Settings` > `All Settings` > `Sender Names`.
|
||||
2. Change your sender name.
|
||||
|
||||
### Configure Supabase
|
||||
|
||||
To set up Textlocal as your SMS provider in Supabase, follow these steps:
|
||||
|
||||
1. In your Supabase Dashboard, go to the [Auth Providers section](https://supabase.com/dashboard/project/_/auth/providers).
|
||||
2. From the list of available authentication providers, select `Phone`.
|
||||
3. Toggle `Enable Phone Provider`.
|
||||
4. Under `SMS Provider`, select `Textlocal.`
|
||||
5. Enter your Textlocal API Key and Sender Name.
|
||||
6. Customize your SMS Message (optional).
|
||||
|
||||
## Next steps
|
||||
|
||||
To implement Phone Login, see the docs on [using Phone Login](/docs/guides/auth/phone-login#using-phone-login).
|
||||
|
||||
## Resources
|
||||
|
||||
- [Textlocal Signup](https://www.textlocal.com/sign-up)
|
||||
- [Supabase Dashboard](https://supabase.com/dashboard)
|
||||
@@ -11,7 +11,7 @@ Authenticating users via SMS can become expensive. Adjust your project's rate li
|
||||
|
||||
</Admonition>
|
||||
|
||||
## Preqrequisites
|
||||
## Prerequisites
|
||||
|
||||
You'll need:
|
||||
|
||||
@@ -32,7 +32,7 @@ To set up Twilio Verify, you will need to:
|
||||
When using Twilio Verify, OTPs are generated by Twilio. This means that:
|
||||
|
||||
- Unlike other providers, the OTP expiry duration and message content fields are not configurable via the Supabase dashboard. Please head to Twilio Verify to configure these settings.
|
||||
- The token remains the same during its validity period until the verification is successful. This means if your user make another request within that period, they will receive the same token.
|
||||
- The token remains the same during its validity period until the verification is successful. This means if your user makes another request within that period, they will receive the same token.
|
||||
- Twilio Verify has a separate set of rate limits that apply. Visit Twilio's [Rate Limit and Timeouts page](https://www.twilio.com/docs/verify/api/rate-limits-and-timeouts) to find out more.
|
||||
|
||||
<Admonition type="caution">
|
||||
|
||||
@@ -40,11 +40,11 @@ where auth.uid() = todos.user_id;
|
||||
-- Policy is implicitly added.
|
||||
```
|
||||
|
||||
## Authenticated and anonymous roles
|
||||
## Authenticated and unauthenticated roles
|
||||
|
||||
Supabase Auth maps every request to one of the roles:
|
||||
|
||||
- `anon`: an anonymous request (the user is not logged in)
|
||||
- `anon`: an unauthenticated request (the user is not logged in)
|
||||
- `authenticated`: an authenticated request (the user is logged in)
|
||||
|
||||
These are actually [Postgres Roles](/docs/guides/database/postgres/roles), and so they have significant value for the [performance](/docs/guides/database/postgres/row-level-security#specify-roles-in-your-policies) of your RLS Policies. You can use these roles within your Policies using the `TO` clause:
|
||||
|
||||
@@ -153,7 +153,7 @@ This is traditionally done by setting cookies. Here's an example you can add to
|
||||
|
||||
```typescript
|
||||
supabase.auth.onAuthStateChange((event, session) => {
|
||||
if (event === 'SIGNED_OUT' || event === 'USER_DELETED') {
|
||||
if (event === 'SIGNED_OUT') {
|
||||
// delete cookies on sign out
|
||||
const expires = new Date(0).toUTCString()
|
||||
document.cookie = `my-access-token=; path=/; expires=${expires}; SameSite=Lax; secure`
|
||||
|
||||
@@ -203,6 +203,7 @@ It's safe to trust `getUser()` because it sends a request to the Supabase Auth s
|
||||
<CH.Code className="min-h-[800px]">
|
||||
|
||||
```ts middleware.ts
|
||||
import { type NextRequest } from 'next/server'
|
||||
import { updateSession } from '@/utils/supabase/middleware'
|
||||
|
||||
export async function middleware(request: NextRequest) {
|
||||
@@ -400,7 +401,7 @@ If you have email confirmation turned on (the default), a new user will receive
|
||||
|
||||
Change the email template to support a server-side authentication flow.
|
||||
|
||||
Go to the [Auth templates](https://supabase.com/dashboard/project/_/auth/templates) page in your dashboard. In each email template, change `{{ .ConfirmationURL }}` to `{{ .SiteURL }}/auth/confirm?token_hash={{ .TokenHash }}&type=signup`.
|
||||
Go to the [Auth templates](https://supabase.com/dashboard/project/_/auth/templates) page in your dashboard. In the `Confirm signup` template, change `{{ .ConfirmationURL }}` to `{{ .SiteURL }}/auth/confirm?token_hash={{ .TokenHash }}&type=signup`.
|
||||
|
||||
</StepHikeCompact.Details>
|
||||
|
||||
|
||||
@@ -207,6 +207,17 @@ app.get("/auth/callback", async function (req, res) {
|
||||
|
||||
Let's point our `.signInWithOAuth` method's redirect to the callback route we create above:
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
size="small"
|
||||
type="underlined"
|
||||
defaultActiveId="client"
|
||||
queryGroup="environment"
|
||||
>
|
||||
<TabPanel id="client" label="Client">
|
||||
|
||||
In the browser, `signInWithOAuth` automatically redirects to the SSO provider's authentication endpoint, which then redirects to your endpoint.
|
||||
|
||||
```js
|
||||
await supabase.auth.signInWithOAuth({
|
||||
provider,
|
||||
@@ -215,3 +226,26 @@ await supabase.auth.signInWithOAuth({
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel id="server" label="Server">
|
||||
|
||||
In the server, you need to handle the redirect to the SSO provider's authentication endpoint. The `signInWithOAuth` method returns the endpoint URL, which you can redirect to.
|
||||
|
||||
```js
|
||||
const { data, error } = await supabase.auth.signInWithOAuth({
|
||||
provider,
|
||||
options: {
|
||||
redirectTo: '/auth/callback',
|
||||
},
|
||||
})
|
||||
|
||||
if (data.url) {
|
||||
redirect(data.url) // use the redirect API for your server framework
|
||||
}
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
|
||||
</Tabs>
|
||||
@@ -452,8 +452,7 @@ Before you can use Sign in with Google, you need to obtain a [Google Cloud Platf
|
||||
|
||||
## Using Google sign-in with Kotlin Multiplatform
|
||||
|
||||
When using [Compose Multiplatform](https://www.jetbrains.com/lp/compose-multiplatform/), you can use the [compose-auth](https://supabase.com/docs/reference/kotlin/installing) plugin. On Android it uses Google OneTap (or Credential Manager on Android 14+) automatically and on other platforms it uses `auth.signInWith(Google)`.
|
||||
Alternatively, you can have a look at [Google One Tap](https://developers.google.com/identity/one-tap/android/overview), just login with the IdToken after you receive it.
|
||||
When using [Compose Multiplatform](https://www.jetbrains.com/lp/compose-multiplatform/), you can use the [compose-auth](https://supabase.com/docs/reference/kotlin/installing) plugin. On Android it uses the Credential Manager automatically and on other platforms it uses `auth.signInWith(Google)`.
|
||||
|
||||
**Initialize the Supabase Client**
|
||||
|
||||
|
||||
@@ -123,9 +123,20 @@ npm update supabase --save-dev
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
If you have any Supabase containers running locally, remember to restart them after upgrading to use the new features.
|
||||
If you have any Supabase containers running locally, stop them and delete their data volumes before proceeding with the upgrade. This ensures that Supabase managed services can apply new migrations on a clean state of the local database.
|
||||
|
||||
```bash
|
||||
<Admonition type="tip" label="Logical backup">
|
||||
|
||||
Remember to save any local schema and data changes before stopping because the `--no-backup` flag will delete them.
|
||||
|
||||
```sh
|
||||
supabase db diff my_schema
|
||||
supabase db dump --local --data-only > supabase/seed.sql
|
||||
```
|
||||
|
||||
</Admonition>
|
||||
|
||||
```sh
|
||||
supabase stop --no-backup
|
||||
supabase start
|
||||
```
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
---
|
||||
id: 'serverless-drivers'
|
||||
title: 'Serverless Drivers'
|
||||
description: 'Connecting to your Postgres database in serverless environments.'
|
||||
subtitle: 'Connecting to your Postgres database in serverless environments.'
|
||||
---
|
||||
|
||||
Supabase provides several options for connecting to your Postgres database from serverless environments.
|
||||
|
||||
[supabase-js](/reference/javascript/introduction) is an isomorphic JavaScript client that uses the [auto-generated REST API](/guides/api) and therefore works in any environment that supports HTTPS connections. This API has a built-in [connection pooler](/guides/database/connecting-to-postgres#connection-pooler) and can serve thousands of simultaneous requests, and therefore is ideal for Serverless workloads.
|
||||
|
||||
## Vercel Edge Functions
|
||||
|
||||
Vercel's [Edge runtime](https://vercel.com/docs/functions/runtimes/edge-runtime) is built on top of the [V8 engine](https://v8.dev/), that provides a limited set of Web Standard APIs.
|
||||
|
||||
### Quickstart
|
||||
|
||||
Choose one of these Vercel Deploy Templates which use our [Vercel Deploy Integration](https://vercel.com/integrations/supabase) to automatically configure your connection strings as environment variables on your Vercel project!
|
||||
|
||||
<div>
|
||||
<div className="grid grid-cols-12 gap-6 not-prose">
|
||||
{[
|
||||
{
|
||||
title: 'supabase-js',
|
||||
hasLightIcon: true,
|
||||
href: 'https://supabase.link/nextjs-with-supabase-starter',
|
||||
description: 'A Next.js App Router template configured with cookie-based auth using Supabase, TypeScript and Tailwind CSS.'
|
||||
},
|
||||
{
|
||||
title: 'Drizzle',
|
||||
hasLightIcon: true,
|
||||
href: 'https://supabase.link/nextjs-supabase-drizzle',
|
||||
description: "Simple Next.js template that uses Supabase as the database and Drizzle as the ORM.",
|
||||
},
|
||||
{
|
||||
title: 'Kysely',
|
||||
hasLightIcon: true,
|
||||
href: 'https://supabase.link/nextjs-supabase-kysely',
|
||||
description: 'Simple Next.js template that uses Supabase as the database and Kysely as the query builder.',
|
||||
},
|
||||
/* { TODO: figure out how to get around Prisma accelerate requirement...
|
||||
title: 'Prisma',
|
||||
hasLightIcon: true,
|
||||
href: 'https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fvercel%2Fexamples%2Ftree%2Fmain%2Fstorage%2Fpostgres-prisma&project-name=postgres-prisma&repository-name=postgres-prisma&demo-title=Vercel%20Postgres%20%2B%20Prisma%20Next.js%20Starter&demo-description=Simple%20Next.js%20template%20that%20uses%20Vercel%20Postgres%20as%20the%20database%20and%20Prisma%20as%20the%20ORM.&demo-url=https%3A%2F%2Fpostgres-prisma.vercel.app%2F&demo-image=https%3A%2F%2Fpostgres-prisma.vercel.app%2Fopengraph-image.png&integration-ids=oac_VqOgBHqhEoFTPzGkPd7L0iH6',
|
||||
description: 'Simple Next.js template that uses Vercel Postgres as the database and Prisma as the ORM.',
|
||||
} */
|
||||
].map((resource) => {
|
||||
return (
|
||||
<Link
|
||||
href={`${resource.href}`}
|
||||
key={resource.title}
|
||||
className={'col-span-12 md:col-span-4'}
|
||||
passHref
|
||||
>
|
||||
<GlassPanel {...resource} background={false} showIconBg={true}>
|
||||
{resource.description}
|
||||
</GlassPanel>
|
||||
</Link>
|
||||
)
|
||||
|
||||
})}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
### Manual configuration
|
||||
|
||||
In your [`Database Settings`](https://supabase.com/dashboard/project/_/settings/database), make sure `Use connection pooler` is checked and `Transaction` mode is selected, then copy the URI and save it as the `POSTGRES_URL` environment variable. Remember to replace the password placeholder with your actual database password and add the following suffix `?workaround=supabase-pooler.vercel`.
|
||||
|
||||
```txt .env.local
|
||||
POSTGRES_URL="postgres://postgres.cfcxynqnhdybqtbhjemm:[YOUR-PASSWORD]@aws-0-ap-southeast-1.pooler.supabase.com:6543/postgres?workaround=supabase-pooler.vercel"
|
||||
```
|
||||
|
||||
<Tabs scrollable defaultActiveId="drizzle" type="underlined" size="small">
|
||||
|
||||
<TabPanel id="drizzle" label="Drizzle">
|
||||
|
||||
```ts lib/drizzle.ts
|
||||
import { pgTable, serial, text, timestamp, uniqueIndex } from 'drizzle-orm/pg-core'
|
||||
import { InferSelectModel, InferInsertModel } from 'drizzle-orm'
|
||||
import { sql } from '@vercel/postgres'
|
||||
import { drizzle } from 'drizzle-orm/vercel-postgres'
|
||||
|
||||
export const UsersTable = pgTable(
|
||||
'users',
|
||||
{
|
||||
id: serial('id').primaryKey(),
|
||||
name: text('name').notNull(),
|
||||
email: text('email').notNull(),
|
||||
image: text('image').notNull(),
|
||||
createdAt: timestamp('createdAt').defaultNow().notNull(),
|
||||
},
|
||||
(users) => {
|
||||
return {
|
||||
uniqueIdx: uniqueIndex('unique_idx').on(users.email),
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
export type User = InferSelectModel<typeof UsersTable>
|
||||
export type NewUser = InferInsertModel<typeof UsersTable>
|
||||
|
||||
// Connect to Vercel Postgres
|
||||
export const db = drizzle(sql)
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="kysely" label="Kysely">
|
||||
|
||||
```ts lib/kysely.ts
|
||||
import { Generated, ColumnType } from 'kysely'
|
||||
import { createKysely } from '@vercel/postgres-kysely'
|
||||
|
||||
interface UserTable {
|
||||
// Columns that are generated by the database should be marked
|
||||
// using the `Generated` type. This way they are automatically
|
||||
// made optional in inserts and updates.
|
||||
id: Generated<number>
|
||||
name: string
|
||||
email: string
|
||||
image: string
|
||||
|
||||
// You can specify a different type for each operation (select, insert and
|
||||
// update) using the `ColumnType<SelectType, InsertType, UpdateType>`
|
||||
// wrapper. Here we define a column `createdAt` that is selected as
|
||||
// a `Date`, can optionally be provided as a `string` in inserts and
|
||||
// can never be updated:
|
||||
createdAt: ColumnType<Date, string | undefined, never>
|
||||
}
|
||||
|
||||
// Keys of this interface are table names.
|
||||
export interface Database {
|
||||
users: UserTable
|
||||
}
|
||||
|
||||
export const db = createKysely<Database>()
|
||||
export { sql } from 'kysely'
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
## Cloudflare Workers
|
||||
|
||||
Cloudflare's Workers runtime also uses the [V8 engine](https://v8.dev/) but provides polyfills for a subset of Node.js APIs and [TCP Sockets API](https://developers.cloudflare.com/workers/runtime-apis/tcp-sockets/), giving you a couple of options:
|
||||
|
||||
- [supabase-js](https://developers.cloudflare.com/workers/databases/native-integrations/supabase/)
|
||||
- [Postgres.js](https://github.com/porsager/postgres?tab=readme-ov-file#cloudflare-workers-support)
|
||||
- [node-postgres](https://developers.cloudflare.com/workers/tutorials/postgres/)
|
||||
|
||||
## Supabase Edge Functions
|
||||
|
||||
Supabase Edge Functions uses the [Deno runtime](https://deno.com/) which has native support for TCP connections allowing you to choose your favorite client:
|
||||
|
||||
- [supabase-js](/guides/functions/connect-to-postgres#using-supabase-js)
|
||||
- [Deno Postgres driver](/guides/functions/connect-to-postgres#using-a-postgres-client)
|
||||
- [Postgres.js](https://github.com/porsager/postgres)
|
||||
- [Drizzle](/guides/functions/connect-to-postgres#using-drizzle)
|
||||
@@ -36,7 +36,7 @@ drop
|
||||
extension pgtap;
|
||||
```
|
||||
|
||||
Even though the SQL code is `create extension`, this is the equivalent of "enabling the extension".
|
||||
Even though the SQL code is `create extension`, this is the equivalent of enabling the extension.
|
||||
To disable an extension call `drop extension`.
|
||||
|
||||
</TabPanel>
|
||||
@@ -48,7 +48,7 @@ Most extensions are installed under the `extensions` schema, which is accessible
|
||||
|
||||
If you need to restrict user access to tables managed by extensions, we recommend creating a separate schema for installing that specific extension.
|
||||
|
||||
Some extensions can only be created under a specific schema, for eg. `postgis_tiger_geocoder` extension creates a schema named `tiger`. Before enabling such extensions, make sure you have not created a conflicting schema with the same name.
|
||||
Some extensions can only be created under a specific schema, for example, `postgis_tiger_geocoder` extension creates a schema named `tiger`. Before enabling such extensions, make sure you have not created a conflicting schema with the same name.
|
||||
|
||||
In addition to the pre-configured extensions, you can also install your own SQL extensions directly in the database using Supabase's SQL editor. The SQL code for the extensions, including plpgsql extensions, can be added through the SQL editor.
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ The motivation for HypoPG is to allow users to quickly search for an index to im
|
||||
|
||||
1. Go to the [Database](https://supabase.com/dashboard/project/_/database/tables) page in the Dashboard.
|
||||
2. Click on **Extensions** in the sidebar.
|
||||
3. Search for "hypopg" and enable the extension.
|
||||
3. Search for `hypopg` and enable the extension.
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="sql" label="SQL">
|
||||
@@ -35,7 +35,7 @@ create extension hypopg with schema extensions;
|
||||
drop extension if exists hypopg;
|
||||
```
|
||||
|
||||
Even though the SQL code is `create extension`, this is the equivalent of "enabling the extension".
|
||||
Even though the SQL code is `create extension`, this is the equivalent of enabling the extension.
|
||||
To disable an extension you can call `drop extension`.
|
||||
|
||||
It's good practice to create the extension within a separate schema (like `extensions`) to keep the `public` schema clean.
|
||||
@@ -104,10 +104,10 @@ create index on account(id);
|
||||
|
||||
- [`hypo_create_index(text)`](https://hypopg.readthedocs.io/en/rel1_stable/usage.html#create-a-hypothetical-index): A function to create a hypothetical index.
|
||||
- [`hypopg_list_indexes`](https://hypopg.readthedocs.io/en/rel1_stable/usage.html#manipulate-hypothetical-indexes): A View that lists all hypothetical indexes that have been created.
|
||||
- [`hypopg()`](https://hypopg.readthedocs.io/en/rel1_stable/usage.html#manipulate-hypothetical-indexes): A function that lists all hypothetical indexes that have been created with the same format as pg_index.
|
||||
- [`hypopg()`](https://hypopg.readthedocs.io/en/rel1_stable/usage.html#manipulate-hypothetical-indexes): A function that lists all hypothetical indexes that have been created with the same format as `pg_index`.
|
||||
- [`hypopg_get_index_def(oid)`](https://hypopg.readthedocs.io/en/rel1_stable/usage.html#manipulate-hypothetical-indexes): A function to display the `create index` statement that would create the index.
|
||||
- [`hypopg_get_relation_size(oid)`](https://hypopg.readthedocs.io/en/rel1_stable/usage.html#manipulate-hypothetical-indexes): A function to estimate how large a hypothetical index would be.
|
||||
- [`hypopg_drop_index(oid)`](https://hypopg.readthedocs.io/en/rel1_stable/usage.html#manipulate-hypothetical-indexes): A function to remove a given hypothetical index by oid.
|
||||
- [`hypopg_drop_index(oid)`](https://hypopg.readthedocs.io/en/rel1_stable/usage.html#manipulate-hypothetical-indexes): A function to remove a given hypothetical index by `oid`.
|
||||
- [`hypopg_reset()`](https://hypopg.readthedocs.io/en/rel1_stable/usage.html#manipulate-hypothetical-indexes): A function to remove all hypothetical indexes.
|
||||
|
||||
## Resources
|
||||
|
||||
@@ -19,7 +19,7 @@ description: 'Validate json/jsonb with JSON Schema in PostgreSQL.'
|
||||
|
||||
1. Go to the [Database](https://supabase.com/dashboard/project/_/database/tables) page in the Dashboard.
|
||||
2. Click on **Extensions** in the sidebar.
|
||||
3. Search for "pg_jsonschema" and enable the extension.
|
||||
3. Search for `pg_jsonschema` and enable the extension.
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="sql" label="SQL">
|
||||
@@ -33,7 +33,7 @@ create extension pg_jsonschema with schema extensions;
|
||||
drop extension if exists pg_jsonschema;
|
||||
```
|
||||
|
||||
Even though the SQL code is `create extension`, this is the equivalent of "enabling the extension".
|
||||
Even though the SQL code is `create extension`, this is the equivalent of enabling the extension.
|
||||
To disable an extension you can call `drop extension`.
|
||||
|
||||
It's good practice to create the extension within a separate schema (like `extensions`) to keep the `public` schema clean.
|
||||
|
||||
@@ -4,7 +4,7 @@ title: 'pg_plan_filter: Restrict Total Cost'
|
||||
description: 'Block queries over a total cost limit'
|
||||
---
|
||||
|
||||
[pg_plan_filter](https://github.com/pgexperts/pg_plan_filter) is PostgreSQL extension to block execution of statements where query planner's estimate of the total cost exceeds a threshold. This is intended to give database administrators a way to restrict the contribution an individual query has on database load.
|
||||
[`pg_plan_filter`](https://github.com/pgexperts/pg_plan_filter) is PostgreSQL extension to block execution of statements where query planner's estimate of the total cost exceeds a threshold. This is intended to give database administrators a way to restrict the contribution an individual query has on database load.
|
||||
|
||||
## Enable the extension
|
||||
|
||||
@@ -27,7 +27,7 @@ alter database some_db set session_preload_libraries = 'plan_filter';
|
||||
`plan_filter.statement_cost_limit`: restricts the maximum total cost for executed statements
|
||||
`plan_filter.limit_select_only`: restricts to `select` statements
|
||||
|
||||
Note that `limit_select_only = true` is not the same as read-only because `select` statements may modify data e.g. through a function call.
|
||||
Note that `limit_select_only = true` is not the same as read-only because `select` statements may modify data, for example, through a function call.
|
||||
|
||||
## Example
|
||||
|
||||
@@ -88,4 +88,4 @@ HINT: The plan for your query shows that it would probably have an excessive ru
|
||||
|
||||
## Resources
|
||||
|
||||
- Official [pg_plan_filter documentation](https://github.com/pgexperts/pg_plan_filter)
|
||||
- Official [`pg_plan_filter` documentation](https://github.com/pgexperts/pg_plan_filter)
|
||||
@@ -4,11 +4,15 @@ title: 'PGAudit: Postgres Auditing'
|
||||
description: 'Session and object auditing via PostgreSQL standard logging'
|
||||
---
|
||||
|
||||
[PGAudit](https://www.pgaudit.org) is a PostgreSQL extension for logging session and object auditing over the standard PostgreSQL logging utility.
|
||||
[PGAudit](https://www.pgaudit.org) is a PostgreSQL extension that extends PostgreSQL's built-in logging abilities. It can be used to selectively track activities within your database.
|
||||
|
||||
PGAudit grants fine grain control over which statements and objects are emitted to logs.
|
||||
This helps you with:
|
||||
|
||||
## Enable the extension
|
||||
- **Compliance**: Meeting audit requirements for regulations
|
||||
- **Security**: Detecting suspicious database activity
|
||||
- **Troubleshooting**: Identifying and fixing database issues
|
||||
|
||||
## Enable the Extension
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
@@ -21,7 +25,7 @@ PGAudit grants fine grain control over which statements and objects are emitted
|
||||
|
||||
1. Go to the [Database](https://supabase.com/dashboard/project/_/database/tables) page in the Dashboard.
|
||||
2. Click on **Extensions** in the sidebar.
|
||||
3. Search for "pgaudit" and enable the extension.
|
||||
3. Search for `pgaudit` and enable the extension.
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="sql" label="SQL">
|
||||
@@ -35,74 +39,364 @@ create extension pgaudit;
|
||||
drop extension if exists pgaudit;
|
||||
```
|
||||
|
||||
Even though the SQL code is `create extension`, this is the equivalent of "enabling the extension".
|
||||
To disable an extension you can call `drop extension`.
|
||||
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
## Settings
|
||||
## Configure the Extension
|
||||
|
||||
The `pgaudit.log` setting controls which statements to log. Available values include:
|
||||
PGAudit can be configured with multiple different levels of precision.
|
||||
|
||||
- **read**: `SELECT` and `COPY` when the source is a relation or a query.
|
||||
- **write**: `INSERT`, `UPDATE`, `DELETE`, `TRUNCATE`, and `COPY` when the destination is a relation.
|
||||
- **function**: Function calls and `DO` blocks.
|
||||
- **role**: Statements related to roles and privileges: `GRANT`, `REVOKE`, `CREATE/ALTER/DROP ROLE`.
|
||||
- **ddl**: All `DDL` that is not included in the `ROLE` class.
|
||||
- **misc**: Miscellaneous commands, e.g. `DISCARD`, `FETCH`, `CHECKPOINT`, `VACUUM`, `SET`.
|
||||
- **misc_set**: Miscellaneous `SET` commands, e.g. `SET ROLE`.
|
||||
**PGAudit Logging Precision:**
|
||||
|
||||
- **all**: Include all of the above.
|
||||
- **[Session](#session-logging):** Logs activity within a connection, such as a [PSQL](https://supabase.com/docs/guides/database/connecting-to-postgres#connecting-with-psql) connection.
|
||||
- **[User](#user-logging):** Logs activity by a particular database user (e.g., anon or postgres).
|
||||
- **[Global](#global-logging):** Logs activity across the entire database.
|
||||
- **[Object](#object-mode-settings):** Logs events related to specific database objects (for example, the auth.users table).
|
||||
|
||||
For a full list of available settings see [settings docs](https://github.com/pgaudit/pgaudit/blob/master/README.md#settings). Be aware that the `all` setting will generate a very large volume of logs.
|
||||
Although Session, User, and Global modes differ in their precision, they are all considered variants of **Session Mode** and are configured with the same input categories.
|
||||
|
||||
### Session Mode Categories
|
||||
|
||||
These modes can monitor predefined categories of database operations:
|
||||
|
||||
| Category | What it Logs | Description |
|
||||
| -------- | --------------------------------------------------------------------- | -------------------------------------------------------------------------- |
|
||||
| read | Data retrieval (SELECT, COPY) | Tracks what data is being accessed. |
|
||||
| write | Data modification (INSERT, DELETE, UPDATE, TRUNCATE, COPY) | Tracks changes made to your database. |
|
||||
| function | FUNCTION, PROCEDURE, and DO/END block executions | Tracks routine/function executions |
|
||||
| role | User management actions (CREATE, DROP, ALTER on users and privileges) | Tracks changes to user permissions and access. |
|
||||
| ddl | Schema changes (CREATE, DROP, ALTER statements) | Monitors modifications to your database structure (tables, indexes, etc.). |
|
||||
| misc | Less common commands (FETCH, CHECKPOINT) | Captures obscure actions for deeper analysis if needed. |
|
||||
| all | Everything above | Comprehensive logging for complete audit trails. |
|
||||
|
||||
Below is a limited example of how to assign pgaudit to monitor specific categories.
|
||||
|
||||
```sql
|
||||
-- log all CREATE, ALTER, and DROP events
|
||||
... pgaudit.log = 'ddl';
|
||||
|
||||
-- log all CREATE, ALTER, DROP, and SELECT events
|
||||
... pgaudit.log = 'read, ddl';
|
||||
|
||||
-- log nothing
|
||||
... pgaudit.log = 'none';
|
||||
```
|
||||
|
||||
### Session Logging
|
||||
|
||||
When you are connecting in a session environment, such as a PSQL connection, you can configure pgaudit to record events initiated within the session.
|
||||
|
||||
<Admonition type="note">
|
||||
|
||||
We don't currently support configuring `pgaudit.log_parameter` because it may log secrets in encrypted columns if you are using [pgsodium](/docs/guides/database/extensions/pgsodium) or [Vault](/docs/guides/database/vault).
|
||||
|
||||
You can upvote this [feature request](https://github.com/orgs/supabase/discussions/20183) if you'd like this restriction lifted.
|
||||
The [Dashboard](https://supabase.com/dashboard/project/_) is a transactional environment and will not sustain a session.
|
||||
|
||||
</Admonition>
|
||||
|
||||
## Example
|
||||
Inside a session, by default, pgaudit will log nothing:
|
||||
|
||||
Given a pgaudit setting
|
||||
|
||||
{/* prettier-ignore */}
|
||||
```sql
|
||||
set pgaudit.log = 'read, ddl';
|
||||
-- returns 'none'
|
||||
show pgaudit.log;
|
||||
```
|
||||
|
||||
The following create table, insert and select statements
|
||||
In the session, you can `SET` the `pgaudit.log` variable to record events:
|
||||
|
||||
```sql
|
||||
-- log CREATE, ALTER, and DROP events
|
||||
set pgaudit.log = 'ddl';
|
||||
|
||||
-- log all CREATE, ALTER, DROP, and SELECT events
|
||||
set pgaudit.log = 'read, ddl';
|
||||
|
||||
-- log nothing
|
||||
set pgaudit.log = 'none';
|
||||
```
|
||||
|
||||
### User Logging
|
||||
|
||||
There are some cases where you may want to monitor a database user's actions. For instance, let's say you connected your database to [Zapier](https://supabase.com/partners/integrations/zapier) and created a custom role for it to use:
|
||||
|
||||
```sql
|
||||
create user "zapier" with password '<new password>';
|
||||
```
|
||||
|
||||
You may want to log all actions initiated by `zapier`, which can be done with the following command:
|
||||
|
||||
```sql
|
||||
alter role "zapier" set pgaudit.log to 'all';
|
||||
```
|
||||
|
||||
To remove the settings, execute the following code:
|
||||
|
||||
```sql
|
||||
-- disables role's log
|
||||
alter role "zapier" set pgaudit.log TO 'none';
|
||||
|
||||
-- check to make sure the changes are finalized:
|
||||
select rolname,
|
||||
rolconfig
|
||||
from pg_roles
|
||||
where rolname = 'zapier';
|
||||
-- should return a rolconfig path with "pgaudit.log=none" present
|
||||
```
|
||||
|
||||
### Global Logging
|
||||
|
||||
<Admonition type="caution">
|
||||
|
||||
Use global logging sparingly as it can generate many logs and make it difficult to find important events. Consider limiting the scope of what is logged by using user or object logging where possible.
|
||||
|
||||
</Admonition>
|
||||
|
||||
The below SQL configures pgaudit to record all events associated with the "postgres" role. Since postgres has extensive privileges, this effectively monitors all database activity.
|
||||
|
||||
```sql
|
||||
alter role "postgres" set pgaudit.log to 'all';
|
||||
```
|
||||
|
||||
To check if the postgres role is auditing, execute the following command:
|
||||
|
||||
```sql
|
||||
select
|
||||
rolname,
|
||||
rolconfig
|
||||
from pg_roles
|
||||
where rolname = 'postgres';
|
||||
-- should return a rolconfig path with "pgaudit.log=all" present
|
||||
```
|
||||
|
||||
To remove the settings, execute the following code:
|
||||
|
||||
```sql
|
||||
alter role "postgres" set pgaudit.log to 'none';
|
||||
```
|
||||
|
||||
## Object Mode Settings
|
||||
|
||||
Assigning a role to pgaudit will result in the logging of **any actions** that role is **capable** of performing, _regardless of who or what actually initiated the event_.
|
||||
|
||||
### Object Logging
|
||||
|
||||
To fine-tune what pgaudit will record, you must create a custom database role with limited permissions:
|
||||
|
||||
```sql
|
||||
create role "some_audit_role" noinherit;
|
||||
```
|
||||
|
||||
No other Postgres user can assume or login via this role. It solely exists to securely define what pgaudit will record.
|
||||
|
||||
Once the role is created, you can direct pgaudit to log by assigning it to the `pgaudit.role` variable:
|
||||
|
||||
```sql
|
||||
alter role "postgres" set pgaudit.role to 'some_audit_role';
|
||||
```
|
||||
|
||||
You can then assign the role to monitor only approved events, such as `select` statements that include a specific table:
|
||||
|
||||
```sql
|
||||
grant select on random_table to "some_audit_role";
|
||||
```
|
||||
|
||||
With this privilege granted, pgaudit will record all select statements against the `random_table`. Note, that this does include joins that feature the table, too. All assignignable priveleges can be viewed in the [PostgreSQL documentation](https://www.postgresql.org/docs/current/ddl-priv.html).
|
||||
|
||||
If you would no longer like to use object logging, you will need to unnasign the `pgaudit.role` variable:
|
||||
|
||||
```sql
|
||||
-- change pgaudit.role to no longer reference some_audit_role
|
||||
alter role "postgres" set pgaudit.role to '';
|
||||
|
||||
-- view if pgaudit.role changed with the following command:
|
||||
select
|
||||
rolname,
|
||||
rolconfig
|
||||
from pg_roles
|
||||
where rolname = 'postgres';
|
||||
-- should return a rolconfig path with "pgaudit.role="
|
||||
```
|
||||
|
||||
If you are no longer using a `ROLE`, it is best to `DROP`it:
|
||||
|
||||
```sql
|
||||
drop role "some_audit_role";
|
||||
```
|
||||
|
||||
## Interpreting Audit Logs
|
||||
|
||||
pgaudit was designed for storing logs as CSV files with the following headers:
|
||||
|
||||
> Referenced from the [pgaudit official docs](https://github.com/pgaudit/pgaudit/blob/master/README.md#format)
|
||||
|
||||
| header | Description |
|
||||
| --------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| AUDIT_TYPE | SESSION or OBJECT |
|
||||
| STATEMENT_ID | Unique statement ID for this session. Sequential even if some statements are not logged. |
|
||||
| SUBSTATEMENT_ID | Sequential ID for each sub-statement within the main statement. Continuous even if some are not logged. |
|
||||
| CLASS | e.g., READ, ROLE (see pgaudit.log). |
|
||||
| COMMAND | e.g., ALTER TABLE, SELECT. |
|
||||
| OBJECT_TYPE | TABLE, INDEX, VIEW, etc. Available for SELECT, DML, and most DDL statements. |
|
||||
| OBJECT_NAME | The fully qualified object name (e.g., public.account). Available for SELECT, DML, and most DDL. |
|
||||
| STATEMENT | Statement executed on the backend. |
|
||||
| PARAMETER | If pgaudit.log_parameter is set, this field contains the statement parameters as quoted CSV, or \<none\>. Otherwise, it's \<not logged\>. |
|
||||
|
||||
A log made from the following create statement:
|
||||
|
||||
{/* prettier-ignore */}
|
||||
```sql
|
||||
create table account (
|
||||
id int primary key,
|
||||
name text,
|
||||
description text
|
||||
);
|
||||
|
||||
insert into account (id, name, description)
|
||||
values (1, 'Foo Barsworth', 'Customer account');
|
||||
|
||||
select * from account;
|
||||
```
|
||||
|
||||
Results in the log output
|
||||
Generates the following log in the [Dashboard's Postgres Logs](https://supabase.com/dashboard/project/_/logs/postgres-logs):
|
||||
|
||||
```text
|
||||
AUDIT: SESSION,1,1,DDL,CREATE TABLE,TABLE,public.account,create table account(
|
||||
```
|
||||
AUDIT: SESSION,1,1,DDL,CREATE TABLE,TABLE,public.account,create table account(
|
||||
id int,
|
||||
name text,
|
||||
description text
|
||||
);,<not logged>
|
||||
AUDIT: SESSION,2,1,READ,SELECT,,,select * from account,,<not logged>
|
||||
); <not logged>
|
||||
```
|
||||
|
||||
Note that the insert statement is not logged because we did not include the `write` option for `pgaudit.log`.
|
||||
## Finding and Filtering Audit Logs
|
||||
|
||||
Logs generated by pgaudit can be found in [Postgres Logs](https://supabase.com/dashboard/project/_/logs/postgres-logs?s=AUDIT). To find a specific log, you can use the log explorer. Below is a basic example to extract logs referencing `CREATE TABLE` events
|
||||
|
||||
```sql
|
||||
select
|
||||
cast(t.timestamp as datetime) as timestamp,
|
||||
event_message
|
||||
from
|
||||
postgres_logs as t
|
||||
cross join unnest(metadata) as m
|
||||
cross join unnest(m.parsed) as p
|
||||
where event_message like 'AUDIT%CREATE TABLE%'
|
||||
order by timestamp desc
|
||||
limit 100;
|
||||
```
|
||||
|
||||
## Practical Examples
|
||||
|
||||
### Monitoring API events
|
||||
|
||||
<Admonition type="note">
|
||||
|
||||
API requests are already recorded in the [API Edge Network](https://supabase.com/dashboard/project/_/logs/edge-logs) logs.
|
||||
|
||||
</Admonition>
|
||||
|
||||
To monitor all writes initiated by the Postgrest API roles:
|
||||
|
||||
```sql
|
||||
alter role "authenticator" set pgaudit.log to 'write';
|
||||
|
||||
-- the above is the practical equivalent to:
|
||||
-- alter role "anon" set pgaudit.log TO 'write';
|
||||
-- alter role "authenticated" set pgaudit.log TO 'write';
|
||||
-- alter role "service_role" set pgaudit.log TO 'write';
|
||||
```
|
||||
|
||||
### Monitoring the `auth.users` Table
|
||||
|
||||
In the worst case scenario, where a privileged roles' password is exposed, you can use pgaudit to monitor if the `auth.users` table was targeted. It should be stated that API requests are already monitored in the [API Edge Network](https://supabase.com/dashboard/project/_/logs/edge-logs) and this is more about providing greater clarity about what is happening at the database level.
|
||||
|
||||
Logging `auth.user` should be done in Object Mode and requires a custom role:
|
||||
|
||||
```sql
|
||||
-- create logging role
|
||||
create role "auth_auditor" noinherit;
|
||||
|
||||
-- give role permission to observe relevant table events
|
||||
grant select on auth.users to "auth_auditor";
|
||||
grant delete on auth.users to "auth_auditor";
|
||||
|
||||
-- assign auth_auditor to pgaudit.role
|
||||
alter role "postgres" set pgaudit.role to 'auth_auditor';
|
||||
|
||||
```
|
||||
|
||||
With the above code, any query involving reading or deleting the auth.users email column will be logged.
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Over-logging: Help! Too many logs!
|
||||
|
||||
pgaudit, if not configured mindfully, can log all database events, including background tasks. This can generate an undesirablely large amount of logs in a few hours.
|
||||
|
||||
The first step to solve this problem is to identify which database users pgaudit is observing:
|
||||
|
||||
```sql
|
||||
-- find all users monitored by pgaudit
|
||||
select
|
||||
rolname,
|
||||
rolconfig
|
||||
from pg_roles
|
||||
where
|
||||
exists (
|
||||
select
|
||||
1
|
||||
from UNNEST(rolconfig) as c
|
||||
where c like '%pgaudit.role%' or c like '%pgaudit.log%'
|
||||
);
|
||||
```
|
||||
|
||||
To prevent pgaudit from monitoring the problematic roles, you'll want to change their `pgaudit.log` values to `none` and `pgaudit.role` values to `empty quotes ''`
|
||||
|
||||
```sql
|
||||
-- Use to disable object level logging
|
||||
alter role "<role name>" set pgaudit.role to '';
|
||||
|
||||
-- Use to disable global and user level logging
|
||||
alter role "<role name>" set pgaudit.log to 'none';
|
||||
```
|
||||
|
||||
## FAQ
|
||||
|
||||
#### How can I use pgaudit to debug database functions?
|
||||
|
||||
Technically yes, but it is not the best approach. It is better to check out our [function debugging guide](https://supabase.com/docs/guides/database/functions#general-logging) instead.
|
||||
|
||||
#### How can I download database logs?
|
||||
|
||||
In the [Logs Dashboard](https://supabase.com/dashboard/project/_/logs/postgres-logs) you can download logs as CSVs.
|
||||
|
||||
#### Can I log observed table rows?
|
||||
|
||||
By default, pgaudit records queries, but not the returned rows. You can modify this behavior with the `pgaudit.log_rows` variable:
|
||||
|
||||
```sql
|
||||
--enable
|
||||
alter role "postgres" set pgaudit.log_rows to 'on';
|
||||
|
||||
-- disable
|
||||
alter role "postgres" set pgaudit.log_rows to 'off';
|
||||
```
|
||||
|
||||
You should not do this unless you are _absolutely_ certain it is necessary for your use case. It can expose sensitive values to your logs that ideally should not be preserved. Furthermore, if done in excess, it can noticably reduce database performance.
|
||||
|
||||
#### Can I log function parameters?
|
||||
|
||||
We don't currently support configuring pgaudit.log_parameter because it may log secrets in encrypted columns if you are using [pgsodium](https://supabase.com/docs/guides/database/extensions/pgsodium) or[Vault](https://supabase.com/docs/guides/database/vault).
|
||||
|
||||
You can upvote this [feature request](https://github.com/orgs/supabase/discussions/20183) with your use-case if you'd like this restriction lifted.
|
||||
|
||||
#### Why do the official pgaudit docs differ from the Supabase one?
|
||||
|
||||
pgaudit allows settings to be applied at 3 different database scopes:
|
||||
|
||||
| Scope | Definition | Configuration File/Command |
|
||||
| -------- | ------------------ | -------------------------- |
|
||||
| System | Entire server | ALTER SYSTEM commands |
|
||||
| Database | Specific database | ALTER DATABASE commands |
|
||||
| Role | Specific user/role | ALTER ROLE commands |
|
||||
|
||||
Supabase limits full privileges for file system and database variables, meaning pgaudit modifications can only occur at the ROLE level. Assigning pgaudit to the 'postgres' role provides it with the role's database visibility, allowing `ROLE` level adjustments a practical alternative to DATABASE/SYSTEM level configurations.
|
||||
|
||||
pgaudit's [official documentation](https://www.pgaudit.org) focuses on system and database level configs, but its docs officially supports `ROLE` level configs, too:
|
||||
|
||||
## Resources
|
||||
|
||||
- Official [`PGAudit` documentation](https://www.pgaudit.org)
|
||||
- [Database Function Logging](https://supabase.com/docs/guides/database/functions#general-logging)
|
||||
- [Supabase Logging](https://supabase.com/docs/guides/platform/logs)
|
||||
- [Self-Hosting Logs](https://supabase.com/docs/reference/self-hosting-analytics/introduction)
|
||||
@@ -4,7 +4,7 @@ title: 'pgjwt: JSON Web Tokens'
|
||||
description: 'Encode and decode JWTs in PostgreSQL'
|
||||
---
|
||||
|
||||
The [pgjwt](https://github.com/michelp/pgjwt) (PostgreSQL JSON Web Token) extension allows you to create and parse [JSON Web Tokens (JWTs)](https://en.wikipedia.org/wiki/JSON_Web_Token) within a PostgreSQL database. JWTs are commonly used for authentication and authorization in web applications and services.
|
||||
The [`pgjwt`](https://github.com/michelp/pgjwt) (PostgreSQL JSON Web Token) extension allows you to create and parse [JSON Web Tokens (JWTs)](https://en.wikipedia.org/wiki/JSON_Web_Token) within a PostgreSQL database. JWTs are commonly used for authentication and authorization in web applications and services.
|
||||
|
||||
## Enable the extension
|
||||
|
||||
@@ -19,7 +19,7 @@ The [pgjwt](https://github.com/michelp/pgjwt) (PostgreSQL JSON Web Token) extens
|
||||
|
||||
1. Go to the [Database](https://supabase.com/dashboard/project/_/database/tables) page in the Dashboard.
|
||||
2. Click on **Extensions** in the sidebar.
|
||||
3. Search for "pgjwt" and enable the extension.
|
||||
3. Search for `pgjwt` and enable the extension.
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="sql" label="SQL">
|
||||
@@ -33,7 +33,7 @@ create extension pgjwt schema extensions;
|
||||
drop extension if exists pgjwt;
|
||||
```
|
||||
|
||||
Even though the SQL code is `create extension`, this is the equivalent of "enabling the extension".
|
||||
Even though the SQL code is `create extension`, this is the equivalent of enabling the extension.
|
||||
To disable an extension you can call `drop extension`.
|
||||
|
||||
It's good practice to create the extension within a separate schema (like `extensions`) to keep the `public` schema clean.
|
||||
@@ -67,7 +67,7 @@ select
|
||||
);
|
||||
```
|
||||
|
||||
The pgjwt_encode function returns a string that represents the JWT, which can then be safely transmitted between parties.
|
||||
The `pgjwt_encode` function returns a string that represents the JWT, which can then be safely transmitted between parties.
|
||||
|
||||
{/* prettier-ignore */}
|
||||
```
|
||||
|
||||
@@ -20,7 +20,7 @@ tocVideo: 'Mmmv9g_MiBA'
|
||||
|
||||
1. Go to the [Database](https://supabase.com/dashboard/project/_/database/tables) page in the Dashboard.
|
||||
2. Click on **Extensions** in the sidebar.
|
||||
3. Search for "pgroonga" and enable the extension.
|
||||
3. Search for `pgroonga` and enable the extension.
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="sql" label="SQL">
|
||||
@@ -34,7 +34,7 @@ create extension pgroonga with schema extensions;
|
||||
drop extension if exists pgroonga;
|
||||
```
|
||||
|
||||
Even though the SQL code is `create extension`, this is the equivalent of "enabling the extension".
|
||||
Even though the SQL code is `create extension`, this is the equivalent of enabling the extension.
|
||||
To disable an extension you can call `drop extension`.
|
||||
|
||||
</TabPanel>
|
||||
@@ -92,7 +92,7 @@ Index Scan using ix_memos_content on memos (cost=0.00..1.11 rows=1 width=36)
|
||||
(2 rows)
|
||||
```
|
||||
|
||||
The pgroonga index is used to retrieve the result set:
|
||||
The `pgroonga` index is used to retrieve the result set:
|
||||
|
||||
```markdown
|
||||
| id | content |
|
||||
@@ -102,7 +102,7 @@ The pgroonga index is used to retrieve the result set:
|
||||
|
||||
## Full text search
|
||||
|
||||
The `&@~` operator performs full text search. It returns any matching results. Unlike `LIKE` operator, pgroonga can search any text that contains the keyword case insensitive.
|
||||
The `&@~` operator performs full text search. It returns any matching results. Unlike `LIKE` operator, `pgroonga` can search any text that contains the keyword case insensitive.
|
||||
|
||||
Take the following example:
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ title: 'pgrouting: Geospatial Routing'
|
||||
description: 'Extends PostGIS with Geospatial Routing'
|
||||
---
|
||||
|
||||
[pgRouting](http://pgrouting.org) is PostgreSQL and [PostGIS](http://postgis.net) extension adding geospatial routing functionality.
|
||||
[`pgRouting`](http://pgrouting.org) is PostgreSQL and [PostGIS](http://postgis.net) extension adding geospatial routing functionality.
|
||||
|
||||
The core functionality of `pgRouting` is a set of path finding algorithms including:
|
||||
|
||||
@@ -33,7 +33,7 @@ The core functionality of `pgRouting` is a set of path finding algorithms includ
|
||||
|
||||
1. Go to the [Database](https://supabase.com/dashboard/project/_/database/tables) page in the Dashboard.
|
||||
2. Click on **Extensions** in the sidebar.
|
||||
3. Search for "pgrouting" and enable the extension.
|
||||
3. Search for `pgrouting` and enable the extension.
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="sql" label="SQL">
|
||||
@@ -47,7 +47,7 @@ create extension pgrouting cascade;
|
||||
drop extension if exists pgRouting;
|
||||
```
|
||||
|
||||
Even though the SQL code is `create extension`, this is the equivalent of "enabling the extension".
|
||||
Even though the SQL code is `create extension`, this is the equivalent of enabling the extension.
|
||||
To disable an extension you can call `drop extension`.
|
||||
|
||||
</TabPanel>
|
||||
@@ -55,9 +55,9 @@ To disable an extension you can call `drop extension`.
|
||||
|
||||
## Example
|
||||
|
||||
As an example, we'll solve the [traveling salesman problem](https://en.wikipedia.org/wiki/Travelling_salesman_problem) using the pgRouting's `pgr_TSPeuclidean` function from some PostGIS coordinates.
|
||||
As an example, we'll solve the [traveling salesperson problem](https://en.wikipedia.org/wiki/Travelling_salesman_problem) using the `pgRouting`'s `pgr_TSPeuclidean` function from some PostGIS coordinates.
|
||||
|
||||
A summary of the traveling salesman problem is, given a set of city coordinates, solve for a path that goes through each city and minimizes the total distance traveled.
|
||||
A summary of the traveling salesperson problem is, given a set of city coordinates, solve for a path that goes through each city and minimizes the total distance traveled.
|
||||
|
||||
First we populate a table with some X, Y coordinates
|
||||
|
||||
|
||||
@@ -4,13 +4,13 @@ title: 'pgsodium: Encryption Features'
|
||||
description: 'Encryption library for PostgreSQL'
|
||||
---
|
||||
|
||||
[pgsodium](https://github.com/michelp/pgsodium) is a PostgreSQL extension which provides SQL access to [libsodium's](https://doc.libsodium.org/) high-level cryptographic algorithms.
|
||||
[`pgsodium`](https://github.com/michelp/pgsodium) is a PostgreSQL extension which provides SQL access to [`libsodium`'s](https://doc.libsodium.org/) high-level cryptographic algorithms.
|
||||
|
||||
libsodium is a modern, easy-to-use software library for encryption, decryption, signatures, password hashing, and more. It is a portable, cross-compilable, installable, and packageable fork of the [NaCl](http://nacl.cr.yp.to/) library, with a compatible but extended API to improve usability even further.
|
||||
`libsodium` is a modern, easy-to-use software library for encryption, decryption, signatures, password hashing, and more. It is a portable, cross-compilable, installable, and packageable fork of the [NaCl](http://nacl.cr.yp.to/) library, with a compatible but extended API to improve usability even further.
|
||||
|
||||
The design choices emphasize security and ease of use. But despite the emphasis on high security, primitives are faster across-the-board than most implementations.
|
||||
|
||||
pgsodium exposes the following libsodium APIs to SQL:
|
||||
`pgsodium` exposes the following `libsodium` APIs to SQL:
|
||||
|
||||
- [Generating Random Data](https://github.com/michelp/pgsodium/#generating-random-data)
|
||||
- [Secret key cryptography](https://github.com/michelp/pgsodium/#secret-key-cryptography)
|
||||
@@ -53,7 +53,7 @@ Note that Supabase projects are already encrypted at rest by default.
|
||||
|
||||
## Get the root encryption key for your Supabase project
|
||||
|
||||
Encryption requires keys. Keeping the keys in the same database as the encrypted data would be unsafe. For more information about managing the pgsodium root encryption key on your Supabase project see **[encryption key location](/docs/guides/database/vault#encryption-key-location)**. This key is required to decrypt values stored in [Supabase Vault](/docs/guides/database/vault) and data encrypted with Transparent Column Encryption.
|
||||
Encryption requires keys. Keeping the keys in the same database as the encrypted data would be unsafe. For more information about managing the `pgsodium` root encryption key on your Supabase project see **[encryption key location](/docs/guides/database/vault#encryption-key-location)**. This key is required to decrypt values stored in [Supabase Vault](/docs/guides/database/vault) and data encrypted with Transparent Column Encryption.
|
||||
|
||||
## Enable the extension
|
||||
|
||||
@@ -68,7 +68,7 @@ Encryption requires keys. Keeping the keys in the same database as the encrypted
|
||||
|
||||
1. Go to the [Database](https://supabase.com/dashboard/project/_/database/tables) page in the Dashboard.
|
||||
2. Click on **Extensions** in the sidebar.
|
||||
3. Search for "pgsodium" and enable the extension.
|
||||
3. Search for `pgsodium` and enable the extension.
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="sql" label="SQL">
|
||||
|
||||
@@ -26,7 +26,7 @@ Let's cover some basic concepts:
|
||||
|
||||
1. Go to the [Database](https://supabase.com/dashboard/project/_/database/tables) page in the Dashboard.
|
||||
2. Click on **Extensions** in the sidebar.
|
||||
3. Search for "pgtap" and enable the extension.
|
||||
3. Search for `pgtap` and enable the extension.
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="sql" label="SQL">
|
||||
@@ -39,7 +39,7 @@ create extension pgtap with schema extensions;
|
||||
drop extension if exists pgtap;
|
||||
```
|
||||
|
||||
Even though the SQL code is `create extension`, this is the equivalent of "enabling the extension".
|
||||
Even though the SQL code is `create extension`, this is the equivalent of enabling the extension.
|
||||
To disable an extension you can call `drop extension`.
|
||||
|
||||
It's good practice to create the extension within a separate schema (like `extensions`) to keep the `public` schema clean.
|
||||
@@ -149,7 +149,7 @@ rollback;
|
||||
API:
|
||||
|
||||
- [`function_returns()`](https://pgtap.org/documentation.html#function_returns): Tests that a particular function returns a particular data type
|
||||
- [`is_definer()`](https://pgtap.org/documentation.html#is_definer): Tests that a function is a security definer (i.e., a “setuid” function).
|
||||
- [`is_definer()`](https://pgtap.org/documentation.html#is_definer): Tests that a function is a security definer (that is, a `setuid` function).
|
||||
|
||||
## Resources
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ The `plv8` extension allows you use JavaScript within Postgres.
|
||||
|
||||
## Overview
|
||||
|
||||
While Postgres natively runs SQL, it can also run other "procedural languages".
|
||||
While Postgres natively runs SQL, it can also run other procedural languages.
|
||||
`plv8` allows you to run JavaScript code - specifically any code that runs on the [V8 JavaScript engine](https://v8.dev).
|
||||
|
||||
It can be used for database functions, triggers, queries and more.
|
||||
@@ -39,7 +39,7 @@ create extension plv8;
|
||||
drop extension if exists plv8;
|
||||
```
|
||||
|
||||
Even though the SQL code is `create extension`, this is the equivalent of "enabling the extension".
|
||||
Even though the SQL code is `create extension`, this is the equivalent of enabling the extension.
|
||||
To disable an extension, call `drop extension`.
|
||||
|
||||
Procedural languages are automatically installed within `pg_catalog`, so you don't need to specify a schema.
|
||||
|
||||
@@ -30,6 +30,8 @@ For this guide we'll use the following example data:
|
||||
>
|
||||
<TabPanel id="data" label="Data">
|
||||
|
||||
{/* <!-- vale off --> */}
|
||||
|
||||
| id | title | author | description |
|
||||
| --- | ----------------------------------- | ---------------------- | ------------------------------------------------------------------ |
|
||||
| 1 | The Poky Little Puppy | Janette Sebring Lowrey | Puppy is slower than other, bigger animals. |
|
||||
@@ -38,6 +40,8 @@ For this guide we'll use the following example data:
|
||||
| 4 | Green Eggs and Ham | Dr. Seuss | Sam has changing food preferences and eats unusually colored food. |
|
||||
| 5 | Harry Potter and the Goblet of Fire | J.K. Rowling | Fourth year of school starts, big drama ensues. |
|
||||
|
||||
{/* <!-- vale on --> */}
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="sql" label="SQL">
|
||||
|
||||
@@ -80,7 +84,7 @@ The functions we'll cover in this guide are:
|
||||
|
||||
### `to_tsvector()` [#to-tsvector]
|
||||
|
||||
Converts your data into searchable "tokens". `to_tsvector()` stands for "to text search vector". For example:
|
||||
Converts your data into searchable tokens. `to_tsvector()` stands for "to text search vector." For example:
|
||||
|
||||
```sql
|
||||
select to_tsvector('green eggs and ham');
|
||||
@@ -91,10 +95,10 @@ Collectively these tokens are called a "document" which Postgres can use for com
|
||||
|
||||
### `to_tsquery()` [#to-tsquery]
|
||||
|
||||
Converts a query string into "tokens" to match. `to_tsquery()` stands for "to text search query".
|
||||
Converts a query string into tokens to match. `to_tsquery()` stands for "to text search query."
|
||||
|
||||
This conversion step is important because we will want to "fuzzy match" on keywords.
|
||||
For example if a user searches for "eggs", and a column has the value "egg", we probably still want to return a match.
|
||||
For example if a user searches for `eggs`, and a column has the value `egg`, we probably still want to return a match.
|
||||
|
||||
### Match: `@@` [#match]
|
||||
|
||||
@@ -253,11 +257,15 @@ val data = supabase.from("books").select {
|
||||
</TabPanel>
|
||||
<TabPanel id="data" label="Data">
|
||||
|
||||
{/* <!-- vale off --> */}
|
||||
|
||||
| id | title | author | description |
|
||||
| --- | ----------------------------------- | ----------------- | ----------------------------------------------- |
|
||||
| 3 | Tootle | Gertrude Crampton | Little toy train has big dreams. |
|
||||
| 5 | Harry Potter and the Goblet of Fire | J.K. Rowling | Fourth year of school starts, big drama ensues. |
|
||||
|
||||
{/* <!-- vale on --> */}
|
||||
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
@@ -338,11 +346,15 @@ val data = supabase.from("books").select {
|
||||
</TabPanel>
|
||||
<TabPanel id="data" label="Data">
|
||||
|
||||
{/* <!-- vale off --> */}
|
||||
|
||||
| id | title | author | description |
|
||||
| --- | --------------------- | ---------------------- | ------------------------------------------- |
|
||||
| 1 | The Poky Little Puppy | Janette Sebring Lowrey | Puppy is slower than other, bigger animals. |
|
||||
| 3 | Tootle | Gertrude Crampton | Little toy train has big dreams. |
|
||||
|
||||
{/* <!-- vale on --> */}
|
||||
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
@@ -403,10 +415,14 @@ val data = supabase.from("books").select {
|
||||
</TabPanel>
|
||||
<TabPanel id="data" label="Data">
|
||||
|
||||
{/* <!-- vale off --> */}
|
||||
|
||||
| id | title | author | description |
|
||||
| --- | ------ | ----------------- | -------------------------------- |
|
||||
| 3 | Tootle | Gertrude Crampton | Little toy train has big dreams. |
|
||||
|
||||
{/* <!-- vale on --> */}
|
||||
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
@@ -467,11 +483,15 @@ val data = supabase.from("books").select {
|
||||
</TabPanel>
|
||||
<TabPanel id="data" label="Data">
|
||||
|
||||
{/* <!-- vale off --> */}
|
||||
|
||||
| id | title | author | description |
|
||||
| --- | --------------------- | ---------------------- | ------------------------------------------- |
|
||||
| 1 | The Poky Little Puppy | Janette Sebring Lowrey | Puppy is slower than other, bigger animals. |
|
||||
| 3 | Tootle | Gertrude Crampton | Little toy train has big dreams. |
|
||||
|
||||
{/* <!-- vale on --> */}
|
||||
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
@@ -514,6 +534,8 @@ from books;
|
||||
</TabPanel>
|
||||
<TabPanel id="data" label="Data">
|
||||
|
||||
{/* <!-- vale off --> */}
|
||||
|
||||
| id | fts |
|
||||
| --- | --------------------------------------------------------------------------------------------------------------- |
|
||||
| 1 | 'anim':7 'bigger':6 'littl':10 'poki':9 'puppi':1,11 'slower':3 |
|
||||
@@ -522,6 +544,8 @@ from books;
|
||||
| 4 | 'chang':3 'color':9 'eat':7 'egg':12 'food':4,10 'green':11 'ham':14 'prefer':5 'sam':1 'unus':8 |
|
||||
| 5 | 'big':6 'drama':7 'ensu':8 'fire':15 'fourth':1 'goblet':13 'harri':9 'potter':10 'school':4 'start':5 'year':2 |
|
||||
|
||||
{/* <!-- vale on --> */}
|
||||
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
@@ -578,10 +602,14 @@ val data = supabase.from("books").select {
|
||||
</TabPanel>
|
||||
<TabPanel id="data" label="Data">
|
||||
|
||||
{/* <!-- vale off --> */}
|
||||
|
||||
| id | title | author | description | fts |
|
||||
| --- | ------ | ----------------- | -------------------------------- | ------------------------------------------------------- |
|
||||
| 3 | Tootle | Gertrude Crampton | Little toy train has big dreams. | 'big':5 'dream':6 'littl':1 'tootl':7 'toy':2 'train':3 |
|
||||
|
||||
{/* <!-- vale on --> */}
|
||||
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
|
||||
@@ -336,6 +336,174 @@ GRANT EXECUTE ON FUNCTION hello_world TO authenticated;
|
||||
GRANT EXECUTE ON FUNCTION hello_world TO service_role;
|
||||
```
|
||||
|
||||
### Debugging functions
|
||||
|
||||
You can add logs to help you debug functions. This is especially recommended for complex functions.
|
||||
|
||||
Good targets to log include:
|
||||
|
||||
- Values of (non-sensitive) variables
|
||||
- Returned results from queries
|
||||
|
||||
#### General logging
|
||||
|
||||
To create custom logs in the [Dashboard's Postgres Logs](/apps/docs/public/img/database/postgres-logs.png), you can use the `raise` keyword. By default, there are 3 observed severity levels:
|
||||
|
||||
- `log`
|
||||
- `warning`
|
||||
- `exception` (error level)
|
||||
|
||||
```sql
|
||||
create function logging_example(
|
||||
log_message text,
|
||||
warning_message text,
|
||||
error_message text
|
||||
)
|
||||
returns void
|
||||
language plpgsql
|
||||
as $$
|
||||
begin
|
||||
raise log 'logging message: %', log_message;
|
||||
raise warning 'logging warning: %', warning_message;
|
||||
|
||||
-- immediately ends function and reverts transaction
|
||||
raise exception 'logging error: %', error_message;
|
||||
end;
|
||||
$$;
|
||||
|
||||
select logging_example('LOGGED MESSAGE', 'WARNING MESSAGE', 'ERROR MESSAGE');
|
||||
```
|
||||
|
||||
#### Error handling
|
||||
|
||||
You can create custom errors with the `raise exception` keywords.
|
||||
|
||||
A common pattern is to throw an error when a variable doesn't meet a condition:
|
||||
|
||||
```sql
|
||||
create or replace function error_if_null(some_val text)
|
||||
returns text
|
||||
language plpgsql
|
||||
as $$
|
||||
begin
|
||||
-- error if some_val is null
|
||||
if some_val is null then
|
||||
raise exception 'some_val should not be NULL';
|
||||
end if;
|
||||
-- return some_val if it is not null
|
||||
return some_val;
|
||||
end;
|
||||
$$;
|
||||
|
||||
select error_if_null(null);
|
||||
```
|
||||
|
||||
Value checking is common, so Postgres provides a shorthand: the `assert` keyword. It uses the following format:
|
||||
|
||||
```sql
|
||||
-- throw error when condition is false
|
||||
assert <some condition>, 'message';
|
||||
```
|
||||
|
||||
Below is an example
|
||||
|
||||
```sql
|
||||
create function assert_example(name text)
|
||||
returns uuid
|
||||
language plpgsql
|
||||
as $$
|
||||
declare
|
||||
student_id uuid;
|
||||
begin
|
||||
-- save a user's id into the user_id variable
|
||||
select
|
||||
id into student_id
|
||||
from attendance_table
|
||||
where student = name;
|
||||
|
||||
-- throw an error if the student_id is null
|
||||
assert student_id is not null, 'assert_example() ERROR: student not found';
|
||||
|
||||
-- otherwise, return the user's id
|
||||
return student_id;
|
||||
end;
|
||||
$$;
|
||||
|
||||
select assert_example('Harry Potter');
|
||||
```
|
||||
|
||||
Error messages can also be captured and modified with the `exception` keyword:
|
||||
|
||||
```sql
|
||||
create function error_example()
|
||||
returns void
|
||||
language plpgsql
|
||||
as $$
|
||||
begin
|
||||
-- fails: cannot read from nonexistent table
|
||||
select * from table_that_does_not_exist;
|
||||
|
||||
exception
|
||||
when others then
|
||||
raise exception 'An error occurred in function <function name>: %', sqlerrm;
|
||||
end;
|
||||
$$;
|
||||
```
|
||||
|
||||
#### Advanced logging
|
||||
|
||||
For more complex functions or complicated debugging, try logging:
|
||||
|
||||
- Formatted variables
|
||||
- Individual rows
|
||||
- Start and end of function calls
|
||||
|
||||
```sql
|
||||
create or replace function advanced_example(num int default 10)
|
||||
returns text
|
||||
language plpgsql
|
||||
as $$
|
||||
declare
|
||||
var1 int := 20;
|
||||
var2 text;
|
||||
begin
|
||||
-- Logging start of function
|
||||
raise log 'logging start of function call: (%)', (select now());
|
||||
|
||||
-- Logging a variable from a SELECT query
|
||||
select
|
||||
col_1 into var1
|
||||
from some_table
|
||||
limit 1;
|
||||
raise log 'logging a variable (%)', var1;
|
||||
|
||||
-- It is also possible to avoid using variables, by returning the values of your query to the log
|
||||
raise log 'logging a query with a single return value(%)', (select col_1 from some_table limit 1);
|
||||
|
||||
-- If necessary, you can even log an entire row as JSON
|
||||
raise log 'logging an entire row as JSON (%)', (select to_jsonb(some_table.*) from some_table limit 1);
|
||||
|
||||
-- When using INSERT or UPDATE, the new value(s) can be returned
|
||||
-- into a variable.
|
||||
-- When using DELETE, the deleted value(s) can be returned.
|
||||
-- All three operations use "RETURNING value(s) INTO variable(s)" syntax
|
||||
insert into some_table (col_2)
|
||||
values ('new val')
|
||||
returning col_2 into var2;
|
||||
|
||||
raise log 'logging a value from an INSERT (%)', var2;
|
||||
|
||||
return var1 || ',' || var2;
|
||||
exception
|
||||
-- Handle exceptions here if needed
|
||||
when others then
|
||||
raise exception 'An error occurred in function <advanced_example>: %', sqlerrm;
|
||||
end;
|
||||
$$;
|
||||
|
||||
select advanced_example();
|
||||
```
|
||||
|
||||
## Resources
|
||||
|
||||
- Official Client libraries: [JavaScript](../../reference/javascript/rpc) and [Flutter](../../reference/dart/rpc)
|
||||
|
||||
@@ -260,3 +260,60 @@ GET https://[REF].supabase.co/rest/v1/teams?select=id,team_name,users(id,name)
|
||||
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
## Specifying the `ON` clause for joins with multiple foreign keys
|
||||
|
||||
For example, if you have a project that tracks when employees check in and out of work shifts:
|
||||
|
||||
```sql
|
||||
-- Employees
|
||||
create table users (
|
||||
"id" serial primary key,
|
||||
"name" text
|
||||
);
|
||||
|
||||
-- Badge scans
|
||||
create table scans (
|
||||
"id" serial primary key,
|
||||
"user_id" int references users,
|
||||
"badge_scan_time" timestamp
|
||||
);
|
||||
|
||||
-- Work shifts
|
||||
create table shifts (
|
||||
"id" serial primary key,
|
||||
"user_id" int references users,
|
||||
"scan_id_start" int references scans, -- clocking in
|
||||
"scan_id_end" int references scans, -- clocking out
|
||||
"attendance_status" text
|
||||
);
|
||||
```
|
||||
|
||||
In this case, you need to explicitly define the join because the joining column on `shifts` is ambiguous.
|
||||
|
||||
To fetch all the `shifts` with `scan_id_start` related to a specific `scan`, use the following syntax:
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
size="small"
|
||||
type="underlined"
|
||||
defaultActiveId="js"
|
||||
queryGroup="language"
|
||||
>
|
||||
<TabPanel id="js" label="JavaScript">
|
||||
|
||||
```js
|
||||
const { data, error } = await supabase.from('shifts').select(
|
||||
`
|
||||
*,
|
||||
scans:scan_id_start (
|
||||
id,
|
||||
user_id,
|
||||
badge_scan_time
|
||||
)
|
||||
`
|
||||
)
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
@@ -6,7 +6,7 @@ description: 'How to change your database timezone.'
|
||||
---
|
||||
|
||||
Every Supabase database is set to UTC timezone by default. We strongly recommend keeping it this way, even if your users are in a different location.
|
||||
This is because it makes it much easier to calculate differences between timezones if you adopt the mental model that "everything in my database is in UTC time".
|
||||
This is because it makes it much easier to calculate differences between timezones if you adopt the mental model that "everything in my database is in UTC time."
|
||||
|
||||
### Change timezone
|
||||
|
||||
|
||||
@@ -6,10 +6,10 @@ footerHelpType: 'postgres'
|
||||
|
||||
There are 5 options for foreign key constraint deletes:
|
||||
|
||||
1. **CASCADE:** When a row is deleted from the parent table, all related rows in the child table(s) are deleted as well.
|
||||
2. **RESTRICT:** When a row is deleted from the parent table, the delete operation is aborted if there are any related rows in the child table(s).
|
||||
3. **SET NULL:** When a row is deleted from the parent table, the values of the foreign key columns in the child table(s) are set to NULL.
|
||||
4. **SET DEFAULT:** When a row is deleted from the parent table, the values of the foreign key columns in the child table(s) are set to their default values.
|
||||
1. **CASCADE:** When a row is deleted from the parent table, all related rows in the child tables are deleted as well.
|
||||
2. **RESTRICT:** When a row is deleted from the parent table, the delete operation is aborted if there are any related rows in the child tables.
|
||||
3. **SET NULL:** When a row is deleted from the parent table, the values of the foreign key columns in the child tables are set to NULL.
|
||||
4. **SET DEFAULT:** When a row is deleted from the parent table, the values of the foreign key columns in the child tables are set to their default values.
|
||||
5. **NO ACTION:** This option is similar to RESTRICT, but it also has the option to be “deferred” to the end of a transaction. This means that other cascading deletes can run first, and then this delete constraint will only throw an error if there is referenced data remaining _at the end of the transaction_.
|
||||
|
||||
These options can be specified when defining a foreign key constraint using the "ON DELETE" clause. For example, the following SQL statement creates a foreign key constraint with the `CASCADE` option:
|
||||
@@ -20,7 +20,7 @@ add constraint fk_parent foreign key (parent_id) references parent_table (id)
|
||||
on delete cascade;
|
||||
```
|
||||
|
||||
This means that when a row is deleted from the "parent_table", all related rows in the "child_table" will be deleted as well.
|
||||
This means that when a row is deleted from the `parent_table`, all related rows in the `child_table` will be deleted as well.
|
||||
|
||||
## `RESTRICT` vs `NO ACTION`
|
||||
|
||||
@@ -28,11 +28,11 @@ The difference between `NO ACTION` and `RESTRICT` is subtle and can be a bit con
|
||||
|
||||
Both `NO ACTION` and `RESTRICT` are used to prevent deletion of a row in a parent table if there are related rows in a child table. However, there is a subtle difference in how they behave.
|
||||
|
||||
When a foreign key constraint is defined with the option `RESTRICT`, it means that if a row in the parent table is deleted, the database will immediately raise an error and prevent the deletion of the row in the parent table. The database will not delete, update or set to NULL any rows in the referenced table(s).
|
||||
When a foreign key constraint is defined with the option `RESTRICT`, it means that if a row in the parent table is deleted, the database will immediately raise an error and prevent the deletion of the row in the parent table. The database will not delete, update or set to NULL any rows in the referenced tables.
|
||||
|
||||
When a foreign key constraint is defined with the option `NO ACTION`, it means that if a row in the parent table is deleted, the database will also raise an error and prevent the deletion of the row in the parent table. However unlike `RESTRICT`, `NO ACTION` has the option defer the check using `INITIALLY DEFERRED`. This will only raise the above error _if_ the referenced rows still exist at the end of the transaction.
|
||||
|
||||
The difference from `RESTRICT` is that a constraint marked as `NO ACTION INITIALLY DEFERRED` is deferred until the end of the transaction, rather than running immediately. If, for example there is another foreign key constraint between the same tables marked as `CASCADE`, the cascade will occur first and delete the referenced rows, and no error will be thrown by the deferred constraint. Otherwise if there are still rows referencing the parent row by the end of the transaction, an error will be raised just like before. Just like `RESTRICT`, the database will not delete, update or set to NULL any rows in the referenced table(s).
|
||||
The difference from `RESTRICT` is that a constraint marked as `NO ACTION INITIALLY DEFERRED` is deferred until the end of the transaction, rather than running immediately. If, for example there is another foreign key constraint between the same tables marked as `CASCADE`, the cascade will occur first and delete the referenced rows, and no error will be thrown by the deferred constraint. Otherwise if there are still rows referencing the parent row by the end of the transaction, an error will be raised just like before. Just like `RESTRICT`, the database will not delete, update or set to NULL any rows in the referenced tables.
|
||||
|
||||
In practice, you can use either `NO ACTION` or `RESTRICT` depending on your needs. `NO ACTION` is the default behavior if you do not specify anything. If you prefer to defer the check until the end of the transaction, use `NO ACTION INITIALLY DEFERRED`.
|
||||
|
||||
@@ -48,10 +48,10 @@ Let's further illustrate the difference with an example. We'll use the following
|
||||
|
||||
`parent`
|
||||
|
||||
| id | name | parent_id |
|
||||
| --- | ------- | --------- |
|
||||
| 1 | Charles | 1 |
|
||||
| 2 | Diana | 1 |
|
||||
| id | name | `parent_id` |
|
||||
| --- | ------- | ----------- |
|
||||
| 1 | Charles | 1 |
|
||||
| 2 | Diana | 1 |
|
||||
|
||||
`child`
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ This data can further be used in conjunction with the [`explain`](https://www.po
|
||||
## Managing timezones
|
||||
|
||||
Every Supabase database is set to UTC timezone by default. We strongly recommend keeping it this way, even if your users are in a different location.
|
||||
This is because it makes it much easier to calculate differences between timezones if you adopt the mental model that "everything in my database is in UTC time".
|
||||
This is because it makes it much easier to calculate differences between timezones if you adopt the mental model that "everything in my database is in UTC time."
|
||||
|
||||
### Change timezone
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ description: 'Managing access to your Postgres database and configuring permissi
|
||||
subtitle: 'Managing access to your Postgres database and configuring permissions.'
|
||||
---
|
||||
|
||||
Postgres manages database access permissions using the concept of roles. Generally you wouldn't use these roles for your own application - they are mostly for configuring _system access_ to your database. If you want to configure _application access_, then you should use [Row Level Security](/docs/guides/database/postgres/row-level-security).
|
||||
Postgres manages database access permissions using the concept of roles. Generally you wouldn't use these roles for your own application - they are mostly for configuring _system access_ to your database. If you want to configure _application access_, then you should use [Row Level Security](/docs/guides/database/postgres/row-level-security) (RLS). You can also implement [Role-based Access Control](/docs/guides/auth/custom-claims-and-role-based-access-control-rbac) on top of RLS.
|
||||
|
||||
## Users vs roles
|
||||
|
||||
@@ -38,11 +38,11 @@ Your Postgres database is the core of your Supabase project, so it's important t
|
||||
|
||||
### Special symbols in passwords
|
||||
|
||||
If you use special symbols in your postgres password, you must remember to [percent-encode](https://en.wikipedia.org/wiki/Percent-encoding) your password later if using the postgres connection string e.g. `postgresql://postgres.projectref:p%3Dword@aws-0-us-east-1.pooler.supabase.com:6543/postgres`
|
||||
If you use special symbols in your postgres password, you must remember to [percent-encode](https://en.wikipedia.org/wiki/Percent-encoding) your password later if using the postgres connection string, for example, `postgresql://postgres.projectref:p%3Dword@aws-0-us-east-1.pooler.supabase.com:6543/postgres`
|
||||
|
||||
### Changing your project password
|
||||
|
||||
When you created your project you were also asked to enter a password. This is actually the password for the `postgres` role in your database. You can update this from the Dashboard under the [database settings](https://supabase.com/dashboard/project/_/settings/database) page. You should _never_ give this to 3rd-party service unless you absolutely trust them. Instead, we recommend that you create a new user for every service that you want to give access too. This will also help you with debugging - you can see every query that each role is executing in your database within `pg_stat_statements`.
|
||||
When you created your project you were also asked to enter a password. This is actually the password for the `postgres` role in your database. You can update this from the Dashboard under the [database settings](https://supabase.com/dashboard/project/_/settings/database) page. You should _never_ give this to third-party service unless you absolutely trust them. Instead, we recommend that you create a new user for every service that you want to give access too. This will also help you with debugging - you can see every query that each role is executing in your database within `pg_stat_statements`.
|
||||
|
||||
## Granting permissions
|
||||
|
||||
@@ -86,7 +86,7 @@ The default Postgres role. This has admin privileges.
|
||||
|
||||
### `anon`
|
||||
|
||||
For "anonymous access". This is the role which the API (PostgREST) will use when a user _is not_ logged in.
|
||||
For unauthenticated, public access. This is the role which the API (PostgREST) will use when a user _is not_ logged in.
|
||||
|
||||
### `authenticator`
|
||||
|
||||
@@ -95,7 +95,7 @@ A special role for the API (PostgREST). It has very limited access, and is used
|
||||
|
||||
### `authenticated`
|
||||
|
||||
For "authenticated access". This is the role which the API (PostgREST) will use when a user _is_ logged in.
|
||||
For "authenticated access." This is the role which the API (PostgREST) will use when a user _is_ logged in.
|
||||
|
||||
### `service_role`
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ When you need granular authorization rules, nothing beats Postgres's [Row Level
|
||||
|
||||
RLS is incredibly powerful and flexible, allowing you to write complex SQL rules that fit your unique business needs. RLS can be [combined with Supabase Auth](/docs/guides/auth/row-level-security) for end-to-end user security from the browser to the database.
|
||||
|
||||
RLS is a Postgres primitive and can provide "[defense in depth](<https://en.wikipedia.org/wiki/Defense_in_depth_(computing)>)" to protect your data from malicious actors even when accessed through 3rd party tooling.
|
||||
RLS is a Postgres primitive and can provide "[defense in depth](<https://en.wikipedia.org/wiki/Defense_in_depth_(computing)>)" to protect your data from malicious actors even when accessed through third-party tooling.
|
||||
|
||||
## Policies
|
||||
|
||||
@@ -219,7 +219,7 @@ to authenticated
|
||||
using ( (select auth.uid()) = user_id );
|
||||
```
|
||||
|
||||
This method works well for JWT functions like `auth.uid()` and `auth.jwt()` as well as `security definer` Functions. Wrapping the function causes an initPlan to be run by the Postgres optimizer, which allows it to "cache" the results per-statement, rather than calling the function on each row.
|
||||
This method works well for JWT functions like `auth.uid()` and `auth.jwt()` as well as `security definer` Functions. Wrapping the function causes an `initPlan` to be run by the Postgres optimizer, which allows it to "cache" the results per-statement, rather than calling the function on each row.
|
||||
|
||||
<Admonition type="caution">
|
||||
|
||||
@@ -239,7 +239,7 @@ You can only use this technique if the results of the query or function do not c
|
||||
|
||||
### Add filters to every query
|
||||
|
||||
Policies are "implicit where clauses", so it's common to run `select` statements without any filters. This is a bad pattern for performance. Instead of doing this (JS client example):
|
||||
Policies are "implicit where clauses," so it's common to run `select` statements without any filters. This is a bad pattern for performance. Instead of doing this (JS client example):
|
||||
|
||||
{/* prettier-ignore */}
|
||||
```js
|
||||
|
||||
@@ -99,7 +99,7 @@ execute function after_delete_function();
|
||||
There are two options available for executing triggers:
|
||||
|
||||
- `for each row`: specifies that the trigger function should be executed once for each affected row.
|
||||
- `for each statement`: the trigger is executed once for the entire operation (eg, once on insert). This can be more efficient than `for each row` when dealing with multiple rows affected by a single SQL statement, as they allow you to perform calculations or updates on groups of rows at once.
|
||||
- `for each statement`: the trigger is executed once for the entire operation (for example, once on insert). This can be more efficient than `for each row` when dealing with multiple rows affected by a single SQL statement, as they allow you to perform calculations or updates on groups of rows at once.
|
||||
|
||||
## Dropping a trigger
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ This guide is intended to:
|
||||
|
||||
This is not a comprehensive resource, but rather a helpful starting point for your optimization journey.
|
||||
|
||||
If you're new to query optimization, you may be interested in [index_advisor](/docs/guides/database/extensions/index_advisor), our tool for automatically detecting indexes that improve performance on a given query.
|
||||
If you're new to query optimization, you may be interested in [`index_advisor`](/docs/guides/database/extensions/index_advisor), our tool for automatically detecting indexes that improve performance on a given query.
|
||||
|
||||
## Example query
|
||||
|
||||
@@ -66,7 +66,7 @@ create index idx_orders_customer_id on orders (customer_id);
|
||||
|
||||
Sorting can also be optimized by indexing:
|
||||
|
||||
- An index on `b.date_of_purchase` wcan improve the sorting process, and is particularly beneficial when a subset of rows is being returned with a `limit` clause.
|
||||
- An index on `b.date_of_purchase` can improve the sorting process, and is particularly beneficial when a subset of rows is being returned with a `limit` clause.
|
||||
|
||||
```sql
|
||||
create index idx_orders_date_of_purchase on orders (date_of_purchase);
|
||||
|
||||
@@ -63,6 +63,6 @@ Result: PASS
|
||||
|
||||
## More resources
|
||||
|
||||
- [Testing RLS policies](/docs/guides/auth/row-level-security#testing-policies)
|
||||
- [Testing RLS policies](/docs/guides/database/extensions/pgtap#testing-rls-policies)
|
||||
- [pgTAP extension](/docs/guides/database/extensions/pgtap)
|
||||
- Official [pgTAP documentation](https://pgtap.org/)
|
||||
@@ -79,7 +79,7 @@ updated_at | 2022-12-14 02:34:23.85159+00
|
||||
|
||||
</details>
|
||||
|
||||
Alternatively, you can create a secret by `insert`ing data into the `vault.secret` table:
|
||||
Alternatively, you can create a secret by inserting data into the `vault.secret` table:
|
||||
|
||||
{/* prettier-ignore */}
|
||||
```sql
|
||||
@@ -206,11 +206,11 @@ updated_at | 2022-12-14 02:51:13.938396+00
|
||||
></iframe>
|
||||
</div>
|
||||
|
||||
As we mentioned, the Vault uses pgsodium's Transparent Column Encryption (TCE) to store secrets in an authenticated encrypted form. There are some details around that you may be curious about, what does authenticated mean, and where are encryption keys store? This section explains those details.
|
||||
As we mentioned, the Vault uses `pgsodium`'s Transparent Column Encryption (TCE) to store secrets in an authenticated encrypted form. There are some details around that you may be curious about, what does authenticated mean, and where are encryption keys store? This section explains those details.
|
||||
|
||||
### Authenticated encryption with associated data
|
||||
|
||||
The first important feature of TCE is that it uses an [Authenticated Encryption with Associated Data](<https://en.wikipedia.org/wiki/Authenticated_encryption#Authenticated_encryption_with_associated_data_(AEAD)>) encryption algorithm (based on libsodium).
|
||||
The first important feature of TCE is that it uses an [Authenticated Encryption with Associated Data](<https://en.wikipedia.org/wiki/Authenticated_encryption#Authenticated_encryption_with_associated_data_(AEAD)>) encryption algorithm (based on `libsodium`).
|
||||
|
||||
### Encryption key location
|
||||
|
||||
@@ -218,7 +218,7 @@ The first important feature of TCE is that it uses an [Authenticated Encryption
|
||||
|
||||
**Associated Data** means that you can include any other columns from the same row as part of the signature computation. This doesn't encrypt those other columns - rather it ensures that your encrypted value is only associated with columns from that row. If an attacker were to copy an encrypted value from another row to the current one, the signature would be rejected (assuming you used a unique column in the associated data).
|
||||
|
||||
Another important feature of pgsodium is that the encryption keys are never stored in the database alongside the encrypted data. Instead, only a **Key ID** is stored, which is a reference to the key that is only accessible outside of SQL. Even if an attacker can capture a dump of your entire database, they will see only encrypted data and key IDs, _never the raw key itself_.
|
||||
Another important feature of `pgsodium` is that the encryption keys are never stored in the database alongside the encrypted data. Instead, only a **Key ID** is stored, which is a reference to the key that is only accessible outside of SQL. Even if an attacker can capture a dump of your entire database, they will see only encrypted data and key IDs, _never the raw key itself_.
|
||||
|
||||
This is an important safety precaution - there is little value in storing the encryption key in the database itself as this would be like locking your front door but leaving the key in the lock! Storing the key outside the database fixes this issue.
|
||||
|
||||
|
||||
@@ -37,6 +37,11 @@ Check out the [Edge Function Examples](https://github.com/supabase/supabase/tree
|
||||
'Combining Kysely with Deno Postgres gives you a convenient developer experience for interacting directly with your Postgres database.',
|
||||
href: '/guides/functions/kysely-postgres',
|
||||
},
|
||||
{
|
||||
name: 'Monitoring with Sentry',
|
||||
description: 'Monitor Edge Functions with the Sentry Deno SDK.',
|
||||
href: '/guides/functions/examples/sentry-monitoring',
|
||||
},
|
||||
{
|
||||
name: 'With CORS headers',
|
||||
description: 'Send CORS headers for invoking from the browser.',
|
||||
|
||||
@@ -90,6 +90,39 @@ Deno.serve(async (_req) => {
|
||||
})
|
||||
```
|
||||
|
||||
## Using Drizzle
|
||||
|
||||
You can use Drizzle together with [Postgres.js](https://github.com/porsager/postgres). Both can be loaded directly from npm:
|
||||
|
||||
```json supabase/functions/import_map.json
|
||||
{
|
||||
"imports": {
|
||||
"drizzle-orm": "npm:drizzle-orm@0.29.1",
|
||||
"drizzle-orm/": "npm:/drizzle-orm@0.29.1/",
|
||||
"postgres": "npm:postgres@3.4.3"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```ts supabase/functions/drizzle/index.ts
|
||||
import { drizzle } from 'drizzle-orm/postgres-js'
|
||||
import postgres from 'postgres'
|
||||
import { countries } from '../_shared/schema.ts'
|
||||
|
||||
const connectionString = Deno.env.get('SUPABASE_DB_URL')!
|
||||
|
||||
Deno.serve(async (_req) => {
|
||||
// Disable prefetch as it is not supported for "Transaction" pool mode
|
||||
const client = postgres(connectionString, { prepare: false })
|
||||
const db = drizzle(client)
|
||||
const allCountries = await db.select().from(countries)
|
||||
|
||||
return Response.json(allCountries)
|
||||
})
|
||||
```
|
||||
|
||||
You can find the full example on [GitHub](https://github.com/thorwebdev/edgy-drizzle).
|
||||
|
||||
## SSL connections
|
||||
|
||||
Deployed edge functions are pre-configured to use SSL for connections to the Supabase database. You don't need to add any extra configurations.
|
||||
|
||||
@@ -13,7 +13,7 @@ Dart Edge is an experimental project - it's probably not ready for production us
|
||||
|
||||
</Admonition>
|
||||
|
||||
## Pre requirements
|
||||
## Prerequisites
|
||||
|
||||
Make sure you have [the edge CLI](https://docs.dartedge.dev/#getting-started) installed in addition to [the Supabase CLI](https://supabase.com/docs/guides/cli#installation).
|
||||
|
||||
@@ -74,7 +74,7 @@ Now let’s try to interact with our Supabase database. We will start by install
|
||||
|
||||
### Add dependencies
|
||||
|
||||
Add [supabase](https://pub.dev/packages/supabase) to interact with Supabase service.
|
||||
Add [`supabase`](https://pub.dev/packages/supabase) to interact with Supabase service.
|
||||
|
||||
```bash
|
||||
dart pub add supabase
|
||||
@@ -125,14 +125,13 @@ supabase:
|
||||
project_path: './supabase'
|
||||
```
|
||||
|
||||
Also, currently Dart Edge doesn't work with the latest versions of Flutter. If you do have the latest version of Flutter on your machine, you can use [FVM](https://fvm.app/) to use an older version of Flutter for your Dart Edge project.
|
||||
Also, currently Dart Edge doesn't work with the latest versions of Flutter. If you do have the latest version of Flutter on your machine, you can use [Flutter Version Management (FVM)](https://fvm.app/) to use an older version of Flutter for your Dart Edge project.
|
||||
|
||||
```bash
|
||||
fvm use 3.7.12 -f
|
||||
```
|
||||
|
||||
You can read more about how to configure FVM (Flutter
|
||||
Version Management) for your project in the [FVM documentation](https://fvm.app/documentation/getting-started).
|
||||
You can read more about how to configure FVM for your project in the [FVM documentation](https://fvm.app/documentation/getting-started).
|
||||
|
||||
Run the following command to compile your Dart code. This command will start a watcher that will recompile anytime you make changes to your code.
|
||||
|
||||
|
||||
@@ -174,7 +174,7 @@ import { Sheets } from 'npm:@googleapis/sheets'
|
||||
|
||||
## Limitations
|
||||
|
||||
- Outgoing connections to ports `25`, `465`, and `587` are not allowed.
|
||||
- Outgoing connections to ports `25` and `587` are not allowed.
|
||||
- Serving of HTML content is not supported (`GET` requests that return `text/html` will be rewritten to `text/plain`).
|
||||
- Deno and Node file system APIs are not available.
|
||||
- Memory limit for the function: 150 MB (heap) + 150 MB (external - array buffers/ WASM)
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
---
|
||||
title: 'Monitoring with Sentry'
|
||||
description: 'Monitor Edge Functions with the Sentry Deno SDK.'
|
||||
---
|
||||
|
||||
Add the [Sentry Deno SDK](https://docs.sentry.io/platforms/javascript/guides/deno/) to your Supabase Edge Functions to easily track exceptions and get notified of errors or performance issues.
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- [Create a Sentry account](https://sentry.io/signup/).
|
||||
- Make sure you have the latest version of the [Supabase CLI](https://supabase.com/docs/guides/cli#installation) installed.
|
||||
|
||||
### 1. Create Supabase function
|
||||
|
||||
Create a new function locally:
|
||||
|
||||
```bash
|
||||
supabase functions new sentryfied
|
||||
```
|
||||
|
||||
### 2. Add the Sentry Deno SDK
|
||||
|
||||
Handle exceptions within your function and send them to Sentry.
|
||||
|
||||
```tsx
|
||||
import * as Sentry from 'https://deno.land/x/sentry/index.mjs'
|
||||
|
||||
Sentry.init({
|
||||
// https://docs.sentry.io/product/sentry-basics/concepts/dsn-explainer/#where-to-find-your-dsn
|
||||
dsn: SENTRY_DSN,
|
||||
defaultIntegrations: false,
|
||||
// Performance Monitoring
|
||||
tracesSampleRate: 1.0,
|
||||
// Set sampling rate for profiling - this is relative to tracesSampleRate
|
||||
profilesSampleRate: 1.0,
|
||||
})
|
||||
|
||||
// Set region and execution_id as custom tags
|
||||
Sentry.setTag('region', Deno.env.get('SB_REGION'))
|
||||
Sentry.setTag('execution_id', Deno.env.get('SB_EXECUTION_ID'))
|
||||
|
||||
Deno.serve(async (req) => {
|
||||
try {
|
||||
const { name } = await req.json()
|
||||
// This will throw, as `name` in our example call will be `undefined`
|
||||
const data = {
|
||||
message: `Hello ${name}!`,
|
||||
}
|
||||
|
||||
return new Response(JSON.stringify(data), { headers: { 'Content-Type': 'application/json' } })
|
||||
} catch (e) {
|
||||
Sentry.captureException(e)
|
||||
return new Response(JSON.stringify({ msg: 'error' }), {
|
||||
status: 500,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
})
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### 3. Deploy and test
|
||||
|
||||
Run function locally:
|
||||
|
||||
```bash
|
||||
supabase start
|
||||
supabase functions serve --no-verify-jwt
|
||||
```
|
||||
|
||||
Test it: http://localhost:54321/functions/v1/sentryfied
|
||||
|
||||
Deploy function to Supabase:
|
||||
|
||||
```bash
|
||||
supabase functions deploy sentryfied --no-verify-jwt
|
||||
```
|
||||
|
||||
### 4. Try it yourself
|
||||
|
||||
Find the complete example on [GitHub](https://github.com/supabase/supabase/tree/master/examples/edge-functions/supabase/functions/sentryfied/index.ts).
|
||||
@@ -0,0 +1,71 @@
|
||||
---
|
||||
id: 'functions-headers'
|
||||
title: 'Logging and Using Headers'
|
||||
description: 'How to log the headers to console.'
|
||||
subtitle: 'How to log the headers received and use them.'
|
||||
---
|
||||
|
||||
When debugging Edge Functions, a common mistake is to try to log headers to the developer console via code like this:
|
||||
|
||||
```ts index.ts
|
||||
Deno.serve(async (req) => {
|
||||
const headers = JSON.stringify(req.headers)
|
||||
console.log(`Request headers: ${headers}`)
|
||||
// OR
|
||||
console.log(`Request headers: ${JSON.stringify(req.headers)}`)
|
||||
return new Response('ok', {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
status: 200,
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
Both attempts will give as output the string `"{}"`, even though retrieving the value using `request.headers.get("Your-Header-Name")` will indeed give you the correct value. This behavior mirrors that of browsers.
|
||||
|
||||
The reason behind this behavior is that [Headers](https://developer.mozilla.org/en-US/docs/Web/API/Headers) objects don't store headers in JavaScript properties that can be enumerated. As a result, neither the developer console nor the JSON stringifier can properly interpret the names and values of the headers. Essentially, it's not an empty object, but rather an opaque one.
|
||||
|
||||
However, `Headers` objects are iterable. You can utilize this feature to craft a couple of succinct one-liners for debugging and printing headers.
|
||||
|
||||
### Convert headers into an object with Object.fromEntries:
|
||||
|
||||
You can use `Object.fromEntries` which is a call to convert the headers into an object:
|
||||
|
||||
```ts index.ts
|
||||
Deno.serve(async (req) => {
|
||||
let headersObject = Object.fromEntries(req.headers)
|
||||
let requestHeaders = JSON.stringify(headersObject, null, 2)
|
||||
console.log(`Request headers: ${requestHeaders}`)
|
||||
return new Response('ok', {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
status: 200,
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
This results in something like:
|
||||
|
||||
```ts index.ts
|
||||
Request headers: {
|
||||
"accept": "*/*",
|
||||
"accept-encoding": "gzip",
|
||||
"authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InN1cGFuYWNobyIsInJvbGUiOiJhbm9uIiwieW91IjoidmVyeSBzbmVha3ksIGh1aD8iLCJpYXQiOjE2NTQ1NDA5MTYsImV4cCI6MTk3MDExNjkxNn0.cwBbk2tq-fUcKF1S0jVKkOAG2FIQSID7Jjvff5Do99Y",
|
||||
"cdn-loop": "cloudflare; subreqs=1",
|
||||
"cf-ew-via": "15",
|
||||
"cf-ray": "8597a2fcc558a5d7-GRU",
|
||||
"cf-visitor": "{\"scheme\":\"https\"}",
|
||||
"cf-worker": "supabase.co",
|
||||
"content-length": "20",
|
||||
"content-type": "application/x-www-form-urlencoded",
|
||||
"host": "edge-runtime.supabase.com",
|
||||
"my-custom-header": "abcd",
|
||||
"user-agent": "curl/8.4.0",
|
||||
"x-deno-subhost": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImtpZCI6InN1cGFiYXNlIn0.eyJkZXBsb3ltZW50X2lkIjoic3VwYW5hY2hvX2M1ZGQxMWFiLTFjYmUtNDA3NS1iNDAxLTY3ZTRlZGYxMjVjNV8wMDciLCJycGNfcm9vdCI6Imh0dHBzOi8vc3VwYWJhc2Utb3JpZ2luLmRlbm8uZGV2L3YwLyIsImV4cCI6MTcwODYxMDA4MiwiaWF0IjoxNzA4NjA5MTgyfQ.-fPid2kEeEM42QHxWeMxxv2lJHZRSkPL-EhSH0r_iV4",
|
||||
"x-forwarded-host": "edge-runtime.supabase.com",
|
||||
"x-forwarded-port": "443",
|
||||
"x-forwarded-proto": "https"
|
||||
}
|
||||
```
|
||||
@@ -183,7 +183,7 @@ hideToc: true
|
||||
})}
|
||||
</div>
|
||||
|
||||
### Web app tutorials
|
||||
### Web app demos
|
||||
|
||||
<div className="grid lg:grid-cols-12 gap-6 not-prose">
|
||||
{
|
||||
|
||||
@@ -29,7 +29,7 @@ Each Supabase project consists of several tools:
|
||||
|
||||
### PostgreSQL (database)
|
||||
|
||||
PostgreSQL is the core of Supabase. We do not abstract the PostgreSQL database — you can access it and use it with full privileges. We simply provide tools which makes PostgreSQL as easy to use as Firebase.
|
||||
PostgreSQL is the core of Supabase. We do not abstract the PostgreSQL database—you can access it and use it with full privileges. We simply provide tools which makes PostgreSQL as easy to use as Firebase.
|
||||
|
||||
- Official Docs: [postgresql.org/docs](https://www.postgresql.org/docs/current/index.html)
|
||||
- Source code: [github.com/postgres/postgres](https://github.com/postgres/postgres) (mirror)
|
||||
@@ -57,7 +57,7 @@ A JWT-based API for managing users and issuing access tokens. This integrates wi
|
||||
### PostgREST (API)
|
||||
|
||||
A standalone web server that turns your PostgreSQL database directly into a RESTful API.
|
||||
We use this with our [pg_graphql](https://github.com/supabase/pg_graphql) extension to provide a GraphQL API.
|
||||
We use this with our [`pg_graphql`](https://github.com/supabase/pg_graphql) extension to provide a GraphQL API.
|
||||
|
||||
- Official Docs: [postgrest.org](https://postgrest.org/)
|
||||
- Source code: [github.com/PostgREST/postgrest](https://github.com/PostgREST/postgrest)
|
||||
@@ -86,8 +86,8 @@ An S3-compatible object storage service that stores metadata in Postgres.
|
||||
|
||||
A modern runtime for JavaScript and TypeScript.
|
||||
|
||||
- Official Docs: [deno.land](https://deno.land/)
|
||||
- Source code: [github.com/denoland/deno](https://github.com/denoland/deno)
|
||||
- Official Docs: [Deno documentation](https://deno.land/)
|
||||
- Source code: [Deno source code](https://github.com/denoland/deno)
|
||||
- License: [MIT](https://github.com/denoland/deno/blob/main/LICENSE.md)
|
||||
- Language: TypeScript / Rust
|
||||
|
||||
@@ -104,14 +104,14 @@ A RESTful API for managing your Postgres. Fetch tables, add roles, and run queri
|
||||
|
||||
A cloud-native, multi-tenant Postgres connection pooler.
|
||||
|
||||
- Official Docs: [Supavisor Github Pages](https://supabase.github.io/supavisor/)
|
||||
- Source code: [supabase/supavisor](https://github.com/supabase/supavisor)
|
||||
- Official Docs: [Supavisor GitHub Pages](https://supabase.github.io/supavisor/)
|
||||
- Source code: [`supabase/supavisor`](https://github.com/supabase/supavisor)
|
||||
- License: [Apache 2.0](https://github.com/supabase/supavisor/blob/main/LICENSE)
|
||||
- Language: Elixir
|
||||
|
||||
### Kong (API gateway)
|
||||
|
||||
A cloud-native API gateway, built on top of Nginx.
|
||||
A cloud-native API gateway, built on top of NGINX.
|
||||
|
||||
- Official Docs: [docs.konghq.com](https://docs.konghq.com/)
|
||||
- Source code: [github.com/kong/kong](https://github.com/kong/kong)
|
||||
@@ -144,7 +144,7 @@ Less, but better.
|
||||
### Everything is portable
|
||||
|
||||
To avoid lock-in, we make it easy to migrate in and out. Our cloud offering is compatible with our self-hosted product.
|
||||
We use existing standards to increase portability (like pg_dump and CSV files). If a new standard emerges which competes with a "Supabase" approach, we will deprecate the approach in favor of the standard.
|
||||
We use existing standards to increase portability (like `pg_dump` and CSV files). If a new standard emerges which competes with a "Supabase" approach, we will deprecate the approach in favor of the standard.
|
||||
This forces us compete on experience. We aim to be the best Postgres hosting service.
|
||||
|
||||
### Play the long game
|
||||
|
||||
@@ -68,7 +68,7 @@ Provide phone logins using a third-party SMS provider. [Docs](/docs/guides/auth/
|
||||
|
||||
Control the data each user can access with Postgres Policies. [Docs](/docs/guides/auth/row-level-security).
|
||||
|
||||
### Serverside Auth helpers
|
||||
### Server-side Auth helpers
|
||||
|
||||
Helpers for implementing user authentication in popular frameworks like [Next.js](/docs/guides/auth/auth-helpers/nextjs) and [SvelteKit](/docs/guides/auth/auth-helpers/sveltekit)
|
||||
|
||||
@@ -82,7 +82,7 @@ Build login and registration pages with custom themes. [Docs](/docs/guides/auth/
|
||||
|
||||
### Auto-generated REST API
|
||||
|
||||
RESTful APIs are autogenerated from your database, without a single line of code. [Docs](/docs/guides/api#rest-api-overview).
|
||||
RESTful APIs are auto-generated from your database, without a single line of code. [Docs](/docs/guides/api#rest-api-overview).
|
||||
|
||||
### Auto-generated GraphQL API
|
||||
|
||||
|
||||
@@ -143,7 +143,7 @@ hideToc: true
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: FutureBuilder<List<Map<String, dynamic>>>(
|
||||
body: FutureBuilder(
|
||||
future: _future,
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData) {
|
||||
|
||||
@@ -92,6 +92,8 @@ hideToc: true
|
||||
|
||||
<StepHikeCompact.Code>
|
||||
|
||||
<CH.Code className="min-h-[34rem]">
|
||||
|
||||
```ts app/notes/page.tsx
|
||||
import { createClient } from '@/utils/supabase/server';
|
||||
|
||||
@@ -103,6 +105,47 @@ hideToc: true
|
||||
}
|
||||
```
|
||||
|
||||
```ts utils/supabase/server.ts
|
||||
import { createServerClient, type CookieOptions } from '@supabase/ssr'
|
||||
import { cookies } from 'next/headers'
|
||||
|
||||
export function createClient() {
|
||||
const cookieStore = cookies()
|
||||
|
||||
return createServerClient(
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||||
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
|
||||
{
|
||||
cookies: {
|
||||
get(name: string) {
|
||||
return cookieStore.get(name)?.value
|
||||
},
|
||||
set(name: string, value: string, options: CookieOptions) {
|
||||
try {
|
||||
cookieStore.set({ name, value, ...options })
|
||||
} catch (error) {
|
||||
// The `set` method was called from a Server Component.
|
||||
// This can be ignored if you have middleware refreshing
|
||||
// user sessions.
|
||||
}
|
||||
},
|
||||
remove(name: string, options: CookieOptions) {
|
||||
try {
|
||||
cookieStore.set({ name, value: '', ...options })
|
||||
} catch (error) {
|
||||
// The `delete` method was called from a Server Component.
|
||||
// This can be ignored if you have middleware refreshing
|
||||
// user sessions.
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
</CH.Code>
|
||||
|
||||
</StepHikeCompact.Code>
|
||||
|
||||
</StepHikeCompact.Step>
|
||||
@@ -125,3 +168,9 @@ hideToc: true
|
||||
</StepHikeCompact.Step>
|
||||
|
||||
</StepHikeCompact>
|
||||
|
||||
## Next steps
|
||||
|
||||
- Set up [Auth](/docs/guides/auth/server-side/overview) for your app
|
||||
- [Insert more data](/docs/guides/database/import-data) into your database
|
||||
- Upload and serve static files using [Storage](/docs/guides/storage)
|
||||
@@ -136,3 +136,9 @@ hideToc: true
|
||||
|
||||
</StepHikeCompact.Step>
|
||||
</StepHikeCompact>
|
||||
|
||||
## Next steps
|
||||
|
||||
- Set up [Auth](/docs/guides/auth/server-side/overview) for your app
|
||||
- [Insert more data](/docs/guides/database/import-data) into your database
|
||||
- Upload and serve static files using [Storage](/docs/guides/storage)
|
||||
@@ -158,3 +158,9 @@ hideToc: true
|
||||
|
||||
</StepHikeCompact.Step>
|
||||
</StepHikeCompact>
|
||||
|
||||
## Next steps
|
||||
|
||||
- Set up [Auth](/docs/guides/auth/server-side/overview) for your app
|
||||
- [Insert more data](/docs/guides/database/import-data) into your database
|
||||
- Upload and serve static files using [Storage](/docs/guides/storage)
|
||||
@@ -492,7 +492,7 @@ export class AvatarComponent {
|
||||
|
||||
### Add the new widget
|
||||
|
||||
And then we can add the widget on top of the **AccountComponent** html template:
|
||||
And then we can add the widget on top of the **AccountComponent** HTML template:
|
||||
|
||||
```html src/app/account.component.html
|
||||
<form [formGroup]="updateProfileForm" (ngSubmit)="updateProfile()" class="form-widget">
|
||||
@@ -525,8 +525,4 @@ export class AccountComponent implements OnInit {
|
||||
}
|
||||
```
|
||||
|
||||
### Storage management
|
||||
|
||||
<StorageManagement />
|
||||
|
||||
At this stage you have a fully functional application!
|
||||
@@ -460,6 +460,8 @@ expo install expo-image-picker
|
||||
Let's create an avatar for the user so that they can upload a profile photo.
|
||||
We can start by creating a new component:
|
||||
|
||||
{/* <!-- vale off --> */}
|
||||
|
||||
```tsx components/Avatar.tsx
|
||||
import { useState, useEffect } from 'react'
|
||||
import { supabase } from '../lib/supabase'
|
||||
@@ -593,6 +595,8 @@ const styles = StyleSheet.create({
|
||||
})
|
||||
```
|
||||
|
||||
{/* <!-- vale on --> */}
|
||||
|
||||
### Add the new widget
|
||||
|
||||
And then we can add the widget to the Account page:
|
||||
@@ -627,8 +631,4 @@ Now you will need to run the prebuild command to get the application working on
|
||||
expo prebuild
|
||||
```
|
||||
|
||||
### Storage management
|
||||
|
||||
<StorageManagement />
|
||||
|
||||
At this stage you have a fully functional application!
|
||||
@@ -856,10 +856,6 @@ class _AccountPageState extends State<AccountPage> {
|
||||
}
|
||||
```
|
||||
|
||||
### Storage management
|
||||
|
||||
<StorageManagement />
|
||||
|
||||
Congratulations, you've built a fully functional user management app using Flutter and Supabase!
|
||||
|
||||
## See also
|
||||
|
||||
@@ -138,7 +138,7 @@ export class SupabaseService {
|
||||
Let's set up a route to manage logins and signups. We'll use Magic Links so users can sign in with their email without using passwords.
|
||||
Create a **LoginPage** with the `ionic g page login` Ionic CLI command.
|
||||
|
||||
> This guide will show the template inline, but the example app will have templateUrls
|
||||
> This guide will show the template inline, but the example app will have `templateUrl`s
|
||||
|
||||
```ts src/app/login/login.page.ts
|
||||
import { Component, OnInit } from '@angular/core'
|
||||
@@ -552,10 +552,6 @@ template: `
|
||||
`
|
||||
```
|
||||
|
||||
### Storage management
|
||||
|
||||
<StorageManagement />
|
||||
|
||||
At this stage, you have a fully functional application!
|
||||
|
||||
## See also
|
||||
|
||||
@@ -493,8 +493,4 @@ return (
|
||||
<Avatar url={profile.avatar_url} onUpload={updateProfile}></Avatar>
|
||||
```
|
||||
|
||||
### Storage management
|
||||
|
||||
<StorageManagement />
|
||||
|
||||
At this stage you have a fully functional application!
|
||||
@@ -553,8 +553,4 @@ export default defineComponent({
|
||||
</script>
|
||||
```
|
||||
|
||||
### Storage management
|
||||
|
||||
<StorageManagement />
|
||||
|
||||
At this stage you have a fully functional application!
|
||||
@@ -33,7 +33,7 @@ Open Android Studio > New Project > Base Activity (Jetpack Compose).
|
||||
|
||||
Create or edit the `local.properties` file at the root (same level as `build.gradle`) of your project.
|
||||
|
||||
> **Note**: Do not commit this file to your source control, e.g. by adding it to your `.gitignore` file!
|
||||
> **Note**: Do not commit this file to your source control, for example, by adding it to your `.gitignore` file!
|
||||
|
||||
```kotlin
|
||||
SUPABASE_ANON_KEY=YOUR_SUPABASE_ANON_KEY
|
||||
@@ -75,7 +75,7 @@ val apiKey = BuildConfig.SUPABASE_ANON_KEY
|
||||
|
||||

|
||||
|
||||
In the `build.gradle` (app) file, add these dependencies then press "Sync now". Replace the dependency version placeholders `$supabase_version` and `$ktor_version` with their respective latest versions.
|
||||
In the `build.gradle` (app) file, add these dependencies then press "Sync now." Replace the dependency version placeholders `$supabase_version` and `$ktor_version` with their respective latest versions.
|
||||
|
||||
```kotlin
|
||||
implementation "io.github.jan-tennert.supabase:postgrest-kt:$supabase_version"
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -434,8 +434,4 @@ async function signOut() {
|
||||
</template>
|
||||
```
|
||||
|
||||
### Storage management
|
||||
|
||||
<StorageManagement />
|
||||
|
||||
That is it! You should now be able to upload a profile photo to Supabase Storage and you have a fully functional application.
|
||||
@@ -396,8 +396,4 @@ return (
|
||||
)
|
||||
```
|
||||
|
||||
### Storage management
|
||||
|
||||
<StorageManagement />
|
||||
|
||||
At this stage you have a fully functional application!
|
||||
@@ -17,12 +17,12 @@ If you get stuck while working through this guide, refer to the [full example on
|
||||
|
||||
A Redwood application is split into two parts: a frontend and a backend. This is represented as two node projects within a single monorepo.
|
||||
|
||||
The frontend project is called **`web`** and the backend project is called **`api`**. For clarity, we will refer to these in prose as **"sides"**, i.e. the "web side" and the "api side".
|
||||
The frontend project is called **`web`** and the backend project is called **`api`**. For clarity, we will refer to these in prose as **"sides,"** that is, the `web side` and the `api side`.
|
||||
They are separate projects because code on the `web side` will end up running in the user's browser while code on the `api side` will run on a server somewhere.
|
||||
|
||||
<Admonition type="note">
|
||||
|
||||
Important: When this guide refers to "API", that means the Supabase API and when it refers to "api side", that means the RedwoodJS `api side`.
|
||||
Important: When this guide refers to "API," that means the Supabase API and when it refers to `api side`, that means the RedwoodJS `api side`.
|
||||
|
||||
</Admonition>
|
||||
|
||||
@@ -51,7 +51,7 @@ RedwoodJS requires Node.js `>= 14.x <= 16.x` and Yarn `>= 1.15`.
|
||||
|
||||
</Admonition>
|
||||
|
||||
Make sure you have installed yarn since RedwoodJS relies on it to [manage its packages in workspaces](https://classic.yarnpkg.com/lang/en/docs/workspaces/) for its `web` and `api` "sides".
|
||||
Make sure you have installed yarn since RedwoodJS relies on it to [manage its packages in workspaces](https://classic.yarnpkg.com/lang/en/docs/workspaces/) for its `web` and `api` "sides."
|
||||
|
||||
### Initialize a RedwoodJS app
|
||||
|
||||
@@ -428,7 +428,7 @@ export default Account
|
||||
```
|
||||
|
||||
You'll see the use of `useAuth()` several times. Redwood's `useAuth` hook provides convenient ways to access
|
||||
logIn, logOut, currentUser, and access the `supabase` authenticate client. We'll use it to get an instance
|
||||
`logIn`, `logOut`, `currentUser`, and access the `supabase` authenticate client. We'll use it to get an instance
|
||||
of the supabase client to interact with your API.
|
||||
|
||||
### Update home page
|
||||
@@ -603,10 +603,6 @@ return (
|
||||
)
|
||||
```
|
||||
|
||||
### Storage management
|
||||
|
||||
<StorageManagement />
|
||||
|
||||
At this stage you have a fully functional application!
|
||||
|
||||
## See also
|
||||
|
||||
@@ -90,7 +90,7 @@ VITE_SUPABASE_URL=YOUR_SUPABASE_URL
|
||||
VITE_SUPABASE_ANON_KEY=YOUR_SUPABASE_ANON_KEY
|
||||
```
|
||||
|
||||
The `supabaseClient` will be used in fetch calls to Supabase endpoints from our app. As we'll see below, the client is instrumental in implementing authentication using refine's auth provider methods and CRUD actions with appropriate data provider methods.
|
||||
The `supabaseClient` will be used in fetch calls to Supabase endpoints from our app. As we'll see below, the client is instrumental in implementing authentication using Refine's auth provider methods and CRUD actions with appropriate data provider methods.
|
||||
|
||||
One optional step is to update the CSS file `src/App.css` to make the app look nice.
|
||||
You can find the full contents of this file [here](https://raw.githubusercontent.com/supabase/supabase/master/examples/user-management/refine-user-management/src/App.css).
|
||||
@@ -404,7 +404,7 @@ Notice above that, we are using three refine hooks, namely the [`useGetIdentity(
|
||||
|
||||
`useForm()`, in contrast, is a data hook that exposes a series of useful objects that serve the edit form. For example, we are grabbing the `onFinish` function to submit the form with the `handleSubmit` event handler. We are also using `formLoading` property to present state changes of the submitted form.
|
||||
|
||||
The `useForm()` hook is a higher-level hook built on top of refine's `useForm()` core hook. It fully supports form state management, field validation and submission using React Hook Form. Behind the scenes, it invokes the `dataProvider.getOne` method to get the user profile data from our Supabase `/profiles` endpoint and also invokes `dataProvider.update` method when `onFinish()` is called.
|
||||
The `useForm()` hook is a higher-level hook built on top of Refine's `useForm()` core hook. It fully supports form state management, field validation and submission using React Hook Form. Behind the scenes, it invokes the `dataProvider.getOne` method to get the user profile data from our Supabase `/profiles` endpoint and also invokes `dataProvider.update` method when `onFinish()` is called.
|
||||
|
||||
### Launch!
|
||||
|
||||
@@ -626,8 +626,4 @@ return (
|
||||
)
|
||||
```
|
||||
|
||||
### Storage management
|
||||
|
||||
<StorageManagement />
|
||||
|
||||
At this stage, you have a fully functional application!
|
||||
@@ -415,8 +415,4 @@ return (
|
||||
)
|
||||
```
|
||||
|
||||
### Storage management
|
||||
|
||||
<StorageManagement />
|
||||
|
||||
At this stage you have a fully functional application!
|
||||
@@ -117,7 +117,7 @@ Let's set up a Svelte component to manage logins and sign ups. We'll use Magic L
|
||||
After a user is signed in we can allow them to edit their profile details and manage their account.
|
||||
Let's create a new component for that called `Account.svelte`.
|
||||
|
||||
```html src/lib/Account.svelte
|
||||
```tsx src/lib/Account.svelte
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte'
|
||||
import type { AuthSession } from '@supabase/supabase-js'
|
||||
@@ -204,8 +204,7 @@ Let's create a new component for that called `Account.svelte`.
|
||||
{loading ? 'Saving ...' : 'Update profile'}
|
||||
</button>
|
||||
</div>
|
||||
<button type="button" class="button block" on:click="{()" ="">
|
||||
supabase.auth.signOut()}> Sign Out
|
||||
<button type="button" class="button block" on:click={() => supabase.auth.signOut()}> Sign Out
|
||||
</button>
|
||||
</form>
|
||||
```
|
||||
@@ -368,8 +367,4 @@ And then we can add the widget to the Account page:
|
||||
</form>
|
||||
```
|
||||
|
||||
### Storage management
|
||||
|
||||
<StorageManagement />
|
||||
|
||||
At this stage you have a fully functional application!
|
||||
@@ -45,31 +45,43 @@ PUBLIC_SUPABASE_ANON_KEY="YOUR_SUPABASE_KEY"
|
||||
|
||||
Optionally, add `src/styles.css` with the [CSS from the example](https://raw.githubusercontent.com/supabase/supabase/master/examples/user-management/sveltekit-user-management/src/styles.css).
|
||||
|
||||
### Supabase Auth Helpers
|
||||
### Creating a Supabase client for SSR:
|
||||
|
||||
SvelteKit is a highly versatile framework offering pre-rendering at build time (SSG), server-side rendering at request time (SSR), API routes, and more.
|
||||
The ssr package configures Supabase to use Cookies, which is required for server-side languages and frameworks.
|
||||
|
||||
It can be challenging to authenticate your users in all these different environments, that's why we've created the Supabase Auth Helpers to make user management and data fetching within SvelteKit as easy as possible.
|
||||
|
||||
Install the auth helpers for SvelteKit:
|
||||
Install the Supabase packages:
|
||||
|
||||
```bash
|
||||
npm install @supabase/auth-helpers-sveltekit @supabase/supabase-js
|
||||
npm install @supabase/ssr @supabase/supabase-js
|
||||
```
|
||||
|
||||
Creating a Supabase client with the ssr package automatically configures it to use Cookies. This means your user's session is available throughout the entire SvelteKit stack - page, layout, server, hooks.
|
||||
|
||||
Add the code below to your `src/hooks.server.ts` to initialize the client on the server:
|
||||
|
||||
```ts src/hooks.server.ts
|
||||
// src/hooks.server.ts
|
||||
import { PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY } from '$env/static/public'
|
||||
import { createSupabaseServerClient } from '@supabase/auth-helpers-sveltekit'
|
||||
import { createServerClient } from '@supabase/ssr'
|
||||
import type { Handle } from '@sveltejs/kit'
|
||||
|
||||
export const handle: Handle = async ({ event, resolve }) => {
|
||||
event.locals.supabase = createSupabaseServerClient({
|
||||
supabaseUrl: PUBLIC_SUPABASE_URL,
|
||||
supabaseKey: PUBLIC_SUPABASE_ANON_KEY,
|
||||
event,
|
||||
event.locals.supabase = createServerClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY, {
|
||||
cookies: {
|
||||
get: (key) => event.cookies.get(key),
|
||||
/**
|
||||
* Note: You have to add the `path` variable to the
|
||||
* set and remove method due to sveltekit's cookie API
|
||||
* requiring this to be set, setting the path to `/`
|
||||
* will replicate previous/standard behaviour (https://kit.svelte.dev/docs/types#public-types-cookies)
|
||||
*/
|
||||
set: (key, value, options) => {
|
||||
event.cookies.set(key, value, { ...options, path: '/' })
|
||||
},
|
||||
remove: (key, options) => {
|
||||
event.cookies.delete(key, { ...options, path: '/' })
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
/**
|
||||
@@ -120,7 +132,9 @@ Create a new `src/routes/+layout.server.ts` file to handle the session on the se
|
||||
|
||||
```ts src/routes/+layout.server.ts
|
||||
// src/routes/+layout.server.ts
|
||||
export const load = async ({ locals: { getSession } }) => {
|
||||
import type { LayoutServerLoad } from './$types'
|
||||
|
||||
export const load: LayoutServerLoad = async ({ locals: { getSession } }) => {
|
||||
return {
|
||||
session: await getSession(),
|
||||
}
|
||||
@@ -134,16 +148,26 @@ Create a new `src/routes/+layout.ts` file to handle the session and the supabase
|
||||
```ts src/routes/+layout.ts
|
||||
// src/routes/+layout.ts
|
||||
import { PUBLIC_SUPABASE_ANON_KEY, PUBLIC_SUPABASE_URL } from '$env/static/public'
|
||||
import { createSupabaseLoadClient } from '@supabase/auth-helpers-sveltekit'
|
||||
import type { LayoutLoad } from './$types'
|
||||
import { createBrowserClient, isBrowser, parse } from '@supabase/ssr'
|
||||
|
||||
export const load = async ({ fetch, data, depends }) => {
|
||||
export const load: LayoutLoad = async ({ fetch, data, depends }) => {
|
||||
depends('supabase:auth')
|
||||
|
||||
const supabase = createSupabaseLoadClient({
|
||||
supabaseUrl: PUBLIC_SUPABASE_URL,
|
||||
supabaseKey: PUBLIC_SUPABASE_ANON_KEY,
|
||||
event: { fetch },
|
||||
serverSession: data.session,
|
||||
const supabase = createBrowserClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY, {
|
||||
global: {
|
||||
fetch,
|
||||
},
|
||||
cookies: {
|
||||
get(key) {
|
||||
if (!isBrowser()) {
|
||||
return JSON.stringify(data.session)
|
||||
}
|
||||
|
||||
const cookie = parse(document.cookie)
|
||||
return cookie[key]
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const {
|
||||
@@ -228,7 +252,7 @@ Add the `Auth` component to your home page
|
||||
</div>
|
||||
```
|
||||
|
||||
Create a `src/routes/+page.server.ts` file that will return our website url to be used in our `redirectTo` above.
|
||||
Create a `src/routes/+page.server.ts` file that will return our website URL to be used in our `redirectTo` above.
|
||||
|
||||
```ts
|
||||
// src/routes/+page.server.ts
|
||||
@@ -393,8 +417,9 @@ and handle all our form actions through the `actions` object.
|
||||
|
||||
```ts
|
||||
import { fail, redirect } from '@sveltejs/kit'
|
||||
import type { Actions, PageServerLoad } from './$types'
|
||||
|
||||
export const load = async ({ locals: { supabase, getSession } }) => {
|
||||
export const load: PageServerLoad = async ({ locals: { supabase, getSession } }) => {
|
||||
const session = await getSession()
|
||||
|
||||
if (!session) {
|
||||
@@ -410,7 +435,7 @@ export const load = async ({ locals: { supabase, getSession } }) => {
|
||||
return { session, profile }
|
||||
}
|
||||
|
||||
export const actions = {
|
||||
export const actions: Actions = {
|
||||
update: async ({ request, locals: { supabase, getSession } }) => {
|
||||
const formData = await request.formData()
|
||||
const fullName = formData.get('fullName') as string
|
||||
@@ -606,8 +631,4 @@ And then we can add the widget to the Account page:
|
||||
</div>
|
||||
```
|
||||
|
||||
### Storage management
|
||||
|
||||
<StorageManagement />
|
||||
|
||||
At this stage you have a fully functional application!
|
||||
@@ -514,8 +514,4 @@ struct Profile: Codable {
|
||||
|
||||
You no longer need the `UpdateProfileParams` struct, as you can now reuse the `Profile` struct for both request and response calls.
|
||||
|
||||
### Storage management
|
||||
|
||||
<StorageManagement />
|
||||
|
||||
At this stage you have a fully functional application!
|
||||
@@ -377,8 +377,4 @@ import Avatar from './Avatar.vue'
|
||||
</template>
|
||||
```
|
||||
|
||||
### Storage management
|
||||
|
||||
<StorageManagement />
|
||||
|
||||
At this stage you have a fully functional application!
|
||||
@@ -36,8 +36,8 @@ Each Supabase organization can have one or more owners. If you no longer want be
|
||||
|
||||
The table below shows the corresponding permissions for each available role you can assign a team member in the Dashboard.
|
||||
|
||||
| Permissions | Owner | Administrator | Developer | Read only [^2] |
|
||||
| ------------------------ | ----------------------- | ----------------------- | ----------------------- | ----------------------- |
|
||||
| Permissions | Owner | Administrator | Developer | Read only [^2] |
|
||||
| ------------------------ | ----------------------- | ----------------------- | ----------------------- | ----------------------------------------------- |
|
||||
| **Organization** |
|
||||
| Change organization name | <IconCheck size={14} /> | | |
|
||||
| Delete organization | <IconCheck size={14} /> | | |
|
||||
@@ -71,7 +71,7 @@ The table below shows the corresponding permissions for each available role you
|
||||
| Resume a project | <IconCheck size={14} /> | <IconCheck size={14} /> | |
|
||||
| Restart a project | <IconCheck size={14} /> | <IconCheck size={14} /> | <IconCheck size={14} /> |
|
||||
| Manage tables | <IconCheck size={14} /> | <IconCheck size={14} /> | <IconCheck size={14} /> |
|
||||
| View Data | <IconCheck size={14} /> | <IconCheck size={14} /> | <IconCheck size={14} /> | <IconCheck size={14} /> |
|
||||
| View Data | <IconCheck size={14} /> | <IconCheck size={14} /> | <IconCheck size={14} /> | <IconCheck className="inline" size={14} /> [^3] |
|
||||
|
||||
[^1]:
|
||||
Invites sent from a SSO account can only be accepted by another SSO account
|
||||
@@ -80,3 +80,6 @@ The table below shows the corresponding permissions for each available role you
|
||||
enterprise systems.
|
||||
|
||||
[^2]: Available on the Teams and Enterprise Plans.
|
||||
[^3]:
|
||||
Only available on projects using PostgreSQL 14 and above. You can upgrade
|
||||
your project through [infrastructure settings](https://supabase.com/dashboard/project/_/settings/infrastructure).
|
||||
@@ -119,7 +119,7 @@ For security reasons, Preview Branches do not contain production data. Future ve
|
||||
|
||||
### Merging production changes
|
||||
|
||||
When you merge your Git branch into the production branch, all changes will be applied to your Production environment.
|
||||
When you merge your Git branch into the production branch, all new migrations will be applied to your Production environment.
|
||||
|
||||
### Git providers
|
||||
|
||||
@@ -438,7 +438,7 @@ The Supabase CLI provides two options: [manual migrations](https://supabase.com/
|
||||
|
||||
The Supabase integration detects the new migration and runs it on the remote Preview Branch. It can take up to 10 minutes for migrations to be applied. If you have a PR for your branch, errors are reflected in the GitHub check run status and in a PR comment.
|
||||
|
||||
If you need to reset your database to a clean state (that is, discard any changes that aren't reflected in the migration files), run `supabase db reset` locally. Then, delete the preview branch and recreate it.
|
||||
If you need to reset your database to a clean state (that is, discard any changes that aren't reflected in the migration files), run `supabase db reset` locally. Then, delete the preview branch and recreate it by closing, and reopening your pull request.
|
||||
|
||||
</StepHikeCompact.Details>
|
||||
|
||||
@@ -566,7 +566,7 @@ The new preview branch is reseeded from your `./supabase/seed.sql` file. Any add
|
||||
|
||||
Your Preview Branches are seeded with sample data from the file `./supabase/seed.sql`.
|
||||
|
||||
The database is only seeded once, when the preview branch is created. To rerun seeding, delete the preview branch and recreate it.
|
||||
The database is only seeded once, when the preview branch is created. To rerun seeding, delete the preview branch and recreate it by closing, and reopening your pull request.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
|
||||
@@ -62,7 +62,7 @@ SSD Disks are attached to your servers and the disk performance depends on the c
|
||||
|
||||
### Bursting and disk IO budget
|
||||
|
||||
Smaller compute instances can burst up to their largest throughput and IOPS for 30 minutes in a day. Beyond that, the performance reverts to the baseline. For example, the free tier can burst up to 2,085 Mbps for 30 minutes a day and reverts to the baseline performance of 87 Mbps. Your disk IO budget gets replenished throughout the day.
|
||||
Smaller compute instances can burst up to their largest throughput and IOPS for 30 minutes in a day. Beyond that, the performance reverts to the baseline. For example, the Micro plan can burst up to 2,085 Mbps for 30 minutes a day and reverts to the baseline performance of 87 Mbps. Your disk IO budget gets replenished throughout the day.
|
||||
|
||||
If you need consistent disk performance, choose the 4XL or larger compute add-on which has the same baseline and maximum disk throughput and IOPS.
|
||||
|
||||
|
||||
@@ -79,6 +79,8 @@ Required outstanding validation records:
|
||||
|
||||
Add the records to your domains' DNS settings. Make sure to trim surrounding whitespace. Use a low TTL value so you can quickly change the records if you make a mistake.
|
||||
|
||||
Some DNS registrars automatically append your domain name to the DNS entries being created. As such, creating a DNS record for `api.example.com` might instead create a record for `api.example.com.example.com`. In such cases, please remove the domain name from the records you're creating; as an example, you would create a TXT record for `api`, instead of `api.example.com`.
|
||||
|
||||
### Verify your domain
|
||||
|
||||
Make sure you've configured all required DNS settings:
|
||||
|
||||
@@ -60,16 +60,16 @@ After developing your project and deciding it's Production Ready, you should run
|
||||
|
||||
- The table below shows the rate limit quotas on the following authentication endpoints. You can configure the auth rate limits for your project [here](/dashboard/project/_/auth/rate-limits).
|
||||
|
||||
| Endpoint | Path | Limited By | Rate Limit |
|
||||
| ------------------------------------------------ | -------------------------------------------------------------- | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| All endpoints that send emails | `/auth/v1/signup` `/auth/v1/recover` `/auth/v1/user`[^1] | Sum of combined requests | Defaults to 30 emails per hour. As of 14th July 2023, this has been updated to 4 emails per hour. Is customizable with custom SMTP set up. |
|
||||
| All endpoints that send One-Time-Passwords (OTP) | `/auth/v1/otp` | Sum of combined requests | Defaults to 30 OTPs per hour. Is customizable. |
|
||||
| Send OTPs or magiclinks | `/auth/v1/otp` | Last request | Defaults to 60 seconds window before a new request is allowed. Is customizable. |
|
||||
| Signup confirmation request | `/auth/v1/signup` | Last request | Defaults to 60 seconds window before a new request is allowed. Is customizable. |
|
||||
| Password Reset Request | `/auth/v1/recover` | Last request | Defaults to 60 seconds window before a new request is allowed. Is customizable. |
|
||||
| Verification requests | `/auth/v1/verify` | IP Address | 360 requests per hour (with bursts up to 30 requests) |
|
||||
| Token refresh requests | `/auth/v1/token` | IP Address | 1800 requests per hour (with bursts up to 30 requests) |
|
||||
| Create or Verify an MFA challenge | `/auth/v1/factors/:id/challenge` `/auth/v1/factors/:id/verify` | IP Address | 15 requests per minute (with bursts up to 30 requests) |
|
||||
| Endpoint | Path | Limited By | Rate Limit |
|
||||
| ------------------------------------------------ | -------------------------------------------------------------- | ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| All endpoints that send emails | `/auth/v1/signup` `/auth/v1/recover` `/auth/v1/user`[^1] | Sum of combined requests | Defaults to 30 emails per hour. As of 14th July 2023, this has been updated to 4 emails per hour. As of 21 Oct 2023, this has been updated to 3 emails per hour. You can only change this with your own custom SMTP setup. |
|
||||
| All endpoints that send One-Time-Passwords (OTP) | `/auth/v1/otp` | Sum of combined requests | Defaults to 30 OTPs per hour. Is customizable. |
|
||||
| Send OTPs or magiclinks | `/auth/v1/otp` | Last request | Defaults to 60 seconds window before a new request is allowed. Is customizable. |
|
||||
| Signup confirmation request | `/auth/v1/signup` | Last request | Defaults to 60 seconds window before a new request is allowed. Is customizable. |
|
||||
| Password Reset Request | `/auth/v1/recover` | Last request | Defaults to 60 seconds window before a new request is allowed. Is customizable. |
|
||||
| Verification requests | `/auth/v1/verify` | IP Address | 360 requests per hour (with bursts up to 30 requests) |
|
||||
| Token refresh requests | `/auth/v1/token` | IP Address | 1800 requests per hour (with bursts up to 30 requests) |
|
||||
| Create or Verify an MFA challenge | `/auth/v1/factors/:id/challenge` `/auth/v1/factors/:id/verify` | IP Address | 15 requests per minute (with bursts up to 30 requests) |
|
||||
|
||||
### Realtime quotas
|
||||
|
||||
|
||||
@@ -78,7 +78,7 @@ We only count compute hours for instances that are active. Paused projects do no
|
||||
|
||||
### Free plan
|
||||
|
||||
Supabase provides two "Free organizations". Each organization can run a `Micro` instance for free. This is a great way to get started with Supabase and try out the platform.
|
||||
Supabase provides two "Free organizations". Each organization can run a `Nano` instance for free. This is a great way to get started with Supabase and try out the platform.
|
||||
|
||||
### Compute credits
|
||||
|
||||
|
||||
Loaded 100 of 1651 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user