mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
Merge branch 'master' into fix/storage-file-explorer-drag-state-2
This commit is contained in:
commit
77556b8e15
1265 files changed
+50736
-13557
No files matched your search
@@ -5,16 +5,15 @@ name: Studio Unit Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
branches: [master]
|
||||
paths:
|
||||
- 'studio/**'
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
branches: [master]
|
||||
paths:
|
||||
- 'studio/**'
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
@@ -23,15 +22,15 @@ jobs:
|
||||
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'npm'
|
||||
- name: Install deps
|
||||
run: npm install
|
||||
working-directory: ./
|
||||
- name: Run tests
|
||||
run: npm test
|
||||
working-directory: ./studio
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'npm'
|
||||
- name: Install deps
|
||||
run: npm ci
|
||||
working-directory: ./
|
||||
- name: Run tests
|
||||
run: npm run test:studio
|
||||
working-directory: ./
|
||||
@@ -195,6 +195,15 @@ The monorepo has a set of shared components under `/packages`:
|
||||
- `/packages/config`: All shared config
|
||||
- `/packages/spec`: Generates documentation using spec files.
|
||||
- `/packages/tsconfig`: Shared Typescript settings
|
||||
- `/packages/ui`: Shared UI components (formerly @supabase/ui)
|
||||
|
||||
To use these 'packages', or any of their components from a Next.JS app, you must use `next-transpile-modules` in the `next.config.js` file. This looks like:
|
||||
|
||||
```tsx
|
||||
// next.config.js
|
||||
const withTM = require('next-transpile-modules')(['ui', 'common'])
|
||||
module.exports = withTM({})
|
||||
```
|
||||
|
||||
### Installing packages
|
||||
|
||||
|
||||
+93
-443
File diff suppressed because it is too large.
Load diff
@@ -11,25 +11,21 @@ A `config.toml` file is generated after running `supabase init`.
|
||||
|
||||
This file is located in the `supabase` folder under `supabase/config.toml`.
|
||||
|
||||
|
||||
|
||||
|
||||
<!-- AUTOGENERATED: DO NOT EDIT DIRECTLY -->
|
||||
|
||||
## General {#general}
|
||||
|
||||
|
||||
|
||||
### `project_id` {#project_id}
|
||||
|
||||
A string used to distinguish different Supabase projects on the same host. Defaults to the working directory name when running `supabase init`.
|
||||
|
||||
|
||||
<ul>
|
||||
<li>Required: <code>true</code></li>
|
||||
<li>Default: <code>None</code></li>
|
||||
<li>
|
||||
Required: <code>true</code>
|
||||
</li>
|
||||
<li>
|
||||
Default: <code>None</code>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<br />
|
||||
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
---
|
||||
id: usage
|
||||
slug: /usage
|
||||
title: Usage
|
||||
toc_max_heading_level: 3
|
||||
---
|
||||
|
||||
import Tabs from '@theme/Tabs'
|
||||
import TabItem from '@theme/TabItem'
|
||||
|
||||
<!-- AUTOGENERATED: DO NOT EDIT DIRECTLY IF THIS IS VERSION "next" -->
|
||||
|
||||
Documentation of the gotrue API.
|
||||
|
||||
<!-- AUTOGENERATED: DO NOT EDIT DIRECTLY IF THIS IS VERSION "next" -->
|
||||
@@ -1,12 +1,12 @@
|
||||
---
|
||||
id: generating-types
|
||||
title: "Generating Types"
|
||||
title: 'Generating Types'
|
||||
slug: generating-types
|
||||
custom_edit_url: https://github.com/supabase/supabase/edit/master/spec/supabase_js_v1_legacy.yml
|
||||
custom_edit_url: https://github.com/supabase/supabase/edit/master/spec/supabase_js_v1.yml
|
||||
---
|
||||
|
||||
import Tabs from '@theme/Tabs';
|
||||
import TabItem from '@theme/TabItem';
|
||||
import Tabs from '@theme/Tabs'
|
||||
import TabItem from '@theme/TabItem'
|
||||
|
||||
Supabase will soon release native type generators that dump your database types for various languages. For now, we support TypeScript [through third-party tools](/docs/guides/api/generating-types).
|
||||
|
||||
@@ -19,12 +19,12 @@ If you don't provide a type for the row you need to explicitly pass `from<any>('
|
||||
|
||||
```ts
|
||||
type Message = {
|
||||
id: number;
|
||||
inserted_at: string;
|
||||
message: string;
|
||||
user_id: string;
|
||||
channel_id: number;
|
||||
author: { username: string };
|
||||
id: number
|
||||
inserted_at: string
|
||||
message: string
|
||||
user_id: string
|
||||
channel_id: number
|
||||
author: { username: string }
|
||||
}
|
||||
|
||||
const response = await supabase
|
||||
@@ -39,4 +39,4 @@ const response = await supabase
|
||||
.select('*, author:user_id(username)')
|
||||
.match({ channel_id: 2 })
|
||||
response.data // Response data will be of type Array<any>.
|
||||
```
|
||||
```
|
||||
@@ -1,12 +1,11 @@
|
||||
---
|
||||
id: index
|
||||
title: "Supabase Client"
|
||||
title: 'Supabase Client'
|
||||
slug: supabase-client
|
||||
custom_edit_url: ../../spec/supabase_js_v1_legacy.yml
|
||||
custom_edit_url: ../../spec/supabase_js_v1.yml
|
||||
---
|
||||
|
||||
import Tabs from '@theme/Tabs';
|
||||
import TabItem from '@theme/TabItem';
|
||||
import Tabs from '@theme/Tabs'
|
||||
import TabItem from '@theme/TabItem'
|
||||
|
||||
|
||||
Supabase JavaScript.
|
||||
Supabase JavaScript.
|
||||
@@ -1,24 +1,20 @@
|
||||
---
|
||||
id: initializing
|
||||
title: "Initializing"
|
||||
title: 'Initializing'
|
||||
slug: initializing
|
||||
custom_edit_url: https://github.com/supabase/supabase/edit/master/spec/supabase_js_v1_legacy.yml
|
||||
custom_edit_url: https://github.com/supabase/supabase/edit/master/spec/supabase_js_v1.yml
|
||||
---
|
||||
|
||||
import Tabs from '@theme/Tabs';
|
||||
import TabItem from '@theme/TabItem';
|
||||
import Tabs from '@theme/Tabs'
|
||||
import TabItem from '@theme/TabItem'
|
||||
|
||||
You can initialize a new Supabase client using the `createClient()` method.
|
||||
|
||||
The Supabase client is your entrypoint to the rest of the Supabase functionality
|
||||
and is the easiest way to interact with everything we offer within the Supabase ecosystem.
|
||||
|
||||
|
||||
|
||||
|
||||
## Parameters
|
||||
|
||||
|
||||
<ul className="method-list-group">
|
||||
|
||||
<li className="method-list-item">
|
||||
@@ -41,7 +37,6 @@ The unique Supabase URL which is supplied when you create a new project in your
|
||||
|
||||
</li>
|
||||
|
||||
|
||||
<li className="method-list-item">
|
||||
<h4 className="method-list-item-label">
|
||||
<span className="method-list-item-label-name">
|
||||
@@ -62,7 +57,6 @@ The unique Supabase Key which is supplied when you create a new project in your
|
||||
|
||||
</li>
|
||||
|
||||
|
||||
<li className="method-list-item">
|
||||
<h4 className="method-list-item-label">
|
||||
<span className="method-list-item-label-name">
|
||||
@@ -77,7 +71,7 @@ The unique Supabase Key which is supplied when you create a new project in your
|
||||
</h4>
|
||||
<div class="method-list-item-description">
|
||||
|
||||
No description provided.
|
||||
No description provided.
|
||||
|
||||
</div>
|
||||
|
||||
@@ -85,36 +79,22 @@ No description provided.
|
||||
|
||||
</ul>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## Examples
|
||||
|
||||
### createClient()
|
||||
|
||||
|
||||
|
||||
```js
|
||||
import { createClient } from '@supabase/supabase-js'
|
||||
|
||||
// Create a single supabase client for interacting with your database
|
||||
const supabase = createClient('https://xyzcompany.supabase.co', 'public-anon-key')
|
||||
const supabase = createClient(
|
||||
'https://xyzcompany.supabase.co',
|
||||
'public-anon-key'
|
||||
)
|
||||
```
|
||||
|
||||
### With additional parameters
|
||||
|
||||
|
||||
|
||||
```js
|
||||
import { createClient } from '@supabase/supabase-js'
|
||||
|
||||
@@ -123,22 +103,28 @@ const options = {
|
||||
headers: { 'x-my-custom-header': 'my-app-name' },
|
||||
autoRefreshToken: true,
|
||||
persistSession: true,
|
||||
detectSessionInUrl: true
|
||||
detectSessionInUrl: true,
|
||||
}
|
||||
const supabase = createClient("https://xyzcompany.supabase.co", "public-anon-key", options)
|
||||
const supabase = createClient(
|
||||
'https://xyzcompany.supabase.co',
|
||||
'public-anon-key',
|
||||
options
|
||||
)
|
||||
```
|
||||
|
||||
### API schemas
|
||||
|
||||
|
||||
|
||||
```js
|
||||
import { createClient } from '@supabase/supabase-js'
|
||||
|
||||
// Provide a custom schema. Defaults to "public".
|
||||
const supabase = createClient('https://xyzcompany.supabase.co', 'public-anon-key', {
|
||||
schema: 'other_schema'
|
||||
})
|
||||
const supabase = createClient(
|
||||
'https://xyzcompany.supabase.co',
|
||||
'public-anon-key',
|
||||
{
|
||||
schema: 'other_schema',
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
By default the API server points to the `public` schema. You can enable other database schemas within the Dashboard.
|
||||
@@ -148,16 +134,18 @@ Note: each client connection can only access a single schema, so the code above
|
||||
|
||||
### Custom `fetch` implementation
|
||||
|
||||
|
||||
|
||||
```js
|
||||
import { createClient } from '@supabase/supabase-js'
|
||||
|
||||
const supabase = createClient('https://xyzcompany.supabase.co', 'public-anon-key', {
|
||||
fetch: fetch.bind(globalThis)
|
||||
})
|
||||
const supabase = createClient(
|
||||
'https://xyzcompany.supabase.co',
|
||||
'public-anon-key',
|
||||
{
|
||||
fetch: fetch.bind(globalThis),
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
`supabase-js` uses the [`cross-fetch`](https://www.npmjs.com/package/cross-fetch) library to make HTTP requests,
|
||||
but an alternative `fetch` implementation can be provided as an option.
|
||||
This is most useful in environments where `cross-fetch` is not compatible (for instance Cloudflare Workers).
|
||||
This is most useful in environments where `cross-fetch` is not compatible (for instance Cloudflare Workers).
|
||||
@@ -1,12 +1,12 @@
|
||||
---
|
||||
id: installing
|
||||
title: "Installing"
|
||||
title: 'Installing'
|
||||
slug: installing
|
||||
custom_edit_url: https://github.com/supabase/supabase/edit/master/spec/supabase_js_v1_legacy.yml
|
||||
custom_edit_url: https://github.com/supabase/supabase/edit/master/spec/supabase_js_v1.yml
|
||||
---
|
||||
|
||||
import Tabs from '@theme/Tabs';
|
||||
import TabItem from '@theme/TabItem';
|
||||
import Tabs from '@theme/Tabs'
|
||||
import TabItem from '@theme/TabItem'
|
||||
|
||||
All JavaScript libraries are built directly by the Supabase team.
|
||||
|
||||
@@ -15,11 +15,13 @@ Other languages are built by the community and supported by Supabase.
|
||||
## JavaScript
|
||||
|
||||
Via NPM
|
||||
|
||||
```bash
|
||||
npm install @supabase/supabase-js
|
||||
```
|
||||
|
||||
Via Yarn
|
||||
|
||||
```bash
|
||||
yarn add @supabase/supabase-js
|
||||
```
|
||||
@@ -27,8 +29,9 @@ yarn add @supabase/supabase-js
|
||||
Find the source code on [GitHub](https://github.com/supabase/supabase-js).
|
||||
|
||||
Or via CDN
|
||||
|
||||
```js
|
||||
<script src="https://cdn.jsdelivr.net/npm/@supabase/supabase-js"></script>
|
||||
//or
|
||||
<script src="https://unpkg.com/@supabase/supabase-js"></script>
|
||||
```
|
||||
```
|
||||
@@ -79,7 +79,7 @@ You can use it completely, or just the features you need.
|
||||
description={'User management with Row Level Security.'}
|
||||
style={{ height: '100%' }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/* Auto-generated APIs */}
|
||||
<div class="col col--4">
|
||||
<ButtonCard
|
||||
@@ -150,7 +150,7 @@ Supabase is just Postgres, which makes it compatible with a large number of tool
|
||||
<div>
|
||||
<div class="row is-multiline">
|
||||
{frameworks.map((x) => (
|
||||
<div key={x.name} class="col col--3">
|
||||
<div key={x.name} class="col col--3">
|
||||
<ButtonCard
|
||||
layout="horizontal"
|
||||
icon={
|
||||
@@ -162,7 +162,12 @@ Supabase is just Postgres, which makes it compatible with a large number of tool
|
||||
/>
|
||||
) : (
|
||||
<ThemedImage
|
||||
style={{ display: 'block', maxHeight: 20, minWidth: 20, margin: 0 }}
|
||||
style={{
|
||||
display: 'block',
|
||||
maxHeight: 20,
|
||||
minWidth: 20,
|
||||
margin: 0,
|
||||
}}
|
||||
alt={x.name}
|
||||
width="20"
|
||||
sources={{
|
||||
|
||||
@@ -21,14 +21,16 @@ A collection of framework-specific Auth utilities for working with Supabase.
|
||||
description={'A pre-built React component for authenticating users.'}
|
||||
style={{ height: '100%' }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/* Next.js */}
|
||||
<div class="col col--4">
|
||||
<ButtonCard
|
||||
class="card"
|
||||
to={useBaseUrl('/guides/auth/auth-helpers/nextjs')}
|
||||
title={'Next.js'}
|
||||
description={'Helpers for authenticating users in Next.js applications.'}
|
||||
description={
|
||||
'Helpers for authenticating users in Next.js applications.'
|
||||
}
|
||||
style={{ height: '100%' }}
|
||||
/>
|
||||
</div>
|
||||
@@ -38,7 +40,9 @@ A collection of framework-specific Auth utilities for working with Supabase.
|
||||
class="card"
|
||||
to={useBaseUrl('/guides/auth/auth-helpers/sveltekit')}
|
||||
title={'SvelteKit'}
|
||||
description={'Helpers for authenticating users in SvelteKit applications.'}
|
||||
description={
|
||||
'Helpers for authenticating users in SvelteKit applications.'
|
||||
}
|
||||
style={{ height: '100%' }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
id: nextjs
|
||||
title: Supabase Auth with Next.js
|
||||
description: Authentication helpers for Next.js API routes, middleware, and SSR.
|
||||
sidebar_label: "Next.js"
|
||||
sidebar_label: 'Next.js'
|
||||
---
|
||||
|
||||
This submodule provides convenience helpers for implementing user authentication in Next.js applications.
|
||||
|
||||
@@ -56,24 +56,28 @@ 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/supabase-js';
|
||||
import { setupSupabaseHelpers } from '@supabase/auth-helpers-sveltekit';
|
||||
import { dev } from '$app/environment';
|
||||
import { env } from '$env/dynamic/public';
|
||||
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, {
|
||||
persistSession: false,
|
||||
autoRefreshToken: false
|
||||
});
|
||||
export const supabaseClient = createClient(
|
||||
env.PUBLIC_SUPABASE_URL,
|
||||
env.PUBLIC_SUPABASE_ANON_KEY,
|
||||
{
|
||||
persistSession: false,
|
||||
autoRefreshToken: false,
|
||||
}
|
||||
)
|
||||
|
||||
setupSupabaseHelpers({
|
||||
supabaseClient,
|
||||
cookieOptions: {
|
||||
secure: !dev
|
||||
}
|
||||
});
|
||||
supabaseClient,
|
||||
cookieOptions: {
|
||||
secure: !dev,
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
### Initialize the client
|
||||
@@ -83,16 +87,16 @@ Edit your `+layout.svelte` file and set up the client-side.
|
||||
```html title="src/routes/+layout.svelte"
|
||||
<script lang="ts">
|
||||
// make sure the supabase instance is initialized on the client
|
||||
import '$lib/db';
|
||||
import { startSupabaseSessionSync } from '@supabase/auth-helpers-sveltekit';
|
||||
import { page } from '$app/stores';
|
||||
import { invalidateAll } from '$app/navigation';
|
||||
import '$lib/db'
|
||||
import { startSupabaseSessionSync } from '@supabase/auth-helpers-sveltekit'
|
||||
import { page } from '$app/stores'
|
||||
import { invalidateAll } from '$app/navigation'
|
||||
|
||||
// this sets up automatic token refreshing
|
||||
startSupabaseSessionSync({
|
||||
page,
|
||||
handleRefresh: () => invalidateAll()
|
||||
});
|
||||
page,
|
||||
handleRefresh: () => invalidateAll(),
|
||||
})
|
||||
</script>
|
||||
|
||||
<slot />
|
||||
@@ -104,16 +108,16 @@ The `hooks.ts` file is where the heavy lifting of this library happens:
|
||||
|
||||
```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 '$lib/db'
|
||||
import { dev } from '$app/environment'
|
||||
import { auth } from '@supabase/auth-helpers-sveltekit/server'
|
||||
|
||||
export const handle = auth();
|
||||
export const handle = auth()
|
||||
|
||||
// use the sequence helper if you have additional Handle methods
|
||||
import { sequence } from '@sveltejs/kit/hooks';
|
||||
import { sequence } from '@sveltejs/kit/hooks'
|
||||
|
||||
export const handle = sequence(auth(), yourHandler);
|
||||
export const handle = sequence(auth(), yourHandler)
|
||||
```
|
||||
|
||||
There are three handle methods available:
|
||||
@@ -130,13 +134,13 @@ There are three handle methods available:
|
||||
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';
|
||||
import type { LayoutServerLoad } from './$types'
|
||||
|
||||
export const load: LayoutServerLoad = async ({ locals }) => {
|
||||
return {
|
||||
session: locals.session
|
||||
};
|
||||
};
|
||||
session: locals.session,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Typings
|
||||
@@ -151,10 +155,10 @@ In order to get the most out of TypeScript and IntelliSense, import the types in
|
||||
// and what to do when importing types
|
||||
declare namespace App {
|
||||
interface Locals {
|
||||
session: import('@supabase/auth-helpers-sveltekit').SupabaseSession;
|
||||
session: import('@supabase/auth-helpers-sveltekit').SupabaseSession
|
||||
}
|
||||
interface PageData {
|
||||
session: import('@supabase/auth-helpers-sveltekit').SupabaseSession;
|
||||
session: import('@supabase/auth-helpers-sveltekit').SupabaseSession
|
||||
}
|
||||
// interface Error {}
|
||||
// interface Platform {}
|
||||
@@ -168,22 +172,23 @@ This library has provided a method to delete the session cookie.
|
||||
Create a server only page with an action to delete the cookie
|
||||
|
||||
```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';
|
||||
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, '/');
|
||||
}
|
||||
};
|
||||
async default({ cookies }) {
|
||||
deleteSession(cookies)
|
||||
throw redirect(303, '/')
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Then make a **POST** request with a form to the server only page above.
|
||||
|
||||
```html
|
||||
<script lang="ts">
|
||||
import { enhanceAndInvalidate } from '@supabase/auth-helpers-sveltekit';
|
||||
import { enhanceAndInvalidate } from '@supabase/auth-helpers-sveltekit'
|
||||
</script>
|
||||
|
||||
<form action="/logout" method="post" use:enhanceAndInvalidate>
|
||||
@@ -197,14 +202,14 @@ You can now determine if a user is authenticated on the client-side by checking
|
||||
|
||||
```html title="src/routes/+page.svelte"
|
||||
<script>
|
||||
import { page } from '$app/stores';
|
||||
import { page } from '$app/stores'
|
||||
</script>
|
||||
|
||||
{#if !$page.data.session.user}
|
||||
<h1>I am not logged in</h1>
|
||||
<h1>I am not logged in</h1>
|
||||
{:else}
|
||||
<h1>Welcome {$page.data.session.user.email}</h1>
|
||||
<p>I am logged in!</p>
|
||||
<h1>Welcome {$page.data.session.user.email}</h1>
|
||||
<p>I am logged in!</p>
|
||||
{/if}
|
||||
```
|
||||
|
||||
@@ -214,23 +219,23 @@ For [row level security](https://supabase.com/docs/learn/auth-deep-dive/auth-row
|
||||
|
||||
```html
|
||||
<script>
|
||||
import { supabaseClient } from '$lib/db';
|
||||
import { page } from '$app/stores';
|
||||
import { supabaseClient } from '$lib/db'
|
||||
import { page } from '$app/stores'
|
||||
|
||||
let loadedData = [];
|
||||
let loadedData = []
|
||||
async function loadData() {
|
||||
const { data } = await supabaseClient.from('test').select('*').limit(20);
|
||||
loadedData = data;
|
||||
const { data } = await supabaseClient.from('test').select('*').limit(20)
|
||||
loadedData = data
|
||||
}
|
||||
|
||||
$: if ($page.data.session.user) {
|
||||
loadData();
|
||||
loadData()
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if $page.data.session.user}
|
||||
<p>client-side data fetching with RLS</p>
|
||||
<pre>{JSON.stringify(loadedData, null, 2)}</pre>
|
||||
<p>client-side data fetching with RLS</p>
|
||||
<pre>{JSON.stringify(loadedData, null, 2)}</pre>
|
||||
{/if}
|
||||
```
|
||||
|
||||
@@ -239,8 +244,8 @@ For [row level security](https://supabase.com/docs/learn/auth-deep-dive/auth-row
|
||||
```html title="src/routes/profile/+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>
|
||||
@@ -281,14 +286,14 @@ Always use the instance returned by `getSupabaseClient()` directly!
|
||||
|
||||
```ts
|
||||
// Bad
|
||||
const supabaseClient = getSupabaseClient();
|
||||
const supabaseClient = getSupabaseClient()
|
||||
|
||||
await supabaseClient.from('table1').select();
|
||||
await supabaseClient.from('table2').select();
|
||||
await supabaseClient.from('table1').select()
|
||||
await supabaseClient.from('table2').select()
|
||||
|
||||
// Good
|
||||
await getSupabaseClient().from('table1').select();
|
||||
await getSupabaseClient().from('table2').select();
|
||||
await getSupabaseClient().from('table1').select()
|
||||
await getSupabaseClient().from('table2').select()
|
||||
```
|
||||
|
||||
## Protecting API routes
|
||||
@@ -324,34 +329,34 @@ If you visit `/api/protected-route` without a valid session cookie, you will get
|
||||
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';
|
||||
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' });
|
||||
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 formData = await request.formData()
|
||||
const content = formData.get('content')
|
||||
|
||||
const { error: createPostError, data: newPost } = await getSupabaseClient()
|
||||
.from('posts')
|
||||
.insert({ content });
|
||||
.insert({ content })
|
||||
|
||||
if (createPostError) {
|
||||
return invalid(500, {
|
||||
supabaseErrorMessage: createPostError.message
|
||||
});
|
||||
supabaseErrorMessage: createPostError.message,
|
||||
})
|
||||
}
|
||||
return {
|
||||
newPost
|
||||
};
|
||||
})
|
||||
};
|
||||
newPost,
|
||||
}
|
||||
}),
|
||||
}
|
||||
```
|
||||
|
||||
If you try to submit a form with the action `?/createPost` without a valid session cookie, you will get a 403 error response.
|
||||
@@ -361,62 +366,62 @@ If you try to submit a form with the action `?/createPost` without a valid sessi
|
||||
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';
|
||||
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 formData = await request.formData()
|
||||
|
||||
const email = formData.get('email') as string;
|
||||
const password = formData.get('password') as string;
|
||||
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`
|
||||
redirectTo: `${url.origin}/logging-in`,
|
||||
}
|
||||
);
|
||||
)
|
||||
|
||||
if (error || !data) {
|
||||
if (error?.status === 400) {
|
||||
return invalid(400, {
|
||||
error: 'Invalid credentials',
|
||||
values: {
|
||||
email
|
||||
}
|
||||
});
|
||||
email,
|
||||
},
|
||||
})
|
||||
}
|
||||
return invalid(500, {
|
||||
error: 'Server error. Try again later.',
|
||||
values: {
|
||||
email
|
||||
}
|
||||
});
|
||||
email,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
saveSession(cookies, data);
|
||||
throw redirect(303, '/dashboard');
|
||||
}
|
||||
};
|
||||
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';
|
||||
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, '/');
|
||||
}
|
||||
};
|
||||
deleteSession(cookies)
|
||||
throw redirect(303, '/')
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## Custom session namespace
|
||||
@@ -426,10 +431,10 @@ If you want to use something other than `locals.session` and `$page.data.session
|
||||
```ts title="src/app.d.ts"
|
||||
declare namespace App {
|
||||
interface Locals {
|
||||
mySupabaseSession: import('@supabase/auth-helpers-sveltekit').SupabaseSession;
|
||||
mySupabaseSession: import('@supabase/auth-helpers-sveltekit').SupabaseSession
|
||||
}
|
||||
interface PageData {
|
||||
mySupabaseSession: import('@supabase/auth-helpers-sveltekit').SupabaseSession;
|
||||
mySupabaseSession: import('@supabase/auth-helpers-sveltekit').SupabaseSession
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -438,20 +443,20 @@ declare namespace App {
|
||||
setupSupabaseServer({
|
||||
supabaseClient,
|
||||
cookieOptions: {
|
||||
secure: !dev
|
||||
secure: !dev,
|
||||
},
|
||||
// --- change location within locals ---
|
||||
getSessionFromLocals: (locals) => locals.mySupabaseSession,
|
||||
setSessionToLocals: (locals, session) => (locals.mySupabaseSession = session)
|
||||
});
|
||||
setSessionToLocals: (locals, session) => (locals.mySupabaseSession = session),
|
||||
})
|
||||
```
|
||||
|
||||
```ts title="src/lib/db.ts"
|
||||
setupSupabaseClient({
|
||||
supabaseClient,
|
||||
// --- change location within pageData ---
|
||||
getSessionFromPageData: (data) => data.mySupabaseSession
|
||||
});
|
||||
getSessionFromPageData: (data) => data.mySupabaseSession,
|
||||
})
|
||||
```
|
||||
|
||||
## Migrate from 0.6.11 and below to 0.7.0 {#migration}
|
||||
@@ -489,25 +494,29 @@ export { supabaseClient };
|
||||
<TabItem value="latest">
|
||||
|
||||
```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';
|
||||
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, {
|
||||
persistSession: false,
|
||||
autoRefreshToken: false
|
||||
});
|
||||
export const supabaseClient = createClient(
|
||||
env.PUBLIC_SUPABASE_URL,
|
||||
env.PUBLIC_SUPABASE_ANON_KEY,
|
||||
{
|
||||
persistSession: false,
|
||||
autoRefreshToken: false,
|
||||
}
|
||||
)
|
||||
|
||||
setupSupabaseHelpers({
|
||||
supabaseClient,
|
||||
cookieOptions: {
|
||||
secure: !dev
|
||||
}
|
||||
});
|
||||
secure: !dev,
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
@@ -526,9 +535,9 @@ setupSupabaseHelpers({
|
||||
|
||||
```html title="src/routes/__layout.svelte"
|
||||
<script>
|
||||
import { session } from '$app/stores';
|
||||
import { supabaseClient } from '$lib/db';
|
||||
import { SupaAuthHelper } from '@supabase/auth-helpers-svelte';
|
||||
import { session } from '$app/stores'
|
||||
import { supabaseClient } from '$lib/db'
|
||||
import { SupaAuthHelper } from '@supabase/auth-helpers-svelte'
|
||||
</script>
|
||||
|
||||
<SupaAuthHelper {supabaseClient} {session}>
|
||||
@@ -545,16 +554,16 @@ The `@supabase/auth-helpers-svelte` library is no longer required as the `@supab
|
||||
```html title="src/routes/+layout.svelte"
|
||||
<script lang="ts">
|
||||
// make sure the supabase instance is initialized on the client
|
||||
import '$lib/db';
|
||||
import { startSupabaseSessionSync } from '@supabase/auth-helpers-sveltekit';
|
||||
import { page } from '$app/stores';
|
||||
import { invalidateAll } from '$app/navigation';
|
||||
import '$lib/db'
|
||||
import { startSupabaseSessionSync } from '@supabase/auth-helpers-sveltekit'
|
||||
import { page } from '$app/stores'
|
||||
import { invalidateAll } from '$app/navigation'
|
||||
|
||||
// this sets up automatic token refreshing
|
||||
startSupabaseSessionSync({
|
||||
page,
|
||||
handleRefresh: () => invalidateAll()
|
||||
});
|
||||
page,
|
||||
handleRefresh: () => invalidateAll(),
|
||||
})
|
||||
</script>
|
||||
|
||||
<slot />
|
||||
@@ -575,18 +584,18 @@ The `@supabase/auth-helpers-svelte` library is no longer required as the `@supab
|
||||
<TabItem value="older">
|
||||
|
||||
```ts title="src/hooks.ts"
|
||||
import { handleAuth } from '@supabase/auth-helpers-sveltekit';
|
||||
import type { GetSession, Handle } from '@sveltejs/kit';
|
||||
import { sequence } from '@sveltejs/kit/hooks';
|
||||
import { handleAuth } from '@supabase/auth-helpers-sveltekit'
|
||||
import type { GetSession, Handle } from '@sveltejs/kit'
|
||||
import { sequence } from '@sveltejs/kit/hooks'
|
||||
|
||||
export const handle: Handle = sequence(...handleAuth());
|
||||
export const handle: Handle = sequence(...handleAuth())
|
||||
|
||||
export const getSession: GetSession = async (event) => {
|
||||
const { user, accessToken, error } = event.locals;
|
||||
return {
|
||||
user,
|
||||
accessToken,
|
||||
error
|
||||
const { user, accessToken, error } = event.locals
|
||||
return {
|
||||
user,
|
||||
accessToken,
|
||||
error,
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -597,23 +606,23 @@ export const getSession: GetSession = async (event) => {
|
||||
|
||||
```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 '$lib/db'
|
||||
import { dev } from '$app/environment'
|
||||
import { auth } from '@supabase/auth-helpers-sveltekit/server'
|
||||
|
||||
export const handle = auth();
|
||||
export const handle = auth()
|
||||
```
|
||||
|
||||
**Optional** *if using 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';
|
||||
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);
|
||||
export const handle = sequence(auth(), yourHandler)
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
@@ -636,16 +645,16 @@ export const handle = sequence(auth(), yourHandler);
|
||||
// for information about these interfaces
|
||||
declare namespace App {
|
||||
interface UserSession {
|
||||
user: import('@supabase/supabase-js').User;
|
||||
accessToken?: string;
|
||||
user: import('@supabase/supabase-js').User
|
||||
accessToken?: string
|
||||
}
|
||||
|
||||
interface Locals extends UserSession {
|
||||
error: import('@supabase/supabase-js').ApiError;
|
||||
error: import('@supabase/supabase-js').ApiError
|
||||
}
|
||||
|
||||
interface Session extends UserSession {}
|
||||
|
||||
|
||||
// interface Platform {}
|
||||
// interface Stuff {}
|
||||
}
|
||||
@@ -663,11 +672,11 @@ declare namespace App {
|
||||
// and what to do when importing types
|
||||
declare namespace App {
|
||||
interface Locals {
|
||||
session: import('@supabase/auth-helpers-sveltekit').SupabaseSession;
|
||||
session: import('@supabase/auth-helpers-sveltekit').SupabaseSession
|
||||
}
|
||||
|
||||
interface PageData {
|
||||
session: import('@supabase/auth-helpers-sveltekit').SupabaseSession;
|
||||
session: import('@supabase/auth-helpers-sveltekit').SupabaseSession
|
||||
}
|
||||
|
||||
// interface Error {}
|
||||
@@ -691,14 +700,14 @@ declare namespace App {
|
||||
|
||||
```html title="src/routes/index.svelte"
|
||||
<script>
|
||||
import { session } from '$app/stores';
|
||||
import { session } from '$app/stores'
|
||||
</script>
|
||||
|
||||
{#if !$session.user}
|
||||
<h1>I am not logged in</h1>
|
||||
<h1>I am not logged in</h1>
|
||||
{:else}
|
||||
<h1>Welcome {$session.user.email}</h1>
|
||||
<p>I am logged in!</p>
|
||||
<h1>Welcome {$session.user.email}</h1>
|
||||
<p>I am logged in!</p>
|
||||
{/if}
|
||||
```
|
||||
|
||||
@@ -708,14 +717,14 @@ declare namespace App {
|
||||
|
||||
```html title="src/routes/+page.svelte"
|
||||
<script>
|
||||
import { page } from '$app/stores';
|
||||
import { page } from '$app/stores'
|
||||
</script>
|
||||
|
||||
{#if !$page.data.session.user}
|
||||
<h1>I am not logged in</h1>
|
||||
<h1>I am not logged in</h1>
|
||||
{:else}
|
||||
<h1>Welcome {$page.data.session.user.email}</h1>
|
||||
<p>I am logged in!</p>
|
||||
<h1>Welcome {$page.data.session.user.email}</h1>
|
||||
<p>I am logged in!</p>
|
||||
{/if}
|
||||
```
|
||||
|
||||
@@ -735,25 +744,30 @@ declare namespace App {
|
||||
|
||||
```html title="src/routes/protected-route.svelte"
|
||||
<script lang="ts" context="module">
|
||||
import { supabaseServerClient, withPageAuth } from '@supabase/auth-helpers-sveltekit';
|
||||
import type { Load } from './__types/protected-page';
|
||||
import {
|
||||
supabaseServerClient,
|
||||
withPageAuth,
|
||||
} from '@supabase/auth-helpers-sveltekit'
|
||||
import type { Load } from './__types/protected-page'
|
||||
|
||||
export const load: Load = async ({ session }) =>
|
||||
withPageAuth(
|
||||
{
|
||||
redirectTo: '/',
|
||||
user: session.user
|
||||
},
|
||||
async () => {
|
||||
const { data } = await supabaseServerClient(session.accessToken).from('test').select('*');
|
||||
return { props: { data, user: session.user } };
|
||||
}
|
||||
);
|
||||
export const load: Load = async ({ session }) =>
|
||||
withPageAuth(
|
||||
{
|
||||
redirectTo: '/',
|
||||
user: session.user,
|
||||
},
|
||||
async () => {
|
||||
const { data } = await supabaseServerClient(session.accessToken)
|
||||
.from('test')
|
||||
.select('*')
|
||||
return { props: { data, user: session.user } }
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
<script>
|
||||
export let data;
|
||||
export let user;
|
||||
export let data
|
||||
export let user
|
||||
</script>
|
||||
|
||||
<div>Protected content for {user.email}</div>
|
||||
@@ -769,13 +783,12 @@ export let user;
|
||||
|
||||
```html title="src/routes/protected-route/+page.svelte"
|
||||
<script lang="ts">
|
||||
import type { PageData } from './$types';
|
||||
import type { PageData } from './$types'
|
||||
|
||||
export let data: PageData;
|
||||
$: ({ tableData, user } = data);
|
||||
export let data: PageData
|
||||
$: ({ tableData, user } = data)
|
||||
</script>
|
||||
|
||||
|
||||
<div>Protected content for {user.email}</div>
|
||||
<p>server-side fetched data with RLS:</p>
|
||||
<pre>{JSON.stringify(tableData, null, 2)}</pre>
|
||||
@@ -784,18 +797,22 @@ export let user;
|
||||
```
|
||||
|
||||
```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';
|
||||
import { withAuth } from '@supabase/auth-helpers-sveltekit'
|
||||
import { redirect } from '@sveltejs/kit'
|
||||
import type { PageLoad } from './$types'
|
||||
|
||||
export const load: PageLoad = withAuth(async ({ session, getSupabaseClient }) => {
|
||||
if (!session.user) {
|
||||
throw redirect(303, '/');
|
||||
export const load: PageLoad = withAuth(
|
||||
async ({ session, getSupabaseClient }) => {
|
||||
if (!session.user) {
|
||||
throw redirect(303, '/')
|
||||
}
|
||||
|
||||
const { data: tableData } = await getSupabaseClient()
|
||||
.from('test')
|
||||
.select('*')
|
||||
return { tableData, user: session.user }
|
||||
}
|
||||
|
||||
const { data: tableData } = await getSupabaseClient().from('test').select('*');
|
||||
return { tableData, user: session.user };
|
||||
});
|
||||
)
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
@@ -813,28 +830,33 @@ export const load: PageLoad = withAuth(async ({ session, getSupabaseClient }) =>
|
||||
<TabItem value="older">
|
||||
|
||||
```ts title="src/routes/api/protected-route.ts"
|
||||
import { supabaseServerClient, withApiAuth } from "@supabase/auth-helpers-sveltekit";
|
||||
import type { RequestHandler } from "./__types/protected-route";
|
||||
import {
|
||||
supabaseServerClient,
|
||||
withApiAuth,
|
||||
} from '@supabase/auth-helpers-sveltekit'
|
||||
import type { RequestHandler } from './__types/protected-route'
|
||||
|
||||
interface TestTable {
|
||||
id: string;
|
||||
created_at: string;
|
||||
id: string
|
||||
created_at: string
|
||||
}
|
||||
|
||||
interface GetOutput {
|
||||
data: TestTable[];
|
||||
data: TestTable[]
|
||||
}
|
||||
|
||||
export const GET: RequestHandler<GetOutput> = async ({ locals, request }) =>
|
||||
withApiAuth({ user: locals.user }, async () => {
|
||||
// Run queries with RLS on the server
|
||||
const { data } = await supabaseServerClient(request).from("test").select("*");
|
||||
const { data } = await supabaseServerClient(request)
|
||||
.from('test')
|
||||
.select('*')
|
||||
|
||||
return {
|
||||
status: 200,
|
||||
body: { data }
|
||||
};
|
||||
});
|
||||
body: { data },
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
|
||||
@@ -110,7 +110,7 @@ http://localhost:54321/auth/v1/ # Auth (GoTrue)
|
||||
:::note
|
||||
To access the database from an edge function in your local Supabase setup, replace `localhost` with `host.docker.internal`.
|
||||
:::
|
||||
|
||||
|
||||
## Database migrations
|
||||
|
||||
Database changes are managed through "migrations." Database migrations are a common way of tracking changes to your database over time.
|
||||
|
||||
@@ -112,7 +112,12 @@ This is the most granular option. Connections are returned to the pool after eve
|
||||
3. Find your Connection Info and Connection String. Connection pooling is on port `6543`.
|
||||
|
||||
<video width="99%" muted playsInline controls="true">
|
||||
<source src="/docs/videos/connection-pool-config.mp4" type="video/mp4" muted playsInline />
|
||||
<source
|
||||
src="/docs/videos/connection-pool-config.mp4"
|
||||
type="video/mp4"
|
||||
muted
|
||||
playsInline
|
||||
/>
|
||||
</video>
|
||||
|
||||
## Connecting with SSL
|
||||
|
||||
@@ -278,10 +278,10 @@ final res = await supabase
|
||||
|
||||
### Database Functions vs Edge Functions
|
||||
|
||||
For data-intensive operations we recommend using [Database Functions](../../guides/database/functions), which are executed within your database
|
||||
For data-intensive operations, use Database Functions, which are executed within your database
|
||||
and can be called remotely using the [REST and GraphQL API](../api).
|
||||
|
||||
For use-cases which require low-latency we recommend [Edge Functions](../../guides/functions), which are globally-distributed and can be written in Typescript.
|
||||
For use-cases which require low-latency, use [Edge Functions](../../guides/functions), which are globally-distributed and can be written in Typescript.
|
||||
|
||||
### Security `definer` vs `invoker`
|
||||
|
||||
@@ -302,6 +302,18 @@ $$;
|
||||
It is best practice to use `security invoker` (which is also the default). If you ever use `security definer`, you _must_ set the `search_path`.
|
||||
This limits the potential damage if you allow access to schemas which the user executing the function should not have.
|
||||
|
||||
### Function privileges
|
||||
|
||||
By default, database functions can be executed by any role. You can restrict this by altering the default privileges and then choosing which roles can execute functions.
|
||||
|
||||
```sql
|
||||
ALTER DEFAULT PRIVILEGES REVOKE EXECUTE ON FUNCTIONS FROM PUBLIC;
|
||||
|
||||
-- Choose which roles can execute functions
|
||||
GRANT EXECUTE ON FUNCTION hello_world TO authenticated;
|
||||
GRANT EXECUTE ON FUNCTION hello_world TO service_role;
|
||||
```
|
||||
|
||||
## Resources
|
||||
|
||||
- Official Client libraries: [JavaScript](../../reference/javascript/rpc) and [Dart](../../reference/dart/rpc)
|
||||
|
||||
@@ -16,11 +16,13 @@ Migrating projects can be achieved using standard PostgreSQL tooling. This is pa
|
||||
## Migrate the database
|
||||
|
||||
In your old project:
|
||||
|
||||
1. Run `ALTER ROLE postgres SUPERUSER` in the [SQL editor](https://app.supabase.com/project/_/sql).
|
||||
1. Run `pg_dump --clean --if-exists --quote-all-identifiers -h $OLD_DB_URL -U postgres > dump.sql` from your terminal.
|
||||
1. Run `ALTER ROLE postgres NOSUPERUSER` in the [SQL editor](https://app.supabase.com/project/_/sql).
|
||||
|
||||
In your new project:
|
||||
|
||||
1. Run `ALTER ROLE postgres SUPERUSER` in the [SQL editor](https://app.supabase.com/project/_/sql).
|
||||
1. Run `psql -h $NEW_DB_URL -U postgres -f dump.sql` from your terminal.
|
||||
1. Run `TRUNCATE storage.objects` in the [SQL editor](https://app.supabase.com/project/_/sql).
|
||||
@@ -94,4 +96,3 @@ const NEW_PROJECT_SERVICE_KEY = 'new-project-service-key-yyy'
|
||||
}
|
||||
})()
|
||||
```
|
||||
|
||||
@@ -241,15 +241,15 @@ You can find a list of useful [Edge Function Examples](https://github.com/supaba
|
||||
<div class="container" style={{ padding: 0 }}>
|
||||
<div class="row is-multiline">
|
||||
{examples.map((x) => (
|
||||
<div class="col col--6">
|
||||
<ButtonCard
|
||||
class="card"
|
||||
to={x.href}
|
||||
title={x.name}
|
||||
description={x.description}
|
||||
style={{ height: '100%' }}
|
||||
/>
|
||||
</div>
|
||||
<div class="col col--6">
|
||||
<ButtonCard
|
||||
class="card"
|
||||
to={x.href}
|
||||
title={x.name}
|
||||
description={x.description}
|
||||
style={{ height: '100%' }}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -29,17 +29,22 @@ Organizations are a way to group your projects. Each organization can be configu
|
||||
You can invite your team members into your organizations to collaborate on projects.
|
||||
|
||||
<video width="99%" muted playsInline controls="true">
|
||||
<source src="/docs/videos/invite-team.mp4" type="video/mp4" muted playsInline />
|
||||
<source
|
||||
src="/docs/videos/invite-team.mp4"
|
||||
type="video/mp4"
|
||||
muted
|
||||
playsInline
|
||||
/>
|
||||
</video>
|
||||
|
||||
You can also assign roles to your team members with different access levels. The table below shows the corresponding permissions for each available role in the Dashboard.
|
||||
|
||||
| Permissions | Owner | Administrator | Developer |
|
||||
| ------------------------ | ----- | ------------- | --------- |
|
||||
| **Organization** |
|
||||
| **Organization** |
|
||||
| Change organization name | ✅ | | |
|
||||
| Delete organization | ✅ | | |
|
||||
| **Members** |
|
||||
| **Members** |
|
||||
| Add an Owner | ✅ | | |
|
||||
| Remove an Owner | ✅ | | |
|
||||
| Add an Adminstrator | ✅ | ✅ | |
|
||||
@@ -48,7 +53,7 @@ You can also assign roles to your team members with different access levels. The
|
||||
| Remove a Developer | ✅ | ✅ | |
|
||||
| Revoke an invite | ✅ | ✅ | |
|
||||
| Resend an invite | ✅ | ✅ | |
|
||||
| **Billing** |
|
||||
| **Billing** |
|
||||
| Read invoices | ✅ | ✅ | ✅ |
|
||||
| Read billing email | ✅ | ✅ | ✅ |
|
||||
| Change billing email | ✅ | | |
|
||||
@@ -60,7 +65,7 @@ You can also assign roles to your team members with different access levels. The
|
||||
| Update tax codes | ✅ | ✅ | |
|
||||
| Read payment methods | ✅ | ✅ | ✅ |
|
||||
| Update payment methods | ✅ | ✅ | |
|
||||
| **Projects** |
|
||||
| **Projects** |
|
||||
| Create a project | ✅ | ✅ | |
|
||||
| Delete a project | ✅ | ✅ | |
|
||||
| Update a project | ✅ | ✅ | |
|
||||
|
||||
@@ -346,7 +346,7 @@ export default function HomePage({ user, isEnrolled }: Props) {
|
||||
<main>
|
||||
<section>
|
||||
<div> Signed in as: {user?.email}</div>
|
||||
<button
|
||||
<button
|
||||
onClick={async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
@@ -540,6 +540,7 @@ export default withApiAuth(async function mfa(
|
||||
res.send({ mfaUrl });
|
||||
});
|
||||
```
|
||||
|
||||
Because the user should be authenticated with Supabase to set up or manage MFA, we can use Supabase's `withApiAuth` wrapper to protect this route.
|
||||
|
||||
The `redirectToSettings` param specifies whether the user should be redirected to the MFA page settings panel after a challenge, rather than redirecting them immediately back to the application.
|
||||
|
||||
@@ -60,4 +60,4 @@ That's it.
|
||||
|
||||
## All done!
|
||||
|
||||
You can read more about PolyScale [here](https://www.polyscale.ai/) or check out our [documentation](https://docs.polyscale.ai/).
|
||||
You can read more about PolyScale [here](https://www.polyscale.ai/) or check out our [documentation](https://docs.polyscale.ai/).
|
||||
@@ -16,19 +16,23 @@ Supabase provides several [tools](https://github.com/supabase-community/firebase
|
||||
## Set up the migration tool {#set-up-migration-tool}
|
||||
|
||||
1. Clone the [firebase-to-supabase](https://github.com/supabase-community/firebase-to-supabase) repository:
|
||||
```
|
||||
git clone https://github.com/supabase-community/firebase-to-supabase.git
|
||||
```
|
||||
|
||||
```
|
||||
git clone https://github.com/supabase-community/firebase-to-supabase.git
|
||||
```
|
||||
|
||||
1. In the `/auth` directory, create a file named `supabase-service.json` with the following contents:
|
||||
```json
|
||||
{
|
||||
"host": "database.server.com",
|
||||
"password": "secretpassword",
|
||||
"user": "postgres",
|
||||
"database": "postgres",
|
||||
"port": 5432
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"host": "database.server.com",
|
||||
"password": "secretpassword",
|
||||
"user": "postgres",
|
||||
"database": "postgres",
|
||||
"port": 5432
|
||||
}
|
||||
```
|
||||
|
||||
1. Go to the [Database settings](https://app.supabase.com/project/_/settings/database) for your project in the Supabase Dashboard.
|
||||
1. Under **Connection Info**, copy the Host string and replace the entry in your `supabase-service.json` file.
|
||||
1. Enter the password you used when you created your Supabase project in the `password` entry in the `supabase-service.json` file.
|
||||
|
||||
@@ -6,15 +6,18 @@ sidebar_label: Firebase Storage
|
||||
---
|
||||
|
||||
Supabase provides several [tools](https://github.com/supabase-community/firebase-to-supabase/tree/main/storage) to convert storage files from Firebase Storage to Supabase Storage. Conversion is a two-step process:
|
||||
|
||||
1. Files are downloaded from a Firebase storage bucket to a local filesystem.
|
||||
2. Files are uploaded from the local filesystem to a Supabase storage bucket.
|
||||
|
||||
## Set up the migration tool {#set-up-migration-tool}
|
||||
|
||||
1. Clone the [firebase-to-supabase](https://github.com/supabase-community/firebase-to-supabase) repository:
|
||||
```
|
||||
git clone https://github.com/supabase-community/firebase-to-supabase.git
|
||||
```
|
||||
|
||||
```
|
||||
git clone https://github.com/supabase-community/firebase-to-supabase.git
|
||||
```
|
||||
|
||||
1. In the `/storage` directory, rename [supabase-keys-sample.js](https://github.com/supabase-community/firebase-to-supabase/blob/main/storage/supabase-keys-sample.js) to `supabase-keys.js`.
|
||||
1. Go to your Supabase project's [API settings](https://app.supabase.com/project/_/settings/api) in the Dashboard.
|
||||
1. Copy the **Project URL** and update the `SUPABASE_URL` value in `supabase-keys.js`.
|
||||
@@ -45,6 +48,7 @@ To process in batches using multiple command-line executions, you must use the s
|
||||
### Upload files to Supabase Storage bucket {#upload-to-supabase-storage-bucket}
|
||||
|
||||
`node upload.js <prefix> <folder> <bucket>`
|
||||
|
||||
- `<prefix>`: The prefix of the files to download. To process all files, use an empty prefix: "".
|
||||
- `<folder>`: Name of subfolder of files to upload. The selected folder is read as a subfolder of the current folder (e.g., `./downloads/`). The default is `downloads`.
|
||||
- `<bucket>`: Name of the bucket to upload to.
|
||||
|
||||
@@ -7,24 +7,28 @@ sidebar_label: Firestore Data
|
||||
|
||||
Supabase provides several [tools](https://github.com/supabase-community/firebase-to-supabase/tree/main/firestore) to convert data from a Firebase Firestore database to a Supabase PostgreSQL database. The process copies the entire contents of a single Firestore `collection` to a single PostgreSQL `table`.
|
||||
|
||||
The Firestore `collection` is "flattened" and converted to a table with basic columns of one of the following types: `text`, `numeric`, `boolean`, or `jsonb`. If your structure is more complex, you can write a program to split the newly-created `json` file into multiple, related tables before you import your `json` file(s) to Supabase.
|
||||
The Firestore `collection` is "flattened" and converted to a table with basic columns of one of the following types: `text`, `numeric`, `boolean`, or `jsonb`. If your structure is more complex, you can write a program to split the newly-created `json` file into multiple, related tables before you import your `json` file(s) to Supabase.
|
||||
|
||||
## Set up the migration tool {#set-up-migration-tool}
|
||||
|
||||
1. Clone the [firebase-to-supabase](https://github.com/supabase-community/firebase-to-supabase) repository:
|
||||
```
|
||||
git clone https://github.com/supabase-community/firebase-to-supabase.git
|
||||
```
|
||||
|
||||
```
|
||||
git clone https://github.com/supabase-community/firebase-to-supabase.git
|
||||
```
|
||||
|
||||
1. In the `/firestore` directory, create a file named `supabase-service.json` with the following contents:
|
||||
```json
|
||||
{
|
||||
"host": "database.server.com",
|
||||
"password": "secretpassword",
|
||||
"user": "postgres",
|
||||
"database": "postgres",
|
||||
"port": 5432
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"host": "database.server.com",
|
||||
"password": "secretpassword",
|
||||
"user": "postgres",
|
||||
"database": "postgres",
|
||||
"port": 5432
|
||||
}
|
||||
```
|
||||
|
||||
1. Go to the [Database settings](https://app.supabase.com/project/_/settings/database) for your project in the Supabase Dashboard.
|
||||
1. Under **Connection Info**, copy the Host string and replace the entry in your `supabase-service.json` file.
|
||||
1. Enter the password you used when you created your Supabase project in the `password` entry in the `supabase-service.json` file.
|
||||
@@ -40,56 +44,52 @@ The Firestore `collection` is "flattened" and converted to a table with basic co
|
||||
## Command line options
|
||||
|
||||
### List all Firestore collections
|
||||
|
||||
`node collections.js`
|
||||
|
||||
### Dump Firestore collection to JSON file
|
||||
|
||||
`node firestore2json.js <collectionName> [<batchSize>] [<limit>]`
|
||||
|
||||
* `batchSize` (optional) defaults to 1000
|
||||
* output filename is `<collectionName>.json`
|
||||
* `limit` (optional) defaults to 0 (no limit)
|
||||
- `batchSize` (optional) defaults to 1000
|
||||
- output filename is `<collectionName>.json`
|
||||
- `limit` (optional) defaults to 0 (no limit)
|
||||
|
||||
#### Customize the JSON file with hooks
|
||||
|
||||
You can customize the way your JSON file is written using a [custom hook](#custom-hooks). A common use for this is to "flatten" the JSON file, or to split nested data into separate, related database tables. For example, you could take a Firestore document that looks like this:
|
||||
|
||||
```json title="Firestore document"
|
||||
[{ "user": "mark",
|
||||
"score": 100,
|
||||
"items": ["hammer","nail","glue"]
|
||||
}]
|
||||
[{ "user": "mark", "score": 100, "items": ["hammer", "nail", "glue"] }]
|
||||
```
|
||||
|
||||
And split it into two files (one table for users and one table for items):
|
||||
|
||||
```json title="Users table"
|
||||
[{ "user": "mark",
|
||||
"score": 100
|
||||
}]
|
||||
[{ "user": "mark", "score": 100 }]
|
||||
```
|
||||
|
||||
```json title="Items table"
|
||||
[{ "user": "mark",
|
||||
"item": "hammer"},
|
||||
{"user": "mark",
|
||||
"item": "nail"},
|
||||
{"user": "mark",
|
||||
"item": "glue"}]
|
||||
[
|
||||
{ "user": "mark", "item": "hammer" },
|
||||
{ "user": "mark", "item": "nail" },
|
||||
{ "user": "mark", "item": "glue" }
|
||||
]
|
||||
```
|
||||
|
||||
### Import JSON file to Supabase (PostgreSQL) {#import-to-supabase}
|
||||
|
||||
`node json2supabase.js <path_to_json_file> [<primary_key_strategy>] [<primary_key_name>]`
|
||||
|
||||
* `<path_to_json_file>` The full path of the file you created in the previous step (`Dump Firestore collection to JSON file
|
||||
`), such as `./my_collection.json`
|
||||
* `[<primary_key_strategy>]` (optional) Is one of:
|
||||
* `none` (default) No primary key is added to the table.
|
||||
* `smallserial` Creates a key using `(id SMALLSERIAL PRIMARY KEY)` (autoincrementing 2-byte integer).
|
||||
* `serial` Creates a key using `(id SERIAL PRIMARY KEY)` (autoincrementing 4-byte integer).
|
||||
* `bigserial` Creates a key using `(id BIGSERIAL PRIMARY KEY)` (autoincrementing 8-byte integer).
|
||||
* `uuid` Creates a key using `(id UUID PRIMARY KEY DEFAULT uuid_generate_v4())` (randomly generated UUID).
|
||||
* `firestore_id` Creates a key using `(id TEXT PRIMARY KEY)` (uses existing `firestore_id` random text as key).
|
||||
* `[<primary_key_name>]` (optional) Name of primary key. Defaults to "id".
|
||||
- `<path_to_json_file>` The full path of the file you created in the previous step (`Dump Firestore collection to JSON file `), such as `./my_collection.json`
|
||||
- `[<primary_key_strategy>]` (optional) Is one of:
|
||||
- `none` (default) No primary key is added to the table.
|
||||
- `smallserial` Creates a key using `(id SMALLSERIAL PRIMARY KEY)` (autoincrementing 2-byte integer).
|
||||
- `serial` Creates a key using `(id SERIAL PRIMARY KEY)` (autoincrementing 4-byte integer).
|
||||
- `bigserial` Creates a key using `(id BIGSERIAL PRIMARY KEY)` (autoincrementing 8-byte integer).
|
||||
- `uuid` Creates a key using `(id UUID PRIMARY KEY DEFAULT uuid_generate_v4())` (randomly generated UUID).
|
||||
- `firestore_id` Creates a key using `(id TEXT PRIMARY KEY)` (uses existing `firestore_id` random text as key).
|
||||
- `[<primary_key_name>]` (optional) Name of primary key. Defaults to "id".
|
||||
|
||||
## Custom hooks
|
||||
|
||||
@@ -108,10 +108,11 @@ If your Firestore collection is called `users`, create a file called `users.js`
|
||||
#### Construct your .js file
|
||||
|
||||
The basic format of a hook file looks like this:
|
||||
|
||||
```js
|
||||
module.exports = (collectionName, doc, recordCounters, writeRecord) => {
|
||||
// modify the doc here
|
||||
return doc;
|
||||
return doc
|
||||
}
|
||||
```
|
||||
|
||||
@@ -121,9 +122,9 @@ module.exports = (collectionName, doc, recordCounters, writeRecord) => {
|
||||
- `doc`: The current document (JSON object) being processed.
|
||||
- `recordCounters`: An internal object that keeps track of how many records have been processed in each collection.
|
||||
- `writeRecord`: This function automatically handles the process of writing data to other JSON files (useful for "flatting" your document into separate JSON files to be written to separate database tables). `writeRecord` takes the following parameters:
|
||||
- `name`: Name of the JSON file to write to.
|
||||
- `doc`: The document to write to the file.
|
||||
- `recordCounters`: The same `recordCounters` object that was passed to this hook (just passes it on).
|
||||
- `name`: Name of the JSON file to write to.
|
||||
- `doc`: The document to write to the file.
|
||||
- `recordCounters`: The same `recordCounters` object that was passed to this hook (just passes it on).
|
||||
|
||||
### Examples
|
||||
|
||||
@@ -131,8 +132,8 @@ module.exports = (collectionName, doc, recordCounters, writeRecord) => {
|
||||
|
||||
```js
|
||||
module.exports = (collectionName, doc, recordCounters, writeRecord) => {
|
||||
doc.unique_key = (recordCounter[collectionName] + 1);
|
||||
return doc;
|
||||
doc.unique_key = recordCounter[collectionName] + 1
|
||||
return doc
|
||||
}
|
||||
```
|
||||
|
||||
@@ -140,8 +141,8 @@ module.exports = (collectionName, doc, recordCounters, writeRecord) => {
|
||||
|
||||
```js
|
||||
module.exports = (collectionName, doc, recordCounters, writeRecord) => {
|
||||
doc.dump_time = new Date().toISOString();
|
||||
return doc;
|
||||
doc.dump_time = new Date().toISOString()
|
||||
return doc
|
||||
}
|
||||
```
|
||||
|
||||
@@ -150,56 +151,56 @@ module.exports = (collectionName, doc, recordCounters, writeRecord) => {
|
||||
Flatten the `users` collection into separate files:
|
||||
|
||||
```json
|
||||
[{"uid": "abc123",
|
||||
"name": "mark",
|
||||
"score": 100,
|
||||
"weapons": ["toothpick","needle","rock"]},
|
||||
{"uid": "xyz789",
|
||||
"name": "chuck",
|
||||
"score": 9999999,
|
||||
"weapons": ["hand","foot","head"]}]
|
||||
[
|
||||
{
|
||||
"uid": "abc123",
|
||||
"name": "mark",
|
||||
"score": 100,
|
||||
"weapons": ["toothpick", "needle", "rock"]
|
||||
},
|
||||
{
|
||||
"uid": "xyz789",
|
||||
"name": "chuck",
|
||||
"score": 9999999,
|
||||
"weapons": ["hand", "foot", "head"]
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
The `users.js` hook file:
|
||||
|
||||
```js
|
||||
module.exports = (collectionName, doc, recordCounters, writeRecord) => {
|
||||
for (let i=0; i < doc.weapons.length; i++) {
|
||||
for (let i = 0; i < doc.weapons.length; i++) {
|
||||
const weapon = {
|
||||
"uid": doc.uid,
|
||||
"weapon": doc.weapons[i]
|
||||
uid: doc.uid,
|
||||
weapon: doc.weapons[i],
|
||||
}
|
||||
writeRecord("weapons", weapon, recordCounters);
|
||||
writeRecord('weapons', weapon, recordCounters)
|
||||
}
|
||||
delete doc.weapons; // moved to separate file
|
||||
return doc;
|
||||
delete doc.weapons // moved to separate file
|
||||
return doc
|
||||
}
|
||||
```
|
||||
|
||||
The result is two separate JSON files:
|
||||
|
||||
```json title="users.json"
|
||||
[{"uid": "abc123",
|
||||
"name": "mark",
|
||||
"score": 100},
|
||||
{"uid": "xyz789",
|
||||
"name": "chuck",
|
||||
"score": 9999999}]
|
||||
[
|
||||
{ "uid": "abc123", "name": "mark", "score": 100 },
|
||||
{ "uid": "xyz789", "name": "chuck", "score": 9999999 }
|
||||
]
|
||||
```
|
||||
|
||||
```json title="weapons.json"
|
||||
[{"uid": "abc123",
|
||||
"weapon": "toothpick"},
|
||||
{"uid": "abc123",
|
||||
"weapon": "needle"},
|
||||
{"uid": "abc123",
|
||||
"weapon": "rock"},
|
||||
{"uid": "xyz789",
|
||||
"weapon": "hand"},
|
||||
{"uid": "xyz789",
|
||||
"weapon": "foot"},
|
||||
{"uid": "xyz789",
|
||||
"weapon": "head"}]
|
||||
[
|
||||
{ "uid": "abc123", "weapon": "toothpick" },
|
||||
{ "uid": "abc123", "weapon": "needle" },
|
||||
{ "uid": "abc123", "weapon": "rock" },
|
||||
{ "uid": "xyz789", "weapon": "hand" },
|
||||
{ "uid": "xyz789", "weapon": "foot" },
|
||||
{ "uid": "xyz789", "weapon": "head" }
|
||||
]
|
||||
```
|
||||
|
||||
## Resources
|
||||
|
||||
@@ -5,7 +5,7 @@ description: 'Migrate your Heroku Postgres database to Supabase.'
|
||||
sidebar_label: Heroku
|
||||
---
|
||||
|
||||
Supabase is one of the best [free alternatives to Heroku Postgres](https://supabase.com/alternatives/supabase-vs-heroku-postgres). This guide shows how to migrate your Heroku Postgres database to Supabase. This migration requires the [pg_dump](https://www.postgresql.org/docs/current/app-pgdump.html) and [psql](https://www.postgresql.org/docs/current/app-psql.html) CLI tools, which are installed automatically as part of the complete PostgreSQL installation package.
|
||||
Supabase is one of the best [free alternatives to Heroku Postgres](https://supabase.com/alternatives/supabase-vs-heroku-postgres). This guide shows how to migrate your Heroku Postgres database to Supabase. This migration requires the [pg_dump](https://www.postgresql.org/docs/current/app-pgdump.html) and [psql](https://www.postgresql.org/docs/current/app-psql.html) CLI tools, which are installed automatically as part of the complete PostgreSQL installation package.
|
||||
|
||||
Alternatively, use the [Heroku to Supabase migration tool](https://migrate.supabase.com/) to migrate in just a few clicks.
|
||||
|
||||
@@ -25,7 +25,7 @@ Alternatively, use the [Heroku to Supabase migration tool](https://migrate.supab
|
||||
1. Log in to your [Heroku account](https://heroku.com) and select the project you want to migrate.
|
||||
1. Click **Resources** in the menu and select your **Heroku Postgres** database.
|
||||
1. Click **Settings** in the menu.
|
||||
1. Click **View Credentials** and save the following information:
|
||||
1. Click **View Credentials** and save the following information:
|
||||
- Host (`$HEROKU_HOST`)
|
||||
- Database (`$HEROKU_DATABASE`)
|
||||
- User (`$HEROKU_USER`)
|
||||
@@ -52,7 +52,7 @@ pg_dump --clean --if-exists --quote-all-identifiers \
|
||||
Use `psql` to import the Heroku database file to your Supabase project.
|
||||
|
||||
```
|
||||
psql -h $SUPABASE_HOST -U postgres -f heroku_dump.sql
|
||||
psql -h $SUPABASE_HOST -U postgres -f heroku_dump.sql
|
||||
```
|
||||
|
||||
## (Optional) Post processing {#post-processing}
|
||||
@@ -67,7 +67,7 @@ select graphql.rebuild_schema();
|
||||
|
||||
- To only migrate a single database schema, add the `--schema=PATTERN` parameter to your `pg_dump` command.
|
||||
- To exclude a schema: `--exclude-schema=PATTERN`.
|
||||
- To only migrate a single table: `--table=PATTERN`.
|
||||
- To only migrate a single table: `--table=PATTERN`.
|
||||
- To exclude a table: `--exclude-table=PATTERN`.
|
||||
|
||||
Run `pg_dump --help` for a full list of options.
|
||||
@@ -4,7 +4,6 @@ title: Disk space usage
|
||||
description: Learn how database disk space usage is reported.
|
||||
---
|
||||
|
||||
|
||||
Database disk space usage refers to the _monthly average disk usage_, as reported by Postgres. This metric is reported in your project's [billing page](https://app.supabase.com/project/_/settings/billing) and is updated daily.
|
||||
|
||||
For an instantaneous live view of the DB disk space being used by your project, you can execute in Postgres:
|
||||
|
||||
@@ -6,8 +6,8 @@ description: Getting started with Supabase Platform Log Browser
|
||||
|
||||
import Tabs from '@theme/Tabs'
|
||||
import TabItem from '@theme/TabItem'
|
||||
import ThemedImage from '@theme/ThemedImage';
|
||||
import useBaseUrl from '@docusaurus/useBaseUrl';
|
||||
import ThemedImage from '@theme/ThemedImage'
|
||||
import useBaseUrl from '@docusaurus/useBaseUrl'
|
||||
|
||||
The Supabase Platform includes a Logs Explorer that allows log tracing and debugging. Log retention is based on your [project's pricing plan](https://supabase.com/pricing).
|
||||
|
||||
@@ -124,6 +124,7 @@ For example, to query the edge logs without any joins:
|
||||
```sql
|
||||
select timestamp, metadata from edge_logs t
|
||||
```
|
||||
|
||||
The resulting `metadata` key is rendered as an array of objects in the Logs Explorer. In the following diagram, each box represents a nested array of objects:
|
||||
|
||||
<!-- Scene is here https://app.excalidraw.com/s/8gj16loJfGZ/3HzccK9MyLx -->
|
||||
@@ -131,13 +132,12 @@ The resulting `metadata` key is rendered as an array of objects in the Logs Expl
|
||||
<ThemedImage
|
||||
alt="Without Unnesting"
|
||||
sources={{
|
||||
light: useBaseUrl("/img/guides/platform/logs/unnesting-none.png"),
|
||||
dark: useBaseUrl("/img/guides/platform/logs/unnesting-none-dark.png"),
|
||||
light: useBaseUrl('/img/guides/platform/logs/unnesting-none.png'),
|
||||
dark: useBaseUrl('/img/guides/platform/logs/unnesting-none-dark.png'),
|
||||
}}
|
||||
style={{maxHeight: 200}}
|
||||
style={{ maxHeight: 200 }}
|
||||
/>
|
||||
|
||||
|
||||
Perform a `cross join unnest()` to work with the keys nested in the `metadata` key.
|
||||
|
||||
To query for a nested value, add a join for each array level:
|
||||
@@ -155,10 +155,10 @@ This surfaces the following columns available for selection:
|
||||
<ThemedImage
|
||||
alt="With Two Level Unnesting"
|
||||
sources={{
|
||||
light: useBaseUrl("/img/guides/platform/logs/unnesting-2.png"),
|
||||
dark: useBaseUrl("/img/guides/platform/logs/unnesting-2-dark.png"),
|
||||
light: useBaseUrl('/img/guides/platform/logs/unnesting-2.png'),
|
||||
dark: useBaseUrl('/img/guides/platform/logs/unnesting-2-dark.png'),
|
||||
}}
|
||||
style={{maxHeight: 200}}
|
||||
style={{ maxHeight: 200 }}
|
||||
/>
|
||||
|
||||
This allows you to select the `method` and `cf_ipcountry` columns. In JS dot notation, the full paths for each selected column are:
|
||||
@@ -174,30 +174,30 @@ The Logs Explorer has a maximum of 1000 rows per run. Use `LIMIT` to optimize yo
|
||||
|
||||
1. Include a filter over **timestamp**
|
||||
|
||||
Querying your entire log history might seem appealing. For **Enterprise** customers that have a large retention range, you run the risk of timeouts due additional time required to scan the larger dataset.
|
||||
Querying your entire log history might seem appealing. For **Enterprise** customers that have a large retention range, you run the risk of timeouts due additional time required to scan the larger dataset.
|
||||
|
||||
2. Avoid selecting large nested objects. Select individual values instead.
|
||||
|
||||
When querying large objects, the columnar storage engine selects each column associated with each nested key, resulting in a large number of columns being selected. This inadvertently impacts the query speed and may result in timeouts or memory errors, especially for projects with a lot of logs.
|
||||
When querying large objects, the columnar storage engine selects each column associated with each nested key, resulting in a large number of columns being selected. This inadvertently impacts the query speed and may result in timeouts or memory errors, especially for projects with a lot of logs.
|
||||
|
||||
Instead, select only the values required.
|
||||
Instead, select only the values required.
|
||||
|
||||
```sql
|
||||
-- ❌ Avoid doing this
|
||||
select
|
||||
datetime(timestamp),
|
||||
m as metadata -- <- metadata contains many nested keys
|
||||
from edge_logs t
|
||||
cross join unnest(t.metadata) as m;
|
||||
|
||||
-- ✅ Do this
|
||||
select
|
||||
```sql
|
||||
-- ❌ Avoid doing this
|
||||
select
|
||||
datetime(timestamp),
|
||||
r.method -- <- select only the required values
|
||||
from edge_logs t
|
||||
cross join unnest(t.metadata) as m
|
||||
cross join unnest(m.request) as r
|
||||
```
|
||||
m as metadata -- <- metadata contains many nested keys
|
||||
from edge_logs t
|
||||
cross join unnest(t.metadata) as m;
|
||||
|
||||
-- ✅ Do this
|
||||
select
|
||||
datetime(timestamp),
|
||||
r.method -- <- select only the required values
|
||||
from edge_logs t
|
||||
cross join unnest(t.metadata) as m
|
||||
cross join unnest(m.request) as r
|
||||
```
|
||||
|
||||
### Examples and Templates
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ title: Broadcast
|
||||
description: Getting started with Realtime's Broadcast feature
|
||||
---
|
||||
|
||||
Supabase Broadcast allows a client to send messages, and multiple receivers to receive the messages.
|
||||
Supabase Broadcast allows a client to send messages, and multiple receivers to receive the messages.
|
||||
|
||||
The broadcasted messages are ephemeral. They are not persisted to the database and are directly relayed through the Realtime server. This is ideal for sending information like mouse moments where latency is important, but need not be persisted.
|
||||
|
||||
|
||||
@@ -165,7 +165,7 @@ These variables will be exposed on the browser, and that's completely fine since
|
||||
[Row Level Security](/docs/guides/auth#row-level-security) enabled on our Database.
|
||||
|
||||
```ts title="lib/supabase.ts"
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage'
|
||||
import { createClient } from '@supabase/supabase-js'
|
||||
|
||||
const supabaseUrl = YOUR_REACT_NATIVE_SUPABASE_URL
|
||||
@@ -243,10 +243,18 @@ export default function Auth() {
|
||||
/>
|
||||
</View>
|
||||
<View style={[styles.verticallySpaced, styles.mt20]}>
|
||||
<Button title="Sign in" disabled={loading} onPress={() => signInWithEmail()} />
|
||||
<Button
|
||||
title="Sign in"
|
||||
disabled={loading}
|
||||
onPress={() => signInWithEmail()}
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.verticallySpaced}>
|
||||
<Button title="Sign up" disabled={loading} onPress={() => signUpWithEmail()} />
|
||||
<Button
|
||||
title="Sign up"
|
||||
disabled={loading}
|
||||
onPress={() => signUpWithEmail()}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
@@ -360,16 +368,26 @@ export default function Account({ session }: { session: Session }) {
|
||||
<Input label="Email" value={session?.user?.email} disabled />
|
||||
</View>
|
||||
<View style={styles.verticallySpaced}>
|
||||
<Input label="Username" value={username || ''} onChangeText={(text) => setUsername(text)} />
|
||||
<Input
|
||||
label="Username"
|
||||
value={username || ''}
|
||||
onChangeText={(text) => setUsername(text)}
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.verticallySpaced}>
|
||||
<Input label="Website" value={website || ''} onChangeText={(text) => setWebsite(text)} />
|
||||
<Input
|
||||
label="Website"
|
||||
value={website || ''}
|
||||
onChangeText={(text) => setWebsite(text)}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View style={[styles.verticallySpaced, styles.mt20]}>
|
||||
<Button
|
||||
title={loading ? 'Loading ...' : 'Update'}
|
||||
onPress={() => updateProfile({ username, website, avatar_url: avatarUrl })}
|
||||
onPress={() =>
|
||||
updateProfile({ username, website, avatar_url: avatarUrl })
|
||||
}
|
||||
disabled={loading}
|
||||
/>
|
||||
</View>
|
||||
@@ -425,7 +443,11 @@ export default function App() {
|
||||
|
||||
return (
|
||||
<View>
|
||||
{session && session.user ? <Account key={session.user.id} session={session} /> : <Auth />}
|
||||
{session && session.user ? (
|
||||
<Account key={session.user.id} session={session} />
|
||||
) : (
|
||||
<Auth />
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -461,12 +483,16 @@ We can start by creating a new component:
|
||||
import { useState, useEffect } from 'react'
|
||||
import { supabase } from '../lib/supabase'
|
||||
import { StyleSheet, View, Alert, Image, Button } from 'react-native'
|
||||
import DocumentPicker, { isCancel, isInProgress, types } from 'react-native-document-picker'
|
||||
import DocumentPicker, {
|
||||
isCancel,
|
||||
isInProgress,
|
||||
types,
|
||||
} from 'react-native-document-picker'
|
||||
|
||||
interface Props {
|
||||
size: number
|
||||
url: string | null
|
||||
onUpload: (filePath: string) => void
|
||||
url: string | null
|
||||
onUpload: (filePath: string) => void
|
||||
}
|
||||
|
||||
export default function Avatar({ url, size = 150, onUpload }: Props) {
|
||||
@@ -483,7 +509,7 @@ export default function Avatar({ url, size = 150, onUpload }: Props) {
|
||||
const { data, error } = await supabase.storage
|
||||
.from('avatars')
|
||||
.download(path)
|
||||
|
||||
|
||||
if (error) {
|
||||
throw error
|
||||
}
|
||||
@@ -508,17 +534,17 @@ export default function Avatar({ url, size = 150, onUpload }: Props) {
|
||||
presentationStyle: 'fullScreen',
|
||||
copyTo: 'cachesDirectory',
|
||||
type: types.images,
|
||||
mode: 'open'
|
||||
mode: 'open',
|
||||
})
|
||||
|
||||
const photo = {
|
||||
uri: file.fileCopyUri,
|
||||
type: file.type,
|
||||
name: file.name
|
||||
name: file.name,
|
||||
}
|
||||
|
||||
const formData = new FormData()
|
||||
formData.append("file", photo)
|
||||
formData.append('file', photo)
|
||||
|
||||
const fileExt = file.name.split('.').pop()
|
||||
const filePath = `${Math.random()}.${fileExt}`
|
||||
@@ -537,7 +563,9 @@ export default function Avatar({ url, size = 150, onUpload }: Props) {
|
||||
console.warn('cancelled')
|
||||
// User cancelled the picker, exit any dialogs or menus and move on
|
||||
} else if (isInProgress(error)) {
|
||||
console.warn('multiple pickers were opened, only the last will be considered')
|
||||
console.warn(
|
||||
'multiple pickers were opened, only the last will be considered'
|
||||
)
|
||||
} else if (error instanceof Error) {
|
||||
Alert.alert(error.message)
|
||||
} else {
|
||||
@@ -551,12 +579,20 @@ export default function Avatar({ url, size = 150, onUpload }: Props) {
|
||||
return (
|
||||
<View>
|
||||
{avatarUrl ? (
|
||||
<Image source={{ uri: avatarUrl }} accessibilityLabel="Avatar" style={[avatarSize, styles.avatar, styles.image]} />
|
||||
<Image
|
||||
source={{ uri: avatarUrl }}
|
||||
accessibilityLabel="Avatar"
|
||||
style={[avatarSize, styles.avatar, styles.image]}
|
||||
/>
|
||||
) : (
|
||||
<View style={[avatarSize, styles.avatar, styles.noImage]} />
|
||||
)}
|
||||
<View>
|
||||
<Button title={uploading ? 'Uploading ...' : 'Upload'} onPress={uploadAvatar} disabled={uploading} />
|
||||
<Button
|
||||
title={uploading ? 'Uploading ...' : 'Upload'}
|
||||
onPress={uploadAvatar}
|
||||
disabled={uploading}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
@@ -566,7 +602,7 @@ const styles = StyleSheet.create({
|
||||
avatar: {
|
||||
borderRadius: 5,
|
||||
overflow: 'hidden',
|
||||
maxWidth: '100%'
|
||||
maxWidth: '100%',
|
||||
},
|
||||
image: {
|
||||
objectFit: 'cover',
|
||||
@@ -575,7 +611,7 @@ const styles = StyleSheet.create({
|
||||
noImage: {
|
||||
backgroundColor: '#333',
|
||||
border: '1px solid rgb(200, 200, 200)',
|
||||
borderRadius: 5
|
||||
borderRadius: 5,
|
||||
},
|
||||
})
|
||||
```
|
||||
@@ -593,13 +629,13 @@ import Avatar from './Avatar'
|
||||
<View>
|
||||
{/* Add to the body */}
|
||||
<View>
|
||||
<Avatar
|
||||
<Avatar
|
||||
size={200}
|
||||
url={avatarUrl}
|
||||
url={avatarUrl}
|
||||
onUpload={(url: string) => {
|
||||
setAvatarUrl(url)
|
||||
updateProfile({ username, website, avatar_url: url })
|
||||
}}
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
{/* ... */}
|
||||
|
||||
@@ -188,7 +188,7 @@ Let's set up a Vue component to manage logins and sign ups. We'll use Magic Link
|
||||
|
||||
```html title="/components/Auth.vue"
|
||||
<template>
|
||||
<form class="flex row flex-center" @submit.prevent="handleLogin">
|
||||
<form class="row flex-center flex" @submit.prevent="handleLogin">
|
||||
<div class="col-6 form-widget">
|
||||
<h1 class="header">Supabase + Nuxt 3</h1>
|
||||
<p class="description">Sign in via magic link with your email below</p>
|
||||
@@ -203,7 +203,7 @@ Let's set up a Vue component to manage logins and sign ups. We'll use Magic Link
|
||||
<div>
|
||||
<input
|
||||
type="submit"
|
||||
class="block button"
|
||||
class="button block"
|
||||
:value="loading ? 'Loading' : 'Send magic link'"
|
||||
:disabled="loading"
|
||||
/>
|
||||
@@ -260,14 +260,14 @@ Let's create a new component for that called `Account.vue`.
|
||||
<div>
|
||||
<input
|
||||
type="submit"
|
||||
class="block button primary"
|
||||
class="button primary block"
|
||||
:value="loading ? 'Loading ...' : 'Update'"
|
||||
:disabled="loading"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button class="block button" @click="signOut" :disabled="loading">
|
||||
<button class="button block" @click="signOut" :disabled="loading">
|
||||
Sign Out
|
||||
</button>
|
||||
</div>
|
||||
@@ -346,7 +346,7 @@ Now that we have all the components in place, let's update `app.vue`:
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const user = useSupabaseUser();
|
||||
const user = useSupabaseUser()
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -385,7 +385,7 @@ Let's create an avatar for the user so that they can upload a profile photo. We
|
||||
/>
|
||||
|
||||
<div style="width: 10em; position: relative;">
|
||||
<label class="block button primary" for="single">
|
||||
<label class="button primary block" for="single">
|
||||
{{ uploading ? "Uploading ..." : "Upload" }}
|
||||
</label>
|
||||
<input
|
||||
@@ -481,14 +481,14 @@ And then we can add the widget to the Account page:
|
||||
<div>
|
||||
<input
|
||||
type="submit"
|
||||
class="block button primary"
|
||||
class="button primary block"
|
||||
:value="loading ? 'Loading ...' : 'Update'"
|
||||
:disabled="loading"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button class="block button" @click="signOut" :disabled="loading">
|
||||
<button class="button block" @click="signOut" :disabled="loading">
|
||||
Sign Out
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -272,14 +272,14 @@ import { Component, createEffect, createSignal } from 'solid-js'
|
||||
import { supabase } from './supabaseClient'
|
||||
|
||||
interface Props {
|
||||
session: AuthSession
|
||||
session: AuthSession;
|
||||
}
|
||||
|
||||
const Account: Component<Props> = ({ session }) => {
|
||||
const [loading, setLoading] = createSignal(true)
|
||||
const [username, setUsername] = createSignal<string | null>(null)
|
||||
const [website, setWebsite] = createSignal<string | null>(null)
|
||||
const [avatarUrl, setAvatarUrl] = createSignal<string | null>(null)
|
||||
const [username, setUsername] = (createSignal < string) | (null > null)
|
||||
const [website, setWebsite] = (createSignal < string) | (null > null)
|
||||
const [avatarUrl, setAvatarUrl] = (createSignal < string) | (null > null)
|
||||
|
||||
createEffect(() => {
|
||||
getProfile()
|
||||
@@ -366,11 +366,19 @@ const Account: Component<Props> = ({ session }) => {
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<button type="submit" class="button primary block" disabled={loading()}>
|
||||
<button
|
||||
type="submit"
|
||||
class="button primary block"
|
||||
disabled={loading()}
|
||||
>
|
||||
{loading() ? 'Saving ...' : 'Update profile'}
|
||||
</button>
|
||||
</div>
|
||||
<button type="button" class="button block" onClick={() => supabase.auth.signOut()}>
|
||||
<button
|
||||
type="button"
|
||||
class="button block"
|
||||
onClick={() => supabase.auth.signOut()}
|
||||
>
|
||||
Sign Out
|
||||
</button>
|
||||
</form>
|
||||
|
||||
@@ -192,7 +192,7 @@ Let's set up a Svelte component to manage logins and sign ups. We'll use Magic L
|
||||
|
||||
```html title="src/lib/Auth.svelte"
|
||||
<script lang="ts">
|
||||
import { supabase } from "src/supabaseClient";
|
||||
import { supabase } from 'src/supabaseClient'
|
||||
|
||||
let loading = false
|
||||
let email = ''
|
||||
@@ -217,7 +217,7 @@ Let's set up a Svelte component to manage logins and sign ups. We'll use Magic L
|
||||
<div class="col-6 form-widget" aria-live="polite">
|
||||
<h1 class="header">Supabase + Svelte</h1>
|
||||
<p class="description">Sign in via magic link with your email below</p>
|
||||
<form class="form-widget" on:submit|preventDefault={handleLogin}>
|
||||
<form class="form-widget" on:submit|preventDefault="{handleLogin}">
|
||||
<div>
|
||||
<label for="email">Email</label>
|
||||
<input
|
||||
@@ -225,11 +225,16 @@ Let's set up a Svelte component to manage logins and sign ups. We'll use Magic L
|
||||
class="inputField"
|
||||
type="email"
|
||||
placeholder="Your email"
|
||||
bind:value={email}
|
||||
bind:value="{email}"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<button type="submit" class="button block" aria-live="polite" disabled={loading}>
|
||||
<button
|
||||
type="submit"
|
||||
class="button block"
|
||||
aria-live="polite"
|
||||
disabled="{loading}"
|
||||
>
|
||||
<span>{loading ? 'Loading' : 'Send magic link'}</span>
|
||||
</button>
|
||||
</div>
|
||||
@@ -264,13 +269,13 @@ Let's create a new component for that called `Account.svelte`.
|
||||
try {
|
||||
loading = true
|
||||
const { user } = session
|
||||
|
||||
|
||||
const { data, error, status } = await supabase
|
||||
.from('profiles')
|
||||
.select('username, website, avatar_url')
|
||||
.eq('id', user.id)
|
||||
.single()
|
||||
|
||||
|
||||
if (error && status !== 406) throw error
|
||||
|
||||
if (data) {
|
||||
@@ -301,7 +306,7 @@ Let's create a new component for that called `Account.svelte`.
|
||||
}
|
||||
|
||||
let { error } = await supabase.from('profiles').upsert(updates)
|
||||
|
||||
|
||||
if (error) {
|
||||
throw error
|
||||
}
|
||||
@@ -342,10 +347,10 @@ Now that we have all the components in place, let's update `App.svelte`:
|
||||
|
||||
```html title="src/App.svelte"
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { supabase } from './supabaseClient';
|
||||
import type { AuthSession } from '@supabase/supabase-js';
|
||||
import Account from './lib/Account.svelte';
|
||||
import { onMount } from 'svelte'
|
||||
import { supabase } from './supabaseClient'
|
||||
import type { AuthSession } from '@supabase/supabase-js'
|
||||
import Account from './lib/Account.svelte'
|
||||
import Auth from './lib/Auth.svelte'
|
||||
|
||||
let session: AuthSession
|
||||
@@ -363,9 +368,9 @@ Now that we have all the components in place, let's update `App.svelte`:
|
||||
|
||||
<div class="container" style="padding: 50px 0 100px 0">
|
||||
{#if !session}
|
||||
<Auth />
|
||||
<Auth />
|
||||
{:else}
|
||||
<Account {session} />
|
||||
<Account {session} />
|
||||
{/if}
|
||||
</div>
|
||||
```
|
||||
@@ -392,12 +397,12 @@ Let's create an avatar for the user so that they can upload a profile photo. We
|
||||
|
||||
```html title="src/lib/Avatar.svelte"
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import { supabase } from "../supabaseClient"
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { supabase } from '../supabaseClient'
|
||||
|
||||
export let size: number
|
||||
export let url: string
|
||||
|
||||
|
||||
let avatarUrl: string = null
|
||||
let uploading = false
|
||||
let files: FileList
|
||||
@@ -406,7 +411,9 @@ Let's create an avatar for the user so that they can upload a profile photo. We
|
||||
|
||||
const downloadImage = async (path: string) => {
|
||||
try {
|
||||
const { data, error } = await supabase.storage.from('avatars').download(path)
|
||||
const { data, error } = await supabase.storage
|
||||
.from('avatars')
|
||||
.download(path)
|
||||
|
||||
if (error) {
|
||||
throw error
|
||||
@@ -436,7 +443,7 @@ Let's create an avatar for the user so that they can upload a profile photo. We
|
||||
let { error } = await supabase.storage
|
||||
.from('avatars')
|
||||
.upload(filePath, file)
|
||||
|
||||
|
||||
if (error) {
|
||||
throw error
|
||||
}
|
||||
@@ -456,18 +463,9 @@ Let's create an avatar for the user so that they can upload a profile photo. We
|
||||
</script>
|
||||
|
||||
<div style="width: {size}px" aria-live="polite">
|
||||
{#if avatarUrl}
|
||||
<img
|
||||
src={avatarUrl}
|
||||
alt={avatarUrl ? 'Avatar' : 'No image'}
|
||||
class="avatar image"
|
||||
style="height: {size}px, width: {size}px"
|
||||
/>
|
||||
{:else}
|
||||
<div
|
||||
class="avatar no-image"
|
||||
style="height: {size}px, width: {size}px"
|
||||
/>
|
||||
{#if avatarUrl} <img src={avatarUrl} alt={avatarUrl ? 'Avatar' : 'No image'}
|
||||
class="avatar image" style="height: {size}px, width: {size}px" /> {:else}
|
||||
<div class="avatar no-image" style="height: {size}px, width: {size}px" />
|
||||
{/if}
|
||||
<div style="width: {size}px">
|
||||
<label class="button primary block" for="single">
|
||||
@@ -479,8 +477,8 @@ Let's create an avatar for the user so that they can upload a profile photo. We
|
||||
id="single"
|
||||
accept="image/*"
|
||||
bind:files
|
||||
on:change={uploadAvatar}
|
||||
disabled={uploading}
|
||||
on:change="{uploadAvatar}"
|
||||
disabled="{uploading}"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
@@ -497,9 +495,9 @@ And then we can add the widget to the Account page:
|
||||
import Avatar from './Avatar.svelte'
|
||||
</script>
|
||||
|
||||
<form on:submit|preventDefault={updateProfile} class="form-widget">
|
||||
<form on:submit|preventDefault="{updateProfile}" class="form-widget">
|
||||
<!-- Add to body -->
|
||||
<Avatar bind:url={avatarUrl} size={150} on:upload={updateProfile} />
|
||||
<Avatar bind:url="{avatarUrl}" size="{150}" on:upload="{updateProfile}" />
|
||||
|
||||
<!-- Other form elements -->
|
||||
</form>
|
||||
|
||||
@@ -252,27 +252,27 @@ Let's create a new component for that called `Profile.svelte`.
|
||||
let avatar_url = null
|
||||
|
||||
function getProfile(node) {
|
||||
try {
|
||||
loading = true;
|
||||
const user = supabase.auth.user();
|
||||
try {
|
||||
loading = true
|
||||
const user = supabase.auth.user()
|
||||
|
||||
supabase
|
||||
.from("profiles")
|
||||
.select(`username, website, avatar_url`)
|
||||
.eq("id", user?.id)
|
||||
.single()
|
||||
.then(({ data, error, status }) => {
|
||||
if (data) {
|
||||
username = data.username;
|
||||
website = data.website;
|
||||
avatar_url = data.avatar_url;
|
||||
}
|
||||
if (error && status !== 406) throw error;
|
||||
});
|
||||
supabase
|
||||
.from('profiles')
|
||||
.select(`username, website, avatar_url`)
|
||||
.eq('id', user?.id)
|
||||
.single()
|
||||
.then(({ data, error, status }) => {
|
||||
if (data) {
|
||||
username = data.username
|
||||
website = data.website
|
||||
avatar_url = data.avatar_url
|
||||
}
|
||||
if (error && status !== 406) throw error
|
||||
})
|
||||
} catch (error) {
|
||||
alert(error.message);
|
||||
alert(error.message)
|
||||
} finally {
|
||||
loading = false;
|
||||
loading = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -311,7 +311,6 @@ Let's create a new component for that called `Profile.svelte`.
|
||||
} finally {
|
||||
loading = false
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -130,7 +130,16 @@ function recurseThroughParams(paramDefinition: TsDoc.TypeDefinition) {
|
||||
if (param.type?.declaration?.children) {
|
||||
children = param.type?.declaration?.children
|
||||
} else if (isUnion(param)) {
|
||||
children = param.type.types
|
||||
// We don't want to show the union types if it's a literal
|
||||
const nonLiteralVariants = param.type.types.filter(
|
||||
({ type }) => type !== 'literal'
|
||||
)
|
||||
|
||||
if (nonLiteralVariants.length === 0) {
|
||||
children = null
|
||||
} else {
|
||||
children = nonLiteralVariants
|
||||
}
|
||||
} else if (param.type === 'reflection') {
|
||||
children = param.declaration.children
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ const sidebars = {
|
||||
|
||||
sidebar: [
|
||||
'intro',
|
||||
// 'usage', // Hide until the OpenAPI spec is updated to v3
|
||||
'config',
|
||||
'release-notes',
|
||||
// {
|
||||
|
||||
@@ -24,19 +24,20 @@
|
||||
"gen:cli": "npm-run-all gen:cli:commands gen:cli:config",
|
||||
"gen:cli:commands": "ts-node ./generator/index.ts gen --type cli --input ../../spec/cli_v1_commands.yaml --output ./_cli/generated/usage.mdx ",
|
||||
"gen:cli:config": "ts-node ./generator/index.ts gen --type config --input ../../spec/cli_v1_config.yaml --output ./_cli/generated/config.mdx",
|
||||
"gen:gotrue": "npm-run-all gen:gotrue:config",
|
||||
"gen:gotrue": "npm-run-all gen:gotrue:config gen:gotrue:usage",
|
||||
"gen:gotrue:config": "ts-node ./generator/index.ts gen --type config --input ../../spec/gotrue_v1_config.yaml --output ./_gotrue/config.mdx",
|
||||
"gen:gotrue:usage": "ts-node ./generator/index.ts gen --type api --input ../../spec/transforms/auth_v1_openapi_deparsed.json --output ./_gotrue/usage.mdx",
|
||||
"gen:storage": "npm-run-all gen:storage:api gen:storage:config",
|
||||
"gen:storage:api": "ts-node ./generator/index.ts gen --type api --input ../../spec/transforms/storage_v0_openapi_deparsed.json --output ./_storage/generated/usage.mdx",
|
||||
"gen:storage:config": "ts-node ./generator/index.ts gen --type config --input ../../spec/storage_v0_config.yaml --output ./_storage/generated/config.mdx",
|
||||
"gen:supabase-dart:v0": "npm-run-all gen:supabase-dart:v0:ref",
|
||||
"gen:supabase-dart:v0:ref": "ts-node ./generator/index.ts gen --type legacy --input ../../spec/supabase_dart_v0_legacy.yml --output ./_supabase_dart_versioned_docs/version-v0/generated",
|
||||
"gen:supabase-dart:v0:ref": "ts-node ./generator/index.ts gen --type legacy --input ../../spec/supabase_dart_v0.yml --output ./_supabase_dart_versioned_docs/version-v0/generated",
|
||||
"gen:supabase-dart:v1": "npm-run-all gen:supabase-dart:v1:ref",
|
||||
"gen:supabase-dart:v1:ref": "ts-node ./generator/index.ts gen --type legacy --input ../../spec/supabase_dart_v1_legacy.yml --output ./_supabase_dart/generated",
|
||||
"gen:supabase-dart:v1:ref": "ts-node ./generator/index.ts gen --type legacy --input ../../spec/supabase_dart_v1.yml --output ./_supabase_dart/generated",
|
||||
"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_legacy.yml --output ./_supabase_js_versioned_docs/version-v1/generated",
|
||||
"gen:supabase-js:v1:ref": "ts-node ./generator/index.ts gen --type legacy --input ../../spec/supabase_js_v1.yml --output ./_supabase_js_versioned_docs/version-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_legacy.yml --output ./_supabase_js/generated"
|
||||
"gen:supabase-js:v2:ref": "ts-node ./generator/index.ts gen --type legacy --input ../../spec/supabase_js_v2.yml --output ./_supabase_js/generated"
|
||||
},
|
||||
"dependencies": {
|
||||
"@docusaurus/core": "^2.0.1",
|
||||
|
||||
@@ -13,8 +13,6 @@ date: '06-01-2020'
|
||||
|
||||
It has been a monster month at [Supabase](/) and we're back with a video update. It's a longer video this month because we have a couple of housekeeping items.
|
||||
|
||||
<!--truncate-->
|
||||
|
||||
<iframe
|
||||
className="w-full"
|
||||
width="700"
|
||||
|
||||
@@ -14,8 +14,6 @@ date: '06-15-2020'
|
||||
|
||||
This month we welcome [Steve Chavez](https://github.com/steve-chavez) to the team. Steve is a maintainer of PostgREST, one of the core tools which makes Supabase possible.
|
||||
|
||||
<!--truncate-->
|
||||
|
||||
Steve and I have been in contact since he [added one of my old blog posts](https://github.com/PostgREST/postgrest-docs/commit/3dde972d31519d3afc319015bce1dc0f73adeb8e#commitcomment-35312841) to the PostgREST docs. He has a rich history in open source and we're excited to have him in our team.
|
||||
|
||||
Steve is helping us build one of our most demanded features - our Auth system. We will support him to build some exciting features which he has planned for PostgREST, including enhanced PostGIS support.
|
||||
@@ -13,8 +13,6 @@ date: '07-01-2020'
|
||||
|
||||
We're now 4 months into building [Supabase](/), which means another major update. Here's a few things we think you'll love in this release.
|
||||
|
||||
<!--truncate-->
|
||||
|
||||
### 4 minute demo
|
||||
|
||||
Watch a full demo:
|
||||
|
||||
@@ -13,8 +13,6 @@ date: '07-09-2020'
|
||||
|
||||
Whenever you create a new database in Postgres, you are actually [basing it off an already present database](https://www.postgresql.org/docs/current/manage-ag-templatedbs.html) in your cluster.
|
||||
|
||||
<!--truncate-->
|
||||
|
||||
This database, `template1`, and another, called `template0`, are standard system databases that exist in every newly created database cluster. Don't believe me? Why not quickly [spin up a database](/docs/postgres/server/about) and see it for yourself with this query:
|
||||
|
||||
```sql
|
||||
|
||||
@@ -13,8 +13,6 @@ date: '07-10-2020'
|
||||
|
||||
On May 27 Supabase hit the [top of Hacker News](https://news.ycombinator.com/item?id=23319901) and stayed on the front page for more than 24 hours.
|
||||
|
||||
<!--truncate-->
|
||||
|
||||
Since then, Supabase has been featured on the [Stack Overflow podcast](https://stackoverflow.blog/2020/06/05/podcast-241-new-tools-for-new-times/), hit the [trending page](https://twitter.com/supabase/status/1268062559023685633) on GitHub, and scaled to over 1000 databases.
|
||||
|
||||
Here is everything that went wrong along the way.
|
||||
|
||||
@@ -15,8 +15,6 @@ PostgreSQL backups can be categorized into two types: _logical_ and _physical_.
|
||||
|
||||
We'll cover some of the many tools you can use for Postgres backups in future posts.
|
||||
|
||||
<!--truncate-->
|
||||
|
||||
## Logical Backups
|
||||
|
||||
This form of backup is typically achieved by [translating all the data into a set of SQL commands and writing it into a single file](https://www.postgresql.org/docs/current/backup-dump.html). This can then be fed to any database cluster to recreate everything. In your CLI, performing logical backups can be as easy as:
|
||||
|
||||
@@ -13,8 +13,6 @@ date: '08-02-2020'
|
||||
|
||||
Have you ever wanted to restore your database's state to a particular moment in time? This post explains how, using WAL-G.
|
||||
|
||||
<!--truncate-->
|
||||
|
||||
## Introduction
|
||||
|
||||
[WAL-G](https://github.com/wal-g/wal-g) is an [open-source continuous archiving tool](https://www.citusdata.com/blog/introducing-wal-g-faster-restores-for-postgres/) used to easily set up and recover from [physical backups](/blog/postgresql-physical-logical-backups) in Postgres. It mainly handles the storage and retrieval of physical backups and WAL archives to and from a chosen cloud storage provider. In this post, we will walk you through on how to effortlessly set up WAL-G for your database as well as guide you on what to do if and when disaster strikes.
|
||||
|
||||
@@ -13,8 +13,6 @@ date: '08-02-2020'
|
||||
|
||||
After 5 months of building, we're releasing one of our most anticipated features: Supabase Auth.
|
||||
|
||||
<!--truncate-->
|
||||
|
||||
### Quick Demo
|
||||
|
||||
Watch a full demo:
|
||||
|
||||
@@ -16,8 +16,6 @@ Supabase is an open source Firebase alternative. We are building the features of
|
||||
|
||||
Two months ago a developer discovered Supabase and (unexpectedly) [launched us on Hacker News](https://news.ycombinator.com/item?id=23319901). Although we had completed only 3 months of development the community support was both incredible and humbling.
|
||||
|
||||
<!--truncate-->
|
||||
|
||||

|
||||
|
||||
Developers were obviously excited about the prospect of an open source Firebase alternative, but the comments were dominated by one emphatic feature request: Auth.
|
||||
|
||||
@@ -13,8 +13,6 @@ date: '09-03-2020'
|
||||
|
||||
We're 6 months into building our hosted database platform and we've made some major improvements to our auth system and table view.
|
||||
|
||||
<!--truncate-->
|
||||
|
||||
### Quick Demo
|
||||
|
||||
Watch a full demo:
|
||||
|
||||
@@ -14,8 +14,6 @@ date: '09-11-2020'
|
||||
|
||||
October is looming, and for open source communities and contributors this means one thing: [Hacktoberfest](https://hacktoberfest.digitalocean.com/).
|
||||
|
||||
<!--truncate-->
|
||||
|
||||
## What is Hacktoberfest?
|
||||
|
||||
Hacktoberfest is a time to contribute to open source, improve your skills, and build greater technology in the process. Last year during Hacktoberfest 483,127 pull requests were opened and 61,871 people completed the challenge.
|
||||
|
||||
@@ -14,8 +14,6 @@ date: '10-03-2020'
|
||||
|
||||
We're now 7 months into building Supabase. This update is a big one!
|
||||
|
||||
<!--truncate-->
|
||||
|
||||
### Quick demo
|
||||
|
||||
Watch a full demo:
|
||||
|
||||
@@ -14,8 +14,6 @@ date: '10-30-2020'
|
||||
|
||||
**UPDATE 16/08/2022**: supabase-js v2 is out and focuses on “quality-of-life” improvements for developers. V2 includes Type support, new auth methods, realtime multiplayer sneak peek, and more: [**Read the blog post**](https://supabase.com/blog/supabase-js-v2)
|
||||
|
||||
<!--truncate-->
|
||||
|
||||
### New Docs
|
||||
|
||||
Before digging into the improvements, we're excited to point out our new [developer docs](/docs/reference/javascript/installing). While they're still a work in progress, here are some things we think you'll like:
|
||||
|
||||
@@ -14,8 +14,6 @@ date: '11-02-2020'
|
||||
|
||||
We're now 8 months into building Supabase. We're focused on performance, stability, and reliability but that hasn't prevented us from shipping some great features.
|
||||
|
||||
<!--truncate-->
|
||||
|
||||
### Quick demo
|
||||
|
||||
Watch a full demo:
|
||||
|
||||
@@ -14,8 +14,6 @@ date: '11-18-2020'
|
||||
|
||||
A quick summary of Postgres views, materialized views, and why you should use them.
|
||||
|
||||
<!--truncate-->
|
||||
|
||||
### What is a View?
|
||||
|
||||
A [view](https://www.postgresql.org/docs/12/sql-createview.html) is a convenient shortcut to a query. Creating a view does not involve new tables or data. When run, an underlying query is executed, returning its results to the user.
|
||||
|
||||
@@ -14,8 +14,6 @@ date: '12-01-2020'
|
||||
|
||||
We've been building for 9 months now, and we're getting even closer to Beta.
|
||||
|
||||
<!--truncate-->
|
||||
|
||||
### Quick demo
|
||||
|
||||
Watch a full demo:
|
||||
|
||||
@@ -17,8 +17,6 @@ Omar Kamali is the founder of [Monitoro](https://www.monitoro.xyz/), a service f
|
||||
|
||||
Learn how Monitoro was able to build fast, ensure reliability, and keep scaling by building with Supabase and Svelte.
|
||||
|
||||
<!--truncate-->
|
||||
|
||||
### From Zero to One Million
|
||||
|
||||
In one month, Monitoro scaled from nothing to nearly one million API requests per day.
|
||||
|
||||
@@ -15,8 +15,6 @@ date: '12-02-2020'
|
||||
|
||||
[**TAYFA**](https://usetayfa.com) is the fastest no-code approach to create and iterate on bespoke frontends. Learn how its solo founder, [Sarup Banskota](https://twitter.com/sarupbanskota?lang=en), was able to launch quickly with [Supabase](/), Next.js, and Vercel.
|
||||
|
||||
<!--truncate-->
|
||||
|
||||
### Meet the founder
|
||||
|
||||
Sarup's background is in open-source, web frontends, and devtools. In the summer of 2020, he was rapidly building MVPs as he explored different problem spaces. Sarup found that reusing his frontend work across projects, wasn't as simple as he'd have liked.
|
||||
|
||||
@@ -16,8 +16,6 @@ date: '12-02-2020'
|
||||
|
||||
[Xendit](https://www.xendit.co/) is one of South East Asia's largest payment processors. They use Supabase to run automated checks against international sanctions lists.
|
||||
|
||||
<!--truncate-->
|
||||
|
||||
### About Xendit
|
||||
|
||||
Xendit is a leading payment gateway for Indonesia and Southeast Asia. They enable businesses to accept payments in Indonesia with a single integration for credit and debit cards, e-wallets, and bank transfer.
|
||||
|
||||
@@ -14,8 +14,6 @@ tags:
|
||||
date: '12-02-2020'
|
||||
---
|
||||
|
||||
<!--truncate-->
|
||||
|
||||
### Supabase and Strive School - YC Summer 20 batchmates
|
||||
|
||||
We are proud to announce we are teaming up with fellow YC Summer 20 Alumni [Strive School](https://strive.school/), to help teach the next generation of software developers about open source software development.
|
||||
@@ -30,8 +28,6 @@ Tobia and Diego, the founders, launched Strive School to help individuals gain t
|
||||
|
||||
Their business model means the better students, who want it the most, have a shot at making it - regardless of their socio-economic background.
|
||||
|
||||
<!--truncate-->
|
||||
|
||||
### Working together to help students learn open source
|
||||
|
||||
Strive School has an initial focus on full-stack Web Engineering and AI Engineering. They deeply care about open-source and help their students understand the importance of open-source software in the modern workforce. Almost all technology companies use open-source software, so Strive School sees it as essential that their students leave the course with an understanding of open source development.
|
||||
@@ -42,8 +38,6 @@ We are huge Postgres fans and believe that Supabase will make learning and imple
|
||||
|
||||
We will be working closely with Strive School in the coming months to produce exclusive content for their code school students. We are also happy to announce we will be taking on a Strive school graduate as an intern at Supabase. This isn't a role we were actively hiring for, but we have been so impressed with the standard of their graduates that we didn't want to miss out of the chance to work with talent from Strive School.
|
||||
|
||||
<!--truncate-->
|
||||
|
||||
### Launching the Supabase Student Developer Pack with Strive School
|
||||
|
||||
As part of this process, we have put together a Student Developer Pack. Strive School are the perfect partner to launch our Student Developer Pack with as we both feel passionate about the importance of open-source software to empower developers. We are looking forward to working closely together in the coming months to help their students embrace the world of open-source.
|
||||
|
||||
@@ -11,8 +11,6 @@ tags:
|
||||
date: '12-13-2020'
|
||||
---
|
||||
|
||||
<!--truncate-->
|
||||
|
||||
The Supabase dashboard has become more feature-rich in the last month. We have a powerful SQL editor backed by [Monaco](https://microsoft.github.io/monaco-editor/). We built an Airtable-like view of your database, making editing a breeze.
|
||||
|
||||
> Features, performance, DX - choose three
|
||||
|
||||
@@ -15,8 +15,6 @@ date: '02-02-2021'
|
||||
|
||||
New year, new features. We've been busy at Supabase during January and our community has been even busier. Here's a few things you'll find interesting.
|
||||
|
||||
<!--truncate-->
|
||||
|
||||
### Quick demo
|
||||
|
||||
Watch a full demo:
|
||||
|
||||
@@ -44,14 +44,6 @@ Over 100K users submitted drawings in a 24 hour span. At this point, Brad knew h
|
||||
|
||||
Paint.Wtf has continued to get sustained coverage in the media and has continued to pick up new and unexpected use cases. For example, remote teams are using Paint.wtf as part of their daily ice breaker activities during COVID to keep up team social cohesion.
|
||||
|
||||
<!-- > _"Supabase meant we could have the exact functionality we needed for our leaderboard._
|
||||
>
|
||||
> _We got the benefits of PostgreSQL with a great developer experience._
|
||||
>
|
||||
> _With Supabase we could launch our product quickly and yet still handle the huge user volumes that Paint.wtf generated."_
|
||||
>
|
||||
> Brad Dwyer, CTO Roboflow. -->
|
||||
|
||||

|
||||
|
||||
## Ship fast, and carry on scaling
|
||||
|
||||
@@ -14,8 +14,6 @@ date: '03-02-2021'
|
||||
|
||||
Supabase is an open source Firebase alternative. We've now been building for one year. Here's what we released last month.
|
||||
|
||||
<!--truncate-->
|
||||
|
||||
### Quick demo
|
||||
|
||||
Watch a full demo:
|
||||
|
||||
@@ -17,8 +17,6 @@ A Supabase user asked recently if they can trigger a webhook periodically. We ha
|
||||
|
||||
It is. Here's how.
|
||||
|
||||
<!--truncate-->
|
||||
|
||||
### What's cron?
|
||||
|
||||
A "cron job" is a script[^1] that runs periodically at fixed times, dates, or intervals. Traditionally you'd set it up on a Linux server. An example might be an hourly script that downloads emails to your computer.
|
||||
|
||||
@@ -39,11 +39,11 @@ We were absolutely blown away by [all the amazing projects](/blog/hackathon-winn
|
||||
|
||||
<blockquote class="twitter-tweet" data-dnt="true" data-theme="dark">
|
||||
<p lang="en" dir="ltr">
|
||||
Who else deserves the Gold Supabase Tee? 👀{' '}
|
||||
Who else deserves the Gold Supabase Tee? 👀
|
||||
<a href="https://t.co/XPWw02kZad">https://t.co/XPWw02kZad</a>
|
||||
</p>
|
||||
— Supabase (@supabase) <a href="https://twitter.com/supabase/status/1440737587895799809?ref_src=twsrc%5Etfw">September 22, 2021</a>
|
||||
</blockquote> <script async src="https://platform.twitter.com/widgets.js" charSet="utf-8"></script>{' '}
|
||||
</blockquote> <script async src="https://platform.twitter.com/widgets.js" charSet="utf-8"></script>
|
||||
|
||||
### Prize categories
|
||||
|
||||
@@ -91,12 +91,11 @@ We'll be contacting and announcing winners [on Twitter](https://twitter.com/supa
|
||||
|
||||
<blockquote class="twitter-tweet" data-dnt="true" data-theme="dark">
|
||||
<p lang="en" dir="ltr">
|
||||
Absolutely buzzing to have won the{' '}
|
||||
<a href="https://twitter.com/supabase?ref_src=twsrc%5Etfw">@supabase</a> hackathon! 🥳{' '}
|
||||
<a href="https://t.co/rm5HBuju73">https://t.co/rm5HBuju73</a>
|
||||
Absolutely buzzing to have won the
|
||||
<a href="https://twitter.com/supabase?ref_src=twsrc%5Etfw">@supabase</a> hackathon! 🥳 <a href="https://t.co/rm5HBuju73">https://t.co/rm5HBuju73</a>
|
||||
</p>
|
||||
— Josh Cawthorne (@cawthornejosh) <a href="https://twitter.com/cawthornejosh/status/1424985377136390183?ref_src=twsrc%5Etfw">August 10, 2021</a>
|
||||
</blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>{' '}
|
||||
</blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
|
||||
|
||||
### Rules
|
||||
|
||||
|
||||
@@ -19,16 +19,16 @@ Back in May 2020, we [tweeted about](https://twitter.com/supabase/status/1265922
|
||||
<div>
|
||||
<blockquote className="twitter-tweet" data-theme="dark">
|
||||
<p lang="en" dir="ltr">
|
||||
Really impressed with{' '}
|
||||
<a href="https://twitter.com/logflare_logs?ref_src=twsrc%5Etfw">@logflare_logs</a>. We've
|
||||
been on the front page of{' '}
|
||||
Really impressed with
|
||||
<a href="https://twitter.com/logflare_logs?ref_src=twsrc%5Etfw">@logflare_logs</a>. We've been
|
||||
on the front page of
|
||||
<a href="https://twitter.com/hashtag/hackernews?src=hash&ref_src=twsrc%5Etfw">
|
||||
#hackernews
|
||||
</a>{' '}
|
||||
for 24 hours anticipating the HN hug of death. Looks like we made it through OK and Logflare
|
||||
has been ingesting everything without breaking a sweat{' '}
|
||||
<a href="https://t.co/RWKHIgmWh6">https://t.co/RWKHIgmWh6</a>{' '}
|
||||
<a href="https://t.co/dQ2xqFTdco">pic.twitter.com/dQ2xqFTdco</a>
|
||||
</a>
|
||||
for 24 hours anticipating the HN hug of death. Looks like we made it through OK and Logflare has
|
||||
been ingesting everything without breaking a sweat <a href="https://t.co/RWKHIgmWh6">
|
||||
https://t.co/RWKHIgmWh6
|
||||
</a> <a href="https://t.co/dQ2xqFTdco">pic.twitter.com/dQ2xqFTdco</a>
|
||||
</p>
|
||||
— Supabase (@supabase) <a href="https://twitter.com/supabase/status/1265922290237071361?ref_src=twsrc%5Etfw">May 28, 2020</a>
|
||||
</blockquote>
|
||||
@@ -115,7 +115,6 @@ Over the next few weeks, we plan to release these features (once we have a chanc
|
||||
allowFullScreen
|
||||
></iframe>
|
||||
|
||||
|
||||
## Open source
|
||||
|
||||
Right now, Logflare works natively with Big Query. But as our track record has proven, we're 100% committed to open source. We're working on the future of a fully open-source version of Logflare, with support for various storage engines.
|
||||
@@ -125,5 +124,3 @@ Right now, Logflare works natively with Big Query. But as our track record has p
|
||||
If you're an existing Logflare user, we'll be migrating everything to Supabase in the coming months. We'll work closely with you to ensure that the transition is seamless, and introduce the same [pricing levels](https://logflare.app/pricing) that Logflare offers today.
|
||||
|
||||
We've worked with Chase on a list of potential questions you might have about the transition. If you have any concerns or questions, you can reach out to support@supabase.io.
|
||||
|
||||
<script async src="https://platform.twitter.com/widgets.js" charSet="utf-8"></script>
|
||||
@@ -34,8 +34,8 @@ The Supabase team will be participating as well, for example [Jon](https://twitt
|
||||
|
||||
<blockquote class="twitter-tweet" data-theme="dark">
|
||||
<p lang="en" dir="ltr">
|
||||
Hacktoberfest Hackathon Swag finally arrived! 🥇🥈🥨 see the winners here:{' '}
|
||||
<a href="https://t.co/wgRBu92Bq4">https://t.co/wgRBu92Bq4</a>{' '}
|
||||
Hacktoberfest Hackathon Swag finally arrived! 🥇🥈🥨 see the winners here:
|
||||
<a href="https://t.co/wgRBu92Bq4">https://t.co/wgRBu92Bq4</a>
|
||||
<a href="https://t.co/i1eD1pdA8I">pic.twitter.com/i1eD1pdA8I</a>
|
||||
</p>
|
||||
— Supabase (@supabase) <a href="https://twitter.com/supabase/status/1463355998928736262?ref_src=twsrc%5Etfw">November 24, 2021</a>
|
||||
@@ -84,12 +84,12 @@ We'll be contacting and announcing winners [on Twitter](https://twitter.com/supa
|
||||
|
||||
<blockquote class="twitter-tweet" data-dnt="true" data-theme="dark">
|
||||
<p lang="en" dir="ltr">
|
||||
Absolutely buzzing to have won the{' '}
|
||||
<a href="https://twitter.com/supabase?ref_src=twsrc%5Etfw">@supabase</a> hackathon! 🥳{' '}
|
||||
Absolutely buzzing to have won the
|
||||
<a href="https://twitter.com/supabase?ref_src=twsrc%5Etfw">@supabase</a> hackathon! 🥳
|
||||
<a href="https://t.co/rm5HBuju73">https://t.co/rm5HBuju73</a>
|
||||
</p>
|
||||
— Josh Cawthorne (@cawthornejosh) <a href="https://twitter.com/cawthornejosh/status/1424985377136390183?ref_src=twsrc%5Etfw">August 10, 2021</a>
|
||||
</blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>{' '}
|
||||
</blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
|
||||
|
||||
### Rules
|
||||
|
||||
|
||||
@@ -17,12 +17,10 @@ Last week [Supabase UI](https://ui.supabase.com/), our open-source UI component
|
||||
|
||||
<blockquote class="twitter-tweet" data-lang="en" data-theme="dark">
|
||||
<p lang="en" dir="ltr">
|
||||
🏆 Supabase UI has been nominated for the{' '}
|
||||
<a href="https://twitter.com/ProductHunt?ref_src=twsrc%5Etfw">@ProductHunt</a>{' '}
|
||||
<a href="https://twitter.com/hashtag/GoldenKittyAwards2021?src=hash&ref_src=twsrc%5Etfw">
|
||||
🏆 Supabase UI has been nominated for the
|
||||
<a href="https://twitter.com/ProductHunt?ref_src=twsrc%5Etfw">@ProductHunt</a> <a href="https://twitter.com/hashtag/GoldenKittyAwards2021?src=hash&ref_src=twsrc%5Etfw">
|
||||
#GoldenKittyAwards2021
|
||||
</a>{' '}
|
||||
<p>💚 Thank you to all the contributors and upvoters out there, we appreciate you!</p>
|
||||
</a> <p>💚 Thank you to all the contributors and upvoters out there, we appreciate you!</p>
|
||||
<a href="https://t.co/xOek6sM4LG">https://t.co/xOek6sM4LG</a>
|
||||
</p>
|
||||
— Supabase (@supabase) <a href="https://twitter.com/supabase/status/1481672574023221260?ref_src=twsrc%5Etfw">January 13, 2022</a>
|
||||
|
||||
@@ -111,15 +111,16 @@ Divjoy announced their [Supabase Integration](https://divjoy.com/?database=supab
|
||||
|
||||
<blockquote class="twitter-tweet" data-theme="dark">
|
||||
<p lang="en" dir="ltr">
|
||||
We released n8n@0.158.0 with new nodes for{' '}
|
||||
<a href="https://twitter.com/supabase?ref_src=twsrc%5Etfw">@supabase</a>,{' '}
|
||||
<a href="https://twitter.com/syncromsp?ref_src=twsrc%5Etfw">@syncromsp</a>, and{' '}
|
||||
We released n8n@0.158.0 with new nodes for
|
||||
<a href="https://twitter.com/supabase?ref_src=twsrc%5Etfw">@supabase</a>,<a href="https://twitter.com/syncromsp?ref_src=twsrc%5Etfw">
|
||||
@syncromsp
|
||||
</a>, and
|
||||
<a href="https://twitter.com/Microsoft?ref_src=twsrc%5Etfw">@Microsoft</a> Graph Security. We
|
||||
also made improvements to existing nodes ✨{' '}
|
||||
also made improvements to existing nodes ✨
|
||||
<a href="https://t.co/IdeUe4eWBK">pic.twitter.com/IdeUe4eWBK</a>
|
||||
</p>
|
||||
— n8n.io (@n8n_io) <a href="https://twitter.com/n8n_io/status/1480502781320572931?ref_src=twsrc%5Etfw">January 10, 2022</a>
|
||||
</blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>{' '}
|
||||
</blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
|
||||
|
||||
### Python Upgrades
|
||||
|
||||
@@ -138,7 +139,7 @@ We're having a 24/7\*365 meme-fest on [Twitter](https://twitter.com/supabase)
|
||||
invest in good relationships <a href="https://t.co/bGiBE7FRFQ">pic.twitter.com/bGiBE7FRFQ</a>
|
||||
</p>
|
||||
— Supabase (@supabase) <a href="https://twitter.com/supabase/status/1475876907212316678?ref_src=twsrc%5Etfw">December 28, 2021</a>
|
||||
</blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>{' '}
|
||||
</blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
|
||||
|
||||
### TikTok
|
||||
|
||||
|
||||
@@ -116,9 +116,9 @@ create table audit.record_version(
|
||||
<details>
|
||||
<summary>Postgres version compatibility</summary>
|
||||
<p>
|
||||
The table above uses PostgreSQL's built-in{' '}
|
||||
<a href="https://www.postgresql.org/docs/14/functions-uuid.html">uuid functionality</a>, which
|
||||
is available from version 14. For backwards compatibility you can use the uuid-ossp extension.
|
||||
The table above uses PostgreSQL's built-in
|
||||
<a href="https://www.postgresql.org/docs/14/functions-uuid.html">uuid functionality</a>, which is
|
||||
available from version 14. For backwards compatibility you can use the uuid-ossp extension.
|
||||
</p>
|
||||
<p>
|
||||
<code>create extension if not exists "uuid-ossp";</code>
|
||||
|
||||
@@ -200,8 +200,6 @@ With the release of our new Enterprise Tier, we're announcing a tonne of new fea
|
||||
|
||||
### Point-in-Time Recovery
|
||||
|
||||
<!-- - [ ] IMAGE -->
|
||||
|
||||
Disaster Recovery is a critical process for any company, even for the most fault-tolerant products. What happens when you accidentally delete that database column
|
||||
because of a clumsy `where` clause? Even with Supabase's daily backups, a day's worth of data can be lost if disaster strikes at the most inopportune time.
|
||||
|
||||
@@ -219,8 +217,6 @@ If you want access to the Prometheus Endpoint, [contact the Supabase Enterprise
|
||||
|
||||
### SLAs & Enterprise Support
|
||||
|
||||
<!-- - [ ] TODO - image -->
|
||||
|
||||
For our enterprise customers we know that service level agreements and support response times are also critical features. We continue to treat support as an
|
||||
important priority for all tiers, though enterprise users require confidence that our response times meet their business needs. We now offer faster response times for Enterprise customers,
|
||||
alongside Priority and Priority Plus support packages for those who need more comprehensive support. You can find further details around our SLAs & support
|
||||
@@ -239,8 +235,7 @@ We have a lot more Enterprise features under development, available to early-acc
|
||||
|
||||
### SOC2
|
||||
|
||||
Supabase is now SOC2 Type 1 compliant, as announced during Launch Week 5. You can read all about the process we went through to get there in this [blog post](https://supabase.com/blog/supabase-soc2). Getting the Type 1 certification is just the start, and we will be working on getting certified for SOC2 Type 2 and HIPAA next.
|
||||
|
||||
Supabase is now SOC2 Type 1 compliant, as announced during Launch Week 5. You can read all about the process we went through to get there in this [blog post](https://supabase.com/blog/supabase-soc2). Getting the Type 1 certification is just the start, and we will be working on getting certified for SOC2 Type 2 and HIPAA next.
|
||||
|
||||
### Log ingestion
|
||||
|
||||
|
||||
@@ -38,9 +38,8 @@ Finally, we're delighted to be partnering with [the Future Forest Company](https
|
||||
|
||||
<blockquote class="twitter-tweet" data-theme="dark">
|
||||
<p lang="en" dir="ltr">
|
||||
Hacktoberfest Hackathon Swag finally arrived! 🥇🥈🥨 see the winners here:{' '}
|
||||
<a href="https://t.co/wgRBu92Bq4">https://t.co/wgRBu92Bq4</a>{' '}
|
||||
<a href="https://t.co/i1eD1pdA8I">pic.twitter.com/i1eD1pdA8I</a>
|
||||
Hacktoberfest Hackathon Swag finally arrived! 🥇🥈🥨 see the winners here:
|
||||
<a href="https://t.co/wgRBu92Bq4">https://t.co/wgRBu92Bq4</a> <a href="https://t.co/i1eD1pdA8I">pic.twitter.com/i1eD1pdA8I</a>
|
||||
</p>
|
||||
— Supabase (@supabase) <a href="https://twitter.com/supabase/status/1463355998928736262?ref_src=twsrc%5Etfw">November 24, 2021</a>
|
||||
</blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
|
||||
@@ -91,12 +90,11 @@ We'll be contacting and announcing winners [on Twitter](https://twitter.com/supa
|
||||
|
||||
<blockquote class="twitter-tweet" data-dnt="true" data-theme="dark">
|
||||
<p lang="en" dir="ltr">
|
||||
Absolutely buzzing to have won the{' '}
|
||||
<a href="https://twitter.com/supabase?ref_src=twsrc%5Etfw">@supabase</a> hackathon! 🥳{' '}
|
||||
<a href="https://t.co/rm5HBuju73">https://t.co/rm5HBuju73</a>
|
||||
Absolutely buzzing to have won the
|
||||
<a href="https://twitter.com/supabase?ref_src=twsrc%5Etfw">@supabase</a> hackathon! 🥳 <a href="https://t.co/rm5HBuju73">https://t.co/rm5HBuju73</a>
|
||||
</p>
|
||||
— Josh Cawthorne (@cawthornejosh) <a href="https://twitter.com/cawthornejosh/status/1424985377136390183?ref_src=twsrc%5Etfw">August 10, 2021</a>
|
||||
</blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>{' '}
|
||||
</blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
|
||||
|
||||
### Rules
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ To take advantage of the open-source version of Metabase, you can use the Metaba
|
||||
|
||||
### Launching Metabase
|
||||
|
||||
To launch Metabase, simply go to "<http://localhost:3000/setup/>" which is the default port that the Metabase server will be listening to.
|
||||
To launch Metabase, simply go to [http://localhost:3000/setup/](http://localhost:3000/setup/) which is the default port that the Metabase server will be listening to.
|
||||
|
||||
After Metabase is launched, select your preferred language and add your contact information. In the _Add your data_ markdown, you will need to choose PostgreSQL.
|
||||
|
||||
@@ -135,6 +135,7 @@ With our [free tier Supabase account](https://app.supabase.com/), you can start
|
||||
If you have any questions please reach out via [Twitter](https://twitter.com/supabase) or join our [Discord](https://discord.supabase.com).
|
||||
|
||||
## More Python and Supabase resources
|
||||
|
||||
- [supabase-py](https://github.com/supabase-community/supabase-py)
|
||||
- [Slack Consolidate: a slackbot built with Python and Supabase](slack-consolidate-slackbot-to-consolidate-messages)
|
||||
- [Supabase-py (Database) on Replit](https://replit.com/@Supabase/Supabase-py-Database?v=1)
|
||||
@@ -34,9 +34,8 @@ This time, we're switching things around:
|
||||
little beauty (the hat, not the cat).
|
||||
<br />
|
||||
<br />
|
||||
Now I've just got to pry it away from Peanut 🥜 😂 <a href="https://t.co/POBdVZrF5N">
|
||||
pic.twitter.com/POBdVZrF5N
|
||||
</a>
|
||||
Now I've just got to pry it away from Peanut 🥜 😂
|
||||
<a href="https://t.co/POBdVZrF5N">pic.twitter.com/POBdVZrF5N</a>
|
||||
</p>
|
||||
— Daniel Cranney 🇬🇧 (@danielcranney) <a href="https://twitter.com/danielcranney/status/1557249589182447616?ref_src=twsrc%5Etfw">August 10, 2022</a>
|
||||
</blockquote>
|
||||
|
||||
+10
-3
@@ -26,8 +26,15 @@ Along our jurney we will:
|
||||
- implement **magic link sign in** and user authentication with Angular
|
||||
- make use of **the realtime capabilities**!
|
||||
|
||||
<iframe width="560" height="315" src="https://www.youtube-nocookie.com/embed/jmCiI_OSarA" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe> Watch the video version of the tutorial.
|
||||
|
||||
<iframe
|
||||
width="560"
|
||||
height="315"
|
||||
src="https://www.youtube-nocookie.com/embed/jmCiI_OSarA"
|
||||
title="YouTube video player"
|
||||
frameborder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowfullscreen
|
||||
></iframe> Watch the video version of the tutorial.
|
||||
|
||||
Since there are quite some code snippets we need I've put together the [full source code on Github](https://github.com/saimon24/supabase-trello-angular) so you can easily run the project yourself!
|
||||
|
||||
@@ -205,7 +212,7 @@ create policy "Users can edit cards if they are part of the board" on cards for
|
||||
|
||||
Finally we need a **trigger** that reacts to changes in our database.
|
||||
|
||||
In our case we want to listen to the creation of new boards, which will automatically create the board <-> user connection in the `user_boards` table.
|
||||
In our case we want to listen to the creation of new boards, which will automatically create the board < - > user connection in the `user_boards` table.
|
||||
|
||||
Additionally we will also add every new authenticated user to our `users` table since you later don't have access to the internal auth table of Supabase!
|
||||
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
---
|
||||
title: Supabase Beta September 2022
|
||||
description: We were too focused on clearing out the backlog so we didn't ship anything new last month... or did we?!
|
||||
author: ant_wilson
|
||||
image: 2022-september/monthly-update-september-2022.jpg
|
||||
thumb: 2022-september/monthly-update-september-2022.jpg
|
||||
tags:
|
||||
- release-notes
|
||||
date: '2022-10-05'
|
||||
toc_depth: 3
|
||||
---
|
||||
We were too focused on clearing out the backlog so we didn't ship anything new last month... or did we?!
|
||||
|
||||
## Kaizen Week
|
||||
|
||||
We did something a bit strange during September - we *didn't* work on features. Quite the opposite. After Launch Week we did three subsequent weeks of [Kaizen](https://en.wikipedia.org/wiki/Kaizen), a term we use internally to deliver constant and incremental improvement. Each week had a different focus:
|
||||
|
||||
- Week 1: QA and testing
|
||||
- Week 2: Documentation
|
||||
- Week 3: Issue Backlog and Automation
|
||||
|
||||
It's pretty rare for a company to stop feature development altogether, but luckily we're just a bunch of developers so we all know the pain of technical debt. After 5 Launch Weeks, working on testing and backlogs feels like a bit of a relief.
|
||||
|
||||
We saw a lot of progress across our Open Issues - closing over 250 issues and 50 Pull Requests.
|
||||
|
||||

|
||||
|
||||
… <br/>
|
||||
… <br/>
|
||||
… <br/>
|
||||
|
||||
OK, we lied. We shipped a few things:
|
||||
|
||||
## Auth UI on Product Hunt
|
||||
|
||||

|
||||
|
||||
We launched the new Auth UI on Product Hunt! Auth UI is a pre-built React component for authenticating users with Supabase Auth. It supports custom themes and extensible styles to match your brand and aesthetic.
|
||||
|
||||
[Check out the launch.](https://www.producthunt.com/posts/auth-ui-login-ui-for-your-web-app)
|
||||
|
||||
## Postgres WASM
|
||||
|
||||

|
||||
|
||||
If you've ever wondered, "can I run Postgres inside a browser, using an embeddable Linux Virtual Machine?", wonder no longer. With our friends at [Snaplet](https://www.snaplet.dev/?utm_campaign=Beta%20update%20emails&utm_source=hs_email&utm_medium=email&_hsenc=p2ANqtz-8u0x9grbiydYqh56hC_8Dta4ZiTdWbW0eXWkVCnUEUGWWoVDFXCpWzJWImkLfM-lG-o2JX), we've released an open source Postgres WASM.
|
||||
|
||||
[Read the blog post](https://supabase.com/blog/postgres-wasm) <br/>
|
||||
[Comment on Hacker News](https://news.ycombinator.com/item?id=33067962) <br/>
|
||||
Visit the repo: [Snaplet](https://github.com/snaplet/postgres-wasm) | [Supabase](https://github.com/supabase-community/postgres-wasm)
|
||||
|
||||
## Security updates
|
||||
|
||||
We're making some changes to the way the Dashboard interacts with your database. These changes simplify the database permissions so that it's easier for you to migrate in and out of Supabase, and they reduce the security surface area considerably. The change will be applied automatically in November, or you can run it today via the Dashboard.
|
||||
|
||||
[Review the Security Notice](https://github.com/supabase/supabase/discussions/9314).
|
||||
|
||||
## Quick product updates
|
||||
|
||||
- Edge functions free plan script size is bumped to 2 MB <br/>
|
||||
- [functions-go](https://github.com/supabase-community/functions-go) was just contributed to supabase-community by [Zain Khan](https://twitter.com/Za1n_K) <br/>
|
||||
- A new [guide](https://supabase.com/docs/guides/auth/auth-captcha) to add captcha to your sign-in, sign-up, and password reset forms <br/>
|
||||
- New Postgres Extension for monitoring your PostgreSQL database network traffic: [pg_netstat](https://github.com/supabase/pg_netstat) <br/>
|
||||
- Added [docs](https://supabase.com/docs/guides/storage-cdn) for how caching works in Supabase <br/>
|
||||
|
||||
## Migrate from Heroku to Supabase
|
||||
|
||||

|
||||
|
||||
In case you missed it, Heroku is shutting down their free tier. We're big fans of Heroku and take a lot of inspiration from what they've built. Supabase is compatible with Heroku's PostgreSQL product (because we're just Postgres too), and if you have a free project running on Heroku we've created a [tool to help migrate](http://migrate.supabase.com/) to our free tier.
|
||||
|
||||
## Made with Supabase
|
||||
|
||||

|
||||
|
||||
[Supabase Schema](https://supabase-schema.vercel.app/): An open source, free, login-less and secured Supabase Schema Visualizer. Simply plug in your URL and anon key and see a visual representation of your database schema! Built by the amazing [Zernonia](https://twitter.com/zernonia) using Vue 3, Viti, and WindiCSS.
|
||||
|
||||
## Extended Community Highlights
|
||||
|
||||

|
||||
|
||||
- Building a viral application to visualize train routes. [Blog](https://vercel.com/blog/building-a-viral-application-to-visualize-train-routes) <br/>
|
||||
- OTP Authentication with Supabase and Twilio in React. [Tutorial](https://refine.dev/blog/supabase-twilio-otp-authentication-in-react/) <br/>
|
||||
- Join Vercel and us for [Next.js Conf](https://nextjs.org/conf). <br/>
|
||||
- Building $1K - $10K MRR Micro SaaS around Supabase. [Blog](https://microsaasidea.substack.com/p/micro-saas-products-around-supabase) <br/>
|
||||
- Watch How Netlify and Supabase Enables "Supa" DX. [Webinar](https://www.netlify.com/resources/webinars/how-netlify-supabase-enables-supa-dx/) <br/>
|
||||
- Postgres database functions are your next favorite feature! [Blog](https://mirror.xyz/0x7D15080A13c8128dBAf90a2c2326058b5c1D5eac/MaiOq5m1xSa9QfMuiLYq4bJdA0owWoTpOZNlQv76_so) <br/>
|
||||
- Create a blog with Vue, Tailwindcss & Supabase. [Part 1](https://youtube.com/watch?v=0NB9lgFcOT4) and [Part 2](https://youtube.com/watch?v=UP2mHUpcv6g) <br/>
|
||||
- Flutter Supabase Authentication. [Tutorial](https://www.sandromaglione.com/techblog/flutter-supabase-authentication-complete-tutorial) <br/>
|
||||
- Vue 3 + Pinia + Supabase Auth (Email). [Tutorial](https://towardsdev.com/vue-3-pinia-supabase-auth-email-1228f5f8502e) <br/>
|
||||
- Power up your ClickUp skills with Supabase, Figma, and DhiWise in a day. [Tutorial](https://dev.to/saloni137/power-up-your-clickup-skills-with-supabase-figma-and-dhiwise-in-a-day-3pak) <br/>
|
||||
- Creating an Email Authentication using Supabase in React. [Tutorial](https://dev.to/surajondev/creating-an-email-authentication-using-supabase-in-react-5883) <br/>
|
||||
|
||||
|
||||
## Meme Zone
|
||||
|
||||
If you made it this far in the blog post you deserve a treat. [Follow us on Twitter](https://twitter.com/supabase) for more.
|
||||
|
||||

|
||||
|
||||
## Get started
|
||||
|
||||
- Start using Supabase today: **[app.supabase.com](https://app.supabase.com/)**
|
||||
- Make sure to **[star us on GitHub](https://github.com/supabase/supabase)**
|
||||
- Follow us **[on Twitter](https://twitter.com/supabase)**
|
||||
- Subscribe to our **[YouTube channel](https://www.youtube.com/c/supabase)**
|
||||
@@ -51,9 +51,9 @@ const BlogListItem = ({ post }: Props) => {
|
||||
<p className="text-scale-1100 max-w-sm text-base">{post.description}</p>
|
||||
</div>
|
||||
<div className="flex items-center -space-x-2">
|
||||
{author.map((author: any) => {
|
||||
{author.map((author: any, i: number) => {
|
||||
return (
|
||||
<div className="dark:ring-scale-200 w-10 rounded-full ring-2 ring-white">
|
||||
<div className="dark:ring-scale-200 w-10 rounded-full ring-2 ring-white" key={i}>
|
||||
{author.author_image_url && (
|
||||
<Image
|
||||
src={author.author_image_url}
|
||||
|
||||
@@ -42,22 +42,19 @@ const EnterpriseContactForm = () => {
|
||||
const [formSubmitted, setFormSubmitted] = useState<boolean>(false)
|
||||
|
||||
const handleFormSubmit = async (values: typeof INITIAL_VALUES, { resetForm }: any) => {
|
||||
const { error } = await supabase.from('enterprise_contacts').insert(
|
||||
[
|
||||
{
|
||||
company_name: values.companyName,
|
||||
contact_name: `${values.contactFirstName} ${values.contactLastName}`,
|
||||
contact_first_name: values.contactFirstName,
|
||||
contact_last_name: values.contactLastName,
|
||||
contact_email: values.contactEmail,
|
||||
contact_phone: values.contactPhone,
|
||||
company_size: values.companySize,
|
||||
details: values.details,
|
||||
country: values.country,
|
||||
},
|
||||
],
|
||||
{ returning: 'minimal' }
|
||||
)
|
||||
const { error } = await supabase.from('enterprise_contacts').insert([
|
||||
{
|
||||
company_name: values.companyName,
|
||||
contact_name: `${values.contactFirstName} ${values.contactLastName}`,
|
||||
contact_first_name: values.contactFirstName,
|
||||
contact_last_name: values.contactLastName,
|
||||
contact_email: values.contactEmail,
|
||||
contact_phone: values.contactPhone,
|
||||
company_size: values.companySize,
|
||||
details: values.details,
|
||||
country: values.country,
|
||||
},
|
||||
])
|
||||
|
||||
// TODO: handle error
|
||||
console.log('error:', error)
|
||||
|
||||
@@ -117,9 +117,9 @@ const CodeExamples = () => {
|
||||
activeId={example}
|
||||
type="underlined"
|
||||
>
|
||||
{Object.values(exampleList).map((x) => {
|
||||
{Object.values(exampleList).map((x, i) => {
|
||||
return (
|
||||
<Tabs.Panel id={x.id} label={x.name}>
|
||||
<Tabs.Panel id={x.id} label={x.name} key={i}>
|
||||
<span></span>
|
||||
</Tabs.Panel>
|
||||
)
|
||||
|
||||
@@ -42,22 +42,19 @@ const MfaContactForm = () => {
|
||||
const [formSubmitted, setFormSubmitted] = useState<boolean>(false)
|
||||
|
||||
const handleFormSubmit = async (values: typeof INITIAL_VALUES, { resetForm }: any) => {
|
||||
const { error } = await supabase.from('mfa_early_access_contacts').insert(
|
||||
[
|
||||
{
|
||||
company_name: values.companyName,
|
||||
contact_name: `${values.contactFirstName} ${values.contactLastName}`,
|
||||
contact_first_name: values.contactFirstName,
|
||||
contact_last_name: values.contactLastName,
|
||||
contact_email: values.contactEmail,
|
||||
contact_phone: values.contactPhone,
|
||||
company_size: values.companySize,
|
||||
details: values.details,
|
||||
country: values.country,
|
||||
},
|
||||
],
|
||||
{ returning: 'minimal' }
|
||||
)
|
||||
const { error } = await supabase.from('mfa_early_access_contacts').insert([
|
||||
{
|
||||
company_name: values.companyName,
|
||||
contact_name: `${values.contactFirstName} ${values.contactLastName}`,
|
||||
contact_first_name: values.contactFirstName,
|
||||
contact_last_name: values.contactLastName,
|
||||
contact_email: values.contactEmail,
|
||||
contact_phone: values.contactPhone,
|
||||
company_size: values.companySize,
|
||||
details: values.details,
|
||||
country: values.country,
|
||||
},
|
||||
])
|
||||
|
||||
// TODO: handle error
|
||||
console.log('error:', error)
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { SupabaseClient } from '@supabase/supabase-js'
|
||||
import { Button, Form, Input, InputNumber, Select } from '@supabase/ui'
|
||||
import { useState } from 'react'
|
||||
import countries from '~/data/Countries.json'
|
||||
import { PartnerContact } from '~/types/partners'
|
||||
import { SupabaseClient } from '~/lib/supabase'
|
||||
|
||||
const INITIAL_VALUES = {
|
||||
type: 'expert',
|
||||
@@ -41,24 +40,21 @@ export default function BecomeAPartner({ supabase }: { supabase: SupabaseClient
|
||||
const [formSubmitted, setFormSubmitted] = useState<boolean>(false)
|
||||
|
||||
const handleFormSubmit = async (values: any) => {
|
||||
const { error } = await supabase.from<PartnerContact>('partner_contacts').insert(
|
||||
[
|
||||
{
|
||||
type: values.type,
|
||||
first: values.first,
|
||||
last: values.last,
|
||||
company: values.company,
|
||||
size: Number(values.size),
|
||||
title: values.title,
|
||||
email: values.email,
|
||||
website: values.email.split('@')[1],
|
||||
phone: values.phone,
|
||||
country: values.country,
|
||||
details: values.details,
|
||||
},
|
||||
],
|
||||
{ returning: 'minimal' }
|
||||
)
|
||||
const { error } = await supabase.from('partner_contacts').insert([
|
||||
{
|
||||
type: values.type,
|
||||
first: values.first,
|
||||
last: values.last,
|
||||
company: values.company,
|
||||
size: Number(values.size),
|
||||
title: values.title,
|
||||
email: values.email,
|
||||
website: values.email.split('@')[1],
|
||||
phone: values.phone,
|
||||
country: values.country,
|
||||
details: values.details,
|
||||
},
|
||||
])
|
||||
|
||||
// TODO: handle error
|
||||
console.log('error:', error)
|
||||
@@ -172,8 +168,10 @@ export default function BecomeAPartner({ supabase }: { supabase: SupabaseClient
|
||||
name="country"
|
||||
layout="vertical"
|
||||
>
|
||||
{countries.map(({ code, name }) => (
|
||||
<Select.Option value={code}>{name}</Select.Option>
|
||||
{countries.map(({ code, name }: any, i: number) => (
|
||||
<Select.Option key={i} value={code}>
|
||||
{name}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
@@ -67,9 +67,9 @@ export const PricingTableRowDesktop = (props: any) => {
|
||||
<td className="bg-scale-50 dark:bg-scale-300 px-6 py-5"></td>
|
||||
</tr>
|
||||
|
||||
{category.features.map((feat: any) => {
|
||||
{category.features.map((feat: any, i: number) => {
|
||||
return (
|
||||
<tr className="divide-scale-600 dark:divide-scale-400 divide-x">
|
||||
<tr className="divide-scale-600 dark:divide-scale-400 divide-x" key={i}>
|
||||
<th
|
||||
className="text-scale-1200 flex items-center px-6 py-3 text-left text-sm font-normal dark:text-white"
|
||||
scope="row"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"show": true,
|
||||
"show": false,
|
||||
"text": "Auth UI Launch 🚀",
|
||||
"cta": "Vote now",
|
||||
"link": "https://www.producthunt.com/posts/pre-built-auth-ui-for-react",
|
||||
|
||||
@@ -79,6 +79,10 @@
|
||||
{
|
||||
"text": "Guides",
|
||||
"url": "/docs/guides/examples"
|
||||
},
|
||||
{
|
||||
"text": "Changelog",
|
||||
"url": "/changelog"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -1,25 +1,22 @@
|
||||
import authors from 'lib/authors.json'
|
||||
import hydrate from 'next-mdx-remote/hydrate'
|
||||
import { NextSeo } from 'next-seo'
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import React from 'react'
|
||||
import { Gfm } from 'remark-gfm'
|
||||
import CTABanner from '~/components/CTABanner'
|
||||
import DefaultLayout from '~/components/Layouts/Default'
|
||||
import { generateReadingTime } from '~/lib/helpers'
|
||||
import { MDXRemote } from 'next-mdx-remote'
|
||||
|
||||
interface Props {
|
||||
components: React.ReactNode
|
||||
props: any
|
||||
gfm: Gfm
|
||||
slug: string
|
||||
}
|
||||
|
||||
const LayoutComparison = ({ components, props, gfm, slug }: Props) => {
|
||||
const LayoutComparison = ({ components, props }: Props) => {
|
||||
// @ts-ignore
|
||||
const content = hydrate(props.blog.content, { components })
|
||||
const content = props.blog.content
|
||||
|
||||
const authorArray = props.blog.author.split(',')
|
||||
|
||||
@@ -98,7 +95,7 @@ const LayoutComparison = ({ components, props, gfm, slug }: Props) => {
|
||||
<div className="text-scale-900 flex justify-center space-x-3 text-sm">
|
||||
<p>{props.blog.date}</p>
|
||||
<p>•</p>
|
||||
<p>{generateReadingTime(props.blog.content.renderedOutput)}</p>
|
||||
<p>{generateReadingTime(props.blog.source)}</p>
|
||||
</div>
|
||||
<div className="flex justify-center gap-3">
|
||||
{author.map((author: any) => {
|
||||
@@ -133,7 +130,9 @@ const LayoutComparison = ({ components, props, gfm, slug }: Props) => {
|
||||
</div>
|
||||
<div className="">
|
||||
{/* Content */}
|
||||
<div className="prose prose-docs max-w-none">{content}</div>
|
||||
<div className="prose prose-docs max-w-none">
|
||||
<MDXRemote {...content} components={components} />
|
||||
</div>
|
||||
<div className="py-16">
|
||||
<div className="text-scale-900 dark:text-scale-1000 text-sm">Share this article</div>
|
||||
<div className="mt-4 flex items-center space-x-4">
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
export type Json = string | number | boolean | null | { [key: string]: Json } | Json[]
|
||||
|
||||
export interface Database {
|
||||
public: {
|
||||
Tables: {
|
||||
partners: {
|
||||
Row: {
|
||||
id: number
|
||||
slug: string
|
||||
type: Database['public']['Enums']['partner_type']
|
||||
category: string
|
||||
developer: string
|
||||
title: string
|
||||
description: string
|
||||
logo: string
|
||||
images: string[]
|
||||
video: string
|
||||
overview: string
|
||||
website: string
|
||||
docs: string
|
||||
approved: boolean
|
||||
}
|
||||
Insert: {
|
||||
id?: number
|
||||
slug?: string
|
||||
type?: Database['public']['Enums']['partner_type']
|
||||
category?: string
|
||||
developer?: string
|
||||
title?: string
|
||||
description?: string
|
||||
logo?: string
|
||||
images?: string[]
|
||||
video?: string
|
||||
overview?: string
|
||||
website?: string
|
||||
docs?: string
|
||||
approved?: boolean
|
||||
}
|
||||
Update: {
|
||||
id?: number
|
||||
slug?: string
|
||||
type?: Database['public']['Enums']['partner_type']
|
||||
category?: string
|
||||
developer?: string
|
||||
title?: string
|
||||
description?: string
|
||||
logo?: string
|
||||
images?: string[]
|
||||
video?: string
|
||||
overview?: string
|
||||
website?: string
|
||||
docs?: string
|
||||
approved?: boolean
|
||||
}
|
||||
}
|
||||
partner_contacts: {
|
||||
Row: {
|
||||
id: number
|
||||
type: Database['public']['Enums']['partner_type']
|
||||
company: string
|
||||
country: string
|
||||
details?: string
|
||||
email: string
|
||||
first: string
|
||||
last: string
|
||||
phone?: string
|
||||
size?: number
|
||||
title?: string
|
||||
website: string
|
||||
}
|
||||
Insert: {
|
||||
id?: number
|
||||
type?: Database['public']['Enums']['partner_type']
|
||||
company?: string
|
||||
country?: string
|
||||
details?: string
|
||||
email?: string
|
||||
first?: string
|
||||
last?: string
|
||||
phone?: string
|
||||
size?: number
|
||||
title?: string
|
||||
website?: string
|
||||
}
|
||||
Update: {
|
||||
id?: number
|
||||
type?: Database['public']['Enums']['partner_type']
|
||||
company?: string
|
||||
country?: string
|
||||
details?: string
|
||||
email?: string
|
||||
first?: string
|
||||
last?: string
|
||||
phone?: string
|
||||
size?: number
|
||||
title?: string
|
||||
website?: string
|
||||
}
|
||||
}
|
||||
}
|
||||
Views: {}
|
||||
Functions: {
|
||||
derive_label_sort_from_label: {
|
||||
Args: { label: string }
|
||||
Returns: string
|
||||
}
|
||||
}
|
||||
Enums: {
|
||||
partner_type: 'technology' | 'expert'
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import Image from 'next/image'
|
||||
import Avatar from '~/components/Avatar'
|
||||
import CodeBlock from '~/components/CodeBlock/CodeBlock'
|
||||
import ImageGrid from '~/components/ImageGrid'
|
||||
import Quote from '~/components/Quote'
|
||||
|
||||
// import all components used in blog articles here
|
||||
// to do: move this into a helper/utils, it is used elsewhere
|
||||
|
||||
const ignoreClass = 'ignore-on-export'
|
||||
|
||||
export default function mdxComponents(type?: 'blog' | 'lp' | undefined) {
|
||||
const components = {
|
||||
CodeBlock,
|
||||
Quote,
|
||||
Avatar,
|
||||
pre: (props: any) => {
|
||||
if (props.className !== ignoreClass) {
|
||||
return <CodeBlock {...props.children.props} />
|
||||
} else {
|
||||
return <code {...props} />
|
||||
}
|
||||
},
|
||||
ImageGrid,
|
||||
img: (props: any) => {
|
||||
if (props.className !== ignoreClass) {
|
||||
return (
|
||||
<div
|
||||
className={[
|
||||
'next-image--dynamic-fill',
|
||||
type === 'blog' && 'to-scale-400 from-scale-500 rounded-lg border bg-gradient-to-r',
|
||||
].join(' ')}
|
||||
>
|
||||
<Image
|
||||
{...props}
|
||||
className={['next-image--dynamic-fill', type === 'blog' && 'rounded-md border'].join(
|
||||
' '
|
||||
)}
|
||||
layout="fill"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return <img {...props} />
|
||||
},
|
||||
}
|
||||
|
||||
return components
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { serialize } from 'next-mdx-remote/serialize'
|
||||
import rehypeSlug from 'rehype-slug'
|
||||
import remarkGfm from 'remark-gfm'
|
||||
|
||||
export async function mdxSerialize(source: string) {
|
||||
const mdxSource: any = await serialize(source, {
|
||||
mdxOptions: {
|
||||
remarkPlugins: [remarkGfm],
|
||||
rehypePlugins: [
|
||||
// @ts-ignore
|
||||
rehypeSlug, // add IDs to any h1-h6 tag that doesn't have one, using a slug made from its text
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
return mdxSource
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,69 @@
|
||||
module.exports = [
|
||||
{
|
||||
source: '/:path*',
|
||||
destination: `/:path*`,
|
||||
},
|
||||
{
|
||||
source: '/docs',
|
||||
destination: `${process.env.NEXT_PUBLIC_DOCS_URL}`,
|
||||
},
|
||||
{
|
||||
// redirect /docs/
|
||||
// trailing slash caused by docusaurus issue with multizone
|
||||
source: '/docs/',
|
||||
destination: `${process.env.NEXT_PUBLIC_DOCS_URL}`,
|
||||
},
|
||||
{
|
||||
source: '/docs/:path*',
|
||||
destination: `${process.env.NEXT_PUBLIC_DOCS_URL}/:path*`,
|
||||
},
|
||||
// rewrite to keep existing ticket urls working
|
||||
{
|
||||
source: '/launch-week/tickets/:path',
|
||||
destination: `${process.env.NEXT_PUBLIC_LAUNCHWEEKSITE_URL}/tickets/:path`,
|
||||
},
|
||||
// rewrite to move ticket website to another path
|
||||
{
|
||||
source: '/launch-week-register',
|
||||
destination: `${process.env.NEXT_PUBLIC_LAUNCHWEEKSITE_URL}`,
|
||||
},
|
||||
{
|
||||
source: '/launch-week-register/:path*',
|
||||
destination: `${process.env.NEXT_PUBLIC_LAUNCHWEEKSITE_URL}/:path*`,
|
||||
},
|
||||
{
|
||||
source: '/new-docs',
|
||||
destination: `${process.env.NEXT_PUBLIC_REFERENCE_DOCS_URL}`,
|
||||
},
|
||||
{
|
||||
// redirect /docs/
|
||||
// trailing slash caused by docusaurus issue with multizone
|
||||
source: '/new-docs/',
|
||||
destination: `${process.env.NEXT_PUBLIC_REFERENCE_DOCS_URL}`,
|
||||
},
|
||||
{
|
||||
source: '/new-docs/:path*',
|
||||
destination: `${process.env.NEXT_PUBLIC_REFERENCE_DOCS_URL}/:path*`,
|
||||
},
|
||||
// misc rewrites
|
||||
{
|
||||
source: '/humans.txt',
|
||||
destination: `${process.env.NEXT_PUBLIC_DOCS_URL}/humans.txt`,
|
||||
},
|
||||
{
|
||||
source: '/lawyers.txt',
|
||||
destination: `${process.env.NEXT_PUBLIC_DOCS_URL}/lawyers.txt`,
|
||||
},
|
||||
{
|
||||
source: '/.well-known/security.txt',
|
||||
destination: `${process.env.NEXT_PUBLIC_DOCS_URL}/.well-known/security.txt`,
|
||||
},
|
||||
{
|
||||
source: '/oss',
|
||||
destination: `${process.env.NEXT_PUBLIC_DOCS_URL}/oss`,
|
||||
},
|
||||
{
|
||||
source: '/feed.xml',
|
||||
destination: `/rss.xml`,
|
||||
},
|
||||
]
|
||||
@@ -1,9 +1,12 @@
|
||||
import { createClient } from '@supabase/supabase-js'
|
||||
import { Database } from './database.types'
|
||||
|
||||
const SUPABASE_URL = 'https://obuldanrptloktxcffvn.supabase.co'
|
||||
const SUPABASE_ANON_KEY =
|
||||
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlhdCI6MTYzNzY1NjAxNSwiZXhwIjoxOTUzMjMyMDE1fQ.0sfp_Njf7l4g-nOCF5a1TQE11rPqtz8Y10uctIetkBA'
|
||||
|
||||
const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY)
|
||||
const supabase = createClient<Database>(SUPABASE_URL, SUPABASE_ANON_KEY)
|
||||
|
||||
export type SupabaseClient = typeof supabase
|
||||
|
||||
export default supabase
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,68 @@
|
||||
import nextMdx from '@next/mdx'
|
||||
|
||||
import remarkGfm from 'remark-gfm'
|
||||
import rehypeSlug from 'rehype-slug'
|
||||
|
||||
import rewrites from './lib/rewrites.js'
|
||||
import redirects from './lib/redirects.js'
|
||||
|
||||
const withMDX = nextMdx({
|
||||
extension: /\.mdx?$/,
|
||||
options: {
|
||||
remarkPlugins: [remarkGfm],
|
||||
rehypePlugins: [rehypeSlug],
|
||||
},
|
||||
})
|
||||
|
||||
const nextConfig = {
|
||||
basePath: '',
|
||||
pageExtensions: ['js', 'jsx', 'ts', 'tsx', 'md', 'mdx'],
|
||||
trailingSlash: false,
|
||||
images: {
|
||||
dangerouslyAllowSVG: true,
|
||||
domains: [
|
||||
'avatars.githubusercontent.com',
|
||||
'github.com',
|
||||
'ca.slack-edge.com',
|
||||
'res.cloudinary.com',
|
||||
'images.unsplash.com',
|
||||
'supabase.com',
|
||||
'obuldanrptloktxcffvn.supabase.co',
|
||||
'avatars.githubusercontent.com',
|
||||
'colab.research.google.com',
|
||||
'api.producthunt.com',
|
||||
'https://s3-us-west-2.amazonaws.com',
|
||||
's3-us-west-2.amazonaws.com',
|
||||
'user-images.githubusercontent.com',
|
||||
],
|
||||
},
|
||||
async headers() {
|
||||
return [
|
||||
{
|
||||
source: '/(.*)',
|
||||
headers: [
|
||||
{
|
||||
key: 'Strict-Transport-Security',
|
||||
value: '',
|
||||
},
|
||||
{
|
||||
key: 'X-Robots-Tag',
|
||||
value: 'all',
|
||||
},
|
||||
{
|
||||
key: 'X-Frame-Options',
|
||||
value: 'DENY',
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
},
|
||||
async rewrites() {
|
||||
return rewrites
|
||||
},
|
||||
async redirects() {
|
||||
return redirects
|
||||
},
|
||||
}
|
||||
|
||||
export default withMDX(nextConfig)
|
||||
@@ -18,7 +18,7 @@
|
||||
"@mdx-js/react": "^1.6.22",
|
||||
"@next/mdx": "^10.0.7",
|
||||
"@supabase/auth-ui-react": "^0.1.6",
|
||||
"@supabase/supabase-js": "^1.6.0",
|
||||
"@supabase/supabase-js": "^2.0.0-rc.10",
|
||||
"@supabase/ui": "v0.37.0-alpha.80",
|
||||
"classnames": "^2.3.1",
|
||||
"date-fns": "2.10.0",
|
||||
@@ -28,7 +28,7 @@
|
||||
"markdown-toc": "^1.2.0",
|
||||
"marked": "^4.0.12",
|
||||
"next": "latest",
|
||||
"next-mdx-remote": "^2.1.3",
|
||||
"next-mdx-remote": "^4.1.0",
|
||||
"next-seo": "^4.20.0",
|
||||
"nodemailer": "^6.7.3",
|
||||
"parse-numeric-range": "^1.3.0",
|
||||
@@ -40,10 +40,10 @@
|
||||
"react-syntax-highlighter": "^15.5.0",
|
||||
"react-tooltip": "^4.2.17",
|
||||
"react-transition-group": "^4.4.1",
|
||||
"rehype-slug": "^4.0.1",
|
||||
"remark": "^13.0.0",
|
||||
"remark-gfm": "^1.0.0",
|
||||
"remark-html": "10.0.0",
|
||||
"rehype-slug": "^5.0.1",
|
||||
"remark-gfm": "^3.0.1",
|
||||
"remark-html": "^15.0.1",
|
||||
"swiper": "^6.5.0",
|
||||
"swr": "^0.5.4",
|
||||
"typescript": "^4.0.3",
|
||||
|
||||
@@ -1,30 +1,12 @@
|
||||
import matter from 'gray-matter'
|
||||
import authors from 'lib/authors.json'
|
||||
import hydrate from 'next-mdx-remote/hydrate'
|
||||
import renderToString from 'next-mdx-remote/render-to-string'
|
||||
import React from 'react'
|
||||
import Avatar from '~/components/Avatar'
|
||||
import CodeBlock from '~/components/CodeBlock/CodeBlock'
|
||||
import ImageGrid from '~/components/ImageGrid'
|
||||
import Quote from '~/components/Quote'
|
||||
import LayoutComparison from '~/layouts/comparison'
|
||||
import mdxComponents from '~/lib/mdx/mdxComponents'
|
||||
import { mdxSerialize } from '~/lib/mdx/mdxSerialize'
|
||||
import { getAllPostSlugs, getPostdata, getSortedPosts } from '~/lib/posts'
|
||||
|
||||
// import all components used in blog articles here
|
||||
// for instance, if you use a button, you must add `Button` in the components object below.
|
||||
const components = {
|
||||
CodeBlock,
|
||||
Quote,
|
||||
Avatar,
|
||||
code: (props: any) => {
|
||||
return <CodeBlock {...props} />
|
||||
},
|
||||
ImageGrid,
|
||||
}
|
||||
|
||||
// plugins for next-mdx-remote
|
||||
const gfm = require('remark-gfm')
|
||||
const slug = require('rehype-slug')
|
||||
|
||||
// table of contents extractor
|
||||
const toc = require('markdown-toc')
|
||||
@@ -42,14 +24,7 @@ export async function getStaticProps({ params }: any) {
|
||||
const postContent = await getPostdata(filePath, '_alternatives')
|
||||
const { data, content } = matter(postContent)
|
||||
|
||||
const mdxSource: any = await renderToString(content, {
|
||||
components,
|
||||
scope: data,
|
||||
mdxOptions: {
|
||||
remarkPlugins: [gfm],
|
||||
rehypePlugins: [slug],
|
||||
},
|
||||
})
|
||||
const mdxSource: any = await mdxSerialize(content)
|
||||
|
||||
const relatedPosts = getSortedPosts('_alternatives', 5, mdxSource.scope.tags)
|
||||
|
||||
@@ -72,6 +47,7 @@ export async function getStaticProps({ params }: any) {
|
||||
blog: {
|
||||
slug: `${params.slug}`,
|
||||
content: mdxSource,
|
||||
source: content,
|
||||
...data,
|
||||
toc: toc(content, { maxdepth: data.toc_depth ? data.toc_depth : 2 }),
|
||||
},
|
||||
@@ -81,7 +57,8 @@ export async function getStaticProps({ params }: any) {
|
||||
|
||||
function BlogPostPage(props: any) {
|
||||
// @ts-ignore
|
||||
const content = hydrate(props.blog.content, { components })
|
||||
// const content = hydrate(props.blog.content, { components })
|
||||
// const content = props.blog.content
|
||||
const authorArray = props.blog.author.split(',')
|
||||
|
||||
const author = []
|
||||
@@ -95,7 +72,7 @@ function BlogPostPage(props: any) {
|
||||
)
|
||||
}
|
||||
|
||||
return <LayoutComparison components={components} props={props} gfm={gfm} slug={slug} />
|
||||
return <LayoutComparison components={mdxComponents()} props={props} />
|
||||
}
|
||||
|
||||
export default BlogPostPage
|
||||
@@ -112,9 +112,9 @@ function AuthPage() {
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
{AuthProviders.map((auth) => {
|
||||
{AuthProviders.map((auth, i) => {
|
||||
return (
|
||||
<div className="flex w-fit items-center">
|
||||
<div className="flex w-fit items-center" key={i}>
|
||||
<Image
|
||||
src={`${basePath}/images/product/auth/${auth.name}-icon.svg`}
|
||||
alt={`${auth.name} auth login icon`}
|
||||
|
||||
@@ -167,9 +167,9 @@ function FeaturedThumb(blog: PostTypes) {
|
||||
</div>
|
||||
|
||||
<div className="grid w-max grid-flow-col grid-rows-4 gap-4">
|
||||
{author.map((author: any) => {
|
||||
{author.map((author: any, i: number) => {
|
||||
return (
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="flex items-center space-x-3" key={i}>
|
||||
{author.author_image_url && (
|
||||
<div className="relative h-10 w-10 overflow-auto">
|
||||
<Image
|
||||
|
||||
+17
-132
@@ -1,61 +1,19 @@
|
||||
import { Badge, Card, Divider, IconChevronLeft, IconFile, Space } from '@supabase/ui'
|
||||
import { Badge, Divider, IconChevronLeft, IconFile } from '@supabase/ui'
|
||||
import matter from 'gray-matter'
|
||||
import authors from 'lib/authors.json'
|
||||
import hydrate from 'next-mdx-remote/hydrate'
|
||||
import renderToString from 'next-mdx-remote/render-to-string'
|
||||
import { MDXRemote } from 'next-mdx-remote'
|
||||
import { NextSeo } from 'next-seo'
|
||||
import Link from 'next/link'
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import React from 'react'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
import CodeBlock from '~/components/CodeBlock/CodeBlock'
|
||||
import CTABanner from '~/components/CTABanner'
|
||||
import DefaultLayout from '~/components/Layouts/Default'
|
||||
import Quote from '~/components/Quote'
|
||||
import Avatar from '~/components/Avatar'
|
||||
import ImageGrid from '~/components/ImageGrid'
|
||||
import { generateReadingTime } from '~/lib/helpers'
|
||||
import mdxComponents from '~/lib/mdx/mdxComponents'
|
||||
import { mdxSerialize } from '~/lib/mdx/mdxSerialize'
|
||||
import { getAllPostSlugs, getPostdata, getSortedPosts } from '~/lib/posts'
|
||||
|
||||
const ignoreClass = 'ignore-on-export'
|
||||
// import all components used in blog articles here
|
||||
// for instance, if you use a button, you must add `Button` in the components object below.
|
||||
const components = {
|
||||
CodeBlock,
|
||||
Quote,
|
||||
Avatar,
|
||||
code: (props: any) => {
|
||||
if (props.className !== ignoreClass) {
|
||||
return <CodeBlock {...props} />
|
||||
} else {
|
||||
return <code {...props} />
|
||||
}
|
||||
},
|
||||
ImageGrid,
|
||||
img: (props: any) => {
|
||||
if (props.className !== ignoreClass) {
|
||||
return (
|
||||
<div
|
||||
className="
|
||||
next-image--dynamic-fill
|
||||
to-scale-400
|
||||
from-scale-500 rounded-lg
|
||||
border bg-gradient-to-r
|
||||
"
|
||||
>
|
||||
<Image {...props} className="next-image--dynamic-fill rounded-md border" layout="fill" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return <img {...props} />
|
||||
},
|
||||
}
|
||||
|
||||
// plugins for next-mdx-remote
|
||||
const gfm = require('remark-gfm')
|
||||
const slug = require('rehype-slug')
|
||||
|
||||
// table of contents extractor
|
||||
const toc = require('markdown-toc')
|
||||
|
||||
@@ -72,14 +30,7 @@ export async function getStaticProps({ params }: any) {
|
||||
const postContent = await getPostdata(filePath, '_blog')
|
||||
const { data, content } = matter(postContent)
|
||||
|
||||
const mdxSource: any = await renderToString(content, {
|
||||
components,
|
||||
scope: data,
|
||||
mdxOptions: {
|
||||
remarkPlugins: [gfm],
|
||||
rehypePlugins: [slug],
|
||||
},
|
||||
})
|
||||
const mdxSource: any = await mdxSerialize(content)
|
||||
|
||||
const relatedPosts = getSortedPosts('_blog', 5, mdxSource.scope.tags)
|
||||
|
||||
@@ -102,6 +53,7 @@ export async function getStaticProps({ params }: any) {
|
||||
blog: {
|
||||
slug: `${params.slug}`,
|
||||
content: mdxSource,
|
||||
source: content,
|
||||
...data,
|
||||
toc: toc(content, { maxdepth: data.toc_depth ? data.toc_depth : 2 }),
|
||||
},
|
||||
@@ -110,8 +62,7 @@ export async function getStaticProps({ params }: any) {
|
||||
}
|
||||
|
||||
function BlogPostPage(props: any) {
|
||||
// @ts-ignore
|
||||
const content = hydrate(props.blog.content, { components })
|
||||
const content = props.blog.content
|
||||
const authorArray = props.blog.author.split(',')
|
||||
|
||||
const author = []
|
||||
@@ -166,7 +117,7 @@ function BlogPostPage(props: any) {
|
||||
<div>
|
||||
<p className="text-scale-1200 mb-4">On this page</p>
|
||||
<div className="prose-toc">
|
||||
<ReactMarkdown plugins={[gfm]}>{props.blog.toc.content}</ReactMarkdown>
|
||||
<ReactMarkdown>{props.blog.toc.content}</ReactMarkdown>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -235,12 +186,12 @@ function BlogPostPage(props: any) {
|
||||
<div className="text-scale-900 flex space-x-3 text-sm">
|
||||
<p>{props.blog.date}</p>
|
||||
<p>•</p>
|
||||
<p>{generateReadingTime(props.blog.content.renderedOutput)}</p>
|
||||
<p>{generateReadingTime(props.blog.source)}</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3 pt-6 md:flex-row md:gap-0 lg:gap-3">
|
||||
{author.map((author: any) => {
|
||||
{author.map((author: any, i: number) => {
|
||||
return (
|
||||
<div className="mr-4 w-max">
|
||||
<div className="mr-4 w-max" key={i}>
|
||||
<Link href={author.author_url}>
|
||||
<a className="cursor-pointer">
|
||||
<div className="flex items-center gap-3">
|
||||
@@ -284,7 +235,9 @@ function BlogPostPage(props: any) {
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<article className={['prose prose-docs'].join(' ')}>{content}</article>
|
||||
<article className={['prose prose-docs'].join(' ')}>
|
||||
<MDXRemote {...content} components={mdxComponents()} />
|
||||
</article>
|
||||
<div className="py-16">
|
||||
<div className="text-scale-900 dark:text-scale-1000 text-sm">
|
||||
Share this article
|
||||
@@ -348,8 +301,8 @@ function BlogPostPage(props: any) {
|
||||
<p className="text-scale-1200 text-sm">Related articles</p>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
{props.relatedPosts.map((post: any) => (
|
||||
<Link href={`${post.path}`} as={`${post.path}`}>
|
||||
{props.relatedPosts.map((post: any, i: number) => (
|
||||
<Link href={`${post.path}`} as={`${post.path}`} key={i}>
|
||||
<div>
|
||||
<p className="cursor-pointer">
|
||||
<div className="flex gap-2">
|
||||
@@ -381,78 +334,10 @@ function BlogPostPage(props: any) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* <div className="container px-8 sm:px-16 xl:px-20 py-16 mx-auto">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<div className="py-4 grid grid-cols-12 lg:gap-16">
|
||||
<div className="col-span-12 lg:col-span-8">
|
||||
{props.blog.thumb && (
|
||||
<img
|
||||
src={'/images/blog/' + props.blog.thumb}
|
||||
className="object-cover -mt-32 mb-8 border dark:border-gray-600"
|
||||
style={{ maxHeight: '520px', width: '100%' }}
|
||||
/>
|
||||
)}
|
||||
<div className="lg:hidden">{toc}</div>
|
||||
<article className={blogStyles['article']}>
|
||||
<Typography>{content}</Typography>
|
||||
</article>
|
||||
</div>
|
||||
<div className="col-span-12 lg:col-span-4">
|
||||
<Space direction="vertical" size={8} className="lg:mb-16 lg:top-16 lg:sticky">
|
||||
<div className="hidden lg:block">{toc}</div>
|
||||
<div>
|
||||
<h5 className="mb-4">
|
||||
Related articles
|
||||
</h5>
|
||||
<Space direction="vertical">
|
||||
{props.relatedPosts.map((post: any) => (
|
||||
<Link href={`${post.path}`} as={`${post.path}`}>
|
||||
<div>
|
||||
<p className="cursor-pointer">
|
||||
<Space>
|
||||
<IconFile size={'small'} style={{ minWidth: '1.2rem' }} />
|
||||
<span className="hover:text-gray-900 dark:hover:text-white">
|
||||
{post.title}
|
||||
</span>
|
||||
</Space>
|
||||
</p>
|
||||
<Divider light className="mt-2" />
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
<Link href={`/blog`} as={`/blog`}>
|
||||
<div>
|
||||
<p>
|
||||
<span className="hover:text-gray-900 dark:hover:text-white cursor-pointer">
|
||||
View all posts
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</Link>
|
||||
</Space>
|
||||
</div>
|
||||
</Space>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid lg:grid-cols-2 gap-8 py-8">
|
||||
<div>{props.prevPost && <NextCard post={props.prevPost} label="Last post" />}</div>
|
||||
<div>
|
||||
{props.nextPost && (
|
||||
<NextCard post={props.nextPost} label="Next post" className="text-right" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div> */}
|
||||
|
||||
<CTABanner />
|
||||
</DefaultLayout>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
// function BlogPostPage() {
|
||||
// return <h1>blog post</h1>
|
||||
// }
|
||||
|
||||
export default BlogPostPage
|
||||
@@ -45,8 +45,11 @@ function TagBlogsPage(props: Props) {
|
||||
</div>
|
||||
<ol className="grid grid-cols-12 gap-8 py-16 lg:gap-16">
|
||||
{blogs.map((blog: PostTypes, idx: number) => (
|
||||
<div className="col-span-12 mb-16 md:col-span-12 lg:col-span-6 xl:col-span-4">
|
||||
<BlogListItem post={blog} key={idx} />
|
||||
<div
|
||||
className="col-span-12 mb-16 md:col-span-12 lg:col-span-6 xl:col-span-4"
|
||||
key={idx}
|
||||
>
|
||||
<BlogListItem post={blog} />
|
||||
</div>
|
||||
))}
|
||||
</ol>
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
import { IconGitCommit } from '@supabase/ui'
|
||||
import dayjs from 'dayjs'
|
||||
import { MDXRemote, MDXRemoteSerializeResult } from 'next-mdx-remote'
|
||||
import { NextSeo } from 'next-seo'
|
||||
import CTABanner from '~/components/CTABanner'
|
||||
import DefaultLayout from '~/components/Layouts/Default'
|
||||
import mdxComponents from '~/lib/mdx/mdxComponents'
|
||||
import { mdxSerialize } from '~/lib/mdx/mdxSerialize'
|
||||
|
||||
export async function getStaticProps() {
|
||||
const response = await fetch('https://api.github.com/repos/supabase/supabase/releases')
|
||||
const data = await response.json()
|
||||
|
||||
if (!data) {
|
||||
return {
|
||||
props: {
|
||||
notFound: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const changelogRenderToString = await Promise.all(
|
||||
data.map(async (item: any): Promise<any> => {
|
||||
const mdxSource: MDXRemoteSerializeResult = await mdxSerialize(item.body)
|
||||
return {
|
||||
...item,
|
||||
source: mdxSource,
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
return {
|
||||
props: {
|
||||
changelog: changelogRenderToString,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function ChangelogPage(props: any) {
|
||||
return (
|
||||
<>
|
||||
<NextSeo
|
||||
title={'Changelog'}
|
||||
openGraph={{
|
||||
title: 'Changelog',
|
||||
description: 'props.blog.description',
|
||||
url: `https://supabase.com/changelog`,
|
||||
type: 'article',
|
||||
article: {
|
||||
//
|
||||
// to do: add expiration and modified dates
|
||||
// https://github.com/garmeeh/next-seo#article
|
||||
// publishedTime: props.blog.date,
|
||||
},
|
||||
// images: [
|
||||
// {
|
||||
// url: `https://supabase.com${basePath}/images/blog/${
|
||||
// props.blog.image ? props.blog.image : props.blog.thumb
|
||||
// }`,
|
||||
// },
|
||||
// ],
|
||||
}}
|
||||
/>
|
||||
<DefaultLayout>
|
||||
<div
|
||||
className="
|
||||
container mx-auto flex flex-col
|
||||
gap-20
|
||||
px-8 py-10 sm:px-16
|
||||
xl:px-20
|
||||
"
|
||||
>
|
||||
{/* Title and description */}
|
||||
<div className="py-10">
|
||||
<h1 className="h1">Changelog</h1>
|
||||
<p className="text-scale-900 text-lg">New updates and product improvements</p>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div>
|
||||
{props.changelog.map((changelog: any, i: number) => {
|
||||
const date = changelog.published_at.split('T')
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
className="border-scale-400 grid border-l pb-10 lg:grid-cols-12 lg:gap-8"
|
||||
>
|
||||
<div
|
||||
className="col-span-12 mb-8 self-start lg:sticky lg:top-0 lg:col-span-4 lg:-mt-32 lg:pt-32
|
||||
"
|
||||
>
|
||||
<div className="flex w-full items-baseline gap-6">
|
||||
<div className="bg-scale-100 dark:bg-scale-500 border-scale-400 dark:border-scale-600 text-scale-900 -ml-2.5 flex h-5 w-5 items-center justify-center rounded border drop-shadow-sm">
|
||||
<IconGitCommit size={14} strokeWidth={1.5} />
|
||||
</div>
|
||||
<div className="flex w-full flex-col gap-1">
|
||||
{changelog.name && (
|
||||
<h3 className="text-scale-1200 text-2xl">{changelog.name}</h3>
|
||||
)}
|
||||
<p className="text-scale-900 text-lg">
|
||||
{dayjs(date[0]).format('MMM D, YYYY')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-8 ml-8 lg:ml-0">
|
||||
<article className="prose prose-docs max-w-none">
|
||||
<MDXRemote {...changelog.source} components={mdxComponents('blog')} />
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CTABanner />
|
||||
</DefaultLayout>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default ChangelogPage
|
||||
@@ -139,9 +139,12 @@ function Database() {
|
||||
<SectionContainer>
|
||||
<div className="col-span-12 mb-10 space-y-12 lg:col-span-3 lg:mb-0 ">
|
||||
<div className="grid gap-8 rounded md:grid-cols-2 xl:grid-cols-4">
|
||||
{featureBlocks.map((item) => {
|
||||
{featureBlocks.map((item, i) => {
|
||||
return (
|
||||
<div className="bg-scale-100 dark:bg-scale-300 group flex flex-col gap-4 rounded border px-8 py-6">
|
||||
<div
|
||||
className="bg-scale-100 dark:bg-scale-300 group flex flex-col gap-4 rounded border px-8 py-6"
|
||||
key={i}
|
||||
>
|
||||
<div className="bg-scale-300 dark:bg-scale-500 text-scale-1200 group-hover:text-brand-900 flex h-12 w-12 items-center justify-center rounded-md border transition-all group-hover:scale-105">
|
||||
{item.icon ? item.icon : <IconCode strokeWidth={2} />}
|
||||
</div>
|
||||
|
||||
@@ -201,7 +201,7 @@ function Partner({ partner }: { partner: Partner }) {
|
||||
|
||||
// This function gets called at build time
|
||||
export const getStaticPaths: GetStaticPaths = async () => {
|
||||
const { data: slugs } = await supabase.from<Partner>('partners').select('slug')
|
||||
const { data: slugs } = await supabase.from('partners').select('slug')
|
||||
|
||||
const paths: {
|
||||
params: { slug: string }
|
||||
@@ -222,7 +222,7 @@ export const getStaticPaths: GetStaticPaths = async () => {
|
||||
// This also gets called at build time
|
||||
export const getStaticProps: GetStaticProps = async ({ params }) => {
|
||||
let { data: partner } = await supabase
|
||||
.from<Partner>('partners')
|
||||
.from('partners')
|
||||
.select('*')
|
||||
.eq('slug', params!.slug as string)
|
||||
.single()
|
||||
|
||||
@@ -10,7 +10,7 @@ import TileGrid from '../../../components/Partners/TileGrid'
|
||||
|
||||
export async function getStaticProps() {
|
||||
const { data: partners } = await supabase
|
||||
.from<Partner>('partners')
|
||||
.from('partners')
|
||||
.select('*')
|
||||
.eq('approved', true)
|
||||
.eq('type', 'expert')
|
||||
|
||||
Loaded 100 of 1265 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user