Merge pull request #10302 from supabase/master

Prod deploy Dashboard
This commit is contained in:
Joshen Lim authored and GitHub committed 2022-11-14 13:40:29 +08:00
commit b48bb12e7b
48 files changed
+52236 -491

No files matched your search

+1 -1
View File
@@ -12,7 +12,7 @@ jobs:
with:
ref: master
- uses: sobolevn/misspell-fixer-action@master
- uses: peter-evans/create-pull-request@v2.4.4
- uses: peter-evans/create-pull-request@v4
env:
ACTIONS_ALLOW_UNSECURE_COMMANDS: 'true'
with:
@@ -1,3 +1,5 @@
// [Terry] Also need to look at getPageType() in ./lib/helpers
// to set a menu to a page
import SupabaseJsV1Nav from 'data/nav/supabase-js/v1'
import SupabaseJsV2Nav from 'data/nav/supabase-js/v2'
import SupabaseDartV0Nav from 'data/nav/supabase-dart/v0'
@@ -5,6 +7,7 @@ import SupabaseDartV1Nav from 'data/nav/supabase-dart/v1'
import SupabaseCLINav from 'data/nav/supabase-cli'
import SupabaseAPINav from 'data/nav/supabase-api'
import AuthServerNav from 'data/nav/auth-server'
import RealtimeServerNav from 'data/nav/realtime-server'
import StorageServerNav from 'data/nav/storage-server'
import { NavMenu, References } from './Navigation.types'
@@ -178,11 +181,6 @@ export const menuItems: NavMenu = {
url: undefined,
items: [
{ name: 'Overview', url: '/guides/database/extensions', items: [] },
{
name: 'plv8: Javascript Language',
url: '/guides/database/extensions/plv8',
items: [],
},
{ name: 'http: RESTful Client', url: '/guides/database/extensions/http', items: [] },
{
name: 'pg_cron: Job Scheduling',
@@ -195,6 +193,11 @@ export const menuItems: NavMenu = {
items: [],
},
{ name: 'pgTAP: Unit Testing', url: '/guides/database/extensions/pgtap', items: [] },
{
name: 'plv8: Javascript Language',
url: '/guides/database/extensions/plv8',
items: [],
},
{
name: 'uuid-ossp: Unique Identifiers',
url: '/guides/database/extensions/uuid-ossp',
@@ -335,6 +338,7 @@ export const menuItems: NavMenu = {
label: 'Self-hosting',
items: [
{ name: 'Auth Server', url: '/reference/auth', items: [] },
{ name: 'Realtime Server', url: '/reference/realtime', items: [] },
{ name: 'Storage Server', url: '/reference/storage', items: [] },
],
},
@@ -346,5 +350,6 @@ export const menuItems: NavMenu = {
'reference/cli': SupabaseCLINav,
'reference/api': SupabaseAPINav,
'reference/auth': AuthServerNav,
'reference/realtime': RealtimeServerNav,
'reference/storage': StorageServerNav,
}
+1 -1
View File
@@ -86,7 +86,7 @@
},
{
"text": "Open Source",
"url": "/oss"
"url": "/docs/oss"
},
{
"text": "SupaSquad",
+12
View File
@@ -0,0 +1,12 @@
const Nav = [
{
label: 'Realtime',
items: [
{ name: 'Realtime Server', url: '/reference/realtime', items: [] },
{ name: 'Configuration', url: '/reference/realtime/config', items: [] },
{ name: 'Release Notes', url: '/reference/realtime/release-notes', items: [] },
],
},
]
export default Nav
@@ -21,6 +21,8 @@ const nonGeneratedReferencePages = [
'docs/reference/auth/release-notes',
'docs/reference/storage',
'docs/reference/storage/release-notes',
'docs/reference/realtime',
'docs/reference/realtime/release-notes',
]
export default nonGeneratedReferencePages
+6 -7
View File
@@ -199,13 +199,6 @@ JS Reference: [select()](../reference/javascript/select),
### GraphQL API
<Admonition type='note'>
To rebuild your GraphQL schema from the SQL schema, call `select graphql.rebuild_schema();`.
Be sure to rebuild the GraphQL schema after altering the SQL schema.
</Admonition>
You can use any GraphQL client with the Supabase GraphQL API. For our GraphQL example we will use [urql](https://formidable.com/open-source/urql/docs/).
<Tabs
@@ -356,6 +349,12 @@ alter table todos enable row level security;
Never expose the `service_role` key in a browser or anywhere where a user can see it. This Key is designed to bypass Row Level Security - so it should only be used on a private server.
A common use case for the `service_role` key is to run data analytics jobs on the backend. To support joins on user id, it is often useful to grant the service role read access to `auth.users` table.
```sql
grant select on table auth.users to service_role;
```
We have [partnered with GitHub](https://github.blog/changelog/2022-03-28-supabase-is-now-a-github-secret-scanning-partner/) to scan for Supabase `service_role` keys pushed to public repositories.
If they detect any keys with service_role privileges being pushed to GitHub, they will forward the API key to us, so that we can automatically revoke the detected secrets and notify you, protecting your data against malicious actors.
@@ -6,7 +6,7 @@ description: How to generate types for your API and Supabase libraries.
Supabase APIs are generated from your database, which means that we can use database introspection to generate type-safe API definitions.
### Generating types using Supabase CLI
## Generating types using Supabase CLI
The Supabase CLI is a single binary Go application that provides everything you need to setup a local development environment.
@@ -16,13 +16,13 @@ You can [install the CLI](https://www.npmjs.com/package/supabase) via npm or oth
npm i supabase@">=1.8.1" --save-dev
```
Login with your Personal Access Token
Login with your Personal Access Token:
```bash
npx supabase login
```
Generate types for your project to produce the file `types/supabase.ts`
Generate types for your project to produce the `types/supabase.ts` file:
```bash
npx supabase gen types typescript --project-id "$PROJECT_ID" --schema public > types/supabase.ts
+3 -5
View File
@@ -7,11 +7,9 @@ description: Add Captcha Protection to your Supabase project
import Tabs from '@theme/Tabs'
import TabItem from '@theme/TabItem'
## hCaptcha guide
Supabase provides you with the option of adding captcha to your sign-in, sign-up, and password reset forms. This keeps your website safe from bots and malicious scripts. Supabase authentication has support for [hCaptcha](https://www.hcaptcha.com/).
### Sign up for hCaptcha
## Sign up for 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**.
@@ -27,7 +25,7 @@ In the Settings page, look for the **Sitekey** section and copy the key.
![sitekey_settings.png](/docs/img/guides/auth-captcha/sitekey_settings.png)
### Enable hCaptcha protection for your Supabase project
## Enable hCaptcha protection for your Supabase project
Navigate to the **[Authentication](https://app.supabase.com/project/_/settings/auth)** page in the Supabase Dashboard and find the **Enable hCaptcha protection** toggle under the **Security and Protection** section.
@@ -35,7 +33,7 @@ Navigate to the **[Authentication](https://app.supabase.com/project/_/settings/a
Enter your hCaptcha **Secret key** and click **Save**.
### Add the hCaptcha frontend component
## Add the hCaptcha frontend component
The frontend requires some changes to provide the captcha on-screen for the user. This example uses React and the hCaptcha React component, but hCaptcha can be used with any JavaScript framework.
@@ -53,11 +53,351 @@ PUBLIC_SUPABASE_ANON_KEY=your-anon-key
Start off by creating a `db.ts` file inside of the `src/lib` directory and instantiate the `supabaseClient`.
```ts title=src/lib/db.ts
import { createClient } from '@supabase/auth-helpers-sveltekit';
import { env } from '$env/dynamic/public';
// or use the static env
// import { PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY } from '$env/static/public';
export const supabaseClient = createClient(
env.PUBLIC_SUPABASE_URL,
env.PUBLIC_SUPABASE_ANON_KEY
);
```
To make sure the client is initialized on the server and the client, include this file in `src/hooks.server.js` and `src/hooks.client.js`:
```ts
import '$lib/db';
```
### Synchronizing the page store
Edit your `+layout.svelte` file and set up the client side.
```html title=src/routes/+layout.svelte
<script>
import { supabaseClient } from '$lib/db';
import { invalidate } from '$app/navigation';
import { onMount } from 'svelte';
onMount(() => {
const {
data: { subscription }
} = supabaseClient.auth.onAuthStateChange(() => {
invalidate('supabase:auth');
});
return () => {
subscription.unsubscribe();
};
});
</script>
<slot />
```
Every `PageLoad` or `LayoutLoad` using `getSupabase()` will update when `invalidate('supabase:auth')` is called.
If some data is not updated on signin/signout you can fall back to `invalidateAll()`.
### Send session to client
To make the session available to the UI (pages, layouts), pass the session in the root layout server load function:
```ts title=src/routes/+layout.server.ts
import type { LayoutServerLoad } from './$types';
import { getServerSession } from '@supabase/auth-helpers-sveltekit';
export const load: LayoutServerLoad = async (event) => {
return {
session: await getServerSession(event)
};
};
```
In addition you can create a layout load function if you are using `invalidate('supabase:auth')`:
```ts title=src/routes/+layout.ts
import type { LayoutLoad } from './$types';
import { getSupabase } from '@supabase/auth-helpers-sveltekit';
export const load: LayoutLoad = async (event) => {
const { session } = await getSupabase(event);
return { session };
};
```
This results in fewer server calls as the client manages the session on its own.
### Typings
In order to get the most out of TypeScript and it´s intellisense, you should import our types into the `app.d.ts` type definition file that comes with your SvelteKit project.
```ts title=src/app.d.ts
/// <reference types="@sveltejs/kit" />
// See https://kit.svelte.dev/docs/types#app
// for information about these interfaces
// and what to do when importing types
declare namespace App {
interface Supabase {
Database: import('./DatabaseDefinitions').Database;
SchemaName: 'public';
}
// interface Locals {}
interface PageData {
session: import('@supabase/supabase-js').Session | null;
}
// interface Error {}
// interface Platform {}
}
```
### Basic Setup
You can now determine if a user is authenticated on the client-side by checking that the `session` object in `$page.data` is defined.
```html title=src/routes/+page.svelte
<script>
import { page } from '$app/stores';
</script>
{#if !$page.data.session}
<h1>I am not logged in</h1>
{:else}
<h1>Welcome {$page.data.session.user.email}</h1>
<p>I am logged in!</p>
{/if}
```
## Client-side data fetching with RLS
For [row level security](/docs/learn/auth-deep-dive/auth-row-level-security) to work properly when fetching data client-side, you need to make sure to import the `{ supabaseClient }` from `$lib/db` and only run your query once the session is defined client-side in `$page.data`:
```html
<script>
import { supabaseClient } from '$lib/db';
import { page } from '$app/stores';
let loadedData = [];
async function loadData() {
const { data } = await supabaseClient.from('test').select('*').limit(20);
loadedData = data;
}
$: if ($page.data.session) {
loadData();
}
</script>
{#if $page.data.session}
<p>client-side data fetching with RLS</p>
<pre>{JSON.stringify(loadedData, null, 2)}</pre>
{/if}
```
## Server-side data fetching with RLS
```html title=src/routes/profile/+page.svelte
<script>
/** @type {import('./$types').PageData} */
export let data;
$: ({ user, tableData } = data);
</script>
<div>Protected content for {user.email}</div>
<pre>{JSON.stringify(tableData, null, 2)}</pre>
<pre>{JSON.stringify(user, null, 2)}</pre>
```
For [row level security](/docs/learn/auth-deep-dive/auth-row-level-security) to work in a server environment, you need to use the `getSupabase` helper to check if the user is authenticated. The helper requires the `event` and returns `session` and `supabaseClient`:
```ts title=src/routes/profile/+page.ts
import type { PageLoad } from './$types';
import { getSupabase } from '@supabase/auth-helpers-sveltekit';
import { redirect } from '@sveltejs/kit';
export const load: PageLoad = async (event) => {
const { session, supabaseClient } = await getSupabase(event);
if (!session) {
throw redirect(303, '/');
}
const { data: tableData } = await supabaseClient.from('test').select('*');
return {
user: session.user,
tableData
};
};
```
## Protecting API routes
Wrap an API Route to check that the user has a valid session. If they're not logged in the session is `null`.
```ts title=src/routes/api/protected-route/+server.ts
import type { RequestHandler } from './$types';
import { getSupabase } from '@supabase/auth-helpers-sveltekit';
import { json, redirect } from '@sveltejs/kit';
export const GET: RequestHandler = async (event) => {
const { session, supabaseClient } = await getSupabase(event);
if (!session) {
throw redirect(303, '/');
}
const { data } = await supabaseClient.from('test').select('*');
return json({ data });
};
```
If you visit `/api/protected-route` without a valid session cookie, you will get a 303 response.
## Protecting Actions
Wrap an Action to check that the user has a valid session. If they're not logged in the session is `null`.
```ts title=src/routes/posts/+page.server.ts
import type { Actions } from './$types';
import { getSupabase } from '@supabase/auth-helpers-sveltekit';
import { error, invalid } from '@sveltejs/kit';
export const actions: Actions = {
createPost: async (event) => {
const { request } = event;
const { session, supabaseClient } = await getSupabase(event);
if (!session) {
// the user is not signed in
throw error(403, { message: 'Unauthorized' });
}
// we are save, let the user create the post
const formData = await request.formData();
const content = formData.get('content');
const { error: createPostError, data: newPost } = await supabaseClient
.from('posts')
.insert({ content });
if (createPostError) {
return invalid(500, {
supabaseErrorMessage: createPostError.message
});
}
return {
newPost
};
}
};
```
If you try to submit a form with the action `?/createPost` without a valid session cookie, you will get a 403 error response.
## Saving and deleting the session
```ts
import type { Actions } from './$types';
import { invalid, redirect } from '@sveltejs/kit';
import { getSupabase } from '@supabase/auth-helpers-sveltekit';
export const actions: Actions = {
signin: async (event) => {
const { request, cookies, url } = event;
const { session, supabaseClient } = await getSupabase(event);
const formData = await request.formData();
const email = formData.get('email') as string;
const password = formData.get('password') as string;
const { error } = await supabaseClient.auth.signInWithPassword({
email,
password
});
if (error) {
if (error instanceof AuthApiError && error.status === 400) {
return invalid(400, {
error: 'Invalid credentials.',
values: {
email
}
});
}
return invalid(500, {
error: 'Server error. Try again later.',
values: {
email
}
});
}
throw redirect(303, '/dashboard');
},
signout: async (event) => {
const { supabaseClient } = await getSupabase(event);
await supabaseClient.auth.signOut();
throw redirect(303, '/');
}
};
```
## Protecting multiple routes
To avoid writing the same auth logic in every single route you can use the handle hook to
protect multiple routes at once.
```ts title=src/hooks.server.ts
import type { RequestHandler } from './$types';
import { getSupabase } from '@supabase/auth-helpers-sveltekit';
import { redirect, error } from '@sveltejs/kit';
export const handle: Handle = async ({ event, resolve }) => {
// protect requests to all routes that start with /protected-routes
if (event.url.pathname.startsWith('/protected-routes')) {
const { session, supabaseClient } = await getSupabase(event);
if (!session) {
throw redirect(303, '/');
}
}
// protect POST requests to all routes that start with /protected-posts
if (
event.url.pathname.startsWith('/protected-posts') &&
event.request.method === 'POST'
) {
const { session, supabaseClient } = await getSupabase(event);
if (!session) {
throw error(303, '/');
}
}
return resolve(event);
};
```
## Migrate from 0.7.x to 0.8 {#migration}
### Set up the Supabase client {#migration-set-up-supabase-client}
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="older-0.7"
>
<TabPanel id="older-0.7" label="0.7.x">
```js title=src/lib/db.ts
import { createClient } from '@supabase/supabase-js'
import { setupSupabaseHelpers } from '@supabase/auth-helpers-sveltekit'
import { dev } from '$app/environment'
import { env } from '$env/dynamic/public'
// or use the static env
// import { PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY } from '$env/static/public';
export const supabaseClient = createClient(env.PUBLIC_SUPABASE_URL, env.PUBLIC_SUPABASE_ANON_KEY, {
@@ -73,9 +413,34 @@ setupSupabaseHelpers({
})
```
### Initialize the client
</TabPanel>
<TabPanel id="0.8.0" label="0.8.0">
Edit your `+layout.svelte` file and set up the client-side.
```js title=src/lib/db.ts
import { createClient } from '@supabase/auth-helpers-sveltekit'
import { env } from '$env/dynamic/public'
// or use the static env
// import { PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY } from '$env/static/public';
export const supabaseClient = createClient(
env.PUBLIC_SUPABASE_URL,
env.PUBLIC_SUPABASE_ANON_KEY
);
```
</TabPanel>
</Tabs>
### Initialize the client {#migration-initialize-client}
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="older-0.7"
>
<TabPanel id="older-0.7" label="0.7.x">
```html title=src/routes/+layout.svelte
<script lang="ts">
@@ -95,9 +460,43 @@ Edit your `+layout.svelte` file and set up the client-side.
<slot />
```
### Set up hooks
</TabPanel>
<TabPanel id="0.8.0" label="0.8.0">
The `hooks.server.ts` file is where the heavy lifting of this library happens:
```html title=src/routes/+layout.svelte
<script>
import { supabaseClient } from '$lib/db';
import { invalidate } from '$app/navigation';
import { onMount } from 'svelte';
onMount(() => {
const {
data: { subscription }
} = supabaseClient.auth.onAuthStateChange(() => {
invalidate('supabase:auth');
});
return () => {
subscription.unsubscribe();
};
});
</script>
<slot />
```
</TabPanel>
</Tabs>
### Set up hooks {#migration-set-up-hooks}
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="older-0-7"
>
<TabPanel id="older-0-7" label="0.7.x">
```ts title=src/hooks.server.ts
// make sure the supabase instance is initialized on the server
@@ -106,39 +505,45 @@ import { dev } from '$app/environment'
import { auth } from '@supabase/auth-helpers-sveltekit/server'
export const handle = auth()
```
// use the sequence helper if you have additional Handle methods
**Optional** _if using additional handle methods_
```ts title=src/hooks.server.ts
// make sure the supabase instance is initialized on the server
import '$lib/db'
import { dev } from '$app/environment'
import { auth } from '@supabase/auth-helpers-sveltekit/server'
import { sequence } from '@sveltejs/kit/hooks'
export const handle = sequence(auth(), yourHandler)
```
There are three handle methods available:
</TabPanel>
<TabPanel id="0.8.0" label="0.8.0">
- `callback()`:
This creates a handler for `/api/auth/callback`. The `client` forwards the session details here every time `onAuthStateChange` fires on the client-side. This is needed to set up the cookies for your application so that SSR works seamlessly.
- `session()`:
This parses the session from the cookie and populate it in locals
- `auth()`:
a shorthand for `sequence(callback(), session())` that uses both handlers
### Send session to client
In order to make the session available to the UI (e.g., pages and layouts), pass the session in the root layout load function:
```ts title=src/routes/+layout.server.ts
import type { LayoutServerLoad } from './$types'
export const load: LayoutServerLoad = async ({ locals }) => {
return {
session: locals.session,
}
}
```ts title=src/hooks.server.ts
// make sure the supabase instance is initialized on the server
import '$lib/db'
```
### Typings
```ts title=src/hooks.client.ts
// make sure the supabase instance is initialized on the client
import '$lib/db'
```
In order to get the most out of TypeScript and IntelliSense, import the types into the `app.d.ts` type definition file that comes with your SvelteKit project.
</TabPanel>
</Tabs>
### Typings {#migration-typings}
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="older-0-7"
>
<TabPanel id="older-0-7" label="0.7.x">
```ts title=src/app.d.ts
/// <reference types="@sveltejs/kit" />
@@ -150,95 +555,91 @@ declare namespace App {
interface Locals {
session: import('@supabase/auth-helpers-sveltekit').SupabaseSession
}
interface PageData {
session: import('@supabase/auth-helpers-sveltekit').SupabaseSession
}
// interface Error {}
// interface Platform {}
}
```
</TabPanel>
<TabPanel id="0.8.0" label="0.8.0">
```ts title=src/app.d.ts
/// <reference types="@sveltejs/kit" />
// See https://kit.svelte.dev/docs/types#app
// for information about these interfaces
// and what to do when importing types
declare namespace App {
interface Supabase {
Database: import('./DatabaseDefinitions').Database;
SchemaName: 'public';
}
// interface Locals {}
interface PageData {
session: import('@supabase/auth-helpers-sveltekit').SupabaseSession;
}
// interface Error {}
// interface Platform {}
}
```
### Signing out
</TabPanel>
</Tabs>
This library has provided a method to delete the session cookie.
### withPageAuth {#migration-with-page-auth}
Create a server only page with an action to delete the cookie
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="older-0-7"
>
<TabPanel id="older-0-7" label="0.7.x">
```ts title=src/routes/logout/+page.server.ts
import { deleteSession } from '@supabase/auth-helpers-sveltekit/server'
import { redirect } from '@sveltejs/kit'
import type { Actions } from './$types'
export const actions: Actions = {
async default({ cookies }) {
deleteSession(cookies)
throw redirect(303, '/')
},
}
```
Then make a **POST** request with a form to the server only page above.
```html
```html title=src/routes/protected-route/+page.svelte
<script lang="ts">
import { enhanceAndInvalidate } from '@supabase/auth-helpers-sveltekit'
import type { PageData } from './$types'
export let data: PageData
$: ({ tableData, user } = data)
</script>
<form action="/logout" method="post" use:enhanceAndInvalidate>
<button type="submit">Sign out</button>
</form>
<div>Protected content for {user.email}</div>
<p>server-side fetched data with RLS:</p>
<pre>{JSON.stringify(tableData, null, 2)}</pre>
<p>user:</p>
<pre>{JSON.stringify(user, null, 2)}</pre>
```
### Basic setup
```ts title=src/routes/protected-route/+page.ts
import { withAuth } from '@supabase/auth-helpers-sveltekit'
import { redirect } from '@sveltejs/kit'
import type { PageLoad } from './$types'
You can now determine if a user is authenticated on the client-side by checking that the `user` object in `$page.data.session` is defined.
```html title=src/routes/+page.svelte
<script>
import { page } from '$app/stores'
</script>
{#if !$page.data.session.user}
<h1>I am not logged in</h1>
{:else}
<h1>Welcome {$page.data.session.user.email}</h1>
<p>I am logged in!</p>
{/if}
```
## Client-side data fetching with RLS
For [row level security](/docs/learn/auth-deep-dive/auth-row-level-security) to work properly when fetching data client-side, you need to make sure to import the `{ supabaseClient }` from `$lib/db` and only run your query once the user is defined client-side in `$page.data.session`:
```html
<script>
import { supabaseClient } from '$lib/db'
import { page } from '$app/stores'
let loadedData = []
async function loadData() {
const { data } = await supabaseClient.from('test').select('*').limit(20)
loadedData = data
export const load: PageLoad = withAuth(async ({ session, getSupabaseClient }) => {
if (!session.user) {
throw redirect(303, '/')
}
$: if ($page.data.session.user) {
loadData()
}
</script>
{#if $page.data.session.user}
<p>client-side data fetching with RLS</p>
<pre>{JSON.stringify(loadedData, null, 2)}</pre>
{/if}
const { data: tableData } = await getSupabaseClient().from('test').select('*')
return { tableData, user: session.user }
})
```
## Server-side data fetching with RLS
</TabPanel>
<TabPanel id="0.8.0" label="0.8.0">
```html title=src/routes/profile/+page.svelte
```html title=src/routes/protected-route/+page.svelte
<script>
/** @type {import('./$types').PageData} */
export let data
$: ({ user, tableData } = data)
export let data;
$: ({ user, tableData } = data);
</script>
<div>Protected content for {user.email}</div>
@@ -246,52 +647,38 @@ For [row level security](/docs/learn/auth-deep-dive/auth-row-level-security) to
<pre>{JSON.stringify(user, null, 2)}</pre>
```
For [row level security](/docs/learn/auth-deep-dive/auth-row-level-security) to work in a server environment, you need to use the `withAuth` helper to check if the user is authenticated. The helper extends the event with `session` and `getSupabaseClient()`:
```ts title=src/routes/profile/+page.ts
```ts title=src/routes/protected-route/+page.ts
// src/routes/profile/+page.ts
import type { PageLoad } from './$types';
import { withAuth } from '@supabase/auth-helpers-sveltekit';
import { getSupabase } from '@supabase/auth-helpers-sveltekit';
import { redirect } from '@sveltejs/kit';
interface TestTable {
id: string;
created_at: string;
}
export const load: PageLoad = withAuth(async ({ getSupabaseClient, session }) => {
if (!session.user) {
export const load: PageLoad = async (event) => {
const { session, supabaseClient } = await getSupabase(event);
if (!session) {
throw redirect(303, '/');
}
const { data: tableData } = await getSupabaseClient()
.from<TestTable>('test')
.select('*');
const { data: tableData } = await supabaseClient.from('test').select('*');
return {
user: session.user,
tableData
};
);
};
```
**Caution:**
</TabPanel>
</Tabs>
Always use the instance returned by `getSupabaseClient()` directly!
### withApiAuth {#migration-with-api-auth}
```ts
// Bad
const supabaseClient = getSupabaseClient()
await supabaseClient.from('table1').select()
await supabaseClient.from('table2').select()
// Good
await getSupabaseClient().from('table1').select()
await getSupabaseClient().from('table2').select()
```
## Protecting API routes
Wrap an API Route to check that the user has a valid session. If they're not logged in the session is `null`.
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="older-0-7"
>
<TabPanel id="older-0-7" label="0.7.x">
```ts title=src/routes/api/protected-route/+server.ts
import type { RequestHandler } from './$types';
@@ -307,148 +694,38 @@ export const GET: RequestHandler = withAuth(async ({ session, getSupabaseClient
if (!session.user) {
throw redirect(303, '/');
}
const { data } = await getSupabaseClient()
.from<TestTable>('test')
.select('*');
return json({ data });
);
});
```
If you visit `/api/protected-route` without a valid session cookie, you will get a 303 response.
</TabPanel>
<TabPanel id="0.8.0" label="0.8.0">
## Protecting Actions
```ts title=src/routes/api/protected-route/+server.ts
import type { RequestHandler } from './$types';
import { getSupabase } from '@supabase/auth-helpers-sveltekit';
import { json, redirect } from '@sveltejs/kit';
Wrap an Action to check that the user has a valid session. If they're not logged in, the session is `null`.
```ts title=src/routes/posts/+page.server.ts
import type { Actions } from './$types'
import { withAuth } from '@supabase/auth-helpers-sveltekit'
import { error, invalid } from '@sveltejs/kit'
export const actions: Actions = {
createPost: withAuth(async ({ session, getSupabaseClient, request }) => {
if (!session.user) {
// the user is not signed in
throw error(403, { message: 'Unauthorized' })
}
// we are save, let the user create the post
const formData = await request.formData()
const content = formData.get('content')
const { error: createPostError, data: newPost } = await getSupabaseClient()
.from('posts')
.insert({ content })
if (createPostError) {
return invalid(500, {
supabaseErrorMessage: createPostError.message,
})
}
return {
newPost,
}
}),
}
```
If you try to submit a form with the action `?/createPost` without a valid session cookie, you will get a 403 error response.
## Saving and deleting the session
Use `saveSession` to save the session cookies:
```ts
import type { Actions } from './$types'
import { supabaseClient } from '$lib/db'
import { invalid, redirect } from '@sveltejs/kit'
import { saveSession } from '@supabase/auth-helpers-sveltekit/server'
export const actions: Actions = {
async signin({ request, cookies, url }) {
const formData = await request.formData()
const email = formData.get('email') as string
const password = formData.get('password') as string
const { data, error } = await supabaseClient.auth.api.signInWithEmail(email, password, {
redirectTo: `${url.origin}/logging-in`,
})
if (error || !data) {
if (error?.status === 400) {
return invalid(400, {
error: 'Invalid credentials',
values: {
email,
},
})
}
return invalid(500, {
error: 'Server error. Try again later.',
values: {
email,
},
})
}
saveSession(cookies, data)
throw redirect(303, '/dashboard')
},
}
```
Use `deleteSession` to delete the session cookies:
```ts
import type { Actions } from './$types'
import { deleteSession } from '@supabase/auth-helpers-sveltekit/server'
import { redirect } from '@sveltejs/kit'
export const actions: Actions = {
async logout({ cookies }) {
deleteSession(cookies)
throw redirect(303, '/')
},
}
```
## Custom session namespace
If you want to use something other than `locals.session` and `$page.data.session`, update the types and create three helper functions:
```ts title=src/app.d.ts
declare namespace App {
interface Locals {
mySupabaseSession: import('@supabase/auth-helpers-sveltekit').SupabaseSession
export const GET: RequestHandler = async (event) => {
const { session, supabaseClient } = await getSupabase(event);
if (!session) {
throw redirect(303, '/');
}
interface PageData {
mySupabaseSession: import('@supabase/auth-helpers-sveltekit').SupabaseSession
}
}
const { data } = await supabaseClient.from('test').select('*');
return json({ data });
};
```
```ts title=src/hooks.server.ts
setupSupabaseServer({
supabaseClient,
cookieOptions: {
secure: !dev,
},
// --- change location within locals ---
getSessionFromLocals: (locals) => locals.mySupabaseSession,
setSessionToLocals: (locals, session) => (locals.mySupabaseSession = session),
})
```
</TabPanel>
</Tabs>
```ts title=src/lib/db.ts
setupSupabaseClient({
supabaseClient,
// --- change location within pageData ---
getSessionFromPageData: (data) => data.mySupabaseSession,
})
```
## Migrate from 0.6.11 and below to 0.7.0 {#migration}
## Migrate from 0.6.11 and below to 0.7.0 {#migration-0-7}
There are numerous breaking changes in the latest 0.7.0 version of this library.
@@ -456,7 +733,7 @@ There are numerous breaking changes in the latest 0.7.0 version of this library.
The environment variable prefix is now `PUBLIC_` instead of `VITE_` (e.g., `VITE_SUPABASE_URL` is now `PUBLIC_SUPABASE_URL`).
### Set up the Supabase client {#migration-set-up-supabase-client}
### Set up the Supabase client {#migration-set-up-supabase-client-0-7}
<Tabs
scrollable
@@ -478,7 +755,7 @@ export { supabaseClient };
```
</TabPanel>
<TabPanel id="latest" label="0.7.0">
<TabPanel id="0.7.0" label="0.7.0">
```js title=src/lib/db.ts
import { createClient } from '@supabase/supabase-js'
@@ -505,7 +782,7 @@ setupSupabaseHelpers({
</TabPanel>
</Tabs>
### Initialize the client {#migration-initialize-client}
### Initialize the client {#migration-initialize-client-0-7}
<Tabs
scrollable
@@ -528,7 +805,7 @@ setupSupabaseHelpers({
```
</TabPanel>
<TabPanel id="latest" label="0.7.0">
<TabPanel id="0.7.0" label="0.7.0">
The `@supabase/auth-helpers-svelte` library is no longer required as the `@supabase/auth-helpers-sveltekit` library handles all the client-side code.
@@ -553,7 +830,7 @@ The `@supabase/auth-helpers-svelte` library is no longer required as the `@supab
</TabPanel>
</Tabs>
### Set up hooks {#migration-set-up-hooks}
### Set up hooks {#migration-set-up-hooks-0-7}
<Tabs
scrollable
@@ -581,7 +858,7 @@ export const getSession: GetSession = async (event) => {
```
</TabPanel>
<TabPanel id="latest" label="0.7.0">
<TabPanel id="0.7.0" label="0.7.0">
```ts title=src/hooks.server.ts
// make sure the supabase instance is initialized on the server
@@ -607,7 +884,7 @@ export const handle = sequence(auth(), yourHandler)
</TabPanel>
</Tabs>
### Typings {#migration-typings}
### Typings {#migration-typings-0-7}
<Tabs
scrollable
@@ -639,7 +916,7 @@ declare namespace App {
```
</TabPanel>
<TabPanel id="latest" label="0.7.0">
<TabPanel id="0.7.0" label="0.7.0">
```ts title=src/app.d.ts
/// <reference types="@sveltejs/kit" />
@@ -688,7 +965,7 @@ declare namespace App {
```
</TabPanel>
<TabPanel id="latest" label="0.7.0">
<TabPanel id="0.7.0" label="0.7.0">
```html title=src/routes/+page.svelte
<script>
@@ -747,7 +1024,7 @@ declare namespace App {
```
</TabPanel>
<TabPanel id="latest" label="0.7.0">
<TabPanel id="0.7.0" label="0.7.0">
```html title=src/routes/protected-route/+page.svelte
<script lang="ts">
@@ -818,7 +1095,7 @@ export const GET: RequestHandler<GetOutput> = async ({ locals, request }) =>
```
</TabPanel>
<TabPanel id="latest" label="0.7.0">
<TabPanel id="0.7.0" label="0.7.0">
```ts title=src/routes/api/protected-route/+server.ts
import type { RequestHandler } from './$types';
+2 -2
View File
@@ -48,13 +48,13 @@ scoop install supabase
The CLI is available through [Homebrew](https://brew.sh) and Linux packages.
### Homebrew
#### Homebrew
```sh
brew install supabase/tap/supabase
```
### Linux packages
#### Linux packages
Linux packages are provided in [Releases](https://github.com/supabase/cli/releases).
To install, download the `.apk`/`.deb`/`.rpm` file depending on your package manager
@@ -56,7 +56,7 @@ values
The functions we'll cover in this guide are:
### `to_tsvector()`
### `to_tsvector()` {#to-tsvector}
Converts your data into searchable "tokens". `to_tsvector()` stands for "to text search vector". For example:
@@ -68,14 +68,14 @@ select to_tsvector('green eggs and ham')
Collectively these tokens are called a "document" which Postgres can use for comparisons.
### `to_tsquery()`
### `to_tsquery()` {#to-tsquery}
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.
### Match: `@@`
### Match: `@@` {#match}
The `@@` symbol is the "match" symbol for Full Text Search. It returns any matches between a `to_tsvector` result and a `to_tsquery` result.
@@ -450,7 +450,7 @@ final result = await client
Visit [PostgreSQL: Text Search Functions and Operators](https://www.postgresql.org/docs/current/functions-textsearch.html)
to learn about additional query operators you can use to do more advanced `full text queries`, such as:
### Proximity: `<->`
### Proximity: `<->` {#proximity}
The proximity symbol is useful for searching for terms that are a certain "distance" apart.
For example, to find the phrase `big dreams`, where the a match for "big" is followed immediately by a match for "dreams":
@@ -539,7 +539,7 @@ final result = await client
</TabPanel>
</Tabs>
### Negation: `!`
### Negation: `!` {#negation}
The negation symbol can be used to find phrases which _don't_ contain a search term.
For example, to find records that have the word `big` but not `little`:
@@ -11,7 +11,7 @@ Your PostgreSQL database is the core of your Supabase project, so it's important
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:p%3Dword@db.cvwawazfelidkloqmbma.supabase.co:5432/postgres`
### Changing your project password
## Changing your project password
When you created your project you were also asked to enter a password. This is actually the password for your database, specifically for the `postgres` user.
You can update this from the Dashboard under the [database settings](https://app.supabase.com/project/_/settings/database) page.
@@ -55,14 +55,6 @@ Use `psql` to import the Heroku database file to your Supabase project.
psql -h $SUPABASE_HOST -U postgres -f heroku_dump.sql
```
## (Optional) Post processing {#post-processing}
If you're using GraphQL with your Supabase project, you may need to run the following SQL command in the Supabase Dashboard (SQL Editor) to make sure the new tables show up:
```sql
select graphql.rebuild_schema();
```
## Additional options
- To only migrate a single database schema, add the `--schema=PATTERN` parameter to your `pg_dump` command.
+3 -3
View File
@@ -10,17 +10,17 @@ import TabItem from '@theme/TabItem'
Supabase Storage makes it simple to store and serve large files.
### Files
## Files
Files can be any sort of media file. This includes images, GIFs, and videos. It is best practice to store files outside of your database because of their sizes. For security, HTML files are returned as plain text.
### Folders
## Folders
Folders are a way to organize your files (just like on your computer).
There is no right or wrong way to
organize your files. You can store them in whichever folder structure suits your project.
### Buckets
## Buckets
Buckets are distinct containers for files and folders. You can think of them like "super folders".
Generally you would create distinct buckets for different Security and Access Rules. For example, you might
+11 -5
View File
@@ -50,18 +50,24 @@ Reference documentation for self-hosting Supabase features.
<div className="grid grid-cols-2 gap-3">
<div className="w-1/2">
<ButtonCard
to='/reference/auth'
title='Auth Server'
to="/reference/auth"
title="Auth Server"
description=" JSON Web Token (JWT)-based API for managing users and issuing access tokens."
/>
</div>
<div className="w-1/2">
<ButtonCard
to='/reference/storage'
title='Storage'
to="/reference/realtime"
title="Realtime"
description="Build multiplayer applications and listen to your database changes via websockets."
/>
</div>
<div className="w-1/2">
<ButtonCard
to="/reference/storage"
title="Storage"
description="S3-compatible object storage service that integrates with Postgres."
/>
</div>
</div>
</div>
+57
View File
@@ -0,0 +1,57 @@
---
slug: /
sidebar_position: 1
id: realtime
title: Supabase Realtime Server
sidebar_label: Supabase Realtime Server
---
Supabase Realtime is a server built with Elixir using the [Phoenix Framework](https://www.phoenixframework.org) that allows you to listen to changes in your PostgreSQL database via logical replication and then broadcast those changes via WebSockets.
There are two versions of this server: `Realtime` and `Realtime RLS`.
`Realtime` server works by:
1. listening to PostgreSQL's replication functionality (using PostgreSQL's logical decoding)
2. converting the byte stream into JSON
3. broadcasting to all connected clients over WebSockets
`Realtime RLS` server works by:
1. polling PostgreSQL's replication functionality (using PostgreSQL's logical decoding and [wal2json](https://github.com/eulerto/wal2json) output plugin)
2. passing database changes to a [Write Ahead Log Realtime Unified Security (WALRUS)](https://github.com/supabase/walrus) PostgresSQL function and receiving a list of authorized subscribers depending on Row Level Security (RLS) policies
3. converting the changes into JSON
4. broadcasting to authorized subscribers over WebSockets
## Why not just use PostgreSQL's `NOTIFY`?
A few reasons:
1. You don't have to set up triggers on every table.
2. `NOTIFY` has a payload limit of 8000 bytes and will fail for anything larger. The usual solution is to send an ID and then fetch the record, but that's heavy on the database.
3. `Realtime` server consumes two connections to the database, then you can connect many clients to this server. Easier on your database, and to scale up you just add additional `Realtime` servers.
## Benefits
1. The beauty of listening to the replication functionality is that you can make changes to your database from anywhere - your API, directly in the DB, via a console, etc. - and you will still receive the changes via WebSockets.
2. Decoupling. For example, if you want to send a new slack message every time someone makes a new purchase you might build that functionality directly into your API. This allows you to decouple your async functionality from your API.
3. This is built with Phoenix, an [extremely scalable Elixir framework](https://www.phoenixframework.org/blog/the-road-to-2-million-websocket-connections).
## Does this server guarantee delivery of every data change?
Not yet! Due to the following limitations:
1. Postgres database runs out of disk space due to Write-Ahead Logging (WAL) buildup, which can crash the database and prevent Realtime server from receiving and broadcasting changes. This can be mitigated in the Realtime RLS version of this server by setting the Postgres config `max_slot_wal_keep_size` to a reasonable size.
2. Realtime server can crash due to a larger replication lag than available memory, forcing the creation of a new replication slot and resetting replication to read from the latest WAL data.
3. When Realtime server falls too far behind for any reason, for example disconnecting from database as WAL continues to build up, then database can delete WAL segments the server still needs to read from, for example after reconnecting.
## Client libraries
- [JavaScript](https://github.com/supabase/realtime-js)
- [Dart](https://github.com/supabase/realtime-dart)
## Additional Links
- [Source Code](https://github.com/supabase/realtime)
- [Known bugs and issues](https://github.com/supabase/realtime/issues)
- [Realtime Guides](https://supabase.com/docs/guides/realtime)
Whitespace-only changes.
@@ -0,0 +1,7 @@
---
id: release-notes
---
# Release Notes
All release notes can be found in the [GitHub Releases](https://github.com/supabase/realtime/releases) page.
@@ -22,11 +22,11 @@ toc_max_heading_level: 3
<%- parameter.description %>
<ul>
<li>Required: <code><%= parameter.required %></code></li>
<li>Default: <code><%- parameter?.default ? parameter?.default : 'None' %></code></li><% if(parameter?.links?.length){ %>
<ul class="method-list-group">
<li class="method-list-item-label flex items-center gap-4">Required: <code><%= parameter.required %></code></li>
<li class="method-list-item-label flex items-center gap-4">Default: <code><%- parameter?.default ? parameter?.default : 'None' %></code></li><% if(parameter?.links?.length){ %>
<li>
<span>See also:</span>
<h3>See also:</h3>
<ul><% parameter.links?.forEach(function(link){ %><li><a href="<%- link.link %>"><%- link.name %></a></li><% }); %></ul>
</li>
<% } %>
+5 -1
View File
@@ -52,7 +52,11 @@ const Layout: FC<Props> = ({ meta, children, toc, menuItems, currentPage }) => {
: 'col-span-12 lg:col-span-9'
} py-2 lg:py-4 px-2 lg:px-8 mx-auto`}
>
<article className="doc-content-container prose dark:prose-dark dark:bg-scale-200 width-full mt-8 2xl:max-w-[880px] ">
<article
className={`${
meta?.hide_table_of_contents || !hasTableOfContents ? 'xl:min-w-[880px]' : ''
} doc-content-container prose dark:prose-dark dark:bg-scale-200 width-full mt-8 2xl:max-w-[880px]`}
>
{children}
</article>
</div>
+1
View File
@@ -23,6 +23,7 @@ export function getDocsBySlug(slug: string) {
(realSlug.includes('reference/cli/') && !nonGeneratedReferencePages.includes(realSlug)) ||
(realSlug.includes('reference/api/') && !nonGeneratedReferencePages.includes(realSlug)) ||
(realSlug.includes('reference/auth/') && !nonGeneratedReferencePages.includes(realSlug)) ||
(realSlug.includes('reference/realtime/') && !nonGeneratedReferencePages.includes(realSlug)) ||
(realSlug.includes('reference/storage/') && !nonGeneratedReferencePages.includes(realSlug))
? getPathToGeneratedDoc(realSlug)
: realSlug
+2
View File
@@ -20,6 +20,8 @@ export function getPageType(asPath: string) {
page = 'reference/cli'
} else if (asPath.includes('/reference/auth')) {
page = 'reference/auth'
} else if (asPath.includes('/reference/realtime')) {
page = 'reference/realtime'
} else if (asPath.includes('/reference/storage')) {
page = 'reference/storage'
} else if (asPath.includes('/reference')) {
+4 -2
View File
@@ -8,7 +8,7 @@
"start": "next start",
"lint": "next lint",
"postbuild": "node ./scripts/build-search.js",
"generate:all": "npm-run-all --parallel gen:api gen:cli gen:gotrue gen:storage gen:supabase-dart:v0 gen:supabase-dart:v1 gen:supabase-js:v1 gen:supabase-js:v2",
"generate:all": "npm-run-all --parallel gen:api gen:cli gen:gotrue gen:storage gen:supabase-dart:v0 gen:supabase-dart:v1 gen:supabase-js:v1 gen:supabase-js:v2 gen:realtime",
"gen:api": "npm-run-all gen:api:usage",
"gen:api:usage": "ts-node ./generator/index.ts gen --type api --url https://api.supabase.com --input ../../spec/transforms/api_v0_openapi_deparsed.json --output ./docs/reference/api/generated/usage.mdx",
"gen:cli": "npm-run-all gen:cli:commands gen:cli:config",
@@ -27,7 +27,9 @@
"gen:supabase-js:v1": "npm-run-all gen:supabase-js:v1:ref",
"gen:supabase-js:v1:ref": "ts-node ./generator/index.ts gen --type legacy --input ../../spec/supabase_js_v1.yml --output ./docs/reference/javascript/v1/generated",
"gen:supabase-js:v2": "npm-run-all gen:supabase-js:v2:ref",
"gen:supabase-js:v2:ref": "ts-node ./generator/index.ts gen --type legacy --input ../../spec/supabase_js_v2_temp.yml --output ./docs/reference/javascript/generated"
"gen:supabase-js:v2:ref": "ts-node ./generator/index.ts gen --type legacy --input ../../spec/supabase_js_v2_temp.yml --output ./docs/reference/javascript/generated",
"gen:realtime": "npm-run-all gen:realtime:config",
"gen:realtime:config": "ts-node ./generator/index.ts gen --type config --input ../../spec/realtime_v0_config.yaml --output ./docs/reference/realtime/generated/config.mdx"
},
"dependencies": {
"@algolia/autocomplete-js": "^1.7.2",
+2 -2
View File
@@ -63,8 +63,8 @@ export default function Oss({ meta }) {
<div className="">
<h2>Community Maintainers</h2>
<div className="overflow-auto max-w-[300px] md:max-w-none">
<ul className="flex gap-4 items-center p-0">
<div className="overflow-auto md:max-w-none hidden sm:block">
<ul className="flex 2xl:gap-4 items-center p-0">
{maintainerPills.map((x) => (
<li
key={x}
+7 -5
View File
@@ -106,10 +106,6 @@ pre[class*='language-'] {
}
}
.method-list-group li:last-child {
border-bottom: 1px solid var(--colors-scale3);
}
.method-list-title-isChild {
font-family: var(--custom-font-size-xs);
& h5 {
@@ -125,7 +121,7 @@ pre[class*='language-'] {
}
.method-list-group {
@apply p-0 m-0 list-none;
@apply p-0 m-0 list-none border-b;
}
.method-list-group .method-list-group {
@@ -225,3 +221,9 @@ a:has(code) {
.doc-content-container ul li a {
box-shadow: none !important;
}
// fix ToC links when they have <code> inside
.toc-menu li a code {
background: none;
border: none;
}
+6 -7
View File
@@ -221,13 +221,6 @@ JS Reference: [select()](../reference/javascript/select),
### GraphQL API
:::note
To rebuild your GraphQL schema from the SQL schema, call `select graphql.rebuild_schema();`.
Be sure to rebuild the GraphQL schema after altering the SQL schema.
:::
You can use any GraphQL client with the Supabase GraphQL API. For our GraphQL example we will use [urql](https://formidable.com/open-source/urql/docs/).
<Tabs
@@ -392,6 +385,12 @@ alter table todos enable row level security;
Never expose the `service_role` key in a browser or anywhere where a user can see it. This Key is designed to bypass Row Level Security - so it should only be used on a private server.
A common use case for the `service_role` key is to run data analytics jobs on the backend. To support joins on user id, it is often useful to grant the service role read access to `auth.users` table.
```sql
grant select on table auth.users to service_role;
```
We have [partnered with GitHub](https://github.blog/changelog/2022-03-28-supabase-is-now-a-github-secret-scanning-partner/) to scan for Supabase `service_role` keys pushed to public repositories.
If they detect any keys with service_role privileges being pushed to GitHub, they will forward the API key to us, so that we can automatically revoke the detected secrets and notify you, protecting your data against malicious actors.
@@ -55,14 +55,6 @@ Use `psql` to import the Heroku database file to your Supabase project.
psql -h $SUPABASE_HOST -U postgres -f heroku_dump.sql
```
## (Optional) Post processing {#post-processing}
If you're using GraphQL with your Supabase project, you may need to run the following SQL command in the Supabase Dashboard (SQL Editor) to make sure the new tables show up:
```sql
select graphql.rebuild_schema();
```
## Additional options
- To only migrate a single database schema, add the `--schema=PATTERN` parameter to your `pg_dump` command.
@@ -67,9 +67,6 @@ create table "Account"(
"createdAt" timestamp not null,
"updatedAt" timestamp not null
);
-- Rebuild the GraphQL Schema Cache
select graphql.rebuild_schema();
```
Translates to the GraphQL base type
+1 -1
View File
@@ -42,7 +42,7 @@
"realtime": {
"name": "Realtime",
"icon": "M13 10V3L4 14h7v7l9-11h-7z",
"description": "Create multiplayer experiences by sharing, broadcasting, and listenning to changes from other clients or the Database.",
"description": "Create multiplayer experiences by sharing, broadcasting, and listening to changes from other clients or the Database.",
"description_short": "",
"label": "",
"url": "/realtime"
+55 -1
View File
@@ -10415,6 +10415,31 @@
"@types/react-window": "*"
}
},
"node_modules/@types/recharts": {
"version": "1.8.23",
"resolved": "https://registry.npmjs.org/@types/recharts/-/recharts-1.8.23.tgz",
"integrity": "sha512-O/mIPm9f6dwRWfenOI3GQwsGta3x1YWjwqXOCZqC0MATQ6C+A+Jc8VxFnSUr4N3uYv64zkq90RwXFaMNbhJKvg==",
"dev": true,
"dependencies": {
"@types/d3-shape": "^1",
"@types/react": "*"
}
},
"node_modules/@types/recharts/node_modules/@types/d3-path": {
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-1.0.9.tgz",
"integrity": "sha512-NaIeSIBiFgSC6IGUBjZWcscUJEq7vpVu7KthHN8eieTV9d9MqkSOZLH4chq1PmcKy06PNe3axLeKmRIyxJ+PZQ==",
"dev": true
},
"node_modules/@types/recharts/node_modules/@types/d3-shape": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-1.3.8.tgz",
"integrity": "sha512-gqfnMz6Fd5H6GOLYixOZP/xlrMtJms9BaS+6oWxTKHNqPGZ93BkWWupQSCYm6YHqx6h9wjRupuJb90bun6ZaYg==",
"dev": true,
"dependencies": {
"@types/d3-path": "^1"
}
},
"node_modules/@types/resize-observer-browser": {
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/@types/resize-observer-browser/-/resize-observer-browser-0.1.7.tgz",
@@ -37190,6 +37215,7 @@
"@types/react-virtualized-auto-sizer": "^1.0.1",
"@types/react-window": "^1.8.5",
"@types/react-window-infinite-loader": "^1.0.5",
"@types/recharts": "^1.8.23",
"@types/semver": "^7.3.9",
"@types/sqlstring": "^2.3.0",
"@types/uuid": "^8.3.4",
@@ -44752,6 +44778,33 @@
"@types/react-window": "*"
}
},
"@types/recharts": {
"version": "1.8.23",
"resolved": "https://registry.npmjs.org/@types/recharts/-/recharts-1.8.23.tgz",
"integrity": "sha512-O/mIPm9f6dwRWfenOI3GQwsGta3x1YWjwqXOCZqC0MATQ6C+A+Jc8VxFnSUr4N3uYv64zkq90RwXFaMNbhJKvg==",
"dev": true,
"requires": {
"@types/d3-shape": "^1",
"@types/react": "*"
},
"dependencies": {
"@types/d3-path": {
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-1.0.9.tgz",
"integrity": "sha512-NaIeSIBiFgSC6IGUBjZWcscUJEq7vpVu7KthHN8eieTV9d9MqkSOZLH4chq1PmcKy06PNe3axLeKmRIyxJ+PZQ==",
"dev": true
},
"@types/d3-shape": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-1.3.8.tgz",
"integrity": "sha512-gqfnMz6Fd5H6GOLYixOZP/xlrMtJms9BaS+6oWxTKHNqPGZ93BkWWupQSCYm6YHqx6h9wjRupuJb90bun6ZaYg==",
"dev": true,
"requires": {
"@types/d3-path": "^1"
}
}
}
},
"@types/resize-observer-browser": {
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/@types/resize-observer-browser/-/resize-observer-browser-0.1.7.tgz",
@@ -48613,7 +48666,7 @@
"next-themes": "0.0.15",
"next-transpile-modules": "9.0.0",
"npm-run-all": "^4.1.5",
"octokit": "*",
"octokit": "^2.0.10",
"openapi-types": "^12.0.2",
"react": "17.0.2",
"react-copy-to-clipboard": "^5.0.2",
@@ -62566,6 +62619,7 @@
"@types/react-virtualized-auto-sizer": "^1.0.1",
"@types/react-window": "^1.8.5",
"@types/react-window-infinite-loader": "^1.0.5",
"@types/recharts": "^1.8.23",
"@types/semver": "^7.3.9",
"@types/sqlstring": "^2.3.0",
"@types/uuid": "^8.3.4",
+3 -3
View File
@@ -7,7 +7,7 @@ run: download transform format
# Init - prepare the environment
###############################################################################
init:
init:
cd $(REPO_DIR)/parser && npm install
@@ -18,7 +18,7 @@ download: download.api.v1 download.auth.v1 download.storage.v1 download.tsdoc.v2
download.api.v1:
curl -sS https://api.supabase.com/api/v1-json > $(REPO_DIR)/api_v0_openapi.json
download.auth.v1:
curl -sS https://supabase.github.io/gotrue/swagger.json > $(REPO_DIR)/auth_v1_openapi.json
@@ -95,7 +95,7 @@ combine.tsdoc.v2:
> $(REPO_DIR)/enrichments/tsdoc_v2/combined.json
###############################################################################
# Format everything - easier for git to track changes
# Format everything - easier for git to track changes.
###############################################################################
format:
npx prettier --write .
@@ -32,7 +32,7 @@ const PolicyRoles: FC<Props> = ({ roles, selectedRoles, onUpdateSelectedRoles })
<MultiSelect
options={formattedRoles}
value={selectedRoles}
placeholder="Defaults to all roles if none selected"
placeholder="Defaults to all (public) roles if none selected"
searchPlaceholder="Search for a role"
onChange={onUpdateSelectedRoles}
/>
@@ -52,7 +52,6 @@ const PAYGUsage: FC<Props> = ({ dateRange }) => {
startDate={startDate}
endDate={endDate}
interval="1d"
highlight="maximum"
defaultChartStyle="bar"
customDateFormat={'MMM D, YYYY'}
/>
@@ -42,14 +42,18 @@ const HelpPopover: FC<Props> = () => {
</Button>
</Link>
<Link passHref href="https://supabase.com/docs/">
<Button type="text" size="tiny" icon={<IconBookOpen />} as="a">
Docs
</Button>
<a target="_blank" rel="noreferrer">
<Button type="text" size="tiny" icon={<IconBookOpen />}>
Docs
</Button>
</a>
</Link>
<Link passHref href="https://status.supabase.com/">
<Button type="text" size="tiny" icon={<IconActivity />} as="a">
Supabase Status
</Button>
<a target="_blank" rel="noreferrer">
<Button type="text" size="tiny" icon={<IconActivity />}>
Supabase Status
</Button>
</a>
</Link>
</div>
<p className="text-sm text-scale-900">
@@ -8,7 +8,7 @@ import { getWithTimeout } from 'lib/common/fetch'
interface Props {}
const RestoringState: FC<Props> = ({}) => {
const { app, ui } = useStore()
const { app, ui, meta } = useStore()
const project = ui.selectedProject
const checkServerInterval = useRef<number>()
@@ -37,7 +37,7 @@ const RestoringState: FC<Props> = ({}) => {
const onConfirm = async () => {
setLoading(true)
await app.projects.fetchDetail(project?.ref ?? '')
await app.projects.fetchDetail(project?.ref ?? '', (project) => meta.setProjectDetails(project))
}
return (
@@ -6,9 +6,9 @@ import { Dictionary } from 'components/grid'
import { API_URL } from 'lib/constants'
import { get } from 'lib/common/fetch'
import { BarChart, AreaChart } from './ChartRenderer'
import { BarChart } from './ChartRenderer'
import AreaChart from 'components/ui/Charts/AreaChart'
import { ChartData } from './ChartHandler.types'
import { AreaProps } from 'recharts'
interface Props {
label: string
@@ -19,7 +19,6 @@ interface Props {
interval: string
customDateFormat?: string
children?: ReactNode
highlight?: 'total' | 'average' | 'maximum'
defaultChartStyle?: 'bar' | 'line'
hideChartType?: boolean
data?: ChartData
@@ -27,7 +26,6 @@ interface Props {
format?: string
highlightedValue?: string | number
onBarClick?: (v: any) => void
areaType?: AreaProps['type']
}
/**
@@ -48,7 +46,6 @@ const ChartHandler: FC<Props> = ({
interval,
customDateFormat,
children = null,
highlight,
defaultChartStyle = 'bar',
hideChartType = false,
data,
@@ -56,7 +53,6 @@ const ChartHandler: FC<Props> = ({
format,
highlightedValue,
onBarClick,
areaType,
}) => {
const router = useRouter()
const { ref } = router.query
@@ -172,10 +168,10 @@ const ChartHandler: FC<Props> = ({
<AreaChart
data={chartData?.data ?? []}
format={format || chartData?.format}
attribute={attribute}
yAxisLimit={chartData?.yAxisLimit}
xAxisKey="period_start"
yAxisKey={attribute}
highlightedValue={highlightedValue}
label={label}
title={label}
customDateFormat={customDateFormat}
/>
)}
@@ -3,7 +3,6 @@ import { Loading } from 'ui'
import { useState } from 'react'
import {
BarChart as RechartBarChart,
AreaChart as RechartAreaChart,
Area,
Bar,
XAxis,
@@ -268,125 +267,4 @@ export function BarChart({
</div>
</Loading>
)
}
export function AreaChart({
data,
attribute,
yAxisLimit,
format,
highlightedValue,
customDateFormat,
label,
}: any) {
const hasData = dataCheck(data, attribute)
const [focusBar, setFocusBar] = useState<any>(null)
const [mouseLeave, setMouseLeave] = useState<any>(true)
const onMouseMove = (state: any) => {
if (state?.activeTooltipIndex) {
setFocusBar(state.activeTooltipIndex)
setMouseLeave(false)
} else {
setFocusBar(null)
setMouseLeave(true)
}
}
const onMouseLeave = () => {
setFocusBar(false)
setMouseLeave(true)
}
// For future reference: https://github.com/supabase/supabase/pull/5311#discussion_r800852828
const chartHeight = 160
return (
<Loading active={!data}>
<Header
label={label}
attribute={attribute}
focus={focusBar}
highlightedValue={highlightedValue}
data={data}
format={format}
customDateFormat={customDateFormat}
/>
<div
style={{
width: '100%',
height: `${chartHeight}px`,
}}
>
{hasData ? (
<>
<ResponsiveContainer width="100%" height={chartHeight}>
<RechartAreaChart
data={data}
margin={{
top: 0,
right: 0,
left: 0,
bottom: 0,
}}
className="overflow-visible"
onMouseMove={onMouseMove}
onMouseLeave={onMouseLeave}
>
<defs>
<linearGradient id="colorUv" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor={CHART_COLORS.GREEN_1} stopOpacity={0.8} />
<stop offset="95%" stopColor={CHART_COLORS.GREEN_1} stopOpacity={0} />
</linearGradient>
</defs>
<XAxis
dataKey="period_start"
//interval={size === 'small' ? 5 : 1}
interval={data ? data.length - 2 : 0}
angle={0}
// stroke="#4B5563"
tick={{
fontSize: '0px',
color: CHART_COLORS.TICK,
}}
axisLine={{
stroke: CHART_COLORS.AXIS,
}}
tickLine={{
stroke: CHART_COLORS.AXIS,
}}
/>
{yAxisLimit && <YAxis type="number" domain={[0, yAxisLimit]} hide />}
<Tooltip content={<CustomTooltip />} />
<Area
type="monotone"
dataKey={attribute}
stroke={CHART_COLORS.GREEN_1}
fillOpacity={1}
fill="url(#colorUv)"
/>
</RechartAreaChart>
</ResponsiveContainer>
{data && (
<div className="-mt-5 flex items-center justify-between text-xs text-scale-900">
<span>
{dayjs(data[0].period_start).format(
customDateFormat ? customDateFormat : DATE_FORMAT__WITH_TIME
)}
</span>
<span>
{dayjs(data[data?.length - 1]?.period_start).format(
customDateFormat ? customDateFormat : DATE_FORMAT__WITH_TIME
)}
</span>
</div>
)}
</>
) : (
<EmptyState />
)}
</div>
</Loading>
)
}
}
@@ -40,9 +40,9 @@ export default function Introduction({ autoApiService, selectedLang }) {
<h2 className="doc-heading">Client Libraries</h2>
<div className="doc-section">
<article className="text ">
<p>Your API consists of both a RESTful interface and a realtime interface.</p>
<p>Your API consists of both a RESTful interface and a Realtime interface.</p>
<p>
For interacting with the realtime streams, we provide client libraries that handle the
For interacting with the Realtime streams, we provide client libraries that handle the
websockets.
</p>
</article>
@@ -0,0 +1,49 @@
import React from 'react'
import { ComponentMeta } from '@storybook/react'
import Chart, { AreaChartProps } from './AreaChart'
import dayjs from 'dayjs'
export default {
title: 'Charts',
} as ComponentMeta<typeof Chart>
const DATA = [290, 430, 649, 422, 321, 893, 111].map((value, index) => ({
ram_usage: value,
timestamp: dayjs().subtract(index, 'day').toISOString(),
}))
const PROPS: AreaChartProps<typeof DATA[number]> = {
title: 'Memory usage',
xAxisKey: 'timestamp',
yAxisKey: 'ram_usage',
data: DATA,
}
export const AreaChart = () => (
<div className="flex flex-row gap-4 flex-wrap p-10">
{[
{ title: 'Normal', props: {} },
{ title: 'No title', props: { title: undefined } },
{ title: 'Minimal Header', props: { minimalHeader: true } },
{ title: 'Minimal Highlighted Value', props: { minimalHeader: true, highlightedValue: 123 } },
{ title: 'Highlighted Value', props: { highlightedValue: 123, highlightedLabel: "some custom label" } },
{ title: 'Formatter - Auto 2 Precision for floats', props: { data: DATA.map(d=> ({...d, ram_usage: d.ram_usage + 0.1231233})) } },
{ title: 'UTC Dates', props: { displayDateInUtc: true } },
{ title: 'Custom date format', props: { customDateFormat: 'HH:mm' } },
{ title: 'Value format', props: { format: 'c' } },
{ title: 'Minimal Value format', props: { minimalHeader: true, format: 'c' } },
{ title: 'No data', props: { data: [] } },
].map(({ title, props }) => (
<div className="w-72 h-72">
<ExampleName>{title}</ExampleName>
<div className="w-full h-full mt-4">
<Chart {...PROPS} {...props} />
</div>
</div>
))}
</div>
)
const ExampleName: React.FC = ({ children }) => (
<h1 className="font-bold text-scale-1100">{children}</h1>
)
+125
View File
@@ -0,0 +1,125 @@
import { useState } from 'react'
import {
AreaChart as RechartAreaChart,
Area,
XAxis,
YAxis,
Tooltip,
ResponsiveContainer,
} from 'recharts'
import dayjs from 'dayjs'
import { CHART_COLORS, DateTimeFormats } from 'components/ui/Charts/Charts.constants'
import ChartHeader from './ChartHeader'
import { Datum, CommonChartProps } from './Charts.types'
import utc from 'dayjs/plugin/utc'
import ChartNoData from './NoDataPlaceholder'
import { numberFormatter } from './Charts.utils'
dayjs.extend(utc)
export interface AreaChartProps<D = Datum> extends CommonChartProps<D> {
yAxisKey: string
xAxisKey: string
format?: string
customDateFormat?: string
displayDateInUtc?: boolean
}
const AreaChart: React.FC<AreaChartProps> = ({
data,
yAxisKey,
xAxisKey,
format,
customDateFormat = DateTimeFormats.FULL,
title,
highlightedValue,
highlightedLabel,
displayDateInUtc,
minimalHeader,
className = '',
}) => {
const [focusDataIndex, setFocusDataIndex] = useState<number | null>(null)
// For future reference: https://github.com/supabase/supabase/pull/5311#discussion_r800852828
const chartHeight = 160
if (data.length === 0) return <ChartNoData />
const day = (value: number | string) => (displayDateInUtc ? dayjs(value).utc() : dayjs(value))
const resolvedHighlightedLabel =
(focusDataIndex !== null &&
data &&
data[focusDataIndex] &&
day(data[focusDataIndex][xAxisKey]).format(customDateFormat)) ||
highlightedLabel
const resolvedHighlightedValue =
(focusDataIndex !== null ? data[focusDataIndex]?.[yAxisKey] : null) || highlightedValue
return (
<div className={['flex flex-col gap-3', className].join(' ')}>
<ChartHeader
title={title}
format={format}
customDateFormat={customDateFormat}
highlightedValue={
typeof resolvedHighlightedValue === 'number'
? numberFormatter(resolvedHighlightedValue)
: resolvedHighlightedValue
}
highlightedLabel={resolvedHighlightedLabel}
minimalHeader={minimalHeader}
/>
<ResponsiveContainer width="100%" height={chartHeight}>
<RechartAreaChart
data={data}
margin={{
top: 0,
right: 0,
left: 0,
bottom: 0,
}}
className="overflow-visible"
// mouse hover focusing logic
onMouseMove={(e: any) => {
if (e.activeTooltipIndex !== focusDataIndex) {
setFocusDataIndex(e.activeTooltipIndex)
}
}}
onMouseLeave={() => setFocusDataIndex(null)}
>
<defs>
<linearGradient id="colorUv" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor={CHART_COLORS.GREEN_1} stopOpacity={0.8} />
<stop offset="95%" stopColor={CHART_COLORS.GREEN_1} stopOpacity={0} />
</linearGradient>
</defs>
<XAxis
dataKey={xAxisKey}
interval={data.length - 2}
angle={0}
// hide the tick
tick={{ fontSize: '0px' }}
// color the axis
axisLine={{ stroke: CHART_COLORS.AXIS }}
tickLine={{ stroke: CHART_COLORS.AXIS }}
/>
<Tooltip content={() => null} />
<Area
type="monotone"
dataKey={yAxisKey}
stroke={CHART_COLORS.GREEN_1}
fillOpacity={1}
fill="url(#colorUv)"
/>
</RechartAreaChart>
</ResponsiveContainer>
{data && (
<div className="text-scale-900 -mt-5 flex items-center justify-between text-xs">
<span>{dayjs(data[0][xAxisKey]).format(customDateFormat)}</span>
<span>{dayjs(data[data?.length - 1]?.[xAxisKey]).format(customDateFormat)}</span>
</div>
)}
</div>
)
}
export default AreaChart
@@ -0,0 +1,54 @@
import { DateTimeFormats } from './Charts.constants'
export interface ChartHeaderProps {
title?: string
format?: string
customDateFormat?: string
minimalHeader?: boolean
displayDateInUtc?: boolean
highlightedLabel?: number | string | null
highlightedValue?: number | string | null
}
const ChartHeader: React.FC<ChartHeaderProps> = ({
format,
highlightedValue,
highlightedLabel,
title,
minimalHeader = false,
}) => {
const chartTitle = (
<h3 className={'text-scale-900 ' + (minimalHeader ? 'text-xs' : 'text-sm')}>{title}</h3>
)
const highlighted = (
<h5
className={`text-scale-1200 text-xl font-normal ${minimalHeader ? 'text-base' : 'text-2xl'}`}
>
{highlightedValue}
<span className="text-lg">{format}</span>
</h5>
)
const label = <h5 className="text-scale-900 text-xs">{highlightedLabel}</h5>
if (minimalHeader) {
return (
<div className="flex flex-row items-center gap-x-4" style={{ minHeight: '1.8rem' }}>
{chartTitle}
<div className="flex flex-row items-baseline gap-x-2">
{highlightedValue && highlighted}
{label}
</div>
</div>
)
}
return (
<div className="h-16">
{chartTitle}
{highlightedValue && highlighted}
{label}
</div>
)
}
export default ChartHeader
@@ -17,3 +17,8 @@ export const USAGE_COLORS = {
404: 'var(--colors-amber7)',
500: 'var(--colors-red9)',
}
export enum DateTimeFormats {
FULL = 'MMM D, YYYY, hh:mma',
DATE_ONLY = 'MMM D, YYYY',
}
@@ -0,0 +1,35 @@
export interface CommonChartProps<D>
extends Pick<
HeaderType<D>,
| 'highlightedValue'
| 'highlightedLabel'
| 'customDateFormat'
| 'data'
| 'format'
| 'minimalHeader'
| 'displayDateInUtc'
> {
title?: string;
className?: string
}
export type HeaderType<D> = {
attribute: string
focus: number | null
format?: string
highlightedValue?: number | string
highlightedLabel?: string
data: D[]
customDateFormat?: string
label: string
minimalHeader?: boolean
displayDateInUtc?: boolean
}
export interface Datum {
[attribute: string]: number | string
}
export interface TimeseriesDatum extends Datum {
timestamp: string
}
@@ -0,0 +1,38 @@
/**
* Auto formats a number to a default precision if it is a float
*
* @example
* numberFormatter(123) // "123"
* numberFormatter(123.123) // "123.12"
* numberFormatter(123, 2) // "123.00"
*/
export const numberFormatter = (num: number, precision = 2) => isFloat(num) ? precisionFormatter(num, precision) : String(num);
/**
* Tests if a number is a float.
*
* @example
* isFloat(123) // false
* isFloat(123.123) // true
*/
export const isFloat = (num: number) => String(num).includes(".")
/**
* Formats a number to a particular precision.
*
* @example
* precisionFormatter(123, 2) // "123.00"
* precisionFormatter(123.123, 2) // "123.12"
*/
export const precisionFormatter = (num: number, precision: number): string => {
if (isFloat(num)) {
const [head, tail] = String(num).split(".")
return head + "." + tail.slice(0, precision)
} else {
// pad int with 0
return String(num) + "." + ("0".repeat(precision))
}
}
@@ -0,0 +1,26 @@
import { IconBarChart2 } from '@supabase/ui'
interface Props {
title?: string
message?: string
className?: string
}
const NoDataPlaceholder: React.FC<Props> = ({
title = 'No data to show',
message,
className = '',
}) => (
<div
className={
'border-scale-600 flex h-full w-full flex-col items-center justify-center space-y-2 border border-dashed text-center ' +
className
}
>
<IconBarChart2 className="text-scale-800" />
<div>
<p className="text-scale-1100 text-xs">{title}</p>
{message && <p className="text-scale-900 text-xs">{message}</p>}
</div>
</div>
)
export default NoDataPlaceholder
+51111
View File
File diff suppressed because it is too large. Load diff
+1
View File
@@ -113,6 +113,7 @@
"@types/react-virtualized-auto-sizer": "^1.0.1",
"@types/react-window": "^1.8.5",
"@types/react-window-infinite-loader": "^1.0.5",
"@types/recharts": "^1.8.23",
"@types/semver": "^7.3.9",
"@types/sqlstring": "^2.3.0",
"@types/uuid": "^8.3.4",
@@ -0,0 +1,16 @@
import { isFloat, numberFormatter, precisionFormatter } from "components/ui/Charts/Charts.utils"
test("isFloat", () => {
expect(isFloat(123)).toBe(false)
expect(isFloat(123.123)).toBe(true)
})
test("numberFormatter", () => {
expect(numberFormatter(123)).toBe("123")
expect(numberFormatter(123.123)).toBe("123.12")
})
test("precisionFormatter", () => {
expect(precisionFormatter(123, 1)).toBe("123.0")
expect(precisionFormatter(123.12345, 4)).toBe("123.1234")
})