mirror of
https://github.com/supabase/supabase.git
synced 2026-10-07 02:15:05 +03:00
55 files changed
+800
-1368
No files matched your search
+19
-7
@@ -1,8 +1,20 @@
|
||||
node_modules/
|
||||
.vercel
|
||||
.next
|
||||
.env.local
|
||||
.env.production
|
||||
.env.dev
|
||||
.env.*
|
||||
# Root folder of this turbo monorepo
|
||||
# Doesn't send useless context to Docker daemon
|
||||
.git
|
||||
.github
|
||||
about
|
||||
apps
|
||||
docker
|
||||
examples
|
||||
i18n
|
||||
**/node_modules
|
||||
spec
|
||||
supabase
|
||||
tests
|
||||
|
||||
**/.next
|
||||
.vercel
|
||||
.env.*
|
||||
|
||||
.dockerignore
|
||||
studio/Dockerfile
|
||||
@@ -14,12 +14,22 @@ import { useInView } from 'react-intersection-observer'
|
||||
*/
|
||||
|
||||
interface Props {
|
||||
tag: string
|
||||
tag?: string
|
||||
parseAnchors?: boolean
|
||||
customAnchor?: string
|
||||
}
|
||||
|
||||
const Heading: React.FC<Props> = ({ tag, children }) => {
|
||||
/**
|
||||
* This TOC is used in .mdx files and in .tsx files.
|
||||
* In mdx files, we need to parse the content and format them to match the
|
||||
* expected tocList format (text, link,level).
|
||||
*
|
||||
* In tsx files, we can generate this tocList directly. For these files, we don't
|
||||
* need to parse the <a> and generate anchors
|
||||
*/
|
||||
const Heading: React.FC<Props> = ({ tag, parseAnchors = false, customAnchor, children }) => {
|
||||
const HeadingTag = `${tag}` as any
|
||||
const anchor = getAnchor(children)
|
||||
const anchor = customAnchor ? customAnchor : getAnchor(children)
|
||||
const link = `#${anchor}`
|
||||
|
||||
const { ref } = useInView({
|
||||
@@ -33,7 +43,7 @@ const Heading: React.FC<Props> = ({ tag, children }) => {
|
||||
|
||||
return (
|
||||
<HeadingTag id={anchor} ref={ref} className="group scroll-mt-24">
|
||||
{removeAnchor(children)}
|
||||
{parseAnchors ? removeAnchor(children) : children}
|
||||
{anchor && (
|
||||
<a href={link} className="ml-2 opacity-0 group-hover:opacity-100 transition">
|
||||
#
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
If you upload additional profile photos, they'll accumulate
|
||||
in the `avatars` bucket because of their random names with only the latest being referenced
|
||||
from `public.profiles` and the older versions getting orphaned.
|
||||
|
||||
To automatically remove obsolete storage objects, extend the database
|
||||
triggers. Note that it is not sufficient to delete the objects from the
|
||||
`storage.objects` table because that would orphan and leak the actual storage objects in
|
||||
the S3 backend. Instead, invoke the storage API within Postgres via the `http` extension.
|
||||
|
||||
Enable the [http extension for the `extensions` schema](https://app.supabase.com/project/_/database/extensions) in the Dashboard.
|
||||
Then, define the following SQL functions in the SQL Editor to delete
|
||||
storage objects via the API:
|
||||
|
||||
```SQL
|
||||
create or replace function delete_storage_object(bucket text, object text, out status int, out content varchar)
|
||||
returns record
|
||||
language 'plpgsql'
|
||||
security definer
|
||||
as $$
|
||||
declare
|
||||
project_url varchar := '<YOURPROJECTURL>';
|
||||
service_role_key varchar := '<YOURSERVICEROLEKEY>'; -- full access needed
|
||||
url varchar := project_url||'/storage/v1/object/'||bucket||'/'||object;
|
||||
begin
|
||||
select
|
||||
into status, content
|
||||
result.status::int, result.content::varchar
|
||||
FROM extensions.http((
|
||||
'DELETE',
|
||||
url,
|
||||
ARRAY[extensions.http_header('authorization','Bearer '||service_role_key)],
|
||||
NULL,
|
||||
NULL)::extensions.http_request) as result;
|
||||
end;
|
||||
$$;
|
||||
|
||||
create or replace function delete_avatar(avatar_url text, out status int, out content varchar)
|
||||
returns record
|
||||
language 'plpgsql'
|
||||
security definer
|
||||
as $$
|
||||
begin
|
||||
select
|
||||
into status, content
|
||||
result.status, result.content
|
||||
from public.delete_storage_object('avatars', avatar_url) as result;
|
||||
end;
|
||||
$$;
|
||||
|
||||
```
|
||||
|
||||
Next, add a trigger that removes any obsolete avatar whenever the
|
||||
profile is updated or deleted:
|
||||
|
||||
```SQL
|
||||
create or replace function delete_old_avatar()
|
||||
returns trigger
|
||||
language 'plpgsql'
|
||||
security definer
|
||||
as $$
|
||||
declare
|
||||
status int;
|
||||
content varchar;
|
||||
begin
|
||||
if coalesce(old.avatar_url, '') <> ''
|
||||
and (tg_op = 'DELETE' or (old.avatar_url <> new.avatar_url)) then
|
||||
select
|
||||
into status, content
|
||||
result.status, result.content
|
||||
from public.delete_avatar(old.avatar_url) as result;
|
||||
if status <> 200 then
|
||||
raise warning 'Could not delete avatar: % %', status, content;
|
||||
end if;
|
||||
end if;
|
||||
if tg_op = 'DELETE' then
|
||||
return old;
|
||||
end if;
|
||||
return new;
|
||||
end;
|
||||
$$;
|
||||
|
||||
create trigger before_profile_changes
|
||||
before update of avatar_url or delete on public.profiles
|
||||
for each row execute function public.delete_old_avatar();
|
||||
|
||||
```
|
||||
|
||||
Finally, delete the `public.profile` row before a user is deleted.
|
||||
If this step is omitted, you won't be able to delete users without
|
||||
first manually deleting their avatar image.
|
||||
|
||||
```SQL
|
||||
create or replace function delete_old_profile()
|
||||
returns trigger
|
||||
language 'plpgsql'
|
||||
security definer
|
||||
as $$
|
||||
begin
|
||||
delete from public.profiles where id = old.id;
|
||||
return old;
|
||||
end;
|
||||
$$;
|
||||
|
||||
create trigger before_delete_user
|
||||
before delete on auth.users
|
||||
for each row execute function public.delete_old_profile();
|
||||
|
||||
```
|
||||
@@ -101,7 +101,7 @@ const home = [
|
||||
level: 'reference_python',
|
||||
},
|
||||
{
|
||||
label: 'Tools Reference',
|
||||
label: 'Tools',
|
||||
},
|
||||
{
|
||||
label: 'Management API',
|
||||
@@ -112,7 +112,7 @@ const home = [
|
||||
{
|
||||
label: 'Supabase CLI',
|
||||
icon: '/img/icons/menu/reference-cli',
|
||||
href: '/reference/cli/introduction',
|
||||
href: '/guides/cli',
|
||||
level: 'reference_javascript',
|
||||
},
|
||||
],
|
||||
|
||||
@@ -49,6 +49,8 @@ export const gettingstarted = {
|
||||
{ name: 'NextJS', url: '/guides/getting-started/quickstarts/nextjs', items: [] },
|
||||
{ name: 'Flutter', url: '/guides/getting-started/quickstarts/flutter', items: [] },
|
||||
{ name: 'SvelteKit', url: '/guides/getting-started/quickstarts/sveltekit', items: [] },
|
||||
{ name: 'SolidJS', url: '/guides/getting-started/quickstarts/solidjs', items: [] },
|
||||
{ name: 'Vue', url: '/guides/getting-started/quickstarts/vue', items: [] },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -486,6 +488,25 @@ export const storage = {
|
||||
],
|
||||
}
|
||||
|
||||
export const supabase_cli = {
|
||||
icon: 'reference-cli',
|
||||
title: 'Supabase CLI',
|
||||
url: '/guides/cli',
|
||||
items: [
|
||||
{ name: 'Overview', url: '/guides/cli', items: [] },
|
||||
{ name: 'Local Development', url: '/guides/cli/local-development', items: [] },
|
||||
{ name: 'Managing Environments', url: '/guides/cli/managing-environments', items: [] },
|
||||
{
|
||||
name: 'Reference',
|
||||
url: undefined,
|
||||
items: [
|
||||
{ name: 'Commands', url: '/reference/cli/introduction', items: [] },
|
||||
{ name: 'Configuration', url: '/reference/cli/config', items: [] },
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
export const platform = {
|
||||
icon: 'platform',
|
||||
label: 'Platform',
|
||||
@@ -590,26 +611,6 @@ export const resources = {
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Supabase CLI',
|
||||
items: [
|
||||
{
|
||||
name: 'Overview',
|
||||
url: '/guides/resources/supabase-cli',
|
||||
items: [],
|
||||
},
|
||||
{
|
||||
name: 'Local Development',
|
||||
url: '/guides/resources/supabase-cli/local-development',
|
||||
items: [],
|
||||
},
|
||||
{
|
||||
name: 'Managing Environments',
|
||||
url: '/guides/resources/supabase-cli/managing-environments',
|
||||
items: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@@ -105,6 +105,9 @@ const NavigationMenu = () => {
|
||||
case url.includes(`/docs/guides/integrations`) && url:
|
||||
menuState.setMenuLevelId('integrations')
|
||||
break
|
||||
case url.includes(`/docs/guides/cli`) && url:
|
||||
menuState.setMenuLevelId('supabase_cli')
|
||||
break
|
||||
// JS v1
|
||||
case url.includes(`/docs/reference/javascript/v1`) && url:
|
||||
menuState.setMenuLevelId('reference_javascript_v1')
|
||||
@@ -125,6 +128,9 @@ const NavigationMenu = () => {
|
||||
case url.includes(`/docs/reference/python`) && url:
|
||||
menuState.setMenuLevelId('reference_python_v2')
|
||||
break
|
||||
case url.includes(`/docs/reference/cli/config`) && url:
|
||||
menuState.setMenuLevelId('supabase_cli')
|
||||
break
|
||||
case url.includes(`/docs/reference/cli`) && url:
|
||||
menuState.setMenuLevelId('reference_cli')
|
||||
break
|
||||
@@ -164,6 +170,7 @@ const NavigationMenu = () => {
|
||||
const isFunctionsActive = 'functions' === level
|
||||
const isRealtimeActive = 'realtime' === level
|
||||
const isStorageActive = 'storage' === level
|
||||
const issupabase_cliActive = 'supabase_cli' === level
|
||||
const isPlatformActive = 'platform' === level
|
||||
const isResourcesActive = 'resources' === level
|
||||
const isSelfHosting = 'self_hosting' === level
|
||||
@@ -191,6 +198,7 @@ const NavigationMenu = () => {
|
||||
<NavigationMenuGuideList id={'functions'} active={isFunctionsActive} />
|
||||
<NavigationMenuGuideList id={'realtime'} active={isRealtimeActive} />
|
||||
<NavigationMenuGuideList id={'storage'} active={isStorageActive} />
|
||||
<NavigationMenuGuideList id={'supabase_cli'} active={issupabase_cliActive} />
|
||||
<NavigationMenuGuideList id={'platform'} active={isPlatformActive} />
|
||||
<NavigationMenuGuideList id={'resources'} active={isResourcesActive} />
|
||||
<NavigationMenuGuideList id={'self_hosting'} active={isSelfHosting} />
|
||||
|
||||
@@ -179,7 +179,7 @@ const Content: React.FC<INavigationMenuRefList> = ({ id, lib, commonSections, sp
|
||||
if (!sections) console.error('no common sections imported')
|
||||
|
||||
const menu = NavItems[id]
|
||||
console.log(menu)
|
||||
//console.log(menu)
|
||||
const databaseFunctions = find(sections, { title: 'Database' })
|
||||
? find(sections, { title: 'Database' }).items
|
||||
: []
|
||||
|
||||
@@ -20,6 +20,7 @@ import QuickstartIntro from './MDX/quickstart_intro.mdx'
|
||||
import ProjectSetup from './MDX/project_setup.mdx'
|
||||
import SocialProviderSetup from './MDX/social_provider_setup.mdx'
|
||||
import SocialProviderSettingsSupabase from './MDX/social_provider_settings_supabase.mdx'
|
||||
import StorageManagement from './MDX/storage_management.mdx'
|
||||
import { Mermaid } from 'mdx-mermaid/lib/Mermaid'
|
||||
import InlineCodeTag from './CustomHTMLElements/InlineCode'
|
||||
import React from 'react'
|
||||
@@ -49,6 +50,7 @@ const components = {
|
||||
ProjectSetup,
|
||||
SocialProviderSetup,
|
||||
SocialProviderSettingsSupabase,
|
||||
StorageManagement,
|
||||
Mermaid,
|
||||
Extensions,
|
||||
Alert: (props: any) => (
|
||||
|
||||
@@ -39,6 +39,10 @@ const levelsData = {
|
||||
icon: '/docs/img/icons/menu/storage',
|
||||
name: 'Storage',
|
||||
},
|
||||
supabase_cli: {
|
||||
icon: '/docs/img/icons/menu/reference-cli',
|
||||
name: 'Supabase CLI',
|
||||
},
|
||||
platform: {
|
||||
icon: '/docs/img/icons/menu/platform',
|
||||
name: 'Platform',
|
||||
|
||||
@@ -1,161 +0,0 @@
|
||||
import Layout from '~/layouts/DefaultGuideLayout'
|
||||
|
||||
export const meta = {
|
||||
title: 'Overview',
|
||||
description: 'An overview of Supabase\'s architecture and product principles.',
|
||||
}
|
||||
|
||||
Supabase is open source. We choose open source tools which are scalable and make them simple to use.
|
||||
|
||||
Supabase is not a 1-to-1 mapping of Firebase. While we are building many of the features that Firebase offers, we are not going about it the same way:
|
||||
our technological choices are quite different; everything we use is open source; and wherever possible, we use and support existing tools rather than developing from scratch.
|
||||
|
||||
Most notably, we use Postgres rather than a NoSQL store. This choice was deliberate. We believe that no other database offers the functionality required to compete with Firebase,
|
||||
while maintaining the scalability required to go beyond it.
|
||||
|
||||
## Architecture
|
||||
|
||||
Each Supabase project consists of several tools:
|
||||
|
||||

|
||||
|
||||
### PostgreSQL (Database)
|
||||
|
||||
PostgreSQL is the core of Supabase. We do not abstract the PostgreSQL database — you can access it and use it with full privileges. We simply provide tools which makes PostgreSQL as easy to use as Firebase.
|
||||
|
||||
- Official Docs: [postgresql.org/docs](https://www.postgresql.org/docs/current/index.html)
|
||||
- Source code: [github.com/postgres/postgres](https://github.com/postgres/postgres) (mirror)
|
||||
- License: [PostgreSQL Licence](https://www.postgresql.org/about/licence/)
|
||||
- Language: C
|
||||
|
||||
### Studio (Dashboard)
|
||||
|
||||
An open source Dashboard for managing your database and services.
|
||||
|
||||
- Official Docs: [Supabase docs](/docs)
|
||||
- Source code: [github.com/supabase/supabase](https://github.com/supabase/supabase/tree/master/studio)
|
||||
- License: [Apache 2](https://github.com/supabase/supabase/blob/master/LICENSE)
|
||||
- Language: TypeScript
|
||||
|
||||
### GoTrue (Auth)
|
||||
|
||||
A JWT-based API for managing users and issuing access tokens. This integrates with PostgreSQL's Row Level Security and the API servers.
|
||||
|
||||
- Official Docs: [Supabase Auth reference docs](/docs/reference/auth)
|
||||
- Source code: [github.com/supabase/gotrue](https://github.com/supabase/gotrue)
|
||||
- License: [MIT](https://github.com/supabase/gotrue/blob/master/LICENSE)
|
||||
- Language: Go
|
||||
|
||||
### PostgREST (API)
|
||||
|
||||
A standalone web server that turns your PostgreSQL database directly into a RESTful API.
|
||||
We use this with our [pg_graphql](https://github.com/supabase/pg_graphql) extension to provide a GraphQL API.
|
||||
|
||||
- Official Docs: [postgrest.org](https://postgrest.org/)
|
||||
- Source code: [github.com/PostgREST/postgrest](https://github.com/PostgREST/postgrest)
|
||||
- License: [MIT](https://github.com/PostgREST/postgrest/blob/main/LICENSE)
|
||||
- Language: Haskel
|
||||
|
||||
### Realtime (API & multiplayer)
|
||||
|
||||
A scalable websocket engine for managing user Presence, broadcasting messages, and streaming database changes.
|
||||
|
||||
- Official Docs: [Supabase Realtime docs](/docs/guides/realtime)
|
||||
- Source code: [github.com/supabase/realtime](https://github.com/supabase/realtime)
|
||||
- License: [Apache 2](https://github.com/supabase/realtime/blob/main/LICENSE)
|
||||
- Language: Elixir
|
||||
|
||||
### Storage API (large file storage)
|
||||
|
||||
An S3-compatible object storage service that stores metadata in Postgres.
|
||||
|
||||
- Official Docs: [Supabase Storage reference docs](/docs/reference/storage)
|
||||
- Source code: [github.com/supabase/storage-api](https://github.com/supabase/storage-api)
|
||||
- License: [Apache 2.0](https://github.com/supabase/storage-api/blob/master/LICENSE)
|
||||
- Language: NodeJS / TypeScript
|
||||
|
||||
### Deno (Edge Functions)
|
||||
|
||||
A modern runtime for JavaScript and TypeScript.
|
||||
|
||||
- Official Docs: [deno.land](https://deno.land/)
|
||||
- Source code: [github.com/denoland/deno](https://github.com/denoland/deno)
|
||||
- License: [MIT](https://github.com/denoland/deno/blob/main/LICENSE.md)
|
||||
- Language: TypeScript / Rust
|
||||
|
||||
### postgres-meta (Database management)
|
||||
|
||||
A RESTful API for managing your Postgres. Fetch tables, add roles, and run queries.
|
||||
|
||||
- Official Docs: [supabase.github.io/postgres-meta](https://supabase.github.io/postgres-meta/)
|
||||
- Source code: [github.com/supabase/postgres-meta](https://github.com/supabase/postgres-meta)
|
||||
- License: [Apache 2.0](https://github.com/supabase/postgres-meta/blob/master/LICENSE)
|
||||
- Language: NodeJS / TypeScript
|
||||
|
||||
### PgBouncer
|
||||
|
||||
A lightweight connection pooler for PostgreSQL. This is useful for connecting to Postgres when using Serverless functions.
|
||||
|
||||
- Official Docs: [pgbouncer.org](http://www.pgbouncer.org/)
|
||||
- Source code: [pgbouncer/pgbouncer](https://github.com/pgbouncer/pgbouncer)
|
||||
- License: [ISC](https://github.com/pgbouncer/pgbouncer/blob/master/COPYRIGHT)
|
||||
- Language: C
|
||||
|
||||
### Kong (API Gateway)
|
||||
|
||||
A cloud-native API gateway, built on top of Nginx.
|
||||
|
||||
- Official Docs: [docs.konghq.com](https://docs.konghq.com/)
|
||||
- Source code: [github.com/kong/kong](https://github.com/kong/kong)
|
||||
- License: [Apache 2.0](https://github.com/Kong/kong/blob/master/LICENSE)
|
||||
- Language: Lua
|
||||
|
||||
## Product Principles
|
||||
|
||||
It is our goal to provide an architecture that any large-scale company would design for themselves,
|
||||
and then provide tooling around that architecture that is easy-to-use for indie-developers and small teams.
|
||||
|
||||
We use a series of principles to ensure that scalability and usability are never mutually exclusive:
|
||||
|
||||
### Everything works in isolation
|
||||
|
||||
Each system must work as a standalone tool with as few moving parts as possible.
|
||||
The litmus test for this is: "Can a user run this product with nothing but a Postgres database?"
|
||||
|
||||
### Everything is integrated
|
||||
|
||||
Supabase is composable. Even though every product works in isolation, each product on the platform needs to 10x the other products.
|
||||
For integration, each tool should expose an API and Webhooks.
|
||||
|
||||
### Everything is extensible
|
||||
|
||||
We're deliberate about adding a new tool, and prefer instead to extend an existing one.
|
||||
This is the opposite of many cloud providers whose product offering expands into niche use-cases. We provide _primitives_ for developers, which allow them to achieve any goal.
|
||||
Less, but better.
|
||||
|
||||
### Everything is portable
|
||||
|
||||
To avoid lock-in, we make it easy to migrate in and out. Our cloud offering is compatible with our self-hosted product.
|
||||
We use existing standards to increase portability (like pg_dump an CSV files). If a new standard emerges which competes with a "Supabase" approach, we will deprecate the approach in favor of the standard.
|
||||
This forces us compete on experience. We aim to be the best Postgres hosting service.
|
||||
|
||||
### Play the long game
|
||||
|
||||
We sacrifice short-term wins for long-term gains. For example, it is tempting to run a fork of Postgres with additional functionality which only our customers need.
|
||||
Instead, we prefer to support efforts to upstream missing functionality so that the entire community benefits. This has the additional benefit of ensuring portability and longevity.
|
||||
|
||||
### Build for developers
|
||||
|
||||
"Developers" are a specific profile of user: they are _builders_.
|
||||
When assessing impact as a function of effort, developers have a large efficiency due to the type of products and systems they can build.
|
||||
As the profile of a developer changes over time, Supabase will continue to evolve the product to fit this evolving profile.
|
||||
|
||||
### Support existing tools
|
||||
|
||||
Supabase supports existing tools and communities wherever possible. Supabase is more like a "community of communities" - each tool typically has its own community
|
||||
which we work with.
|
||||
Open source is something we approach [collaboratively](https://supabase.com/blog/supabase-series-b#giving-back): we employ maintainers, sponsor projects, invest in businesses, and develop our own open source tools.
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
export default Page
|
||||
@@ -217,7 +217,7 @@ import { createClient, useQuery } from 'urql'
|
||||
// Prepare API key and Authorization header
|
||||
const headers = {
|
||||
apikey: <SUPABASE_ANON_KEY>,
|
||||
authorization: `Bearer ${<SUPABASE_ANON_KEY}`>
|
||||
authorization: `Bearer ${<SUPABASE_ANON_KEY>}`,
|
||||
}
|
||||
|
||||
// Create GraphQL client
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import Layout from '~/layouts/DefaultGuideLayout'
|
||||
|
||||
export const meta = {
|
||||
id: 'cli',
|
||||
title: 'Supabase CLI',
|
||||
description:
|
||||
'The Supabase CLI provides tools to develop your project locally and deploy to the Supabase Platform.',
|
||||
@@ -111,7 +110,8 @@ brew upgrade supabase
|
||||
|
||||
## See also
|
||||
|
||||
- [Supabase CLI Reference](/docs/reference/cli/usage)
|
||||
- [Supabase CLI Reference](/docs/reference/cli/introduction)
|
||||
- [Supabase CLI Configuration](/docs/reference/cli/config)
|
||||
- [Local Development](/docs/guides/cli/local-development)
|
||||
- [Managing Environments](/docs/guides/cli/managing-environments)
|
||||
|
||||
+1
-2
@@ -34,9 +34,8 @@ supabase login
|
||||
<Admonition type="note">
|
||||
|
||||
If you installed the Supabase CLI via NPM you may have to use `npx supabase login`.
|
||||
|
||||
</Admonition>
|
||||
|
||||
</Admonition>
|
||||
|
||||
## Initialize your project
|
||||
|
||||
File renamed without changes.
@@ -25,7 +25,7 @@ By default, Edge Functions have access to these secrets:
|
||||
|
||||
Let's create a local file for storing our secrets, and inside it we can store a secret `MY_NAME`:
|
||||
|
||||
```jsx
|
||||
```bash
|
||||
echo "MY_NAME=Yoda" >> ./supabase/.env.local
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
import Layout from '~/layouts/DefaultGuideLayout'
|
||||
import StepHikeCompact from '~/components/StepHikeCompact'
|
||||
|
||||
export const meta = {
|
||||
title: 'Use Supabase with SolidJS',
|
||||
subtitle:
|
||||
'Learn how to create a Supabase project, add some sample data to your database, and query the data from a SolidJS app.',
|
||||
breadcrumb: 'Framework Quickstarts',
|
||||
}
|
||||
|
||||
<StepHikeCompact>
|
||||
|
||||
<StepHikeCompact.Step step={1}>
|
||||
<StepHikeCompact.Details title="Set up a Supabase project with sample data">
|
||||
|
||||
[Create a new project](https://app.supabase.com) in the Supabase Dashboard.
|
||||
|
||||
After your project is ready, create a table in your Supabase database using the [SQL Editor](https://app.supabase.com/project/_/sql) in the Dashboard. Use the following SQL statement to create a `countries` table with some sample data.
|
||||
|
||||
</StepHikeCompact.Details>
|
||||
|
||||
<StepHikeCompact.Code>
|
||||
|
||||
```sql SQL_EDITOR
|
||||
-- Create the table
|
||||
CREATE TABLE countries (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL
|
||||
);
|
||||
-- Insert some sample data into the table
|
||||
INSERT INTO countries (name) VALUES ('United States');
|
||||
INSERT INTO countries (name) VALUES ('Canada');
|
||||
INSERT INTO countries (name) VALUES ('Mexico');
|
||||
````
|
||||
|
||||
</StepHikeCompact.Code>
|
||||
|
||||
</StepHikeCompact.Step>
|
||||
|
||||
<StepHikeCompact.Step step={2}>
|
||||
|
||||
<StepHikeCompact.Details title="Create a SolidJS app">
|
||||
|
||||
Create a SolidJS app using the `degit` command.
|
||||
|
||||
</StepHikeCompact.Details>
|
||||
|
||||
<StepHikeCompact.Code>
|
||||
|
||||
```bash Terminal
|
||||
npx degit solidjs/templates/js my-app
|
||||
```
|
||||
|
||||
</StepHikeCompact.Code>
|
||||
|
||||
</StepHikeCompact.Step>
|
||||
|
||||
<StepHikeCompact.Step step={3}>
|
||||
<StepHikeCompact.Details title="Install the Supabase client library">
|
||||
|
||||
The fastest way to get started is to use the `supabase-js` client library which provides a convenient interface for working with Supabase from a SolidJS app.
|
||||
|
||||
Navigate to the SolidJS app and install `supabase-js`.
|
||||
|
||||
</StepHikeCompact.Details>
|
||||
|
||||
<StepHikeCompact.Code>
|
||||
|
||||
```bash Terminal
|
||||
cd my-app && npm install @supabase/supabase-js
|
||||
```
|
||||
|
||||
</StepHikeCompact.Code>
|
||||
|
||||
</StepHikeCompact.Step>
|
||||
|
||||
<StepHikeCompact.Step step={4}>
|
||||
<StepHikeCompact.Details title="Query data from the app">
|
||||
|
||||
In `App.jsx`, create a Supabase client using your [Project URL and public API (anon) key](https://app.supabase.com/project/_/settings/api).
|
||||
|
||||
Add a `getCountries` function to fetch the data and display the query result to the page.
|
||||
|
||||
</StepHikeCompact.Details>
|
||||
<StepHikeCompact.Code>
|
||||
|
||||
|
||||
```jsx src/App.jsx
|
||||
import { createClient } from "@supabase/supabase-js";
|
||||
import { createEffect, createSignal, For } from "solid-js";
|
||||
|
||||
const supabase = createClient('https://<project>.supabase.co', '<your-anon-key>');
|
||||
|
||||
function App() {
|
||||
const [countries, setCountries] = createSignal();
|
||||
|
||||
createEffect(() => {
|
||||
getCountries();
|
||||
});
|
||||
|
||||
async function getCountries() {
|
||||
const { data } = await supabase.from("countries").select();
|
||||
setCountries(data);
|
||||
}
|
||||
|
||||
return (
|
||||
<ul>
|
||||
<For each={countries()}>{(country) => <li>{country.name}</li>}</For>
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
```
|
||||
|
||||
</StepHikeCompact.Code>
|
||||
|
||||
</StepHikeCompact.Step>
|
||||
|
||||
<StepHikeCompact.Step step={5}>
|
||||
<StepHikeCompact.Details title="Start the app">
|
||||
|
||||
Start the app and go to http://localhost:3000 in a browser and you should see the list of countries.
|
||||
|
||||
</StepHikeCompact.Details>
|
||||
|
||||
<StepHikeCompact.Code>
|
||||
|
||||
```bash Terminal
|
||||
npm run dev
|
||||
```
|
||||
|
||||
</StepHikeCompact.Code>
|
||||
|
||||
</StepHikeCompact.Step>
|
||||
</StepHikeCompact>
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} hideToc={true} />
|
||||
|
||||
export default Page
|
||||
@@ -77,7 +77,7 @@ export const meta = {
|
||||
<StepHikeCompact.Step step={4}>
|
||||
<StepHikeCompact.Details title="Create the Supabase client">
|
||||
|
||||
Create a `/src/lib` directory of your SvelteKit app, create a file called `supabaseClient.js` and add the following code to initialize the Supabase client with your [project URL and public API (anon) key](https://app.supabase.com/project/_/settings/api).
|
||||
Create a `/src/lib` directory in your SvelteKit app, create a file called `supabaseClient.js` and add the following code to initialize the Supabase client with your [project URL and public API (anon) key](https://app.supabase.com/project/_/settings/api).
|
||||
|
||||
</StepHikeCompact.Details>
|
||||
|
||||
@@ -146,7 +146,7 @@ export const meta = {
|
||||
<StepHikeCompact.Step step={6}>
|
||||
<StepHikeCompact.Details title="Start the app">
|
||||
|
||||
Start the app, go to http://localhost:5173 in a browser, and open the browser console and you should see the list of countries.
|
||||
Start the app and go to http://localhost:5173 in a browser and you should see the list of countries.
|
||||
|
||||
</StepHikeCompact.Details>
|
||||
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
import Layout from '~/layouts/DefaultGuideLayout'
|
||||
import StepHikeCompact from '~/components/StepHikeCompact'
|
||||
|
||||
export const meta = {
|
||||
title: 'Use Supabase with Vue',
|
||||
subtitle:
|
||||
'Learn how to create a Supabase project, add some sample data to your database, and query the data from a Vue app.',
|
||||
breadcrumb: 'Framework Quickstarts',
|
||||
}
|
||||
|
||||
<StepHikeCompact>
|
||||
|
||||
<StepHikeCompact.Step step={1}>
|
||||
<StepHikeCompact.Details title="Set up a Supabase project with sample data">
|
||||
|
||||
[Create a new project](https://app.supabase.com) in the Supabase Dashboard.
|
||||
|
||||
After your project is ready, create a table in your Supabase database using the [SQL Editor](https://app.supabase.com/project/_/sql) in the Dashboard. Use the following SQL statement to create a `countries` table with some sample data.
|
||||
|
||||
</StepHikeCompact.Details>
|
||||
|
||||
<StepHikeCompact.Code>
|
||||
|
||||
```sql SQL_EDITOR
|
||||
-- Create the table
|
||||
CREATE TABLE countries (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL
|
||||
);
|
||||
-- Insert some sample data into the table
|
||||
INSERT INTO countries (name) VALUES ('United States');
|
||||
INSERT INTO countries (name) VALUES ('Canada');
|
||||
INSERT INTO countries (name) VALUES ('Mexico');
|
||||
````
|
||||
|
||||
</StepHikeCompact.Code>
|
||||
|
||||
</StepHikeCompact.Step>
|
||||
|
||||
<StepHikeCompact.Step step={2}>
|
||||
|
||||
<StepHikeCompact.Details title="Create a Vue app">
|
||||
|
||||
Create a Vue app using the `npm init` command.
|
||||
|
||||
</StepHikeCompact.Details>
|
||||
|
||||
<StepHikeCompact.Code>
|
||||
|
||||
```sh Terminal
|
||||
npm init vue@latest my-app
|
||||
```
|
||||
|
||||
</StepHikeCompact.Code>
|
||||
|
||||
</StepHikeCompact.Step>
|
||||
|
||||
<StepHikeCompact.Step step={3}>
|
||||
<StepHikeCompact.Details title="Install the Supabase client library">
|
||||
|
||||
The fastest way to get started is to use the `supabase-js` client library which provides a convenient interface for working with Supabase from a Vue app.
|
||||
|
||||
Navigate to the Vue app and install `supabase-js`.
|
||||
|
||||
</StepHikeCompact.Details>
|
||||
|
||||
<StepHikeCompact.Code>
|
||||
|
||||
```bash Terminal
|
||||
cd my-app && npm install @supabase/supabase-js
|
||||
```
|
||||
|
||||
</StepHikeCompact.Code>
|
||||
|
||||
</StepHikeCompact.Step>
|
||||
|
||||
<StepHikeCompact.Step step={4}>
|
||||
<StepHikeCompact.Details title="Create the Supabase client">
|
||||
|
||||
Create a `/src/lib` directory in your Vue app, create a file called `supabaseClient.js` and add the following code to initialize the Supabase client with your [project URL and public API (anon) key](https://app.supabase.com/project/_/settings/api).
|
||||
|
||||
</StepHikeCompact.Details>
|
||||
|
||||
<StepHikeCompact.Code>
|
||||
|
||||
```js src/lib/supabaseClient.js
|
||||
import { createClient } from '@supabase/supabase-js'
|
||||
|
||||
export const supabase = createClient('https://<project>.supabase.co', '<your-anon-key>')
|
||||
```
|
||||
|
||||
</StepHikeCompact.Code>
|
||||
|
||||
</StepHikeCompact.Step>
|
||||
|
||||
<StepHikeCompact.Step step={5}>
|
||||
<StepHikeCompact.Details title="Query data from the app">
|
||||
|
||||
Replace the existing content in your `App.vue` file with the following code.
|
||||
|
||||
</StepHikeCompact.Details>
|
||||
<StepHikeCompact.Code>
|
||||
|
||||
|
||||
```vue src/App.vue
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { supabase } from './lib/supabaseClient'
|
||||
|
||||
const countries = ref([])
|
||||
|
||||
async function getCountries() {
|
||||
const { data } = await supabase.from('countries').select()
|
||||
countries.value = data
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getCountries()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ul>
|
||||
<li v-for="country in countries" :key="country.id">{{ country.name }}</li>
|
||||
</ul>
|
||||
</template>
|
||||
```
|
||||
|
||||
</StepHikeCompact.Code>
|
||||
|
||||
</StepHikeCompact.Step>
|
||||
|
||||
<StepHikeCompact.Step step={6}>
|
||||
<StepHikeCompact.Details title="Start the app">
|
||||
|
||||
Start the app and go to http://localhost:5173 in a browser and you should see the list of countries.
|
||||
|
||||
</StepHikeCompact.Details>
|
||||
|
||||
<StepHikeCompact.Code>
|
||||
|
||||
```bash Terminal
|
||||
npm run dev
|
||||
```
|
||||
|
||||
</StepHikeCompact.Code>
|
||||
|
||||
</StepHikeCompact.Step>
|
||||
</StepHikeCompact>
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} hideToc={true} />
|
||||
|
||||
export default Page
|
||||
@@ -530,6 +530,10 @@ export class AccountComponent implements OnInit {
|
||||
}
|
||||
```
|
||||
|
||||
### Storage management
|
||||
|
||||
<StorageManagement />
|
||||
|
||||
## Next steps
|
||||
|
||||
At this stage you have a fully functional application!
|
||||
|
||||
@@ -500,6 +500,10 @@ Now you will need to run the prebuild command to get the application working on
|
||||
expo prebuild
|
||||
```
|
||||
|
||||
### Storage management
|
||||
|
||||
<StorageManagement />
|
||||
|
||||
## Next steps
|
||||
|
||||
At this stage you have a fully functional application!
|
||||
|
||||
@@ -793,6 +793,10 @@ class _AccountPageState extends State<AccountPage> {
|
||||
}
|
||||
```
|
||||
|
||||
### Storage management
|
||||
|
||||
<StorageManagement />
|
||||
|
||||
Congratulations, that is it! You have now built a fully functional user management app using Flutter and Supabase!
|
||||
|
||||
## See also
|
||||
|
||||
@@ -522,6 +522,10 @@ template: `
|
||||
`
|
||||
```
|
||||
|
||||
### Storage management
|
||||
|
||||
<StorageManagement />
|
||||
|
||||
## Next steps
|
||||
|
||||
At this stage you have a fully functional application!
|
||||
|
||||
@@ -495,6 +495,10 @@ return (
|
||||
<Avatar url={profile.avatar_url} onUpload={updateProfile}></Avatar>
|
||||
```
|
||||
|
||||
### Storage management
|
||||
|
||||
<StorageManagement />
|
||||
|
||||
## Next steps
|
||||
|
||||
At this stage you have a fully functional application!
|
||||
|
||||
@@ -555,6 +555,10 @@ export default defineComponent({
|
||||
</script>
|
||||
```
|
||||
|
||||
### Storage management
|
||||
|
||||
<StorageManagement />
|
||||
|
||||
## Next steps
|
||||
|
||||
At this stage you have a fully functional application!
|
||||
|
||||
@@ -718,6 +718,10 @@ return (
|
||||
)
|
||||
```
|
||||
|
||||
### Storage management
|
||||
|
||||
<StorageManagement />
|
||||
|
||||
## Next steps
|
||||
|
||||
At this stage you have a fully functional application!
|
||||
|
||||
@@ -420,6 +420,10 @@ And then we can add the widget to the Account page:
|
||||
|
||||
That is it! You should now be able to upload a profile photo to Supabase Storage.
|
||||
|
||||
### Storage management
|
||||
|
||||
<StorageManagement />
|
||||
|
||||
## Next steps
|
||||
|
||||
At this stage you have a fully functional application!
|
||||
|
||||
@@ -397,6 +397,10 @@ return (
|
||||
)
|
||||
```
|
||||
|
||||
### Storage management
|
||||
|
||||
<StorageManagement />
|
||||
|
||||
## Next steps
|
||||
|
||||
At this stage you have a fully functional application!
|
||||
|
||||
@@ -771,6 +771,10 @@ return (
|
||||
)
|
||||
```
|
||||
|
||||
### Storage management
|
||||
|
||||
<StorageManagement />
|
||||
|
||||
## Next steps
|
||||
|
||||
At this stage you have a fully functional application!
|
||||
|
||||
@@ -415,6 +415,10 @@ return (
|
||||
)
|
||||
```
|
||||
|
||||
### Storage management
|
||||
|
||||
<StorageManagement />
|
||||
|
||||
## Next steps
|
||||
|
||||
At this stage you have a fully functional application!
|
||||
|
||||
@@ -368,6 +368,10 @@ And then we can add the widget to the Account page:
|
||||
</form>
|
||||
```
|
||||
|
||||
### Storage management
|
||||
|
||||
<StorageManagement />
|
||||
|
||||
## Next steps
|
||||
|
||||
At this stage you have a fully functional application!
|
||||
|
||||
@@ -434,6 +434,10 @@ And then we can add the widget to the Account page:
|
||||
</form>
|
||||
```
|
||||
|
||||
### Storage management
|
||||
|
||||
<StorageManagement />
|
||||
|
||||
## Next steps
|
||||
|
||||
At this stage you have a fully functional application!
|
||||
|
||||
@@ -379,6 +379,10 @@ And then we can add the widget to the Account page:
|
||||
</template>
|
||||
```
|
||||
|
||||
### Storage management
|
||||
|
||||
<StorageManagement />
|
||||
|
||||
## Next steps
|
||||
|
||||
At this stage you have a fully functional application!
|
||||
|
||||
@@ -64,20 +64,6 @@ export const meta = {
|
||||
|
||||
</div>
|
||||
|
||||
<div className="grid gap-16 md:grid-cols-2">
|
||||
|
||||
<div>
|
||||
|
||||
### Supabase CLI
|
||||
|
||||
Develop your project locally, manage your Supabase projects, handle database migrations, CI/CD workflows, and generate types from your database schema, all from the comfort of your Terminal.
|
||||
|
||||
[Learn more](/docs/guides/resources/supabase-cli)
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{/* end of container */}
|
||||
|
||||
</div>
|
||||
|
||||
@@ -117,10 +117,8 @@ using ( bucket_id = 'public' );
|
||||
-- 1. Allow logged-in access to any files in the "restricted" bucket
|
||||
create policy "Restricted Access"
|
||||
on storage.objects for select
|
||||
using (
|
||||
bucket_id = 'restricted'
|
||||
and auth.role() = 'authenticated'
|
||||
);
|
||||
to authenticated
|
||||
using ( bucket_id = 'restricted' );
|
||||
```
|
||||
|
||||
### Allow individual access to a file
|
||||
|
||||
@@ -1,159 +0,0 @@
|
||||
import Layout from '~/layouts/DefaultLayout'
|
||||
import Link from 'next/link'
|
||||
import { GlassPanel } from 'ui'
|
||||
|
||||
export const meta = {
|
||||
title: 'Tutorials',
|
||||
}
|
||||
|
||||
### Webapps
|
||||
|
||||
<div className="grid grid-cols-12 gap-6 not-prose">
|
||||
{webapps.map((item) => {
|
||||
return (
|
||||
<Link href={`/${item.href}`} key={item.title} passHref>
|
||||
<a className={'col-span-4'}>
|
||||
<GlassPanel title={item.title} span="col-span-6" background={false} icon={item.icon}>
|
||||
{item.description}
|
||||
</GlassPanel>
|
||||
</a>
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
### Mobile
|
||||
|
||||
<div className="grid grid-cols-12 gap-6 not-prose">
|
||||
{mobile.map((item) => {
|
||||
return (
|
||||
<Link href={`/${item.href}`} key={item.title} passHref>
|
||||
<a className={'col-span-4'}>
|
||||
<GlassPanel title={item.title} span="col-span-6" background={false} icon={item.icon}>
|
||||
{item.description}
|
||||
</GlassPanel>
|
||||
</a>
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
### Low code
|
||||
|
||||
<div className="grid grid-cols-12 gap-6 not-prose">
|
||||
{lowCode.map((item) => {
|
||||
return (
|
||||
<Link href={`/${item.href}`} key={item.title} passHref>
|
||||
<a className={'col-span-4'}>
|
||||
<GlassPanel title={item.title} span="col-span-6" background={false} icon={item.icon}>
|
||||
{item.description}
|
||||
</GlassPanel>
|
||||
</a>
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
export const webapps = [
|
||||
{
|
||||
title: 'NextJS',
|
||||
href: '/guides/tutorials/with-nextjs',
|
||||
description:
|
||||
'Learn how to build a user management app with NextJS and Supabase Database, Auth, and Storage functionality.',
|
||||
icon: '/docs/img/icons/nextjs-icon',
|
||||
},
|
||||
{
|
||||
title: 'React',
|
||||
href: '/guides/tutorials/with-react',
|
||||
description:
|
||||
'Learn how to build a user management app with React and Supabase Database, Auth, and Storage functionality.',
|
||||
icon: '/docs/img/icons/react-icon',
|
||||
},
|
||||
{
|
||||
title: 'Vue 3',
|
||||
href: '/guides/tutorials/with-vue-3',
|
||||
description:
|
||||
'Learn how to build a user management app with Vue 3 and Supabase Database, Auth, and Storage functionality.',
|
||||
icon: '/docs/img/icons/vuejs-icon',
|
||||
},
|
||||
{
|
||||
title: 'Nuxt 3',
|
||||
href: '/guides/tutorials/with-nuxt-3',
|
||||
description:
|
||||
'Learn how to build a user management app with Nuxt 3 and Supabase Database, Auth, and Storage functionality.',
|
||||
icon: '/docs/img/icons/nuxt-icon',
|
||||
},
|
||||
{
|
||||
title: 'Angular',
|
||||
href: '/guides/tutorials/with-angular',
|
||||
description:
|
||||
'Learn how to build a user management app with Angular and Supabase Database, Auth, and Storage functionality.',
|
||||
icon: '/docs/img/icons/angular-icon',
|
||||
},
|
||||
{
|
||||
title: 'RedwoodJS',
|
||||
href: '/guides/tutorials/with-redwoodjs',
|
||||
description:
|
||||
'Learn how to build a user management app with RedwoodJS and Supabase Database, Auth, and Storage functionality.',
|
||||
icon: '/docs/img/icons/redwood-icon',
|
||||
},
|
||||
{
|
||||
title: 'Svelte',
|
||||
href: '/guides/tutorials/with-svelte',
|
||||
description:
|
||||
'Learn how to build a user management app with Svelte and Supabase Database, Auth, and Storage functionality.',
|
||||
icon: '/docs/img/icons/svelte-icon',
|
||||
},
|
||||
{
|
||||
title: 'SvelteKit',
|
||||
href: '/guides/tutorials/with-sveltekit',
|
||||
description:
|
||||
'Learn how to build a user management app with SvelteKit and Supabase Database, Auth, and Storage functionality.',
|
||||
icon: '/docs/img/icons/svelte-icon',
|
||||
},
|
||||
]
|
||||
|
||||
export const mobile = [
|
||||
{
|
||||
title: 'Flutter',
|
||||
href: '/guides/tutorials/with-flutter',
|
||||
description:
|
||||
'Learn how to build a user management app with Flutter and Supabase Database, Auth, and Storage functionality.',
|
||||
icon: '/docs/img/icons/flutter-icon',
|
||||
},
|
||||
{
|
||||
title: 'Expo',
|
||||
href: '/guides/tutorials/with-expo',
|
||||
description:
|
||||
'Learn how to build a user management app with Expo and Supabase Database, Auth, and Storage functionality.',
|
||||
icon: '/docs/img/icons/expo-icon',
|
||||
},
|
||||
]
|
||||
|
||||
export const lowCode = [
|
||||
{
|
||||
title: 'Ionic React',
|
||||
href: '/guides/tutorials/with-ionic-react',
|
||||
description:
|
||||
'Learn how to build a user management app with Ionic React and Supabase Database, Auth, and Storage functionality.',
|
||||
icon: '/docs/img/icons/ionic-icon',
|
||||
},
|
||||
{
|
||||
title: 'Ionic Vue',
|
||||
href: '/guides/tutorials/with-ionic-vue',
|
||||
description:
|
||||
'Learn how to build a user management app with Ionic Vue and Supabase Database, Auth, and Storage functionality.',
|
||||
icon: '/docs/img/icons/ionic-icon',
|
||||
},
|
||||
{
|
||||
title: 'Ionic Angular',
|
||||
href: '/guides/tutorials/with-ionic-angular',
|
||||
description:
|
||||
'Learn how to build a user management app with Ionic Angular and Supabase Database, Auth, and Storage functionality.',
|
||||
icon: '/docs/img/icons/ionic-icon',
|
||||
},
|
||||
]
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
export default Page
|
||||
@@ -61,7 +61,7 @@ export const meta = {
|
||||
<div className="hidden">
|
||||
<Button size="small">Get started</Button>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-3 md:gap-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-3 md:gap-6">
|
||||
<Link href={`/guides/getting-started/quickstarts/reactjs`} passHref>
|
||||
<a className="no-underline">
|
||||
<IconPanel title="ReactJS" icon="/docs/img/icons/react-icon" />
|
||||
@@ -69,18 +69,28 @@ export const meta = {
|
||||
</Link>
|
||||
<Link href={`/guides/getting-started/quickstarts/nextjs`} passHref>
|
||||
<a className="no-underline">
|
||||
<IconPanel title="NextJS" icon="/docs/img/icons/nextjs-icon" />
|
||||
</a>
|
||||
<IconPanel title="NextJS" icon="/docs/img/icons/nextjs-icon" />
|
||||
</a>
|
||||
</Link>
|
||||
<Link href={`/guides/getting-started/quickstarts/flutter`} passHref>
|
||||
<a className="no-underline">
|
||||
<IconPanel title="Flutter" icon="/docs/img/icons/flutter-icon" />
|
||||
</a>
|
||||
<IconPanel title="Flutter" icon="/docs/img/icons/flutter-icon" />
|
||||
</a>
|
||||
</Link>
|
||||
<Link href={`/guides/getting-started/quickstarts/sveltekit`} passHref>
|
||||
<a className="no-underline">
|
||||
<IconPanel title="SvelteKit" icon="/docs/img/icons/svelte-icon" />
|
||||
</a>
|
||||
<IconPanel title="SvelteKit" icon="/docs/img/icons/svelte-icon" />
|
||||
</a>
|
||||
</Link>
|
||||
<Link href={`/guides/getting-started/quickstarts/solidjs`} passHref>
|
||||
<a className="no-underline">
|
||||
<IconPanel title="SolidJS" icon="/docs/img/icons/solidjs-icon" />
|
||||
</a>
|
||||
</Link>
|
||||
<Link href={`/guides/getting-started/quickstarts/vue`} passHref>
|
||||
<a className="no-underline">
|
||||
<IconPanel title="Vue" icon="/docs/img/icons/vuejs-icon" />
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
import Layout from '~/layouts/DefaultLayout'
|
||||
import Link from 'next/link'
|
||||
// import { useTheme } from 'common/Providers'
|
||||
import { GlassPanel } from 'ui'
|
||||
|
||||
{/* // export const { isDarkMode } = useTheme() */}
|
||||
|
||||
export const meta = {
|
||||
title: 'Auth',
|
||||
}
|
||||
|
||||
## Add and manage email and password, passwordless, OAuth, and mobile logins to your project through a suite of identity providers and APIs.
|
||||
|
||||
### Popular Guides
|
||||
|
||||
<div className="grid grid-cols-12 gap-6 not-prose">
|
||||
{topArticles.map((item) => {
|
||||
return (
|
||||
<Link
|
||||
href={`/${item.href}`}
|
||||
key={item.title}
|
||||
passHref
|
||||
>
|
||||
<a className={item.span ?? 'col-span-4'}>
|
||||
<GlassPanel
|
||||
title={item.title}
|
||||
header="/docs/img/cards/sample-card-header-4.svg"
|
||||
span="col-span-6"
|
||||
background={false}
|
||||
//* isDarkMode={isDarkMode}
|
||||
>
|
||||
{item.description}
|
||||
</GlassPanel>
|
||||
</a>
|
||||
</Link>
|
||||
)})}
|
||||
|
||||
</div>
|
||||
|
||||
export const topArticles = [
|
||||
{
|
||||
title: 'Enable Captcha Protection',
|
||||
header: '/docs/img/cards/sample-card-header-4.svg',
|
||||
href: '/guides/auth/auth-captcha',
|
||||
description: 'Keep your platform safe from bots and malicious scripts by implementing hCaptcha.',
|
||||
span: 'col-span-6',
|
||||
},
|
||||
{
|
||||
title: 'Auth Helpers',
|
||||
header: '/docs/img/cards/sample-card-header-4.svg',
|
||||
href: '/guides/auth/auth-helpers',
|
||||
description: 'A collection of framework-specific utilities for implementing Supabase Auth.',
|
||||
span: 'col-span-6',
|
||||
},
|
||||
{
|
||||
title: 'Email Login',
|
||||
header: '/docs/img/cards/sample-card-header-4.svg',
|
||||
href: '/guides/auth/auth-email',
|
||||
description: 'Learn how to add an email and password login flow to your platform.',
|
||||
},
|
||||
{
|
||||
title: 'Phone Login',
|
||||
header: '/docs/img/cards/sample-card-header-4.svg',
|
||||
href: '/guides/auth/phone-login',
|
||||
description: 'Learn how to add a login flow so your users can access your platform through one-time passwords via SMS.',
|
||||
},
|
||||
{
|
||||
title: 'Social Login',
|
||||
header: '/docs/img/cards/sample-card-header-4.svg',
|
||||
href: '/guides/auth/social-login',
|
||||
description: 'Learn how to add a login flow so your users can access your platform through social providers like Apple, Facebook, or Google.',
|
||||
},
|
||||
]
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
export default Page
|
||||
Whitespace-only changes.
@@ -2,16 +2,31 @@
|
||||
import specFile from '~/../../spec/cli_v1_config.yaml' assert { type: 'yml' }
|
||||
import { Parameter } from '~/lib/refGenerator/refTypes'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
import GuidesTableOfContents from '~/components/GuidesTableOfContents'
|
||||
import { Heading } from '~/components/CustomHTMLElements'
|
||||
|
||||
// Parameters are grouped on the page by tag
|
||||
const TAGS = ['general', 'auth', 'api', 'database', 'dashboard', 'local', 'edge-functions']
|
||||
|
||||
const tocList = TAGS.map((tag) =>
|
||||
specFile.parameters
|
||||
.filter((param: Parameter) => param.tags[0] === tag)
|
||||
.map((parameter) => {
|
||||
console.log('the param', parameter)
|
||||
const text = parameter.id
|
||||
const link = `#${parameter.id}`
|
||||
const level = '2'
|
||||
return { text, link, level }
|
||||
})
|
||||
).flat()
|
||||
|
||||
export default function Config() {
|
||||
return (
|
||||
<div>
|
||||
<div className="flex my-16">
|
||||
<div className="grid grid-cols-12 relative gap-4">
|
||||
<div className="relative col-span-12 md:col-span-9 transition-all ease-out duration-100">
|
||||
<div className="w-full prose">
|
||||
<h1 className="text-4xl mb-16">{specFile.info.title}</h1>
|
||||
<h1 className="">CLI configuration</h1>
|
||||
<div className="max-w-xs w-32 h-[1px] bg-gradient-to-r from-brand-800 to-brand-900 my-8"></div>
|
||||
<ReactMarkdown>{specFile.info.description}</ReactMarkdown>
|
||||
<div>
|
||||
{TAGS.map((tag) =>
|
||||
@@ -22,11 +37,11 @@ export default function Config() {
|
||||
{index === 0 && <h2 className="text-xl capitalize">{tag}</h2>}
|
||||
<div className="mt-8">
|
||||
<div>
|
||||
<h2 className="text-xl font-medium text-scale-1200 font-mono">
|
||||
<Heading tag="h2" parseAnchors={false} customAnchor={parameter.id}>
|
||||
<span className="mr-2">$</span>
|
||||
{parameter.title}
|
||||
</h2>
|
||||
<div className="grid" id={parameter.id}>
|
||||
</Heading>
|
||||
<div className="grid">
|
||||
<div className="border-b pb-8" key={parameter.id}>
|
||||
<div className=" mb-16">
|
||||
<p className="mb-4 scroll-mt-16 mt-0 text-scale-1100 text-base">
|
||||
@@ -63,6 +78,14 @@ export default function Config() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="md:col-span-3">
|
||||
<div className="sticky top-20 border-l">
|
||||
<span className="block font-mono text-xs uppercase text-scale-1200 pl-5 mb-4">
|
||||
On this page
|
||||
</span>
|
||||
<GuidesTableOfContents list={tocList} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,724 +0,0 @@
|
||||
import Layout from '~/layouts/tutorials/TutorialLayout'
|
||||
|
||||
export const meta = {
|
||||
title: 'Creating a user management dashboard with NextJS and Supabase',
|
||||
author: 'Rich Haines',
|
||||
video: 'https://www.youtube-nocookie.com/embed/0Fs96oZ4se0',
|
||||
}
|
||||
|
||||
In this 25-minute guide, we’ll be building a collaborative user managment app using NextJS and Supabase.
|
||||
As users add and move rectangles in a canvas, changes will be automatically synced and persisted, allowing for a canvas that updates in real-time across clients.
|
||||
Users will also be able to see other users selections, and undo and redo actions.
|
||||
|
||||

|
||||
|
||||
### GitHub
|
||||
|
||||
Should you get stuck while working through the guide, refer to [this repo](https://github.com/supabase/supabase/tree/master/examples/user-management/nextjs-ts-user-management).
|
||||
|
||||
## Building the App
|
||||
|
||||
Let's start building the Next.js app from scratch.
|
||||
|
||||
### Initialize a Next.js app
|
||||
|
||||
We can use [`create-next-app`](https://nextjs.org/docs/getting-started) to initialize
|
||||
an app called `supabase-nextjs`:
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
size="small"
|
||||
type="underlined"
|
||||
defaultActiveId="js"
|
||||
>
|
||||
<Tabs.Panel id="js" label="JavaScript">
|
||||
|
||||
```bash
|
||||
npx create-next-app@latest --use-npm supabase-nextjs
|
||||
cd supabase-nextjs
|
||||
```
|
||||
|
||||
</Tabs.Panel>
|
||||
<Tabs.Panel id="ts" label="TypeScript">
|
||||
|
||||
```bash
|
||||
npx create-next-app@latest --ts --use-npm supabase-nextjs
|
||||
cd supabase-nextjs
|
||||
```
|
||||
|
||||
</Tabs.Panel>
|
||||
</Tabs>
|
||||
|
||||
Then install the Supabase client library: [supabase-js](https://github.com/supabase/supabase-js)
|
||||
|
||||
```bash
|
||||
npm install @supabase/supabase-js
|
||||
```
|
||||
|
||||
And finally we want to save the environment variables in a `.env.local`.
|
||||
All we need are the API URL and the `anon` key that you copied [earlier](#get-the-api-keys).
|
||||
|
||||
```bash title=.env.local
|
||||
NEXT_PUBLIC_SUPABASE_URL=YOUR_SUPABASE_URL
|
||||
NEXT_PUBLIC_SUPABASE_ANON_KEY=YOUR_SUPABASE_ANON_KEY
|
||||
```
|
||||
|
||||
And one optional step is to update the CSS file `styles/globals.css` to make the app look nice.
|
||||
You can find the full contents of this file [here](https://raw.githubusercontent.com/supabase/supabase/master/examples/user-management/nextjs-ts-user-management/styles/globals.css).
|
||||
|
||||
### Set up a Login component
|
||||
|
||||
#### Supabase Auth Helpers
|
||||
|
||||
Next.js is a highly versatile framework offering pre-rendering at build time (SSG), server-side rendering at request time (SSR), API routes, and middleware edge-functions.
|
||||
|
||||
It can be challenging to authenticate your users in all these different environments, that's why we've created the [Supabase Auth Helpers](https://supabase.com/docs/guides/auth/auth-helpers/nextjs) to make user management and data fetching within Next.js as easy as possible.
|
||||
|
||||
Install the auth helpers for React and Next.js
|
||||
|
||||
```bash
|
||||
npm install @supabase/auth-helpers-react @supabase/auth-helpers-nextjs
|
||||
```
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
size="small"
|
||||
type="underlined"
|
||||
defaultActiveId="js"
|
||||
>
|
||||
<Tabs.Panel id="js" label="JavaScript">
|
||||
|
||||
Wrap your `pages/_app.js` component with the `SessionContextProvider` component:
|
||||
|
||||
```jsx title=pages/_app.js
|
||||
import '../styles/globals.css'
|
||||
import { createBrowserSupabaseClient } from '@supabase/auth-helpers-nextjs'
|
||||
import { SessionContextProvider } from '@supabase/auth-helpers-react'
|
||||
|
||||
function MyApp({ Component, pageProps }) {
|
||||
const [supabaseClient] = useState(() => createBrowserSupabaseClient())
|
||||
|
||||
return (
|
||||
<SessionContextProvider
|
||||
supabaseClient={supabaseClient}
|
||||
initialSession={pageProps.initialSession}
|
||||
>
|
||||
<Component {...pageProps} />
|
||||
</SessionContextProvider>
|
||||
)
|
||||
}
|
||||
export default MyApp
|
||||
```
|
||||
|
||||
</Tabs.Panel>
|
||||
<Tabs.Panel id="ts" label="TypeScript">
|
||||
|
||||
Wrap your `pages/_app.tsx` component with the `SessionContextProvider` component:
|
||||
|
||||
```jsx lines=2,8 title=pages/_app.tsx
|
||||
import { createBrowserSupabaseClient } from '@supabase/auth-helpers-nextjs'
|
||||
import { SessionContextProvider, Session } from '@supabase/auth-helpers-react'
|
||||
|
||||
function MyApp({
|
||||
Component,
|
||||
pageProps,
|
||||
}: AppProps<{
|
||||
initialSession: Session,
|
||||
}>) {
|
||||
const [supabaseClient] = useState(() => createBrowserSupabaseClient())
|
||||
|
||||
return (
|
||||
<SessionContextProvider
|
||||
supabaseClient={supabaseClient}
|
||||
initialSession={pageProps.initialSession}
|
||||
>
|
||||
<Component {...pageProps} />
|
||||
</SessionContextProvider>
|
||||
)
|
||||
}
|
||||
export default MyApp
|
||||
```
|
||||
|
||||
See the [Auth Helpers docs](/docs/guides/auth/auth-helpers/nextjs#usage-with-typescript) for more details on usage with TypeScript.
|
||||
|
||||
</Tabs.Panel>
|
||||
</Tabs>
|
||||
|
||||
#### Supabase Auth UI
|
||||
|
||||
We can use the [Supabase Auth UI](https://supabase.com/docs/guides/auth/auth-helpers/auth-ui) a pre-built React component for authenticating users via OAuth, email, and magic links.
|
||||
|
||||
Install the Supabase Auth UI for React
|
||||
|
||||
```bash
|
||||
npm install @supabase/auth-ui-react
|
||||
```
|
||||
|
||||
Add the `Auth` component to your home page
|
||||
|
||||
```jsx title=pages/index.js
|
||||
import { Auth, ThemeSupa } from '@supabase/auth-ui-react'
|
||||
import { useSession, useSupabaseClient } from '@supabase/auth-helpers-react'
|
||||
|
||||
const Home = () => {
|
||||
const session = useSession()
|
||||
const supabase = useSupabaseClient()
|
||||
|
||||
return (
|
||||
<div className="container" style={{ padding: '50px 0 100px 0' }}>
|
||||
{!session ? (
|
||||
<Auth supabaseClient={supabase} appearance={{ theme: ThemeSupa }} theme="dark" />
|
||||
) : (
|
||||
<p>Account page will go here.</p>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Home
|
||||
```
|
||||
|
||||
### Account page
|
||||
|
||||
After a user is signed in we can allow them to edit their profile details and manage their account.
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
size="small"
|
||||
type="underlined"
|
||||
defaultActiveId="js"
|
||||
>
|
||||
<Tabs.Panel id="js" label="JavaScript">
|
||||
|
||||
Let's create a new component for that called `Account.js` within a `components` folder.
|
||||
|
||||
```tsx title=components/Account.js
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useUser, useSupabaseClient } from '@supabase/auth-helpers-react'
|
||||
|
||||
export default function Account({ session }) {
|
||||
const supabase = useSupabaseClient()
|
||||
const user = useUser()
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [username, setUsername] = useState(null)
|
||||
const [website, setWebsite] = useState(null)
|
||||
const [avatar_url, setAvatarUrl] = useState(null)
|
||||
|
||||
useEffect(() => {
|
||||
getProfile()
|
||||
}, [session])
|
||||
|
||||
async function getProfile() {
|
||||
try {
|
||||
setLoading(true)
|
||||
|
||||
let { 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) {
|
||||
setUsername(data.username)
|
||||
setWebsite(data.website)
|
||||
setAvatarUrl(data.avatar_url)
|
||||
}
|
||||
} catch (error) {
|
||||
alert('Error loading user data!')
|
||||
console.log(error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
async function updateProfile({ username, website, avatar_url }) {
|
||||
try {
|
||||
setLoading(true)
|
||||
|
||||
const updates = {
|
||||
id: user.id,
|
||||
username,
|
||||
website,
|
||||
avatar_url,
|
||||
updated_at: new Date().toISOString(),
|
||||
}
|
||||
|
||||
let { error } = await supabase.from('profiles').upsert(updates)
|
||||
if (error) throw error
|
||||
alert('Profile updated!')
|
||||
} catch (error) {
|
||||
alert('Error updating the data!')
|
||||
console.log(error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="form-widget">
|
||||
<div>
|
||||
<label htmlFor="email">Email</label>
|
||||
<input id="email" type="text" value={session.user.email} disabled />
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="username">Username</label>
|
||||
<input
|
||||
id="username"
|
||||
type="text"
|
||||
value={username || ''}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="website">Website</label>
|
||||
<input
|
||||
id="website"
|
||||
type="website"
|
||||
value={website || ''}
|
||||
onChange={(e) => setWebsite(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button
|
||||
className="button primary block"
|
||||
onClick={() => updateProfile({ username, website, avatar_url })}
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? 'Loading ...' : 'Update'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button className="button block" onClick={() => supabase.auth.signOut()}>
|
||||
Sign Out
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
</Tabs.Panel>
|
||||
<Tabs.Panel id="ts" label="TypeScript">
|
||||
|
||||
Let's create a new component for that called `Account.tsx` within a `components` folder.
|
||||
|
||||
```tsx title=components/Account.tsx
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useUser, useSupabaseClient, Session } from '@supabase/auth-helpers-react'
|
||||
import { Database } from '../utils/database.types'
|
||||
type Profiles = Database['public']['Tables']['profiles']['Row']
|
||||
|
||||
export default function Account({ session }: { session: Session }) {
|
||||
const supabase = useSupabaseClient<Database>()
|
||||
const user = useUser()
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [username, setUsername] = useState<Profiles['username']>(null)
|
||||
const [website, setWebsite] = useState<Profiles['website']>(null)
|
||||
const [avatar_url, setAvatarUrl] = useState<Profiles['avatar_url']>(null)
|
||||
|
||||
useEffect(() => {
|
||||
getProfile()
|
||||
}, [session])
|
||||
|
||||
async function getProfile() {
|
||||
try {
|
||||
setLoading(true)
|
||||
if (!user) throw new Error('No user')
|
||||
|
||||
let { 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) {
|
||||
setUsername(data.username)
|
||||
setWebsite(data.website)
|
||||
setAvatarUrl(data.avatar_url)
|
||||
}
|
||||
} catch (error) {
|
||||
alert('Error loading user data!')
|
||||
console.log(error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
async function updateProfile({
|
||||
username,
|
||||
website,
|
||||
avatar_url,
|
||||
}: {
|
||||
username: Profiles['username']
|
||||
website: Profiles['website']
|
||||
avatar_url: Profiles['avatar_url']
|
||||
}) {
|
||||
try {
|
||||
setLoading(true)
|
||||
if (!user) throw new Error('No user')
|
||||
|
||||
const updates = {
|
||||
id: user.id,
|
||||
username,
|
||||
website,
|
||||
avatar_url,
|
||||
updated_at: new Date().toISOString(),
|
||||
}
|
||||
|
||||
let { error } = await supabase.from('profiles').upsert(updates)
|
||||
if (error) throw error
|
||||
alert('Profile updated!')
|
||||
} catch (error) {
|
||||
alert('Error updating the data!')
|
||||
console.log(error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="form-widget">
|
||||
<div>
|
||||
<label htmlFor="email">Email</label>
|
||||
<input id="email" type="text" value={session.user.email} disabled />
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="username">Username</label>
|
||||
<input
|
||||
id="username"
|
||||
type="text"
|
||||
value={username || ''}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="website">Website</label>
|
||||
<input
|
||||
id="website"
|
||||
type="website"
|
||||
value={website || ''}
|
||||
onChange={(e) => setWebsite(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button
|
||||
className="button primary block"
|
||||
onClick={() => updateProfile({ username, website, avatar_url })}
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? 'Loading ...' : 'Update'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button className="button block" onClick={() => supabase.auth.signOut()}>
|
||||
Sign Out
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
</Tabs.Panel>
|
||||
</Tabs>
|
||||
|
||||
### Launch!
|
||||
|
||||
Now that we have all the components in place, let's update `pages/index.js`:
|
||||
|
||||
```jsx lines=3,14 title=pages/index.js
|
||||
import { Auth, ThemeSupa } from '@supabase/auth-ui-react'
|
||||
import { useSession, useSupabaseClient } from '@supabase/auth-helpers-react'
|
||||
import Account from '../components/Account'
|
||||
|
||||
const Home = () => {
|
||||
const session = useSession()
|
||||
const supabase = useSupabaseClient()
|
||||
|
||||
return (
|
||||
<div className="container" style={{ padding: '50px 0 100px 0' }}>
|
||||
{!session ? (
|
||||
<Auth supabaseClient={supabase} appearance={{ theme: ThemeSupa }} theme="dark" />
|
||||
) : (
|
||||
<Account session={session} />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Home
|
||||
```
|
||||
|
||||
Once that's done, run this in a terminal window:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
And then open the browser to [localhost:3000](http://localhost:3000) and you should see the completed app.
|
||||
|
||||
## Bonus: Profile photos
|
||||
|
||||
Every Supabase project is configured with [Storage](/docs/guides/storage) for managing large files like
|
||||
photos and videos.
|
||||
|
||||
### Create an upload widget
|
||||
|
||||
Let's create an avatar widget for the user so that they can upload a profile photo. We can start by creating a new component:
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
size="small"
|
||||
type="underlined"
|
||||
defaultActiveId="js"
|
||||
>
|
||||
<Tabs.Panel id="js" label="JavaScript">
|
||||
|
||||
```jsx title=components/Avatar.js
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useSupabaseClient } from '@supabase/auth-helpers-react'
|
||||
|
||||
export default function Avatar({ uid, url, size, onUpload }) {
|
||||
const supabase = useSupabaseClient()
|
||||
const [avatarUrl, setAvatarUrl] = useState(null)
|
||||
const [uploading, setUploading] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (url) downloadImage(url)
|
||||
}, [url])
|
||||
|
||||
async function downloadImage(path) {
|
||||
try {
|
||||
const { data, error } = await supabase.storage.from('avatars').download(path)
|
||||
if (error) {
|
||||
throw error
|
||||
}
|
||||
const url = URL.createObjectURL(data)
|
||||
setAvatarUrl(url)
|
||||
} catch (error) {
|
||||
console.log('Error downloading image: ', error)
|
||||
}
|
||||
}
|
||||
|
||||
const uploadAvatar = async (event) => {
|
||||
try {
|
||||
setUploading(true)
|
||||
|
||||
if (!event.target.files || event.target.files.length === 0) {
|
||||
throw new Error('You must select an image to upload.')
|
||||
}
|
||||
|
||||
const file = event.target.files[0]
|
||||
const fileExt = file.name.split('.').pop()
|
||||
const fileName = `${uid}.${fileExt}`
|
||||
const filePath = `${fileName}`
|
||||
|
||||
let { error: uploadError } = await supabase.storage
|
||||
.from('avatars')
|
||||
.upload(filePath, file, { upsert: true })
|
||||
|
||||
if (uploadError) {
|
||||
throw uploadError
|
||||
}
|
||||
|
||||
onUpload(filePath)
|
||||
} catch (error) {
|
||||
alert('Error uploading avatar!')
|
||||
console.log(error)
|
||||
} finally {
|
||||
setUploading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{avatarUrl ? (
|
||||
<img
|
||||
src={avatarUrl}
|
||||
alt="Avatar"
|
||||
className="avatar image"
|
||||
style={{ height: size, width: size }}
|
||||
/>
|
||||
) : (
|
||||
<div className="avatar no-image" style={{ height: size, width: size }} />
|
||||
)}
|
||||
<div style={{ width: size }}>
|
||||
<label className="button primary block" htmlFor="single">
|
||||
{uploading ? 'Uploading ...' : 'Upload'}
|
||||
</label>
|
||||
<input
|
||||
style={{
|
||||
visibility: 'hidden',
|
||||
position: 'absolute',
|
||||
}}
|
||||
type="file"
|
||||
id="single"
|
||||
accept="image/*"
|
||||
onChange={uploadAvatar}
|
||||
disabled={uploading}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
</Tabs.Panel>
|
||||
<Tabs.Panel id="ts" label="TypeScript">
|
||||
|
||||
```tsx title=components/Avatar.tsx
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useSupabaseClient } from '@supabase/auth-helpers-react'
|
||||
import { Database } from '../utils/database.types'
|
||||
type Profiles = Database['public']['Tables']['profiles']['Row']
|
||||
|
||||
export default function Avatar({
|
||||
uid,
|
||||
url,
|
||||
size,
|
||||
onUpload,
|
||||
}: {
|
||||
uid: string
|
||||
url: Profiles['avatar_url']
|
||||
size: number
|
||||
onUpload: (url: string) => void
|
||||
}) {
|
||||
const supabase = useSupabaseClient<Database>()
|
||||
const [avatarUrl, setAvatarUrl] = useState<Profiles['avatar_url']>(null)
|
||||
const [uploading, setUploading] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (url) downloadImage(url)
|
||||
}, [url])
|
||||
|
||||
async function downloadImage(path: string) {
|
||||
try {
|
||||
const { data, error } = await supabase.storage.from('avatars').download(path)
|
||||
if (error) {
|
||||
throw error
|
||||
}
|
||||
const url = URL.createObjectURL(data)
|
||||
setAvatarUrl(url)
|
||||
} catch (error) {
|
||||
console.log('Error downloading image: ', error)
|
||||
}
|
||||
}
|
||||
|
||||
const uploadAvatar: React.ChangeEventHandler<HTMLInputElement> = async (event) => {
|
||||
try {
|
||||
setUploading(true)
|
||||
|
||||
if (!event.target.files || event.target.files.length === 0) {
|
||||
throw new Error('You must select an image to upload.')
|
||||
}
|
||||
|
||||
const file = event.target.files[0]
|
||||
const fileExt = file.name.split('.').pop()
|
||||
const fileName = `${uid}.${fileExt}`
|
||||
const filePath = `${fileName}`
|
||||
|
||||
let { error: uploadError } = await supabase.storage
|
||||
.from('avatars')
|
||||
.upload(filePath, file, { upsert: true })
|
||||
|
||||
if (uploadError) {
|
||||
throw uploadError
|
||||
}
|
||||
|
||||
onUpload(filePath)
|
||||
} catch (error) {
|
||||
alert('Error uploading avatar!')
|
||||
console.log(error)
|
||||
} finally {
|
||||
setUploading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{avatarUrl ? (
|
||||
<img
|
||||
src={avatarUrl}
|
||||
alt="Avatar"
|
||||
className="avatar image"
|
||||
style={{ height: size, width: size }}
|
||||
/>
|
||||
) : (
|
||||
<div className="avatar no-image" style={{ height: size, width: size }} />
|
||||
)}
|
||||
<div style={{ width: size }}>
|
||||
<label className="button primary block" htmlFor="single">
|
||||
{uploading ? 'Uploading ...' : 'Upload'}
|
||||
</label>
|
||||
<input
|
||||
style={{
|
||||
visibility: 'hidden',
|
||||
position: 'absolute',
|
||||
}}
|
||||
type="file"
|
||||
id="single"
|
||||
accept="image/*"
|
||||
onChange={uploadAvatar}
|
||||
disabled={uploading}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
</Tabs.Panel>
|
||||
</Tabs>
|
||||
|
||||
### Add the new widget
|
||||
|
||||
And then we can add the widget to the Account page:
|
||||
|
||||
```jsx title=components/Account.js
|
||||
// Import the new component
|
||||
import Avatar from './Avatar'
|
||||
|
||||
// ...
|
||||
|
||||
return (
|
||||
<div className="form-widget">
|
||||
{/* Add to the body */}
|
||||
<Avatar
|
||||
uid={user.id}
|
||||
url={avatar_url}
|
||||
size={150}
|
||||
onUpload={(url) => {
|
||||
setAvatarUrl(url)
|
||||
updateProfile({ username, website, avatar_url: url })
|
||||
}}
|
||||
/>
|
||||
{/* ... */}
|
||||
</div>
|
||||
)
|
||||
```
|
||||
|
||||
## Next steps
|
||||
|
||||
At this stage you have a fully functional application!
|
||||
|
||||
- See the complete [example on GitHub](https://github.com/supabase/supabase/tree/master/examples/user-management/nextjs-ts-user-management) and deploy it to Vercel.
|
||||
- Explore the [pre-built Auth UI for React](https://supabase.com/docs/guides/auth/auth-helpers/auth-ui).
|
||||
- Explore the [Auth Helpers for Next.js](https://supabase.com/docs/guides/auth/auth-helpers/nextjs).
|
||||
- Explore the [Supabase Cache Helpers](https://github.com/psteinroe/supabase-cache-helpers).
|
||||
- See the [Next.js Subscription Payments Starter](https://github.com/vercel/nextjs-subscription-payments) template on GitHub.
|
||||
- Got a question? [Ask here](https://github.com/supabase/supabase/discussions).
|
||||
- Sign in: [app.supabase.com](https://app.supabase.com)
|
||||
|
||||
export default ({ children }) => <Layout meta={meta} children={children} />
|
||||
+26
-90
@@ -1155,7 +1155,7 @@ module.exports = [
|
||||
{
|
||||
permanent: true,
|
||||
source: '/docs/reference/cli/about',
|
||||
destination: '/docs/reference/cli',
|
||||
destination: '/docs/reference/cli/introduction',
|
||||
},
|
||||
{
|
||||
permanent: true,
|
||||
@@ -1165,118 +1165,44 @@ module.exports = [
|
||||
{
|
||||
permanent: true,
|
||||
source: '/docs/reference/cli/supabase-help',
|
||||
destination: '/docs/reference/cli/usage',
|
||||
},
|
||||
{
|
||||
permanent: true,
|
||||
source: '/docs/reference/cli/supabase-login',
|
||||
destination: '/docs/reference/cli/usage',
|
||||
},
|
||||
{
|
||||
permanent: true,
|
||||
source: '/docs/reference/cli/supabase-link',
|
||||
destination: '/docs/reference/cli/usage',
|
||||
},
|
||||
{
|
||||
permanent: true,
|
||||
source: '/docs/reference/cli/supabase-init',
|
||||
destination: '/docs/reference/cli/usage',
|
||||
},
|
||||
{
|
||||
permanent: true,
|
||||
source: '/docs/reference/cli/supabase-start',
|
||||
destination: '/docs/reference/cli/usage',
|
||||
destination: '/docs/reference/cli/global-flags',
|
||||
},
|
||||
{
|
||||
permanent: true,
|
||||
source: '/docs/reference/cli/supabase-db-branch-list',
|
||||
destination: '/docs/reference/cli/usage',
|
||||
destination: '/docs/reference/supabase-db',
|
||||
},
|
||||
{
|
||||
permanent: true,
|
||||
source: '/docs/reference/cli/supabase-db-branch-create',
|
||||
destination: '/docs/reference/cli/usage',
|
||||
destination: '/docs/reference/supabase-db',
|
||||
},
|
||||
{
|
||||
permanent: true,
|
||||
source: '/docs/reference/cli/supabase-db-branch-delete',
|
||||
destination: '/docs/reference/cli/usage',
|
||||
destination: '/docs/reference/supabase-db',
|
||||
},
|
||||
{
|
||||
permanent: true,
|
||||
source: '/docs/reference/cli/supabase-db-switch',
|
||||
destination: '/docs/reference/cli/usage',
|
||||
destination: '/docs/reference/supabase-db',
|
||||
},
|
||||
{
|
||||
permanent: true,
|
||||
source: '/docs/reference/cli/supabase-db-changes',
|
||||
destination: '/docs/reference/cli/usage',
|
||||
destination: '/docs/reference/supabase-db',
|
||||
},
|
||||
{
|
||||
permanent: true,
|
||||
source: '/docs/reference/cli/supabase-db-commit',
|
||||
destination: '/docs/reference/cli/usage',
|
||||
destination: '/docs/reference/supabase-db',
|
||||
},
|
||||
{
|
||||
permanent: true,
|
||||
source: '/docs/reference/cli/supabase-db-remote-set',
|
||||
destination: '/docs/reference/cli/usage',
|
||||
},
|
||||
{
|
||||
permanent: true,
|
||||
source: '/docs/reference/cli/supabase-functions-delete',
|
||||
destination: '/docs/reference/cli/usage',
|
||||
},
|
||||
{
|
||||
permanent: true,
|
||||
source: '/docs/reference/cli/supabase-functions-deploy',
|
||||
destination: '/docs/reference/cli/usage',
|
||||
},
|
||||
{
|
||||
permanent: true,
|
||||
source: '/docs/reference/cli/supabase-functions-new',
|
||||
destination: '/docs/reference/cli/usage',
|
||||
},
|
||||
{
|
||||
permanent: true,
|
||||
source: '/docs/reference/cli/supabase-functions-serve',
|
||||
destination: '/docs/reference/cli/usage',
|
||||
},
|
||||
{
|
||||
permanent: true,
|
||||
source: '/docs/reference/cli/supabase-migration-new',
|
||||
destination: '/docs/reference/cli/usage',
|
||||
},
|
||||
{
|
||||
permanent: true,
|
||||
source: '/docs/reference/cli/supabase-orgs-list',
|
||||
destination: '/docs/reference/cli/usage',
|
||||
},
|
||||
{
|
||||
permanent: true,
|
||||
source: '/docs/reference/cli/supabase-projects-list',
|
||||
destination: '/docs/reference/cli/usage',
|
||||
},
|
||||
{
|
||||
permanent: true,
|
||||
source: '/docs/reference/cli/supabase-projects-create',
|
||||
destination: '/docs/reference/cli/usage',
|
||||
},
|
||||
{
|
||||
permanent: true,
|
||||
source: '/docs/reference/cli/supabase-secrets-list',
|
||||
destination: '/docs/reference/cli/usage',
|
||||
},
|
||||
{
|
||||
permanent: true,
|
||||
source: '/docs/reference/cli/supabase-secrets-set',
|
||||
destination: '/docs/reference/cli/usage',
|
||||
},
|
||||
{
|
||||
permanent: true,
|
||||
source: '/docs/reference/cli/supabase-secrets-unset',
|
||||
destination: '/docs/reference/cli/usage',
|
||||
destination: '/docs/reference/supabase-db',
|
||||
},
|
||||
|
||||
{
|
||||
permanent: true,
|
||||
source: '/docs/reference/tools/reference-auth',
|
||||
@@ -1711,6 +1637,11 @@ module.exports = [
|
||||
source: '/docs/guides/database',
|
||||
destination: '/docs/guides/database/overview',
|
||||
},
|
||||
{
|
||||
permanent: true,
|
||||
source: '/docs/getting-started',
|
||||
destination: '/docs/guides/getting-started/architecture',
|
||||
},
|
||||
{
|
||||
permanent: true,
|
||||
source: '/docs/architecture',
|
||||
@@ -1786,6 +1717,11 @@ module.exports = [
|
||||
source: '/docs/guides/with-ionic-angular',
|
||||
destination: '/docs/guides/getting-started/tutorials/with-ionic-angular',
|
||||
},
|
||||
{
|
||||
permanent: true,
|
||||
source: '/docs/guides/tutorials',
|
||||
destination: '/docs/guides/getting-started#tutorials',
|
||||
},
|
||||
{
|
||||
permanent: true,
|
||||
source: '/docs/guides/hosting/overview',
|
||||
@@ -1808,18 +1744,18 @@ module.exports = [
|
||||
},
|
||||
{
|
||||
permanent: true,
|
||||
source: '/docs/guides/cli',
|
||||
destination: '/docs/guides/resources/supabase-cli',
|
||||
source: '/docs/guides/resources/supabase-cli',
|
||||
destination: '/docs/guides/cli',
|
||||
},
|
||||
{
|
||||
permanent: true,
|
||||
source: '/docs/guides/cli/local-development',
|
||||
destination: '/docs/guides/resources/supabase-cli/local-development',
|
||||
source: '/docs/guides/resources/supabase-cli/local-development',
|
||||
destination: '/docs/guides/cli/local-development',
|
||||
},
|
||||
{
|
||||
permanent: true,
|
||||
source: '/docs/guides/cli/managing-environments',
|
||||
destination: '/docs/guides/resources/supabase-cli/managing-environments',
|
||||
source: '/docs/guides/resources/supabase-cli/managing-environments',
|
||||
destination: '/docs/guides/cli/managing-environments',
|
||||
},
|
||||
{
|
||||
permanent: true,
|
||||
|
||||
@@ -199,7 +199,7 @@ const Index = ({}: Props) => {
|
||||
border-gray-100 bg-white p-5
|
||||
pt-14 dark:border-gray-600"
|
||||
>
|
||||
<a href="mailto:support@supabase.io">
|
||||
<a href="https://app.supabase.com/support/new">
|
||||
<Button size="medium" type="default" iconRight={<IconMail />}>
|
||||
Email Support
|
||||
</Button>
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
node_modules/
|
||||
.vercel
|
||||
.next
|
||||
.env.local
|
||||
.env.production
|
||||
.env.dev
|
||||
.env.*
|
||||
@@ -211,7 +211,7 @@ services:
|
||||
# Comment out everything below this point if you are using an external Postgres database
|
||||
db:
|
||||
container_name: supabase-db
|
||||
image: supabase/postgres:14.1.0.89
|
||||
image: supabase/postgres:14.1.0.106
|
||||
healthcheck:
|
||||
test: pg_isready -U postgres -h localhost
|
||||
interval: 5s
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
clispec: '001'
|
||||
info:
|
||||
id: cli
|
||||
version: 1.33.0
|
||||
version: 1.34.5
|
||||
title: Supabase CLI
|
||||
language: sh
|
||||
source: https://github.com/supabase/cli
|
||||
@@ -247,12 +247,15 @@ commands:
|
||||
- id: supabase-start
|
||||
title: supabase start
|
||||
summary: Start containers for Supabase local development
|
||||
description: |-
|
||||
description: |2
|
||||
|
||||
Starts the Supabase local development stack.
|
||||
|
||||
Requires `supabase/config.toml` to be created in your current working directory by running `supabase init`.
|
||||
|
||||
All service containers are started by default. You can exclude those not needed by passing in `-x` flag.
|
||||
|
||||
Health checks are automatically added to verify the started containers. Use `--ignore-health-check` flag to ignore these errors.
|
||||
examples:
|
||||
- id: basic-usage
|
||||
name: Basic usage
|
||||
@@ -268,6 +271,13 @@ commands:
|
||||
Excluding container: supabase/studio:20221214-4eecc99
|
||||
Seeding data supabase/seed.sql...
|
||||
Started supabase local development setup.
|
||||
- id: ignore-health-check
|
||||
name: Ignore service health checks
|
||||
code: supabase start --ignore-health-check
|
||||
response: |
|
||||
Seeding data supabase/seed.sql...
|
||||
service not healthy: [supabase_storage_cli]
|
||||
Started supabase local development setup.
|
||||
tags:
|
||||
- local-dev
|
||||
links: []
|
||||
@@ -279,6 +289,10 @@ commands:
|
||||
description: |
|
||||
Names of containers to not start. [gotrue, realtime, storage-api, imgproxy, kong, inbucket, postgrest, pgadmin-schema-diff, migra, postgres-meta, studio, deno-relay]
|
||||
default_value: '[]'
|
||||
- id: ignore-health-check
|
||||
name: --ignore-health-check
|
||||
description: Ignore unhealthy services and exit 0
|
||||
default_value: 'false'
|
||||
- id: supabase-secrets
|
||||
title: supabase secrets
|
||||
summary: Manage Supabase secrets
|
||||
@@ -595,7 +609,8 @@ commands:
|
||||
- id: supabase-login
|
||||
title: supabase login
|
||||
summary: Authenticate using an access token
|
||||
description: |-
|
||||
description: |2
|
||||
|
||||
Connect the Supabase CLI to your Supabase account by logging in with your [personal access token](https://app.supabase.com/account/tokens).
|
||||
|
||||
Your access token is stored securely in [native credentials storage](https://github.com/zalando/go-keyring#dependencies). If native credentials storage is unavailable, it will be written to a plain text file at `~/.supabase/access-token`.
|
||||
@@ -620,7 +635,8 @@ commands:
|
||||
- id: supabase-link
|
||||
title: supabase link
|
||||
summary: Link to a Supabase project
|
||||
description: |-
|
||||
description: |2
|
||||
|
||||
Link your local development project to a hosted Supabase project.
|
||||
|
||||
PostgREST configurations are fetched from the Supabase platform and validated against your local configuration file.
|
||||
@@ -659,7 +675,8 @@ commands:
|
||||
- id: supabase-init
|
||||
title: supabase init
|
||||
summary: Initialize a local project
|
||||
description: |-
|
||||
description: |2
|
||||
|
||||
Initialize configurations for Supabase local development.
|
||||
|
||||
A `supabase/config.toml` file is created in your current working directory. This configuration is specific to each local project.
|
||||
@@ -738,6 +755,7 @@ commands:
|
||||
subcommands:
|
||||
- supabase-functions-delete
|
||||
- supabase-functions-deploy
|
||||
- supabase-functions-download
|
||||
- supabase-functions-new
|
||||
- supabase-functions-serve
|
||||
flags: []
|
||||
@@ -770,6 +788,20 @@ commands:
|
||||
usage: supabase functions new <Function name>
|
||||
subcommands: []
|
||||
flags: []
|
||||
- id: supabase-functions-download
|
||||
title: supabase functions download
|
||||
summary: Download a Function from Supabase
|
||||
description: |
|
||||
Download the source code for a Function from the linked Supabase project.
|
||||
tags: []
|
||||
links: []
|
||||
usage: supabase functions download <Function name> [flags]
|
||||
subcommands: []
|
||||
flags:
|
||||
- id: project-ref
|
||||
name: --project-ref <string>
|
||||
description: Project ref of the Supabase project.
|
||||
default_value: ''
|
||||
- id: supabase-functions-deploy
|
||||
title: supabase functions deploy
|
||||
summary: Deploy a Function to Supabase
|
||||
|
||||
@@ -231,6 +231,12 @@
|
||||
"slug": "supabase-functions-new",
|
||||
"type": "cli-command"
|
||||
},
|
||||
{
|
||||
"id": "supabase-functions-download",
|
||||
"title": "Download a function",
|
||||
"slug": "supabase-functions-download",
|
||||
"type": "cli-command"
|
||||
},
|
||||
{
|
||||
"id": "supabase-functions-serve",
|
||||
"title": "Serve a function",
|
||||
|
||||
@@ -17,7 +17,6 @@ info:
|
||||
|
||||
functions:
|
||||
- id: initializing
|
||||
$ref: '@supabase/supabase-js.index.SupabaseClient.constructor'
|
||||
description: |
|
||||
You can initialize a new Supabase client using the `create_client()` method.
|
||||
|
||||
@@ -50,7 +49,6 @@ functions:
|
||||
|
||||
- id: sign-up
|
||||
title: 'sign_up()'
|
||||
$ref: '@supabase/gotrue-js.GoTrueClient.signUp'
|
||||
notes: |
|
||||
- By default, the user needs to verify their email address before logging in. To turn this off, disable **Confirm email** in [your project](https://app.supabase.com/project/_/auth/providers).
|
||||
- **Confirm email** determines if users need to confirm their email address after signing up.
|
||||
@@ -88,7 +86,6 @@ functions:
|
||||
```
|
||||
- id: sign-out
|
||||
title: 'sign_out()'
|
||||
$ref: '@supabase/gotrue-js.GoTrueClient.signOut'
|
||||
notes: |
|
||||
- In order to use the `signOut()` method, the user needs to be signed in first.
|
||||
examples:
|
||||
@@ -100,7 +97,6 @@ functions:
|
||||
```
|
||||
- id: verify-otp
|
||||
title: 'verify_otp()'
|
||||
$ref: '@supabase/gotrue-js.GoTrueClient.verifyOtp'
|
||||
notes: |
|
||||
- The `verifyOtp` method takes in different verification types. If a phone number is used, the type can either be `sms` or `phone_change`. If an email address is used, the type can be one of the following: `signup`, `magiclink`, `recovery`, `invite` or `email_change`.
|
||||
- The verification type used should be determined based on the corresponding auth method called before `verifyOtp` to sign up / sign-in a user.
|
||||
@@ -114,7 +110,6 @@ functions:
|
||||
|
||||
- id: get-session
|
||||
title: 'get_session_from_url'
|
||||
$ref: '@supabase/gotrue-js.GoTrueClient.getSession'
|
||||
examples:
|
||||
- id: get-session
|
||||
name: Get the session data from URL
|
||||
@@ -125,7 +120,6 @@ functions:
|
||||
|
||||
- id: set-session
|
||||
title: 'set_session()'
|
||||
$ref: '@supabase/gotrue-js.GoTrueClient.setSession'
|
||||
notes: |
|
||||
- `setSession()` takes in a refresh token and uses it to get a new session.
|
||||
- The refresh token can only be used once to obtain a new session.
|
||||
@@ -142,7 +136,6 @@ functions:
|
||||
```
|
||||
- id: refresh-session
|
||||
title: 'refresh_ession()'
|
||||
$ref: '@supabase/gotrue-js.GoTrueClient.refreshSession'
|
||||
notes: |
|
||||
- This method will refresh the session whether the current one is expired or not.
|
||||
- Both examples destructure `user` and `session` from `data`. This is not required; so `const { data, error } =` is also valid.
|
||||
@@ -155,7 +148,6 @@ functions:
|
||||
```
|
||||
- id: select
|
||||
title: 'Fetch data: select()'
|
||||
$ref: '@supabase/postgrest-js.PostgrestQueryBuilder.select'
|
||||
notes: |
|
||||
- By default, Supabase projects return a maximum of 1,000 rows. This setting can be changed in your project's [API settings](https://app.supabase.com/project/_/settings/api). It's recommended that you keep it low to limit the payload size of accidental or malicious requests.
|
||||
- `apikey` is a reserved keyword if you're using the [Supabase Platform](/docs/guides/platform) and [should be avoided as a column name](https://github.com/supabase/supabase/issues/5465).
|
||||
@@ -181,7 +173,6 @@ functions:
|
||||
title: 'invoke()'
|
||||
description: |
|
||||
Invoke a Supabase Function.
|
||||
$ref: '@supabase/functions-js.F'
|
||||
notes: |
|
||||
- Requires an Authorization header.
|
||||
- When you pass in a body to your function, we automatically attach the Content-Type header for `Blob`, `ArrayBuffer`, `File`, `FormData` and `String`. If it doesn't match any of these types we assume the payload is `json`, serialise it and attach the `Content-Type` header as `application/json`. You can override this behaviour by passing in a `Content-Type` header of your own.
|
||||
@@ -204,7 +195,6 @@ functions:
|
||||
|
||||
- id: list-buckets
|
||||
title: 'list_buckets()'
|
||||
$ref: '@supabase/storage-js.packages/StorageBucketApi.default.listBuckets'
|
||||
notes: |
|
||||
- RLS policy permissions required:
|
||||
- `buckets` table permissions: `select`
|
||||
@@ -220,7 +210,6 @@ functions:
|
||||
|
||||
- id: get-bucket
|
||||
title: 'get_bucket()'
|
||||
$ref: '@supabase/storage-js.packages/StorageBucketApi.default.getBucket'
|
||||
notes: |
|
||||
- RLS policy permissions required:
|
||||
- `buckets` table permissions: `select`
|
||||
@@ -236,7 +225,6 @@ functions:
|
||||
|
||||
- id: create-bucket
|
||||
title: 'create_bucket()'
|
||||
$ref: '@supabase/storage-js.packages/StorageBucketApi.default.createBucket'
|
||||
notes: |
|
||||
- RLS policy permissions required:
|
||||
- `buckets` table permissions: `insert`
|
||||
@@ -252,7 +240,6 @@ functions:
|
||||
|
||||
- id: empty-bucket
|
||||
title: 'empty_bucket()'
|
||||
$ref: '@supabase/storage-js.packages/StorageBucketApi.default.emptyBucket'
|
||||
notes: |
|
||||
- RLS policy permissions required:
|
||||
- `buckets` table permissions: `select`
|
||||
@@ -267,7 +254,6 @@ functions:
|
||||
```
|
||||
- id: delete-bucket
|
||||
title: 'delete_bucket()'
|
||||
$ref: '@supabase/storage-js.packages/StorageBucketApi.default.deleteBucket'
|
||||
notes: |
|
||||
- RLS policy permissions required:
|
||||
- `buckets` table permissions: `select` and `delete`
|
||||
@@ -283,7 +269,6 @@ functions:
|
||||
|
||||
- id: from-upload
|
||||
title: 'from_.upload()'
|
||||
$ref: '@supabase/storage-js.packages/StorageFileApi.default.upload'
|
||||
notes: |
|
||||
- RLS policy permissions required:
|
||||
- `buckets` table permissions: none
|
||||
@@ -299,7 +284,6 @@ functions:
|
||||
```
|
||||
- id: from-move
|
||||
title: 'from_.move()'
|
||||
$ref: '@supabase/storage-js.packages/StorageFileApi.default.move'
|
||||
notes: |
|
||||
- RLS policy permissions required:
|
||||
- `buckets` table permissions: none
|
||||
@@ -315,7 +299,6 @@ functions:
|
||||
|
||||
- id: from-create-signed-url
|
||||
title: 'from_.create_signed_url()'
|
||||
$ref: '@supabase/storage-js.packages/StorageFileApi.default.createSignedUrl'
|
||||
notes: |
|
||||
- RLS policy permissions required:
|
||||
- `buckets` table permissions: none
|
||||
@@ -331,7 +314,6 @@ functions:
|
||||
|
||||
- id: from-get-public-url
|
||||
title: 'from_.get_public_url()'
|
||||
$ref: '@supabase/storage-js.packages/StorageFileApi.default.getPublicUrl'
|
||||
notes: |
|
||||
- The bucket needs to be set to public, either via [updateBucket()](/docs/reference/javascript/storage-updatebucket) or by going to Storage on [app.supabase.com](https://app.supabase.com), clicking the overflow menu on a bucket and choosing "Make public"
|
||||
- RLS policy permissions required:
|
||||
@@ -348,7 +330,6 @@ functions:
|
||||
|
||||
- id: from-download
|
||||
title: 'from_.download()'
|
||||
$ref: '@supabase/storage-js.packages/StorageFileApi.default.download'
|
||||
notes: |
|
||||
- RLS policy permissions required:
|
||||
- `buckets` table permissions: none
|
||||
@@ -366,7 +347,6 @@ functions:
|
||||
|
||||
- id: from-remove
|
||||
title: 'from_.remove()'
|
||||
$ref: '@supabase/storage-js.packages/StorageFileApi.default.remove'
|
||||
notes: |
|
||||
- RLS policy permissions required:
|
||||
- `buckets` table permissions: none
|
||||
@@ -381,7 +361,6 @@ functions:
|
||||
```
|
||||
- id: from-list
|
||||
title: 'from_.list()'
|
||||
$ref: '@supabase/storage-js.packages/StorageFileApi.default.list'
|
||||
notes: |
|
||||
- RLS policy permissions required:
|
||||
- `buckets` table permissions: none
|
||||
@@ -396,6 +375,5 @@ functions:
|
||||
```
|
||||
- id: subscribe
|
||||
title: on().subscribe()
|
||||
$ref: '@supabase/realtime-js.RealtimeChannel.on'
|
||||
notes: |
|
||||
- We are implementing this feature at the moment. If you have queries feel free to open an issue on the [realtime-py](https://github.com/supabase-community) repository.
|
||||
@@ -1,7 +0,0 @@
|
||||
node_modules/
|
||||
.vercel
|
||||
.next
|
||||
.env.local
|
||||
.env.production
|
||||
.env.dev
|
||||
.env.*
|
||||
+40
-21
@@ -1,9 +1,11 @@
|
||||
# To be run in the root of the turbo monorepo
|
||||
# NOTE: It's highly raccomended to use the new builder, Buildkit. https://docs.docker.com/build/buildkit/
|
||||
## USAGE:
|
||||
# Build: docker build --target production -t supabase/studio:latest .
|
||||
# Build: docker buildx build --target production -t supabase/studio:latest .
|
||||
# Run: docker run -p 3000:3000 supabase/studio
|
||||
# Deploy: docker push supabase/studio:latest
|
||||
# Clean build:
|
||||
# docker build --target production --no-cache -t supabase/studio:latest .
|
||||
# docker buildx build --target production --no-cache -t supabase/studio:latest .
|
||||
# docker builder prune
|
||||
|
||||
FROM node:16-slim as base
|
||||
@@ -11,36 +13,53 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
python3 \
|
||||
build-essential \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
RUN npm i -g turbo
|
||||
|
||||
FROM base as builder
|
||||
WORKDIR /usr/src/app
|
||||
COPY package*.json ./
|
||||
COPY turbo.json .
|
||||
COPY packages packages
|
||||
COPY studio studio
|
||||
# the following command is still unsupported for npm // https://github.com/vercel/turborepo/issues/1830
|
||||
# RUN npm i -g turbo
|
||||
# RUN turbo prune --scope=studio --docker
|
||||
# RUN npm clean-install && npm cache clean --force
|
||||
# Calculate if dependencies are outdated
|
||||
FROM base as turbo
|
||||
WORKDIR /app
|
||||
COPY . .
|
||||
# Prune unneeded dependencies with turbo (from apps/ for example)
|
||||
RUN turbo prune --scope=studio --docker
|
||||
|
||||
# Install dev dependencies (only if needed)
|
||||
FROM base as deps
|
||||
WORKDIR /app
|
||||
COPY --from=turbo /app/out/json/ .
|
||||
COPY --from=turbo /app/out/package-lock.json ./
|
||||
RUN npm clean-install && npm cache clean --force
|
||||
|
||||
FROM deps as deps-prod
|
||||
RUN npm prune --production
|
||||
# Remove packages dependencies (not needed in production)
|
||||
RUN rm -rf /app/packages
|
||||
|
||||
# builder contains dependencies and source code not compiled
|
||||
FROM deps as builder
|
||||
WORKDIR /app
|
||||
COPY --from=turbo /app/out/full ./
|
||||
ENTRYPOINT ["docker-entrypoint.sh"]
|
||||
|
||||
FROM builder as dev
|
||||
COPY --from=builder /usr/src/app ./
|
||||
RUN npm clean-install && npm cache clean --force
|
||||
EXPOSE 8082
|
||||
CMD ["npx", "turbo", "run", "dev", "--filter=studio"]
|
||||
|
||||
# Compile NextJS
|
||||
FROM builder as productionprep
|
||||
WORKDIR /app
|
||||
COPY --from=builder /usr/src/app ./
|
||||
RUN npm clean-install \
|
||||
&& npm cache clean --force \
|
||||
&& npx turbo run build --scope=studio --include-dependencies --no-deps \
|
||||
&& rm -rf ./studio/.next/cache/webpack \
|
||||
&& npm prune --production
|
||||
RUN npx turbo run build --scope=studio --include-dependencies --no-deps \
|
||||
&& rm -rf ./studio/.next/cache/webpack
|
||||
|
||||
# Copy only compiled code and dependencies
|
||||
FROM node:16-slim as production
|
||||
COPY --from=productionprep /app /app
|
||||
# Copy package(s).json and package-lock.json
|
||||
COPY --from=turbo /app/out/json /app
|
||||
# Copy all dependencies for production
|
||||
COPY --from=deps-prod /app /app
|
||||
# Copy necessary files
|
||||
COPY --from=turbo /app/out/full/studio /app/studio
|
||||
COPY --from=productionprep /app/studio/.next /app/studio/.next
|
||||
WORKDIR /app/studio
|
||||
EXPOSE 3000
|
||||
HEALTHCHECK --interval=5s --timeout=5s --retries=3 CMD node -e "require('http').get('http://localhost:3000/api/profile', (r) => {if (r.statusCode !== 200) throw new Error(r.statusCode)})"
|
||||
CMD ["npm", "run", "start"]
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Listbox } from '@supabase/ui'
|
||||
import { Select } from 'ui'
|
||||
import { EditorProps } from '@supabase/react-data-grid'
|
||||
|
||||
import { useTrackedState } from 'components/grid/store'
|
||||
|
||||
interface SelectEditorProps<TRow, TSummaryRow = unknown> extends EditorProps<TRow, TSummaryRow> {
|
||||
options: { label: string; value: string }[]
|
||||
options: { label: string; _value: string }[]
|
||||
}
|
||||
|
||||
export function SelectEditor<TRow, TSummaryRow = unknown>({
|
||||
@@ -19,11 +19,12 @@ export function SelectEditor<TRow, TSummaryRow = unknown>({
|
||||
|
||||
const value = row[column.key as keyof TRow] as unknown as string
|
||||
|
||||
function onChange(value: string) {
|
||||
if (!value || value == '') {
|
||||
function onChange(event: any) {
|
||||
debugger
|
||||
if (!event.target.value || event.target.value == '') {
|
||||
onRowChange({ ...row, [column.key]: null }, true)
|
||||
} else {
|
||||
onRowChange({ ...row, [column.key]: value }, true)
|
||||
onRowChange({ ...row, [column.key]: event.target.value }, true)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +33,7 @@ export function SelectEditor<TRow, TSummaryRow = unknown>({
|
||||
}
|
||||
|
||||
return (
|
||||
<Listbox
|
||||
<Select
|
||||
autoFocus
|
||||
id="select-editor"
|
||||
name="select-editor"
|
||||
@@ -44,14 +45,12 @@ export function SelectEditor<TRow, TSummaryRow = unknown>({
|
||||
onChange={onChange}
|
||||
onBlur={onBlur}
|
||||
>
|
||||
<Listbox.Option id="NULL" label="NULL" value="">
|
||||
NULL
|
||||
</Listbox.Option>
|
||||
{options.map(({ label, value }) => (
|
||||
<Listbox.Option key={value} label={label} value={value}>
|
||||
<Select.Option value="">NULL</Select.Option>
|
||||
{options.map(({ label, _value }) => (
|
||||
<Select.Option key={_value} value={_value} selected={_value === value}>
|
||||
{label}
|
||||
</Listbox.Option>
|
||||
</Select.Option>
|
||||
))}
|
||||
</Listbox>
|
||||
</Select>
|
||||
)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Tabs } from 'ui'
|
||||
import { Button, IconExternalLink, Tabs } from 'ui'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
|
||||
import { TEMPLATES_SCHEMAS } from 'stores/authConfig/schema'
|
||||
@@ -8,10 +8,20 @@ import TemplateEditor from './TemplateEditor'
|
||||
const EmailTemplates = observer(() => {
|
||||
return (
|
||||
<div>
|
||||
<FormHeader
|
||||
title="Email Templates"
|
||||
description="Customize the emails that will be sent out to your users."
|
||||
/>
|
||||
<div className='flex justify-between items-center'>
|
||||
<FormHeader
|
||||
title="Email Templates"
|
||||
description="Customize the emails that will be sent out to your users."
|
||||
/>
|
||||
<Button type="link" icon={<IconExternalLink size={14} strokeWidth={1.5} />}>
|
||||
<a
|
||||
target="_blank"
|
||||
href="https://supabase.com/docs/guides/auth/auth-email-templates"
|
||||
>
|
||||
Email Templates Documentation
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
<FormPanel>
|
||||
<Tabs
|
||||
scrollable
|
||||
|
||||
@@ -131,6 +131,19 @@ const nextConfig = {
|
||||
key: 'X-Frame-Options',
|
||||
value: 'DENY',
|
||||
},
|
||||
{
|
||||
key: 'X-Content-Type-Options',
|
||||
value: 'no-sniff',
|
||||
},
|
||||
{
|
||||
key: 'Content-Security-Policy',
|
||||
value: `
|
||||
frame-ancestors 'none';
|
||||
upgrade-insecure-requests;
|
||||
`
|
||||
.replace(/\s{2,}/g, ' ')
|
||||
.trim(),
|
||||
},
|
||||
{
|
||||
key: 'Referrer-Policy',
|
||||
value: 'strict-origin-when-cross-origin',
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
|
||||
import apiWrapper from 'lib/api/apiWrapper'
|
||||
import { get } from 'lib/common/fetch'
|
||||
import { constructHeaders } from 'lib/api/apiHelpers'
|
||||
import { PG_META_URL } from 'lib/constants'
|
||||
|
||||
export default (req: NextApiRequest, res: NextApiResponse) =>
|
||||
apiWrapper(req, res, handler, { withAuth: true })
|
||||
|
||||
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const { method } = req
|
||||
|
||||
switch (method) {
|
||||
case 'GET':
|
||||
return handleGetAll(req, res)
|
||||
default:
|
||||
res.setHeader('Allow', ['GET'])
|
||||
res.status(405).json({ error: { message: `Method ${method} Not Allowed` } })
|
||||
}
|
||||
}
|
||||
|
||||
const handleGetAll = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const headers = constructHeaders(req.headers)
|
||||
let response = await get(`${PG_META_URL}/foreign-tables`, {
|
||||
headers,
|
||||
})
|
||||
if (response.error) {
|
||||
return res.status(400).json({ error: response.error })
|
||||
}
|
||||
return res.status(200).json(response)
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
|
||||
import apiWrapper from 'lib/api/apiWrapper'
|
||||
import { get } from 'lib/common/fetch'
|
||||
import { constructHeaders } from 'lib/api/apiHelpers'
|
||||
import { PG_META_URL } from 'lib/constants'
|
||||
|
||||
export default (req: NextApiRequest, res: NextApiResponse) =>
|
||||
apiWrapper(req, res, handler, { withAuth: true })
|
||||
|
||||
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const { method } = req
|
||||
|
||||
switch (method) {
|
||||
case 'GET':
|
||||
return handleGetAll(req, res)
|
||||
default:
|
||||
res.setHeader('Allow', ['GET'])
|
||||
res.status(405).json({ error: { message: `Method ${method} Not Allowed` } })
|
||||
}
|
||||
}
|
||||
|
||||
const handleGetAll = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const headers = constructHeaders(req.headers)
|
||||
let response = await get(`${PG_META_URL}/views`, {
|
||||
headers,
|
||||
})
|
||||
if (response.error) {
|
||||
return res.status(400).json({ error: response.error })
|
||||
}
|
||||
return res.status(200).json(response)
|
||||
}
|
||||
@@ -128,7 +128,7 @@ const Wizard: NextPageWithLayout = () => {
|
||||
</Panel.Content>
|
||||
<Panel.Content className="Form section-block--body has-inputs-centered">
|
||||
<Listbox
|
||||
label="Kind"
|
||||
label="Type of organization"
|
||||
layout="horizontal"
|
||||
value={orgKind}
|
||||
onChange={onOrgKindChange}
|
||||
@@ -146,7 +146,7 @@ const Wizard: NextPageWithLayout = () => {
|
||||
{orgKind == 'COMPANY' ? (
|
||||
<Panel.Content className="Form section-block--body has-inputs-centered">
|
||||
<Listbox
|
||||
label="Size"
|
||||
label="Company size"
|
||||
layout="horizontal"
|
||||
value={orgSize}
|
||||
onChange={onOrgSizeChange}
|
||||
|
||||
Reference in new issue
Block a user